From 0dc3eb188430d30a169c698965f4d1f19f268378 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:03:35 +0100 Subject: [PATCH] fix: notify displaced tab when user joins voice from another session When a user joins a voice channel while already connected from another tab, the server now sends a 'displaced' reason on voice_disconnected. The client tears down LiveKit and shows an informational toast. --- packages/server/src/ws/events.ts | 8 ++++++++ packages/shared/src/types.ts | 2 +- packages/web/src/hooks/useWebSocket.ts | 11 ++++++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/packages/server/src/ws/events.ts b/packages/server/src/ws/events.ts index f14cf87c..44171d64 100644 --- a/packages/server/src/ws/events.ts +++ b/packages/server/src/ws/events.ts @@ -537,6 +537,14 @@ function handleVoiceJoin(event: Record, userId: string): void { const left = connectionManager.leaveCurrentRoom(userId); if (left) { broadcastRoomLeave(left.roomId, left.room, userId); + + // Notify all of the user's tabs so the displaced tab tears down LiveKit + connectionManager.sendToUser(userId, { + type: 'voice_disconnected', + userId, + channelId: left.roomId, + reason: 'displaced', + }); } // Cancel any ringing DM rooms where this user is the caller diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 8fb36614..0e3e6d97 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -330,7 +330,7 @@ export type ServerEvent = | { type: 'voice_space_deafened'; userId: string; channelId: string; spaceId: string; deafened: boolean } | { type: 'voice_permission_muted'; userId: string; spaceId: string; muted: boolean } | { type: 'voice_moved'; userId: string; oldChannelId: string; newChannelId: string } - | { type: 'voice_disconnected'; userId: string; channelId: string } + | { type: 'voice_disconnected'; userId: string; channelId: string; reason?: 'displaced' } | { type: 'user_updated'; user: User } | { type: 'member_banned'; spaceId: string; reason: string | null } | { type: 'category_created'; category: ChannelCategory; spaceId: string } diff --git a/packages/web/src/hooks/useWebSocket.ts b/packages/web/src/hooks/useWebSocket.ts index b2612a9f..075f9555 100644 --- a/packages/web/src/hooks/useWebSocket.ts +++ b/packages/web/src/hooks/useWebSocket.ts @@ -9,6 +9,8 @@ import type { ServerEvent, ClientEvent, ActiveCallInfo } from '@backspace/shared import { resolveAssetUrl, normalizeUserAssets, normalizeMessageAssets } from '../utils/assetUrls'; import { broadcastVoiceStatus, broadcastDeafenViaLiveKit } from '../utils/voice'; import { registerSelfId } from '../utils/identity'; +import { getActiveRoom } from './useLiveKit'; +import { useUIStore } from '../stores/uiStore'; // ─── Connection state ───────────────────────────────────────────────────────── @@ -428,7 +430,14 @@ function handleEvent(origin: string, event: ServerEvent): void { case 'voice_disconnected': { const myDisconnectId = isHome ? useAuthStore.getState().user?.id : getMyUserIdForOrigin(origin); if (event.userId === myDisconnectId) { - useVoiceStore.getState().handleForceDisconnect(); + const { currentVoiceChannelId } = useVoiceStore.getState(); + if (event.channelId === currentVoiceChannelId) { + useVoiceStore.getState().handleForceDisconnect(); + getActiveRoom()?.disconnect(); + if (event.reason === 'displaced') { + useUIStore.getState().addToast('Voice disconnected — joined from another session', 'info'); + } + } } break; }