import React, { useEffect, useRef } from 'react'; import { useVoiceStore } from '../../stores/voiceStore'; import { useSpaceStore, getChannelOrigin } from '../../stores/spaceStore'; import { wsSend } from '../../hooks/useWebSocket'; import { parseFederatedUsername } from '../../utils/identity'; import { Avatar } from '../ui/Avatar'; export function IncomingCallModal() { const incomingCall = useVoiceStore((s) => s.incomingCall); const setIncomingCall = useVoiceStore((s) => s.setIncomingCall); const timerRef = useRef | null>(null); // Auto-dismiss after 30 seconds useEffect(() => { if (incomingCall) { timerRef.current = setTimeout(() => { // Auto-reject after timeout 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); } return () => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } }; }, [incomingCall, setIncomingCall]); const dmChannels = useSpaceStore((s) => s.dmChannels); if (!incomingCall) return null; // Look up the caller in DM channel members for homeUserId const dmChannel = dmChannels.find(d => d.id === incomingCall.dmChannelId); const callerMember = dmChannel?.members.find(m => m.id === incomingCall.callerId); const callerAvatarId = callerMember?.homeUserId ?? incomingCall.callerId; const { baseName: callerBaseName } = parseFederatedUsername(incomingCall.callerName); const handleAccept = () => { if (timerRef.current) clearTimeout(timerRef.current); const dmChannelId = incomingCall.dmChannelId; const { callOrigin, federatedCallId, setActiveDmCall, connectFn } = useVoiceStore.getState(); 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); // Connect directly within gesture context (required for iOS audio permission) if (connectFn) connectFn(callDmId, true); }; const handleDecline = () => { if (timerRef.current) clearTimeout(timerRef.current); 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); }; return (
{/* Backdrop */}
{/* Call card */}
{/* Liquid ripple orbs — soft radial gradients with blur */}
{[0, 1.3, 2.6].map((delay, i) => (
))}
{/* Content */}
{/* Caller avatar */}
{/* Caller info */}

{callerBaseName}

Incoming Voice Call...

{/* Action buttons */}
{/* Decline */} {/* Accept */}
); }