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:
- Empty-state for empty search results in chat list (Phase 1 spec line 52).
- Per-conversation "Nur bei @Mentions benachrichtigen" toggle (Phase 2 spec line 72).
- Mentions-on-edit recompute (Phase 2 spec line 141).
- Confetti animation on game-win (Phase 5 spec line 133).
Architecture:
- (1) Drops the existing
EmptyStateprimitive into theChatsPagesearch results when the filter produces zero rows. - (2) Adds a
mentions_only booleancolumn toconversation_members+ a toggle inConversationRowMenunext to the mute submenu. The notification gate suppresses non-mention notifications when set.useMentionNotificationsis untouched — mentions fire regardless. - (3) Extends
editEncryptedMessage()inpackages/shared/src/chat/messages.tsto re-runparseMentionUsernames+insertMentionsafter the text changes. Old mention rows are deleted first. - (4) Adds
canvas-confettidep, fires it inGameModalwhenwinnerIdx === 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 +
querystate (~lines 29-48). -
queryFiltered(or similarly named) memo. -
The render loop over the filtered list.
-
The existing
<EmptyState>import — addimport { 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.tscallers, 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_onlyis 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) — callsparseMentionUsernames+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."