From 24571edc34572ce524c20c1e232a371365fb7b7d Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Tue, 31 Mar 2026 23:40:11 +0200 Subject: [PATCH] feat(federation): client voice store handles federated call data (FED-009) --- packages/shared/src/types.ts | 2 +- packages/web/src/hooks/useWebSocket.ts | 32 ++++++++++++++++++-------- packages/web/src/stores/voiceStore.ts | 12 ++++++++++ 3 files changed, 36 insertions(+), 10 deletions(-) diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index bb7c6c9d..6c0a8a45 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -383,7 +383,7 @@ export type ServerEvent = | { type: 'channel_ack'; channelId: string; messageId: string } | { type: 'friend_request_received'; request: FriendRequest } | { type: 'friend_request_accepted'; friend: Friend; requestId: string } - | { type: 'dm_call_incoming'; dmChannelId: string; callerId: string; callerName: string } + | { type: 'dm_call_incoming'; dmChannelId: string; callerId: string; callerName: string; livekitUrl?: string; livekitToken?: string } | { type: 'dm_call_accepted'; dmChannelId: string } | { type: 'dm_call_rejected'; dmChannelId: string } | { type: 'dm_call_ended'; dmChannelId: string } diff --git a/packages/web/src/hooks/useWebSocket.ts b/packages/web/src/hooks/useWebSocket.ts index dbdff132..7a94165a 100644 --- a/packages/web/src/hooks/useWebSocket.ts +++ b/packages/web/src/hooks/useWebSocket.ts @@ -312,14 +312,18 @@ function handleEvent(origin: string, event: ServerEvent): void { // Restore DM call state from server (all origins — federated DMs live on remote instances) { - const { activeDmCall, setActiveDmCall, setIncomingCall, incomingCall, connectFn, disconnectFn } = useVoiceStore.getState(); + const { activeDmCall, setActiveDmCall, setIncomingCall, incomingCall, connectFn, disconnectFn, setFederatedCallData } = useVoiceStore.getState(); const myId = event.user.id; if (event.activeCalls && event.activeCalls.length > 0) { for (const call of event.activeCalls) { - const isParticipant = call.participants.includes(myId); + // 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) { setActiveDmCall({ dmChannelId: call.dmChannelId }); - // Re-establish LiveKit connection for DM call if needed (WS reconnect) + // 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 (connectFn) { connectFn(call.dmChannelId, true).catch((err) => { console.error('[WS] DM call reconnect failed:', err); @@ -334,6 +338,10 @@ function handleEvent(origin: string, event: ServerEvent): void { callerId: call.callerId, callerName: callerUser?.displayName || callerUser?.username || call.callerId, }); + // Store federated call data for ringing calls too + if (call.livekitUrl && call.livekitToken) { + setFederatedCallData(call.livekitToken, call.livekitUrl); + } } } } else { @@ -745,22 +753,26 @@ function handleEvent(origin: string, event: ServerEvent): void { // ─── DM call events (all origins) ────────────────────────────────────── case 'dm_call_incoming': { - const { setIncomingCall } = useVoiceStore.getState(); + const { setIncomingCall, setFederatedCallData } = useVoiceStore.getState(); setIncomingCall({ dmChannelId: event.dmChannelId, callerId: event.callerId, callerName: event.callerName, }); + // Store federated call data if present (remote LiveKit URL + token) + if (event.livekitUrl && event.livekitToken) { + setFederatedCallData(event.livekitToken, event.livekitUrl); + } break; } case 'dm_call_accepted': { - const { setIncomingCall, setOutgoingCall, setActiveDmCall, connectFn } = useVoiceStore.getState(); + const { setIncomingCall, setOutgoingCall, setActiveDmCall, connectFn, isLiveKitConnected } = useVoiceStore.getState(); setIncomingCall(null); setOutgoingCall(null); setActiveDmCall({ dmChannelId: event.dmChannelId }); - // Initiate LiveKit connection for the DM call - if (connectFn) { + // Only connect if not already connected (federated acceptor connects immediately in handleAccept) + if (connectFn && !isLiveKitConnected) { connectFn(event.dmChannelId, true).catch((err) => { console.error('[WS] DM call connect failed:', err); }); @@ -769,19 +781,21 @@ function handleEvent(origin: string, event: ServerEvent): void { } case 'dm_call_rejected': { - const { setIncomingCall, setOutgoingCall, setActiveDmCall, disconnectFn } = useVoiceStore.getState(); + const { setIncomingCall, setOutgoingCall, setActiveDmCall, disconnectFn, clearFederatedCallData } = useVoiceStore.getState(); setIncomingCall(null); setOutgoingCall(null); setActiveDmCall(null); + clearFederatedCallData(); if (disconnectFn) disconnectFn(); break; } case 'dm_call_ended': { - const { setIncomingCall, setOutgoingCall, setActiveDmCall, disconnectFn } = useVoiceStore.getState(); + const { setIncomingCall, setOutgoingCall, setActiveDmCall, disconnectFn, clearFederatedCallData } = useVoiceStore.getState(); setIncomingCall(null); setOutgoingCall(null); setActiveDmCall(null); + clearFederatedCallData(); if (disconnectFn) disconnectFn(); break; } diff --git a/packages/web/src/stores/voiceStore.ts b/packages/web/src/stores/voiceStore.ts index 9cad5511..3bc9f5b1 100644 --- a/packages/web/src/stores/voiceStore.ts +++ b/packages/web/src/stores/voiceStore.ts @@ -67,6 +67,10 @@ interface VoiceState { setIncomingCall: (call: { dmChannelId: string; callerId: string; callerName: string } | null) => void; setOutgoingCall: (call: { dmChannelId: string } | null) => void; setActiveDmCall: (call: { dmChannelId: string } | null) => void; + federatedCallToken: string | null; + federatedCallUrl: string | null; + setFederatedCallData: (token: string, url: string) => void; + clearFederatedCallData: () => void; setVoiceUsers: (channelId: string, userIds: string[]) => void; addVoiceUser: (channelId: string, userId: string) => void; removeVoiceUser: (channelId: string, userId: string) => void; @@ -240,10 +244,14 @@ export const useVoiceStore = create()( incomingCall: null, outgoingCall: null, activeDmCall: null, + federatedCallToken: null, + federatedCallUrl: null, setIncomingCall: (call) => set({ incomingCall: call }), setOutgoingCall: (call) => set({ outgoingCall: call }), setActiveDmCall: (call) => set({ activeDmCall: call }), + setFederatedCallData: (token, url) => set({ federatedCallToken: token, federatedCallUrl: url }), + clearFederatedCallData: () => set({ federatedCallToken: null, federatedCallUrl: null }), setVoiceUsers: (channelId, userIds) => { set((state) => { @@ -416,6 +424,8 @@ export const useVoiceStore = create()( incomingCall: null, outgoingCall: null, activeDmCall: null, + federatedCallToken: null, + federatedCallUrl: null, // Per-session media state isCameraOn: false, isScreenSharing: false, @@ -539,6 +549,8 @@ export const useVoiceStore = create()( incomingCall: null, outgoingCall: null, activeDmCall: null, + federatedCallToken: null, + federatedCallUrl: null, deafenedUserIds: new Set(), voiceUserStates: new Map(), streamVolumes: new Map(),