initial
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { supabase } from './supabase';
|
||||
|
||||
// Watches the `call-presence:<conversationId>` realtime channel and returns
|
||||
// the set of userIds currently in that call. Independent of whether the
|
||||
// viewer is in the room — used to show "Active call · Join" affordances.
|
||||
//
|
||||
// Supabase realtime 2.103+ dedupes channels by topic: calling
|
||||
// `supabase.channel(topic)` returns an existing channel if one already exists.
|
||||
// That means an observer can't safely register `.on('presence', ...)` because
|
||||
// the tracker (CallContext) may have already subscribed it. We side-step this
|
||||
// by polling `presenceState()` on the (shared or fresh) channel.
|
||||
export function useCallPresence(conversationId: string | undefined): string[] {
|
||||
const [activeUserIds, setActiveUserIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!conversationId) {
|
||||
setActiveUserIds([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const channel = supabase.channel('call-presence:' + conversationId, {
|
||||
config: {
|
||||
presence: {
|
||||
key: 'observer-' + Math.random().toString(36).slice(2, 8),
|
||||
// Supabase realtime only sends presence_state/diff events to a
|
||||
// channel that has presence enabled. Without `enabled: true` (and
|
||||
// no `.on('presence', ...)` bindings) the server treats the channel
|
||||
// as non-presence and `presenceState()` never populates.
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// If Supabase returned a fresh (closed) channel, we own it and must
|
||||
// subscribe/remove it. If it returned an already-subscribed channel
|
||||
// (tracker owns it), we just read state.
|
||||
const ownsChannel = channel.state === 'closed';
|
||||
if (ownsChannel) {
|
||||
void channel.subscribe();
|
||||
}
|
||||
|
||||
const resync = () => {
|
||||
const state = channel.presenceState() as Record<string, Array<Record<string, unknown>>>;
|
||||
const ids = new Set<string>();
|
||||
for (const list of Object.values(state)) {
|
||||
for (const entry of list) {
|
||||
const uid = entry?.userId;
|
||||
if (typeof uid === 'string') ids.add(uid);
|
||||
}
|
||||
}
|
||||
setActiveUserIds((prev) => {
|
||||
const next = Array.from(ids).sort();
|
||||
if (prev.length === next.length && prev.every((v, i) => v === next[i])) return prev;
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
resync();
|
||||
const pollId = window.setInterval(resync, 1500);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(pollId);
|
||||
if (ownsChannel) {
|
||||
void supabase.removeChannel(channel);
|
||||
}
|
||||
};
|
||||
}, [conversationId]);
|
||||
|
||||
return activeUserIds;
|
||||
}
|
||||
Reference in New Issue
Block a user