feat: redesign + avatars + theme + call presence fixes (v0.6.0)
Release desktop app / build (, windows-latest) (push) Has been cancelled
Release desktop app / build (--target universal-apple-darwin --bundles app,updater, macos-14) (push) Has been cancelled

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
This commit is contained in:
2026-04-20 00:26:19 +02:00
parent 0ca29952ba
commit 4db65993d5
33 changed files with 2459 additions and 1094 deletions
+32 -21
View File
@@ -6,11 +6,15 @@ import { supabase } from './supabase';
// 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.
// 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[]>([]);
@@ -24,23 +28,13 @@ export function useCallPresence(conversationId: string | undefined): string[] {
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.
// Server only routes presence to channels that opt-in. Without this
// flag we'd receive zero presence_state/presence_diff messages.
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>();
@@ -57,14 +51,31 @@ export function useCallPresence(conversationId: string | undefined): string[] {
});
};
// 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();
const pollId = window.setInterval(resync, 1500);
// 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);
if (ownsChannel) {
void supabase.removeChannel(channel);
}
// 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]);