import { useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation, useNavigate } from 'react-router-dom'; import { useCall } from '../context/CallContext'; import { useConversationsContext } from '../context/ConversationsContext'; import { useFriendshipsContext } from '../context/FriendshipsContext'; import { ringtone } from '../lib/ringtone'; import { AvatarColorKey, colorKeyFor } from './CallParticipantTile'; import { LockIcon, MonitorShareIcon, PhoneIcon, PhoneOffIcon } from './icons'; // Shell-level mount. Responsibilities: // - Start/stop ringtones by call-state transition. // - Toast-style incoming-call banner for conversations the user isn't in // (clicking the toast routes to the docked IncomingCallPanel). // - PiP widget when an active call is live but the user is looking at a // different route. export function CallUI() { const { state } = useCall(); useEffect(() => { if (state.kind === 'outgoing') ringtone.start('outgoing'); else if (state.kind === 'incoming') ringtone.start('incoming'); else ringtone.stop(); }, [state.kind]); useEffect(() => { return () => ringtone.stop(); }, []); return ( <> ); } // --------------------------------------------------------------------------- const TOAST_AVATAR: Record = { violet: 'bg-violet-500/90 text-white', amber: 'bg-amber-500/90 text-white', rose: 'bg-rose-500/90 text-white', teal: 'bg-teal-500/90 text-white', }; function IncomingCallToast() { const { t } = useTranslation(['app']); const { state, acceptIncoming, rejectIncoming } = useCall(); const { friendships } = useFriendshipsContext(); const { conversations } = useConversationsContext(); const navigate = useNavigate(); const location = useLocation(); if (state.kind !== 'incoming') return null; // When the user is already looking at the target conversation, the docked // IncomingCallPanel handles the UI; hide the toast to avoid double chrome. if (location.pathname === '/chats/' + state.conversationId) return null; const conv = conversations.find((c) => c.id === state.conversationId) ?? null; const callerName = conv?.members.find((m) => m.userId === state.fromUserId)?.profile?.displayName ?? friendships.find((f) => f.peer.userId === state.fromUserId)?.peer.displayName ?? '?'; const isGroup = conv?.type === 'group'; const groupName = isGroup ? (conv?.name ?? t('app:chats.new_group')) : null; const title = isGroup ? groupName ?? callerName : callerName; const letter = title.trim().charAt(0).toUpperCase() || '?'; const color = colorKeyFor(state.fromUserId); return (
); } // --------------------------------------------------------------------------- // PiP widget — shown when the user has an active call but is browsing // somewhere else. Clicking expands back to the call's conversation. // --------------------------------------------------------------------------- function PipCall() { const { t } = useTranslation(['app']); const { state, remoteParticipants, remoteScreenShares, hangup, } = useCall(); const navigate = useNavigate(); const location = useLocation(); const { conversations } = useConversationsContext(); const active = state.kind === 'connected' || state.kind === 'connecting' || state.kind === 'outgoing'; if (!active) return null; // Only render when the user is NOT currently viewing the call's // conversation. Inside that conversation the full dock is visible already. if (location.pathname === '/chats/' + state.conversationId) return null; const conv = conversations.find((c) => c.id === state.conversationId) ?? null; const title = conv?.type === 'group' ? conv?.name ?? t('app:chats.new_group') : conv?.peer?.displayName ?? '—'; const participantCount = 1 + remoteParticipants.length; const someoneSharing = remoteScreenShares.length > 0; return (
navigate('/chats/' + state.conversationId)} className="fixed bottom-5 right-5 z-40 flex w-[260px] animate-slide-in-call cursor-pointer items-center gap-2.5 rounded-[14px] border border-accent bg-surface-3 p-2.5 shadow-pip-call transition hover:-translate-y-0.5" >
{someoneSharing ? ( ) : ( )}

{title} · {participantCount}

); }