# Phase 5C — Spec-Polish (4 leftover sub-items) > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development. Steps use checkbox (`- [ ]`) syntax. **Goal:** Close the 4 documented but never-built sub-items from the fifteen-features spec: 1. Empty-state for empty search results in chat list (Phase 1 spec line 52). 2. Per-conversation "Nur bei @Mentions benachrichtigen" toggle (Phase 2 spec line 72). 3. Mentions-on-edit recompute (Phase 2 spec line 141). 4. Confetti animation on game-win (Phase 5 spec line 133). **Architecture:** - (1) Drops the existing `EmptyState` primitive into the `ChatsPage` search results when the filter produces zero rows. - (2) Adds a `mentions_only boolean` column to `conversation_members` + a toggle in `ConversationRowMenu` next to the mute submenu. The notification gate suppresses non-mention notifications when set. `useMentionNotifications` is untouched — mentions fire regardless. - (3) Extends `editEncryptedMessage()` in `packages/shared/src/chat/messages.ts` to re-run `parseMentionUsernames` + `insertMentions` after the text changes. Old mention rows are deleted first. - (4) Adds `canvas-confetti` dep, fires it in `GameModal` when `winnerIdx === myPlayerIdx`. **Tech Stack:** No new infra. Adds one runtime dep (`canvas-confetti` + types). **Non-goals:** - Cron-based items (7-day view-once purge, 12h watch-together auto-end, 24h game auto-draw) — server-side per spec, out of scope. - New in-conversation message search (only chat-list search empty state). --- ## Pre-flight - [ ] **Verify clean working tree on `main`** Run: `cd "D:\Programmieren\ChatApp-Electron\chat-app" && git status` Expected: clean. - [ ] **Confirm tooling is green** Run: `pnpm --filter @chat-app/shared typecheck && pnpm --filter @chat-app/desktop typecheck && pnpm --filter @chat-app/shared test -- --run` Expected: all green; 71 shared tests pass. --- ## Task 1: Empty-state for empty search results **Files:** - Modify: `apps/desktop/src/pages/ChatsPage.tsx` - [ ] **Step 1: Locate the search-filter render** ``` Read apps/desktop/src/pages/ChatsPage.tsx (offset 1, limit 80) ``` Find: - Search input + `query` state (~lines 29-48). - `queryFiltered` (or similarly named) memo. - The render loop over the filtered list. - The existing `` import — add `import { EmptyState } from '../components/EmptyState';` if missing. - [ ] **Step 2: Render an empty-state when search yields zero results** Wrap the rendered list with a length check: ```tsx {query.trim().length > 0 && filteredList.length === 0 ? ( ) : ( /* existing list render */ )} ``` If `EmptyState`'s prop shape differs (`icon`/`action` required), match the existing chat-list call site (P1.T6). Grep first: `Grep -n "EmptyState" apps/desktop/src/pages/ChatsPage.tsx apps/desktop/src/components/EmptyState.tsx`. Substitute the real variable names from the file (`query` vs `searchText`, `filteredList` vs `queryFiltered`). - [ ] **Step 3: Typecheck** ``` pnpm --filter @chat-app/desktop typecheck ``` Expected: PASS. - [ ] **Step 4: Commit** ```bash cd "D:\Programmieren\ChatApp-Electron\chat-app" git add apps/desktop/src/pages/ChatsPage.tsx git commit -m "feat(P5C.T1): empty-state for empty chat-list search results" ``` --- ## Task 2: Per-conv "Nur bei @Mentions benachrichtigen" toggle **Files:** - Create: `supabase/migrations/20260516000010_mentions_only.sql` - Modify: `packages/db-types/src/index.ts` - Modify: `packages/shared/src/chat/conversations.ts` (or wherever conversation_members helpers live) - Modify: `apps/desktop/src/components/ConversationRowMenu.tsx` - Modify: the notification gate (likely `apps/desktop/src/lib/osNotify.ts` callers, e.g. `useConversationMessages.ts`) - [ ] **Step 1: SQL migration + prod push** Create `supabase/migrations/20260516000010_mentions_only.sql`: ```sql -- Phase 5C: per-conversation "notify only on @mentions" toggle. -- Lives alongside the existing muted_until column on conversation_members. -- When true: the renderer's incoming-message notification gate suppresses -- the alert unless the message contains an @-mention of the local user. -- Mentions always fire regardless (override-by-design). alter table public.conversation_members add column if not exists mentions_only boolean not null default false; ``` ```bash cd "D:\Programmieren\ChatApp-Electron\chat-app" git add supabase/migrations/20260516000010_mentions_only.sql git commit -m "feat(P5C.T2-sql): conversation_members.mentions_only column" bash scripts/prod/push-migrations.sh mentions_only ``` - [ ] **Step 2: db-types extension** In `packages/db-types/src/index.ts`, find the `conversation_members` entry. Add `mentions_only: boolean` to `Row`, `mentions_only?: boolean` to `Insert`, and `mentions_only?: boolean` to `Update`. - [ ] **Step 3: Shared wrapper for the toggle** Find where `conversation_members` mutation helpers live (likely a `setConversationMuted` exists): ``` Grep -rn "conversation_members" packages/shared/src/ ``` Add to that same file (or the most-fitting chat helper): ```ts export async function setConversationMentionsOnly( client: AppSupabaseClient, params: { conversationId: string; mentionsOnly: boolean }, ): Promise { const { data: session } = await client.auth.getUser(); if (!session.user) throw new Error('not authenticated'); const { error } = await client .from('conversation_members') .update({ mentions_only: params.mentionsOnly }) .eq('conversation_id', params.conversationId) .eq('user_id', session.user.id); if (error) throw error; } ``` Add `AppSupabaseClient` import if missing. Also: if the conversation-read wrapper (`listConversations` or similar) projects `muted_until` into a camelCase `mutedUntil`, add `mentionsOnly` alongside it in the same mapper. - [ ] **Step 4: Render the toggle in `ConversationRowMenu.tsx`** ``` Read apps/desktop/src/components/ConversationRowMenu.tsx ``` Find the mute entry (~lines 31-42). Add a sibling menu item below it: ```tsx ``` If the `conv` prop type doesn't yet expose `mentionsOnly`, extend the type in the source (the `Conversation` interface in shared) and the mapper in the read wrapper from Step 3. Add imports: ```ts import { setConversationMentionsOnly } from '@chat-app/shared/chat'; import { supabase } from '../lib/supabase'; ``` (Adapt the `@chat-app/shared/chat` path if Step 3's helper lives in a different sub-path.) - [ ] **Step 5: Suppress non-mention notifications when `mentions_only` is true** ``` Grep -rn "useMentionNotifications\|osNotify" apps/desktop/src/ ``` Find the message-incoming notification path. It's the place that calls `osNotify(...)` on inbound non-self messages. The current shape likely: ```ts if (!isAppFocused() && !isMuted(conv)) { osNotify(...); } ``` Extend it (the cleanest path — assumes `useMentionNotifications` independently fires for every mention, which the recon confirmed): ```ts if (!isAppFocused() && !isMuted(conv)) { if (conv.mentionsOnly) { // Non-mention messages are silenced here. The mention case is handled // by useMentionNotifications (which subscribes to message_mentions // INSERT independently) so we don't lose the @-alert. return; } osNotify(...); } ``` If `conv` isn't in scope at the gate (some hooks only have `conversationId`), look up the conv via the `ConversationsContext` cache. Pattern: `Grep -n "useConversations\|conversations.find\|conversationsById" apps/desktop/src/`. - [ ] **Step 6: Typecheck** ``` pnpm --filter @chat-app/shared typecheck && pnpm --filter @chat-app/desktop typecheck ``` Expected: PASS. - [ ] **Step 7: Commit** ```bash git add packages/db-types/src/index.ts packages/shared/src/chat/ apps/desktop/src/ # verify with git status that only intended files are staged before committing git commit -m "feat(P5C.T2): per-conv 'mentions only' toggle + notification gate" ``` --- ## Task 3: Mentions-on-edit recompute **Files:** - Modify: `packages/shared/src/chat/messages.ts` - [ ] **Step 1: Read the existing edit + send paths** ``` Read packages/shared/src/chat/messages.ts (offset 140, limit 100) ``` Find: - `insertMessage` (~line 145) — calls `parseMentionUsernames` + `insertMentions`. - `editEncryptedMessage` (~lines 208-229) — no mention re-extraction. - [ ] **Step 2: Extend `editEncryptedMessage`** After the existing `UPDATE` on `messages`, add: ```ts // Recompute mentions: edit can add/remove @-tokens. Drop old, insert new. await client.from('message_mentions').delete().eq('message_id', messageId); const mentionUsernames = parseMentionUsernames(plaintext); if (mentionUsernames.length > 0) { await insertMentions(client, { messageId, conversationId, usernames: mentionUsernames, }); } ``` Adapt to the exact arg shapes used by `insertMessage` (precedent). If `editEncryptedMessage`'s signature doesn't accept `plaintext` and `conversationId`, extend the signature and fix all callers. Grep first: `Grep -rn "editEncryptedMessage" apps/desktop/src/ packages/shared/src/`. - [ ] **Step 3: Typecheck + tests** ``` pnpm --filter @chat-app/shared typecheck && pnpm --filter @chat-app/desktop typecheck pnpm --filter @chat-app/shared test -- --run ``` Expected: all green. - [ ] **Step 4: Commit** ```bash git add packages/shared/src/chat/messages.ts apps/desktop/src/ git commit -m "feat(P5C.T3): recompute message_mentions on edit" ``` --- ## Task 4: Confetti on game win **Files:** - Modify: `apps/desktop/package.json` (deps) - Modify: `apps/desktop/src/components/GameModal.tsx` - [ ] **Step 1: Add the dep** ``` cd "D:\Programmieren\ChatApp-Electron\chat-app" pnpm --filter @chat-app/desktop add canvas-confetti pnpm --filter @chat-app/desktop add -D @types/canvas-confetti ``` - [ ] **Step 2: Fire confetti when the local player wins** In `apps/desktop/src/components/GameModal.tsx` (P5B.T5 commit `cd59ee3`), add an effect near the existing keyboard-Esc effect: ```ts import confetti from 'canvas-confetti'; // ... useEffect(() => { if (finished && winnerIdx !== null && winnerIdx === myPlayerIdx) { void confetti({ particleCount: 80, spread: 60, origin: { x: 0.2, y: 0.9 }, }); void confetti({ particleCount: 80, spread: 60, origin: { x: 0.8, y: 0.9 }, }); } }, [finished, winnerIdx, myPlayerIdx]); ``` - [ ] **Step 3: Typecheck** ``` pnpm --filter @chat-app/desktop typecheck ``` Expected: PASS. - [ ] **Step 4: Commit** ```bash git add apps/desktop/package.json apps/desktop/src/components/GameModal.tsx # include pnpm-lock.yaml if changed at the repo root git add pnpm-lock.yaml git commit -m "feat(P5C.T4): confetti burst on game win" ``` --- ## Final gate - [ ] **Step 1: Typecheck both packages** Run: `pnpm --filter @chat-app/shared typecheck && pnpm --filter @chat-app/desktop typecheck` Expected: both PASS. - [ ] **Step 2: Run shared tests** Run: `pnpm --filter @chat-app/shared test -- --run` Expected: PASS — 71 tests (same as baseline; no new tests added). - [ ] **Step 3: Verify no uncommitted changes** Run: `git status` Expected: clean. - [ ] **Step 4: Report** Report: "Phase 5C (Polish) code-complete on `main`; mentions_only migration applied to prod. Fifteen-features spec is now 100 % implemented. Smoke: (1) search 'xyz' in chat list → 'Keine Treffer' card. (2) Conv menu → 'Nur bei @Mentions' → DM with normal text → silent; DM with '@' → notify. (3) Edit a sent message to add @someone → that someone gets a notification. (4) Win a TTT or C4 game → confetti."