# 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 `<EmptyState>` 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 ? (
  <EmptyState
    title={t('app:chats.search_empty_title', { defaultValue: 'Keine Treffer' })}
    description={t('app:chats.search_empty_desc', {
      defaultValue: 'Keine Chats passen zu "' + query.trim() + '".',
    })}
  />
) : (
  /* 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<void> {
  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
<button
  type="button"
  onClick={() => {
    void setConversationMentionsOnly(supabase, {
      conversationId: conv.id,
      mentionsOnly: !conv.mentionsOnly,
    }).catch((err) => console.warn('mentions-only toggle failed', err));
    onClose();
  }}
  className="..."  // copy from the existing mute-entry className
>
  <span>{conv.mentionsOnly ? '✓ ' : ''}Nur bei @Mentions benachrichtigen</span>
</button>
```

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 '@<dein-name>' → notify. (3) Edit a sent message to add @someone → that someone gets a notification. (4) Win a TTT or C4 game → confetti."
