diff --git a/docs/superpowers/specs/2026-05-16-fifteen-features-design.md b/docs/superpowers/specs/2026-05-16-fifteen-features-design.md new file mode 100644 index 0000000..185c738 --- /dev/null +++ b/docs/superpowers/specs/2026-05-16-fifteen-features-design.md @@ -0,0 +1,199 @@ +# Fifteen-Features Initiative — Design + +**Date:** 2026-05-16 +**Scope:** Desktop only (Electron). Mobile is out of scope. +**Status:** Approved by user (sections 1–6). + +## Problem + +The desktop app has reached a stable point (encryption rolled out, Settings refactored). The user identified fifteen feature gaps to close as one coordinated initiative — quality fixes, messaging power, security UI, creative tools, and shared activities. Releasing each piecemeal would mean nine bumps and nine test cycles; instead this initiative ships as one `0.19.0` after a single end-to-end smoke pass. + +## Goals + +- Close all fifteen features without a release until the user signs off. +- Fix the global-shortcut capture bug that currently breaks every keystroke after a hotkey is set. +- No surprise releases — every commit is `git commit` only, no `pnpm release`. Version stays on `0.18.8` throughout, bumps to `0.19.0` once. +- Each phase ends with a clean typecheck + green shared tests. + +## Non-Goals + +- Mobile / React Native port of any of these features. +- Twitch in Watch-Together (YouTube only for the MVP). +- Chess or other mini-games beyond Tic-Tac-Toe + Vier-Gewinnt. +- Operational Transform / CRDT for the Whiteboard (snapshot model only). +- Live video preview or speaking indicators in the existing CallPreviewPanel. +- Hot-patch releases between phases. + +## Phasing + +| Phase | Theme | Features | Rough Effort | +|-------|-------|----------|--------------| +| 1 | Quality & Fixes | Hotkey-Bug Fix, Tray-Badge Audit, Empty-States, Friend-Nicknames, Memory-Wipe | 3–4 days | +| 2 | Messaging | Pinned Messages, Mentions-Notifications, GIF-Picker, View-Once Media | ~1 week | +| 3 | Security & Devices | Session/Device List + Revoke | 2–3 days | +| 4 | Creative Tools | Image-Annotation, Soundboard Cloud-Sync, Whiteboard (Snapshot-Sync) | ~1.5 weeks | +| 5 | Activities | Watch-Together (YouTube), Mini-Games (Tic-Tac-Toe + 4-in-a-Row) | ~1.5 weeks | + +Phases run sequentially. Within a phase, features are independent and can be implemented in any order. + +## Architecture (per feature) + +### Phase 1 — Quality & Fixes + +**Hotkey-Bug Fix.** The current code (`apps/desktop/src/lib/globalShortcut.ts`) registers every hotkey through Electron's `globalShortcut` API, which captures system-wide. Setting "M" as Mute means "M" can't be typed anywhere on the OS while the app is running. Fix: split into two registration modes. + +- **Window-scoped (default):** DOM `keydown` listener on `window`, only fires while the app has focus. Used for mute, deafen, hangup, screen-share, video toggle. +- **Global (opt-in):** Keep `globalShortcut.register()` for PTT and any hotkey the user explicitly toggles "Global" on. + +UI change: every hotkey row in Settings → Voice → Hotkeys gets a `🌐 Global`-toggle next to the key-capture button. Default off. + +**Tray-Badge Audit.** Tray code already exists (`electron/modules/tray.ts`, `lib/trayBadge.ts`, called from `ConversationsContext`). Verify the count is actually pushed and the badge renders on Windows. Likely a wiring or icon-loading bug; spec-time fix scope unknown until investigated. + +**Empty-States.** Four locations: empty chat list, empty friends list, empty conversation (just-created with no messages), empty search results. Each gets an illustration (SVG, light/dark variant), a heading, a description, and a primary action button (e.g., "Friend einladen", "Erste Nachricht schreiben"). Reuses existing icon palette. + +**Friend-Nicknames.** Local-only override stored in `localStorage` keyed by `userId`. Surfaces wherever a profile name is shown: chat header, message bubble sender, friends list, mention autocomplete, call participant tile. UI: right-click friend in friends list → "Spitzname setzen" → input modal. Empty = use real display name. + +**Memory-Wipe.** +- *Always on sign-out:* clear Stronghold (`chatapp.userpriv.*`), IndexedDB (soundboard, message-cache), SQLite cache, conv-key in-memory cache, `localStorage` except theme + locale + installId. +- *Configurable on app-close (Settings → Security → "Cache beim Schließen leeren"):* hooks `before-quit` in main process → IPC to renderer → wipe → quit. Next start requires PIN. + +### Phase 2 — Messaging + +**Pinned Messages.** +- DB: `pinned_messages (conversation_id uuid, message_id uuid, pinned_by uuid, pinned_at timestamptz, PK(conversation_id, message_id))`. CHECK via trigger: max 5 pins per conversation. +- RLS: any accepted conv-member can `SELECT` / `INSERT` / `DELETE`. +- UI: right-click message → "Anpinnen" / "Anheftung aufheben". Conv header shows compact "📌 N angepinnt"-pill; click opens a right-side panel with the pinned list; clicking an item scrolls to + flashes the original message. +- Realtime: subscribe `pinned_messages:` channel for live updates. + +**Mentions-Notifications.** +- `sendEncryptedMessage` parses plaintext for `@` tokens before encrypting. Username → user_id lookup against `conversation_members` joined with `profiles`. Bulk-insert into new `message_mentions (message_id, mentioned_user_id, PK both)`. +- RLS: mentioned user (and message author) can SELECT. +- Realtime: each client subscribes `message_mentions WHERE mentioned_user_id = me`. On insert → fire web/desktop notification with `[@] : ` and the configured mention sound. Bypasses per-conv mute. +- Per-conv setting "Nur bei @Mentions benachrichtigen" (stored alongside the existing mute setting). + +**GIF-Picker.** +- Provider: Tenor v2 (`https://tenor.googleapis.com/v2/search`, no per-user key — public key passable). Trending + Search + Recent (localStorage of last 24 URLs). +- New component `` Popover anchored to the composer's GIF button (next to attachment plus). +- On click: resolved GIF URL is fetched once, uploaded as a normal attachment (mime `image/gif`) so it lives in the same per-conv attachment storage and is end-to-end-encrypted like any other image. Trade-off: a sent GIF doesn't stay free — it counts as an attachment (~few MB). + +**View-Once Media.** +- Per-attachment flag `view_once boolean` (new column on `message_attachments`). +- Sender UI: attachment-picker toggle "👁 Einmal ansehen". +- Recipient UI: bubble shows blurred lock-overlay; tap → fullscreen view; close → bubble flips to "👁 Angesehen, von