diff --git a/apps/desktop/src/components/CallPreviewPanel.tsx b/apps/desktop/src/components/CallPreviewPanel.tsx new file mode 100644 index 0000000..042ef34 --- /dev/null +++ b/apps/desktop/src/components/CallPreviewPanel.tsx @@ -0,0 +1,118 @@ +import type { ConversationSummary } from '@chat-app/shared/chat'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useAuth } from '../context/AuthContext'; +import { useCall } from '../context/CallContext'; +import { useCallPresence } from '../lib/useCallPresence'; +import { Avatar } from './Avatar'; +import { PhoneIcon, SpinnerIcon, XIcon } from './icons'; + +interface Props { + conversation: ConversationSummary; +} + +const MAX_TILES = 7; + +/** + * Discord-DM-style call preview panel. Renders only while peers are in the + * conversation's active call and the local user is NOT in it. Provides large + * avatar tiles plus a single "Beitreten" call-to-action. Calls are still + * STARTED via the topbar phone icon (`ConversationHeader.startCall`); this + * component never initiates — only joins. + */ +export function CallPreviewPanel({ conversation }: Props) { + const { t } = useTranslation(['app']); + const { session } = useAuth(); + const { state, joinActiveCall } = useCall(); + const presentIds = useCallPresence(conversation.id); + const [collapsed, setCollapsed] = useState(false); + + const myId = session?.user.id ?? null; + const iAmIn = + (state.kind === 'connected' || + state.kind === 'connecting' || + state.kind === 'reconnecting') && + state.conversationId === conversation.id; + + const others = presentIds.filter((u) => u !== myId); + if (iAmIn || others.length === 0) return null; + + const visibleTiles = others.slice(0, MAX_TILES); + const overflow = Math.max(0, others.length - MAX_TILES); + const busy = state.kind !== 'idle'; + + const handleJoin = () => { + if (busy) return; + void joinActiveCall(conversation.id, 'audio'); + }; + + return ( +