docs(P5C): Phase 5C polish plan (4 spec sub-items)
This commit is contained in:
@@ -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 `<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."
|
||||
Reference in New Issue
Block a user