From b55ccf899fba5a1c5cec51e6e5311c7d4d951b7d Mon Sep 17 00:00:00 2001 From: byGalax Date: Fri, 15 May 2026 23:56:02 +0200 Subject: [PATCH] =?UTF-8?q?feat(desktop):=20add=20CallPreviewPanel=20?= =?UTF-8?q?=E2=80=94=20Discord-DM-style=20join=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/CallPreviewPanel.tsx | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 apps/desktop/src/components/CallPreviewPanel.tsx 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 ( +
+
+ + + {t('app:call.active_in_conv', { + defaultValue: 'Laufender Anruf · {{count}} im Raum', + count: others.length, + })} + + +
+ + {!collapsed && ( +
+
+ {visibleTiles.map((id) => { + const member = conversation.members.find((m) => m.userId === id); + const name = member?.profile?.displayName ?? '?'; + return ( +
+
+ +
+ {name} +
+ ); + })} + {overflow > 0 && ( +
+ +{overflow} + weitere +
+ )} +
+ + +
+ )} +
+ ); +}