# 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