fix(voice): consistent state on DM-call ↔ space-channel transitions
Two mirror-image bugs from voice/DM-call transitions leaving stale state. DM call → space channel (stuck "Connecting…"): The last participant to leave a DM call for a space channel receives a `dm_call_ended` echo (server empties the DM room on their `voice_join`). The handlers called `disconnectFn()` unconditionally, tearing down the space room they had just connected to. Route `dm_call_ended` / `dm_call_rejected` / terminal `dm_call_undeliverable` through a new `teardownDmCall()` that only disconnects LiveKit when not in a space channel (`currentVoiceChannelId` null). Space channel → DM call (still shown as "in" the voice channel): 1. Entering a DM call never cleared `currentVoiceChannelId`, so `VoiceChannel` mapped the DM call's live LiveKit participants onto the old space channel. Add `clearSpaceVoiceForDmCall()`, called in `connect()` when `isDm`, restoring the invariant that a DM call has no `currentVoiceChannelId`. 2. `dm_call_accepted` gated the caller's connect on `!isLiveKitConnected`, so a caller already in a space channel was never connected to the DM room. Gate on `wasOutgoingCall` only (connect() de-dupes same-room). Tests: teardownDmCall.test.ts, clearSpaceVoiceForDmCall.test.ts. Docs: docs/systems/voice.md.
This commit is contained in:
@@ -58,6 +58,41 @@ export function broadcastDeafenViaLiveKit(): void {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the client-side presence of a *space* voice channel when the local user
|
||||
* transitions into a DM call.
|
||||
*
|
||||
* A DM call and a space voice channel are mutually exclusive: the invariant is
|
||||
* that a DM call has **no** `currentVoiceChannelId` (only `activeDmCall`). The
|
||||
* space→DM transition must therefore drop the space channel's client state, the
|
||||
* mirror of `joinVoiceChannel` clearing `activeDmCall` on the DM→space
|
||||
* transition.
|
||||
*
|
||||
* Why this is necessary even though the server already drops us from the space
|
||||
* room (`dm_call_start` / `dm_call_accept` → `leaveCurrentRoom` →
|
||||
* `broadcastRoomLeave`): `VoiceChannel` renders the occupant list for the
|
||||
* channel equal to `currentVoiceChannelId` from the *live LiveKit participants*
|
||||
* (its "our channel is the source of truth" branch). If `currentVoiceChannelId`
|
||||
* still points at the old space channel, the DM call's participants get mapped
|
||||
* onto it and the local user appears to still be sitting in the space channel.
|
||||
*
|
||||
* Clears `currentVoiceChannelId` directly (not via `setCurrentVoiceChannel`,
|
||||
* which would also wipe the `activeDmCall` the caller/acceptor just set) and
|
||||
* optimistically removes self from the old channel's `voiceUsers` so the
|
||||
* sidebar updates immediately, without waiting for the server's leave
|
||||
* broadcast.
|
||||
*/
|
||||
export function clearSpaceVoiceForDmCall(): void {
|
||||
const { currentVoiceChannelId, removeVoiceUser } = useVoiceStore.getState();
|
||||
if (!currentVoiceChannelId) return;
|
||||
|
||||
const origin = getChannelOrigin(currentVoiceChannelId);
|
||||
const myId = getMyUserIdForOrigin(origin);
|
||||
if (myId) removeVoiceUser(currentVoiceChannelId, myId);
|
||||
|
||||
useVoiceStore.setState({ currentVoiceChannelId: null });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Voice channel join
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user