fix: acceptor sets activeDmCall in click handler, not server response

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.
This commit is contained in:
Jannis Braun
2026-04-08 14:23:41 +02:00
parent aae0b1a74e
commit c037803c5a
2 changed files with 14 additions and 16 deletions
@@ -42,12 +42,19 @@ export function IncomingCallModal() {
const handleAccept = () => { const handleAccept = () => {
if (timerRef.current) clearTimeout(timerRef.current); if (timerRef.current) clearTimeout(timerRef.current);
const dmChannelId = incomingCall.dmChannelId; const dmChannelId = incomingCall.dmChannelId;
const { callOrigin, federatedCallId } = useVoiceStore.getState(); const { callOrigin, federatedCallId, setActiveDmCall, connectFn } = useVoiceStore.getState();
const origin = callOrigin || (dmChannelId ? getChannelOrigin(dmChannelId) : undefined); 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); wsSend({ type: 'dm_call_accept', dmChannelId, federatedCallId }, origin);
// Connect directly within gesture context (required for iOS audio permission) // Connect directly within gesture context (required for iOS audio permission)
const connectFn = useVoiceStore.getState().connectFn; if (connectFn) connectFn(callDmId, true);
if (connectFn) connectFn(dmChannelId || federatedCallId!, true);
}; };
const handleDecline = () => { const handleDecline = () => {
+4 -13
View File
@@ -321,21 +321,12 @@ function handleEvent(origin: string, event: ServerEvent): void {
// For federated calls, check membership via token presence (participants may be empty) // For federated calls, check membership via token presence (participants may be empty)
const isParticipant = call.participants.includes(myId) || !!call.livekitToken; const isParticipant = call.participants.includes(myId) || !!call.livekitToken;
if (call.state === 'active' && isParticipant) { if (call.state === 'active' && isParticipant) {
const callDmId = call.dmChannelId || call.federatedCallId || '';
// Clear any stuck ringing UI from a ringing→active transition during refresh // Clear any stuck ringing UI from a ringing→active transition during refresh
setIncomingCall(null); setIncomingCall(null);
setActiveDmCall({ dmChannelId: callDmId }); // Do NOT set activeDmCall or auto-connect. On refresh/restart, the user
// Store federated call data if present (server already filtered to this user's token) // is no longer in LiveKit — showing "Connecting..." with no connection
if (call.livekitUrl && call.livekitToken) { // is broken UX. The call exists on the server but this client session
setFederatedCallData(call.livekitToken, call.livekitUrl); // has no active LiveKit connection. The user can re-initiate if needed.
}
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.
break; break;
} else if (call.state === 'ringing' && call.callerId !== myId) { } else if (call.state === 'ringing' && call.callerId !== myId) {
const dmCh = event.dmChannels?.find((d: any) => d.id === call.dmChannelId); const dmCh = event.dmChannels?.find((d: any) => d.id === call.dmChannelId);