4db65993d5
Visual:
- Light/dark theme via ThemeContext + CSS vars
- New design tokens (surface/fg/accent/line/etc.) across all pages
- Reusable Avatar component (img + letter fallback) wired into UserBar,
ConversationHeader, ChatsPage, FriendsPage, GroupInfoPanel, IncomingCallPanel
Calls:
- Split call UI: IncomingCallPanel, InCallPanel, CallControls,
CallParticipantTile, ScreenShareViewer
- Active speaker hook (useActiveSpeakers)
- Fix: ActiveCallBanner stayed hidden after hangup while peers in room.
- useCallPresence: bind presence callbacks only when we own subscribe
(Supabase forbids .on() after .subscribe() on shared dedup'd channels)
- useCallPresence: never removeChannel — channel is shared with CallContext
so tearing it down on ConversationHeader unmount killed live tracking
- ActiveCallBanner: lastCallConversationId fallback so banner shows
instantly after hangup, auto-dismiss when room confirmed empty
- Drop unused useAnyActiveCall
Bump tauri version 0.5.0 -> 0.6.0
84 lines
3.4 KiB
TypeScript
84 lines
3.4 KiB
TypeScript
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.
|
|
//
|
|
// Why presence-callbacks + no-removeChannel:
|
|
// - Supabase realtime 2.103+ dedupes channels by topic. CallContext owns
|
|
// the same channel during a live call to track its own presence.
|
|
// - If this hook removed the channel on unmount (e.g. when ConversationHeader
|
|
// hides during an active call), the live tracking connection dies and the
|
|
// banner never shows the rejoin option afterwards.
|
|
// - Binding our own presence callbacks (sync/join/leave) ensures the local
|
|
// presence state is updated even when CallContext also listens on the same
|
|
// channel — multiple callbacks can co-exist.
|
|
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),
|
|
// Server only routes presence to channels that opt-in. Without this
|
|
// flag we'd receive zero presence_state/presence_diff messages.
|
|
enabled: true,
|
|
},
|
|
},
|
|
});
|
|
|
|
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;
|
|
});
|
|
};
|
|
|
|
// Supabase realtime forbids `.on()` after `.subscribe()`. So we only bind
|
|
// callbacks when we own the subscribe (channel was 'closed'). For deduped
|
|
// shared channels, the polling fallback below covers state changes.
|
|
if (channel.state === 'closed') {
|
|
channel
|
|
.on('presence', { event: 'sync' }, resync)
|
|
.on('presence', { event: 'join' }, resync)
|
|
.on('presence', { event: 'leave' }, resync);
|
|
void channel.subscribe();
|
|
}
|
|
|
|
// Initial poll covers the case where the channel is already 'joined' (we
|
|
// dedupe'd onto an existing subscription) and won't fire a fresh sync.
|
|
resync();
|
|
// Belt-and-suspenders: a slow poll catches any drift between callback and
|
|
// server state (e.g. if a presence event was dropped during reconnect).
|
|
const pollId = window.setInterval(resync, 3000);
|
|
|
|
return () => {
|
|
window.clearInterval(pollId);
|
|
// Don't removeChannel — the channel is shared with CallContext (and any
|
|
// sibling observers) via Supabase's topic dedupe. Removing it would
|
|
// tear down a live call's presence tracker. The channel naturally
|
|
// outlives the hook; leaks are bounded by the small number of
|
|
// conversations a user holds open in a session.
|
|
};
|
|
}, [conversationId]);
|
|
|
|
return activeUserIds;
|
|
}
|