From c037803c5aa3172fdd20c8db013bff7974376405 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Wed, 8 Apr 2026 14:23:41 +0200 Subject: [PATCH] fix: acceptor sets activeDmCall in click handler, not server response MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug A: handleAccept relied on dm_call_accepted server response to set activeDmCall. But connectFn's async AudioContext resume yields to the event loop, dm_call_accepted arrives during the yield, finds isLiveKitConnected=false (connectFn just reset it), and skips setActiveDmCall. The acceptor connects to LiveKit but the UI never shows the call. Fix: set activeDmCall and clear incomingCall directly in the click handler. Bug B: ready handler no longer sets activeDmCall for active calls. On refresh/restart the client has no LiveKit connection — showing "Connecting..." with no connection is broken. The call exists on the server but this client session is disconnected. --- .../src/components/voice/IncomingCallModal.tsx | 13 ++++++++++--- packages/web/src/hooks/useWebSocket.ts | 17 ++++------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/web/src/components/voice/IncomingCallModal.tsx b/packages/web/src/components/voice/IncomingCallModal.tsx index 7c591938..d497daae 100644 --- a/packages/web/src/components/voice/IncomingCallModal.tsx +++ b/packages/web/src/components/voice/IncomingCallModal.tsx @@ -42,12 +42,19 @@ export function IncomingCallModal() { const handleAccept = () => { if (timerRef.current) clearTimeout(timerRef.current); const dmChannelId = incomingCall.dmChannelId; - const { callOrigin, federatedCallId } = useVoiceStore.getState(); + const { callOrigin, federatedCallId, setActiveDmCall, connectFn } = useVoiceStore.getState(); const origin = callOrigin || (dmChannelId ? getChannelOrigin(dmChannelId) : undefined); + const callDmId = dmChannelId || federatedCallId!; + + // Immediately transition to active call state — don't wait for server response. + // The dm_call_accepted event races with connectFn's async AudioContext resume, + // causing isLiveKitConnected to be false when it arrives → activeDmCall never set. + setIncomingCall(null); + setActiveDmCall({ dmChannelId: callDmId }); + wsSend({ type: 'dm_call_accept', dmChannelId, federatedCallId }, origin); // Connect directly within gesture context (required for iOS audio permission) - const connectFn = useVoiceStore.getState().connectFn; - if (connectFn) connectFn(dmChannelId || federatedCallId!, true); + if (connectFn) connectFn(callDmId, true); }; const handleDecline = () => { diff --git a/packages/web/src/hooks/useWebSocket.ts b/packages/web/src/hooks/useWebSocket.ts index a8910445..c778f704 100644 --- a/packages/web/src/hooks/useWebSocket.ts +++ b/packages/web/src/hooks/useWebSocket.ts @@ -321,21 +321,12 @@ function handleEvent(origin: string, event: ServerEvent): void { // For federated calls, check membership via token presence (participants may be empty) const isParticipant = call.participants.includes(myId) || !!call.livekitToken; if (call.state === 'active' && isParticipant) { - const callDmId = call.dmChannelId || call.federatedCallId || ''; // Clear any stuck ringing UI from a ringing→active transition during refresh setIncomingCall(null); - setActiveDmCall({ dmChannelId: callDmId }); - // Store federated call data if present (server already filtered to this user's token) - if (call.livekitUrl && call.livekitToken) { - setFederatedCallData(call.livekitToken, call.livekitUrl); - } - if (call.federatedCallId) { - setFederatedCallId(call.federatedCallId); - } - // PASSIVE: do NOT auto-connect to LiveKit on ready. - // The user must click "Join" or re-accept. Auto-connecting causes - // identity conflicts when the same user has multiple sessions — - // both sessions fight for the same LiveKit identity slot. + // Do NOT set activeDmCall or auto-connect. On refresh/restart, the user + // is no longer in LiveKit — showing "Connecting..." with no connection + // is broken UX. The call exists on the server but this client session + // has no active LiveKit connection. The user can re-initiate if needed. break; } else if (call.state === 'ringing' && call.callerId !== myId) { const dmCh = event.dmChannels?.find((d: any) => d.id === call.dmChannelId);