Files
ChatApp/docs/superpowers/plans/2026-05-16-phase5c-polish.md
T

12 KiB

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:

{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
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:

-- 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;
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):

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:

<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:

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:

if (!isAppFocused() && !isMuted(conv)) {
  osNotify(...);
}

Extend it (the cleanest path — assumes useMentionNotifications independently fires for every mention, which the recon confirmed):

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
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:

// 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
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:

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
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."