diff --git a/docs/superpowers/plans/2026-05-16-phase5c-polish.md b/docs/superpowers/plans/2026-05-16-phase5c-polish.md new file mode 100644 index 0000000..ccccaac --- /dev/null +++ b/docs/superpowers/plans/2026-05-16-phase5c-polish.md @@ -0,0 +1,361 @@ +# 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."