feat: add federatedCallId and callOrigin to client call routing
Enable federated DM calls to route accept/reject/end through the correct WebSocket connection using callOrigin, and include federatedCallId in all dm_call payloads for server-side FederatedCallEntry lookup.
This commit is contained in:
@@ -86,7 +86,7 @@ export function NotificationController() {
|
||||
|
||||
// DM call notification
|
||||
useEffect(() => {
|
||||
let prevIncoming: { dmChannelId: string; callerId: string; callerName: string } | null = null;
|
||||
let prevIncoming: { dmChannelId: string | null; callerId: string; callerName: string } | null = null;
|
||||
|
||||
const unsubscribe = useVoiceStore.subscribe((state) => {
|
||||
if (state.incomingCall && !prevIncoming && !windowFocused.current) {
|
||||
|
||||
@@ -102,7 +102,9 @@ export function MainContent() {
|
||||
const handleCancelCall = () => {
|
||||
if (!currentChannelId) return;
|
||||
useVoiceStore.getState().setOutgoingCall(null);
|
||||
wsSend({ type: 'dm_call_end', dmChannelId: currentChannelId }, getChannelOrigin(currentChannelId));
|
||||
const { federatedCallId, callOrigin } = useVoiceStore.getState();
|
||||
const origin = callOrigin || getChannelOrigin(currentChannelId);
|
||||
wsSend({ type: 'dm_call_end', dmChannelId: currentChannelId, federatedCallId }, origin);
|
||||
};
|
||||
|
||||
if (isInDmCall) {
|
||||
|
||||
@@ -98,9 +98,10 @@ export function MobileVoiceFullScreen() {
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
const { activeDmCall, disconnectFn } = useVoiceStore.getState();
|
||||
const { activeDmCall, disconnectFn, federatedCallId, callOrigin } = useVoiceStore.getState();
|
||||
if (activeDmCall) {
|
||||
wsSend({ type: 'dm_call_end', dmChannelId: activeDmCall.dmChannelId }, getChannelOrigin(activeDmCall.dmChannelId));
|
||||
const origin = callOrigin || getChannelOrigin(activeDmCall.dmChannelId);
|
||||
wsSend({ type: 'dm_call_end', dmChannelId: activeDmCall.dmChannelId, federatedCallId }, origin);
|
||||
useVoiceStore.getState().setActiveDmCall(null);
|
||||
} else if (currentVoiceChannelId) {
|
||||
wsSend({ type: 'voice_leave' }, getChannelOrigin(currentVoiceChannelId));
|
||||
|
||||
@@ -95,9 +95,10 @@ export function MobileVoiceMiniBar() {
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const { activeDmCall, disconnectFn } = useVoiceStore.getState();
|
||||
const { activeDmCall, disconnectFn, federatedCallId, callOrigin } = useVoiceStore.getState();
|
||||
if (activeDmCall) {
|
||||
wsSend({ type: 'dm_call_end', dmChannelId: activeDmCall.dmChannelId }, getChannelOrigin(activeDmCall.dmChannelId));
|
||||
const origin = callOrigin || getChannelOrigin(activeDmCall.dmChannelId);
|
||||
wsSend({ type: 'dm_call_end', dmChannelId: activeDmCall.dmChannelId, federatedCallId }, origin);
|
||||
useVoiceStore.getState().setActiveDmCall(null);
|
||||
} else if (currentVoiceChannelId) {
|
||||
wsSend({ type: 'voice_leave' }, getChannelOrigin(currentVoiceChannelId));
|
||||
|
||||
@@ -15,7 +15,9 @@ export function IncomingCallModal() {
|
||||
if (incomingCall) {
|
||||
timerRef.current = setTimeout(() => {
|
||||
// Auto-reject after timeout
|
||||
wsSend({ type: 'dm_call_reject', dmChannelId: incomingCall.dmChannelId }, getChannelOrigin(incomingCall.dmChannelId));
|
||||
const { callOrigin, federatedCallId } = useVoiceStore.getState();
|
||||
const origin = callOrigin || (incomingCall.dmChannelId ? getChannelOrigin(incomingCall.dmChannelId) : undefined);
|
||||
wsSend({ type: 'dm_call_reject', dmChannelId: incomingCall.dmChannelId, federatedCallId }, origin);
|
||||
setIncomingCall(null);
|
||||
}, 30000);
|
||||
}
|
||||
@@ -40,16 +42,20 @@ export function IncomingCallModal() {
|
||||
const handleAccept = () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
const dmChannelId = incomingCall.dmChannelId;
|
||||
const origin = getChannelOrigin(dmChannelId);
|
||||
wsSend({ type: 'dm_call_accept', dmChannelId }, origin);
|
||||
const { callOrigin, federatedCallId } = useVoiceStore.getState();
|
||||
const origin = callOrigin || (dmChannelId ? getChannelOrigin(dmChannelId) : undefined);
|
||||
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, true);
|
||||
if (connectFn) connectFn(dmChannelId || federatedCallId!, true);
|
||||
};
|
||||
|
||||
const handleDecline = () => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current);
|
||||
wsSend({ type: 'dm_call_reject', dmChannelId: incomingCall.dmChannelId }, getChannelOrigin(incomingCall.dmChannelId));
|
||||
const { callOrigin, federatedCallId } = useVoiceStore.getState();
|
||||
const dmChannelId = incomingCall.dmChannelId;
|
||||
const origin = callOrigin || (dmChannelId ? getChannelOrigin(dmChannelId) : undefined);
|
||||
wsSend({ type: 'dm_call_reject', dmChannelId, federatedCallId }, origin);
|
||||
setIncomingCall(null);
|
||||
};
|
||||
|
||||
|
||||
@@ -75,9 +75,10 @@ export function VoiceControls() {
|
||||
};
|
||||
|
||||
const handleDisconnect = () => {
|
||||
const { activeDmCall, disconnectFn } = useVoiceStore.getState();
|
||||
const { activeDmCall, disconnectFn, federatedCallId, callOrigin } = useVoiceStore.getState();
|
||||
if (activeDmCall) {
|
||||
wsSend({ type: 'dm_call_end', dmChannelId: activeDmCall.dmChannelId }, getChannelOrigin(activeDmCall.dmChannelId));
|
||||
const origin = callOrigin || getChannelOrigin(activeDmCall.dmChannelId);
|
||||
wsSend({ type: 'dm_call_end', dmChannelId: activeDmCall.dmChannelId, federatedCallId }, origin);
|
||||
useVoiceStore.getState().setActiveDmCall(null);
|
||||
} else {
|
||||
wsSend({ type: 'voice_leave' }, voiceOrigin);
|
||||
|
||||
@@ -314,20 +314,24 @@ 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, setFederatedCallData } = useVoiceStore.getState();
|
||||
const { activeDmCall, setActiveDmCall, setIncomingCall, incomingCall, connectFn, disconnectFn, setFederatedCallData, setFederatedCallId } = useVoiceStore.getState();
|
||||
const myId = event.user.id;
|
||||
if (event.activeCalls && event.activeCalls.length > 0) {
|
||||
for (const call of event.activeCalls) {
|
||||
// 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 });
|
||||
const callDmId = call.dmChannelId || call.federatedCallId || '';
|
||||
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 (connectFn) {
|
||||
connectFn(call.dmChannelId, true).catch((err) => {
|
||||
if (call.federatedCallId) {
|
||||
setFederatedCallId(call.federatedCallId);
|
||||
}
|
||||
if (connectFn && callDmId) {
|
||||
connectFn(callDmId, true).catch((err) => {
|
||||
console.error('[WS] DM call reconnect failed:', err);
|
||||
});
|
||||
}
|
||||
@@ -344,6 +348,9 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
||||
if (call.livekitUrl && call.livekitToken) {
|
||||
setFederatedCallData(call.livekitToken, call.livekitUrl);
|
||||
}
|
||||
if (call.federatedCallId) {
|
||||
setFederatedCallId(call.federatedCallId);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -770,9 +777,9 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
||||
// ─── DM call events (all origins) ──────────────────────────────────────
|
||||
|
||||
case 'dm_call_incoming': {
|
||||
const { setIncomingCall, setFederatedCallData } = useVoiceStore.getState();
|
||||
const { setIncomingCall, setFederatedCallData, setFederatedCallId, setCallOrigin } = useVoiceStore.getState();
|
||||
setIncomingCall({
|
||||
dmChannelId: event.dmChannelId,
|
||||
dmChannelId: event.dmChannelId ?? null,
|
||||
callerId: event.callerId,
|
||||
callerName: event.callerName,
|
||||
});
|
||||
@@ -780,6 +787,12 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
||||
if (event.livekitUrl && event.livekitToken) {
|
||||
setFederatedCallData(event.livekitToken, event.livekitUrl);
|
||||
}
|
||||
if (event.federatedCallId) {
|
||||
setFederatedCallId(event.federatedCallId);
|
||||
}
|
||||
if (event.callOrigin) {
|
||||
setCallOrigin(event.callOrigin);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -787,10 +800,11 @@ function handleEvent(origin: string, event: ServerEvent): void {
|
||||
const { setIncomingCall, setOutgoingCall, setActiveDmCall, connectFn, isLiveKitConnected } = useVoiceStore.getState();
|
||||
setIncomingCall(null);
|
||||
setOutgoingCall(null);
|
||||
setActiveDmCall({ dmChannelId: event.dmChannelId });
|
||||
const callDmId = event.dmChannelId || event.federatedCallId || '';
|
||||
setActiveDmCall({ dmChannelId: callDmId });
|
||||
// Only connect if not already connected (federated acceptor connects immediately in handleAccept)
|
||||
if (connectFn && !isLiveKitConnected) {
|
||||
connectFn(event.dmChannelId, true).catch((err) => {
|
||||
if (connectFn && !isLiveKitConnected && callDmId) {
|
||||
connectFn(callDmId, true).catch((err: unknown) => {
|
||||
console.error('[WS] DM call connect failed:', err);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,16 +61,20 @@ interface VoiceState {
|
||||
setStreamAttenuationEnabled: (enabled: boolean) => void;
|
||||
setStreamAttenuationStrength: (strength: number) => void;
|
||||
// DM call state
|
||||
incomingCall: { dmChannelId: string; callerId: string; callerName: string } | null;
|
||||
incomingCall: { dmChannelId: string | null; callerId: string; callerName: string } | null;
|
||||
outgoingCall: { dmChannelId: string } | null;
|
||||
activeDmCall: { dmChannelId: string } | null;
|
||||
setIncomingCall: (call: { dmChannelId: string; callerId: string; callerName: string } | null) => void;
|
||||
setIncomingCall: (call: { dmChannelId: string | null; callerId: string; callerName: string } | null) => void;
|
||||
setOutgoingCall: (call: { dmChannelId: string } | null) => void;
|
||||
setActiveDmCall: (call: { dmChannelId: string } | null) => void;
|
||||
federatedCallToken: string | null;
|
||||
federatedCallUrl: string | null;
|
||||
federatedCallId: string | null;
|
||||
callOrigin: string | null;
|
||||
setFederatedCallData: (token: string, url: string) => void;
|
||||
clearFederatedCallData: () => void;
|
||||
setFederatedCallId: (id: string | null) => void;
|
||||
setCallOrigin: (origin: string | null) => void;
|
||||
setVoiceUsers: (channelId: string, userIds: string[]) => void;
|
||||
addVoiceUser: (channelId: string, userId: string) => void;
|
||||
removeVoiceUser: (channelId: string, userId: string) => void;
|
||||
@@ -246,12 +250,16 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
activeDmCall: null,
|
||||
federatedCallToken: null,
|
||||
federatedCallUrl: null,
|
||||
federatedCallId: null,
|
||||
callOrigin: 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 }),
|
||||
clearFederatedCallData: () => set({ federatedCallToken: null, federatedCallUrl: null, federatedCallId: null, callOrigin: null }),
|
||||
setFederatedCallId: (id) => set({ federatedCallId: id }),
|
||||
setCallOrigin: (origin) => set({ callOrigin: origin }),
|
||||
|
||||
setVoiceUsers: (channelId, userIds) => {
|
||||
set((state) => {
|
||||
@@ -426,6 +434,8 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
activeDmCall: null,
|
||||
federatedCallToken: null,
|
||||
federatedCallUrl: null,
|
||||
federatedCallId: null,
|
||||
callOrigin: null,
|
||||
// Per-session media state
|
||||
isCameraOn: false,
|
||||
isScreenSharing: false,
|
||||
@@ -484,6 +494,8 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
outgoingCall: null,
|
||||
federatedCallToken: null,
|
||||
federatedCallUrl: null,
|
||||
federatedCallId: null,
|
||||
callOrigin: null,
|
||||
deafenedUserIds: new Set(),
|
||||
participantMutes: new Map(),
|
||||
streamVolumes: new Map(),
|
||||
@@ -521,6 +533,8 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
outgoingCall: null,
|
||||
federatedCallToken: null,
|
||||
federatedCallUrl: null,
|
||||
federatedCallId: null,
|
||||
callOrigin: null,
|
||||
deafenedUserIds: new Set(),
|
||||
participantMutes: new Map(),
|
||||
streamVolumes: new Map(),
|
||||
@@ -555,6 +569,8 @@ export const useVoiceStore = create<VoiceState>()(
|
||||
activeDmCall: null,
|
||||
federatedCallToken: null,
|
||||
federatedCallUrl: null,
|
||||
federatedCallId: null,
|
||||
callOrigin: null,
|
||||
deafenedUserIds: new Set(),
|
||||
voiceUserStates: new Map(),
|
||||
streamVolumes: new Map(),
|
||||
|
||||
@@ -88,9 +88,10 @@ export function handleDisconnectAction(): void {
|
||||
const { activeDmCall, currentVoiceChannelId, disconnectFn } = voice;
|
||||
|
||||
if (activeDmCall) {
|
||||
const origin = voice.callOrigin || getChannelOrigin(activeDmCall.dmChannelId);
|
||||
wsSend(
|
||||
{ type: 'dm_call_end', dmChannelId: activeDmCall.dmChannelId },
|
||||
getChannelOrigin(activeDmCall.dmChannelId)
|
||||
{ type: 'dm_call_end', dmChannelId: activeDmCall.dmChannelId, federatedCallId: voice.federatedCallId },
|
||||
origin
|
||||
);
|
||||
voice.setActiveDmCall(null);
|
||||
} else if (currentVoiceChannelId) {
|
||||
|
||||
Reference in New Issue
Block a user