diff --git a/docs/superpowers/plans/2026-05-15-call-preview-panel.md b/docs/superpowers/plans/2026-05-15-call-preview-panel.md new file mode 100644 index 0000000..68624ba --- /dev/null +++ b/docs/superpowers/plans/2026-05-15-call-preview-panel.md @@ -0,0 +1,300 @@ +# Call Preview Panel Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the always-on "Sprach-Channel" banner with a Discord-DM-style preview panel that only renders when peers are in an active call, showing them as large avatar tiles with a single "Beitreten" button. + +**Architecture:** `VoiceChannelRail.tsx` is renamed to `CallPreviewPanel.tsx` and rewritten with a new tile-grid layout. Visibility rule drops the `|| isGroup` clause so groups behave identically to 1:1 chats. All call-session wiring (`useCall`, `useCallPresence`, `joinActiveCall`, `ConversationHeader`'s phone icon for starting calls) stays unchanged. + +**Tech Stack:** TypeScript, React 18, Tailwind, react-i18next, existing `useCallPresence` realtime hook. + +**Spec:** `docs/superpowers/specs/2026-05-15-call-preview-panel-design.md` + +--- + +## File Overview + +**New files:** +- `apps/desktop/src/components/CallPreviewPanel.tsx` — the new panel. + +**Deleted files:** +- `apps/desktop/src/components/VoiceChannelRail.tsx` — replaced by `CallPreviewPanel.tsx`. + +**Modified files:** +- `apps/desktop/src/pages/ConversationPage.tsx` — change the import + JSX tag. + +**i18n note:** No JSON resource changes needed. The orphaned keys (`voice_empty`, `voice_open`, `voice_channel`, `voice_count`) only existed as inline `defaultValue:` strings inside the deleted component — they were never in the locale files. The new component reuses the existing `app:call.active_in_conv` and `app:call.join` keys (present in both `de/app.json` and `en/app.json`). + +--- + +## Task 1: Create `CallPreviewPanel.tsx` + +**Files:** +- Create: `apps/desktop/src/components/CallPreviewPanel.tsx` + +- [ ] **Step 1: Create the file** + +`apps/desktop/src/components/CallPreviewPanel.tsx`: + +```tsx +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 +
+ )} +
+ + +
+ )} +
+ ); +} +``` + +- [ ] **Step 2: Verify icon imports resolve** + +``` +cd D:/Programmieren/ChatApp-Electron/chat-app +grep -n "export const PhoneIcon\|export function PhoneIcon\|export const SpinnerIcon\|export function SpinnerIcon\|export const XIcon\|export function XIcon" apps/desktop/src/components/icons.tsx +``` + +Expected: all three icons exported. (Earlier tasks confirmed `PhoneIcon`, `SpinnerIcon`; `XIcon` is also used by other modals — verify.) If `XIcon` is missing under that name, swap the import to whatever the close-icon export is in `icons.tsx` (e.g. `CloseIcon`) and report the substitution. + +- [ ] **Step 3: Typecheck** + +``` +pnpm --filter @chat-app/desktop typecheck 2>&1 | tail -10 +``` + +Expected: zero errors. (After the encryption-UX rollout the desktop typecheck is clean.) + +- [ ] **Step 4: Commit** + +```bash +cd D:/Programmieren/ChatApp-Electron/chat-app +git add apps/desktop/src/components/CallPreviewPanel.tsx +git commit -m "feat(desktop): add CallPreviewPanel — Discord-DM-style join surface" +``` + +--- + +## Task 2: Wire the new panel in `ConversationPage.tsx` and delete the old rail + +**Files:** +- Modify: `apps/desktop/src/pages/ConversationPage.tsx` +- Delete: `apps/desktop/src/components/VoiceChannelRail.tsx` + +- [ ] **Step 1: Swap the import** + +In `apps/desktop/src/pages/ConversationPage.tsx`, replace this line (around line 26): + +```ts +import { VoiceChannelRail } from '../components/VoiceChannelRail'; +``` + +with: + +```ts +import { CallPreviewPanel } from '../components/CallPreviewPanel'; +``` + +- [ ] **Step 2: Swap the JSX usage** + +In the same file, replace the JSX line (around line 657): + +```tsx +{conversation && !incomingHere && } +``` + +with: + +```tsx +{conversation && !incomingHere && } +``` + +- [ ] **Step 3: Delete the dead component** + +``` +rm apps/desktop/src/components/VoiceChannelRail.tsx +``` + +- [ ] **Step 4: Sweep for stragglers** + +``` +grep -rn "VoiceChannelRail" apps/desktop/src +``` + +Expected: no hits. If anything remains, clean it up. + +- [ ] **Step 5: Typecheck** + +``` +pnpm --filter @chat-app/desktop typecheck 2>&1 | tail -10 +``` + +Expected: zero errors. + +- [ ] **Step 6: Build the desktop renderer once to surface any tailwind/runtime issues** + +``` +pnpm --filter @chat-app/desktop build 2>&1 | tail -15 +``` + +Expected: build succeeds. Tailwind classes used here (`bg-surface-3/70`, `border-line`, `bg-emerald-600`, `bg-surface-2`, `text-fg`, `text-fg-muted`, `aspect-square`, `line-clamp-1`, `grid-cols-2 md:grid-cols-3`) are all already used elsewhere in the desktop app and should be in the existing tailwind config. + +- [ ] **Step 7: Commit** + +```bash +cd D:/Programmieren/ChatApp-Electron/chat-app +git add apps/desktop/src/pages/ConversationPage.tsx +git rm apps/desktop/src/components/VoiceChannelRail.tsx +git commit -m "refactor(desktop): replace VoiceChannelRail with CallPreviewPanel + +Drops the always-on 'Sprach-Channel' banner. The preview panel renders +only when peers are in the active call (1:1 and group identical). +Calls are still started via the topbar phone icon." +``` + +--- + +## Task 3: Manual smoke pass (USER) + +This task is for the human operator after the previous two land. Run a fresh dev build and walk through the spec's smoke checklist: + +- [ ] Open a 1:1 chat with no active call → panel not rendered. +- [ ] Open a group chat with no active call → panel not rendered (regression test for the bug). +- [ ] Have a peer start a call → panel appears, peer's avatar tile + "Beitreten" visible. +- [ ] Click "Beitreten" → joins the call; panel disappears (you're now `iAmIn`). +- [ ] Hang up → panel reappears with peer still in. +- [ ] Peer hangs up too → panel disappears. +- [ ] Resize the window narrow → grid collapses to 2 columns. +- [ ] Group call with 9 participants → 7 avatar tiles + one `+2` overflow tile. +- [ ] Click the `✕` in the panel header → tiles collapse, header stays. Click again → tiles re-expand. + +If any step fails, capture the exact behavior and reopen the relevant task. + +--- + +## Self-Review + +**1. Spec coverage:** +- Visibility rule (`!iAmIn && others.length > 0`, no `|| isGroup`) — Task 1. +- Discord-DM tile layout, ~120×120px tiles, 2/3-column responsive grid, `+N` overflow at 8 — Task 1. +- "Aktiver Anruf · n im Channel" header reusing `app:call.active_in_conv` — Task 1. +- Centered "Beitreten" CTA, disabled when busy, `SpinnerIcon` while connecting — Task 1. +- Optional collapse via `useState`, default expanded — Task 1. +- Mount point unchanged (`!incomingHere &&` gate retained) — Task 2. +- Old empty state and `Channel öffnen` removed by deleting the rail — Task 2. +- Manual smoke list — Task 3. + +**2. Placeholder scan:** Clean. Every code block is complete; every command has expected output; the only `defaultValue:` strings are in the i18n calls (not placeholders, just fallback copy). + +**3. Type consistency:** `Props { conversation: ConversationSummary }` matches the type used by the deleted rail and consumed by `ConversationPage.tsx`. `useCallPresence` returns `string[]` per `apps/desktop/src/lib/useCallPresence.ts`. `useCall().state.kind` values (`idle`, `connecting`, `connected`, `reconnecting`) match the existing `CallContext` discriminated union. `joinActiveCall(convId, 'audio')` matches the existing `useCall` API. + +No gaps; plan is complete.