initial
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import type { RealtimeChannel } from '@supabase/supabase-js';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { supabase } from './supabase';
|
||||
|
||||
// Observes the `call-presence:<conversationId>` channel for every conversation
|
||||
// the user is a member of and returns the first one (if any) that currently
|
||||
// has peers other than the viewer in the call. Enables a "call is live —
|
||||
// rejoin" affordance in the sidebar for conversations the viewer never
|
||||
// joined, mirroring Discord's active-voice indicator.
|
||||
//
|
||||
// Uses polling on each channel's `presenceState()` to side-step Supabase's
|
||||
// topic-based channel dedupe (see useCallPresence.ts).
|
||||
export function useAnyActiveCall(
|
||||
conversationIds: readonly string[],
|
||||
myId: string | null,
|
||||
): { conversationId: string; userIds: string[] } | null {
|
||||
const [byConv, setByConv] = useState<Record<string, string[]>>({});
|
||||
|
||||
const key = conversationIds.join(',');
|
||||
|
||||
useEffect(() => {
|
||||
if (conversationIds.length === 0) {
|
||||
setByConv({});
|
||||
return;
|
||||
}
|
||||
|
||||
const tracked: Array<{ id: string; ch: RealtimeChannel; owns: boolean }> = [];
|
||||
for (const id of conversationIds) {
|
||||
const ch = supabase.channel('call-presence:' + id, {
|
||||
config: {
|
||||
presence: {
|
||||
key: 'obs-' + Math.random().toString(36).slice(2, 8),
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
const owns = ch.state === 'closed';
|
||||
if (owns) void ch.subscribe();
|
||||
tracked.push({ id, ch, owns });
|
||||
}
|
||||
|
||||
const resync = () => {
|
||||
setByConv((prev) => {
|
||||
const next: Record<string, string[]> = {};
|
||||
let changed = false;
|
||||
for (const { id, ch } of tracked) {
|
||||
const presState = ch.presenceState() as Record<
|
||||
string,
|
||||
Array<Record<string, unknown>>
|
||||
>;
|
||||
const ids = new Set<string>();
|
||||
for (const list of Object.values(presState)) {
|
||||
for (const e of list) {
|
||||
const uid = e?.userId;
|
||||
if (typeof uid === 'string') ids.add(uid);
|
||||
}
|
||||
}
|
||||
const arr = Array.from(ids).sort();
|
||||
next[id] = arr;
|
||||
const prevArr = prev[id] ?? [];
|
||||
if (prevArr.length !== arr.length || !arr.every((v, i) => v === prevArr[i])) {
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
// Also pick up removed conv ids.
|
||||
if (!changed && Object.keys(prev).length !== Object.keys(next).length) {
|
||||
changed = true;
|
||||
}
|
||||
return changed ? next : prev;
|
||||
});
|
||||
};
|
||||
|
||||
resync();
|
||||
const pollId = window.setInterval(resync, 1500);
|
||||
|
||||
return () => {
|
||||
window.clearInterval(pollId);
|
||||
for (const { ch, owns } of tracked) {
|
||||
if (owns) void supabase.removeChannel(ch);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [key]);
|
||||
|
||||
for (const id of conversationIds) {
|
||||
const users = byConv[id] ?? [];
|
||||
const others = myId ? users.filter((u) => u !== myId) : users;
|
||||
if (others.length > 0) {
|
||||
return { conversationId: id, userIds: others };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user