Commit Graph

363 Commits

Author SHA1 Message Date
byGalax 3d959aaadf fix(chat): auto-rotate stuck conv-keys on chat open (receive-side recovery) 2026-05-18 14:03:35 +02:00
byGalax 787437c3f1 chore(desktop): release v0.21.0 v0.21.0 2026-05-17 20:04:00 +02:00
byGalax be5647281d chore(mobile): track expo-generated .gitignore 2026-05-17 20:00:18 +02:00
byGalax fc8fc275cb fix(soundboard): mount-once hotkey registration to avoid call-state churn 2026-05-17 17:49:08 +02:00
byGalax e19a71e892 feat(profile): preserve animation on GIF/APNG/animated-WebP avatar uploads
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 17:38:07 +02:00
byGalax 11869be443 feat(soundboard): hotkeys fire outside calls with local-only playback
Lifts the `state.kind === 'connected'` guard from the soundboard hotkey
useEffect so OS-level shortcuts are always registered. Inside a call the
existing `playSoundboard` path routes audio into the LiveKit pipeline so
peers hear; outside a call the new `playSoundboardLocal` helper fetches
the blob via `getSoundBlob`, creates a short-lived object URL, and plays
through a fresh HTMLAudioElement on the system default output.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 17:33:50 +02:00
byGalax 3fa8b6dbc1 feat(call): shared annotation overlay on screen share 2026-05-17 17:30:05 +02:00
byGalax f9f8e3fdb8 feat(whiteboard): live cursors via broadcast channel 2026-05-17 17:24:34 +02:00
byGalax 4ed3f04300 fix(view-once): hold-to-view pattern + content-protection during reveal 2026-05-17 17:21:22 +02:00
byGalax a7ffcbff83 feat(voice): playback-speed toggle (1x/1.5x/2x) with per-user default 2026-05-17 17:09:01 +02:00
byGalax 82600915f1 feat(composer): persist text + reply target per chat across restarts 2026-05-17 17:06:43 +02:00
byGalax 93098a74ca docs(phase8): feature batch implementation plan 2026-05-17 16:54:56 +02:00
byGalax c8f0e8efd5 chore(desktop): release v0.20.1 v0.20.1 2026-05-17 15:11:11 +02:00
byGalax fd9b8a88d6 docs(chat-switch): implementation plan for chat-switch flicker fix 2026-05-17 15:09:43 +02:00
byGalax ab2f7130fe fix(chat-switch): seed lastPendingCountRef from outbox to suppress mount scroll 2026-05-17 15:02:40 +02:00
byGalax b9a3dde1aa refactor(chat-switch): drop redundant id-change reset effect 2026-05-17 14:54:31 +02:00
byGalax 65d2446804 fix(chat-switch): remount ConversationPage per conversation id 2026-05-17 14:51:22 +02:00
byGalax faa12a4ebb feat(chat-switch): hydrate useConversationMessages from in-memory cache
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 14:48:48 +02:00
byGalax 37dd1b4f23 feat(chat-switch): in-memory message cache helper 2026-05-17 14:39:01 +02:00
byGalax d803773261 chore(desktop): release v0.20.0 v0.20.0 2026-05-17 13:21:50 +02:00
byGalax 49855c5d3f fix(console-noise): pre-warm via auth.getSession + demote stuck crypto-migration logs to debug 2026-05-17 01:43:53 +02:00
byGalax c9a64bf898 feat(call): remove live-captions feature (privacy-inconsistent with E2E, unused) 2026-05-17 01:39:42 +02:00
byGalax 7f704e80f6 feat(P7.T4): per-attachment view-once toggle on AttachmentPreview phase7-done pre-phase8 2026-05-17 01:25:08 +02:00
byGalax 940432d287 feat(P7.T3): composer toolbar — 5 inline buttons + popover for Bild/Poll/Aktivitäten 2026-05-17 01:18:32 +02:00
byGalax 28b6d64936 feat(P7.T2): ComposerActionsMenu popover (Bild/Datei + Umfrage + Aktivitäten group)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 01:14:34 +02:00
byGalax 92baa626d6 docs(P7): Phase 7 composer redesign plan (Hybrid: + menu + per-attachment view-once) pre-phase7-composer 2026-05-17 01:11:04 +02:00
byGalax cd7ef8dccc fix(P6): AttachmentImage Lightbox blob URL — split unmount-only revoke, lock deps to handle.id 2026-05-17 01:09:09 +02:00
byGalax 58efc66ca7 fix(P4A): guard ImageAnnotator load callbacks with cancelled flag (React strict double-mount) 2026-05-17 01:03:37 +02:00
byGalax 1e139fb86e fix(P4A): ImageAnnotator effect-race — stabilize onCancel via ref so URL.revoke doesn't fire mid-decode 2026-05-17 01:01:27 +02:00
byGalax eeb713f03d fix(P6): convert positional bindings to named-params object for better-sqlite3 2026-05-17 00:59:39 +02:00
byGalax 837b5a326e fix(P6): hoist savedPositionRef above initialTopMostIndex (TDZ) + auto-open DevTools in dev 2026-05-17 00:56:34 +02:00
byGalax b1f37752d6 perf(P6C.T12): optimistic UI for mute / mentions-only / archive / pin / device-revoke
Audit of write-actions revealed that send (and edit via realtime UPDATE) are
already optimistic via local state insertion in `useConversationMessages`,
and friend nicknames are pure-local localStorage. Five user-write actions
were waiting on the ~100-200 ms server roundtrip + realtime echo:

* Toggle mute (`setConversationMutedUntil`)
* Toggle mentions-only (`setConversationMentionsOnly`)
* Toggle archive (`setConversationArchived`)
* Pin / unpin message (`pinMessage` / `unpinMessage`)
* Revoke device (`revokeDevice` RPC)

All five now flip local state synchronously and roll back on failure. The
existing realtime subscriptions reconcile canonically (no-op when the
optimistic patch already matches the server row), so this is purely a UX
latency improvement — no protocol or persistence changes.

Reactions (`toggleReaction` / `voteExclusive`) were intentionally skipped
this round: rollback semantics for the exclusive-vote path with multiple
sequential awaits are messy enough to warrant a dedicated pass.
phase6c-done
2026-05-17 00:44:28 +02:00
byGalax 854c4b91a8 perf(P6C.T11): bundle audit + opt-in visualizer for future audits
Audited the renderer bundle with rollup-plugin-visualizer. Top offenders
(libsodium-sumo 292KB gz, livekit-client 177KB gz, @supabase 149KB gz)
all have justified usage and no viable swap. Mediapipe + track-processors
are already lazy-loaded into a separate chunk on first BackgroundBlur
activation. Bundle has zero duplicate packages, no moment/lodash/dayjs,
no syntax-highlight libs, no polyfills — already lean from T1+T6.

Added rollup-plugin-visualizer as a dev-dep, gated behind ANALYZE=true
so production builds pay no cost. Run with:
  ANALYZE=true pnpm --filter @chat-app/desktop build
to regenerate stats.html (treemap) + stats.json (raw) for future audits.
2026-05-17 00:35:41 +02:00
byGalax d3b708636f perf(P6B.T8): virtualize message list with react-virtuoso
Switches the ConversationPage chat list from a full O(N) render to
windowed rendering via react-virtuoso. On long histories only the
visible rows (plus a 400px overscan buffer) live in the DOM, ending the
scroll jank and layout thrashing that hit conversations with >500
messages.

Preserved behaviors:
- Newest message visible on open via initialTopMostItemIndex.
- Auto-scroll on send via a pending-count-based effect (the old
  setStickToBottom + useLayoutEffect pattern doesn't apply now that
  Virtuoso owns the scroll element).
- Realtime auto-follow only when scrolled to bottom (followOutput).
- 'New messages while away' counter + 'jump to newest' pill via
  atBottomStateChange.
- Pinned-message / reply / search jumps via virtuosoRef.scrollToIndex;
  expands displayCount on the fly if the target is outside the
  rendered slice. Flash highlight unchanged.
- Load-older infinite scroll via Virtuoso startReached (replaces the
  IntersectionObserver-on-sentinel pattern).
- Per-conversation position memory now keys on row index instead of
  pixel scrollTop (the latter isn't meaningful under virtualization).

Also wires the PinnedMessagesPanel onJump callback (previously a TODO
that just closed the panel) into jumpToMessage, since virtualization
made the smooth-scroll-from-pinned UX easy to deliver as a side
benefit.
phase6b-done
2026-05-17 00:20:41 +02:00
byGalax c449943b52 perf(P6B.T7): WebP thumbnails for image attachments (320px max, thumb-first render) 2026-05-17 00:12:03 +02:00
byGalax db59e3f658 perf(P6B.T6): offload Argon2 + userKey unseal to Web Worker
PIN-unlock used to freeze the renderer for ~1-2 s on mid-hardware while
the moderate-preset Argon2id KDF + sealed-key secretbox open ran on the
main thread. Push that work into a Vite-bundled ESM Web Worker so the
unlock screen stays responsive.

The worker (apps/desktop/src/workers/crypto.worker.ts) bundles its own
libsodium-wrappers-sumo instance and registers a fresh CryptoBackend
inside the worker realm. Client wrapper (apps/desktop/src/lib/cryptoWorker.ts)
spawns a one-shot worker per unlock — workers are cheap, PIN-unlock is
once-per-session, and one-shot avoids the request-id bookkeeping that the
existing decrypt.worker needs for high-volume per-message decrypts.

Falls back to inline main-thread openUserKey when the Worker constructor
is unavailable (vitest's jsdom) or when worker spawn / round-trip fails
(strict CSP). All 14 desktop + 71 shared tests still pass — the existing
loadOrUnlockUserKey test exercises the inline-fallback branch.

Private key bytes are transferred (zero-copy) back to the main thread,
detaching the worker-side ArrayBuffer view on transfer.

Build emits crypto.worker-<hash>.js (~2.5 MB, mostly libsodium WASM glue
duplicated from the main bundle). Acceptable trade-off for the unblocked
UI; a future change could lazy-load libsodium on the main thread to drop
the duplication.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 00:05:00 +02:00
byGalax 6c6828006b feat(P6B.T9): PIN-Idle-Auto-Lock setting + idle watcher
Adds opt-in (default OFF) auto-lock: after X minutes of no user input
the app calls signOut() (full memory wipe + PIN re-entry on next open).
Settings dropdown (Aus / 5 / 15 / 30 / 60 min) lives in SecurityCenter
below the existing wipe-on-close toggle. The idle timer is mounted in
AppShell via useIdleAutoLock; activity events are throttled to 1 Hz to
avoid timer thrash on rapid mouse movement. The localStorage key is
added to PRESERVE_LOCAL_STORAGE so a wipe never silently disables the
feature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 23:59:27 +02:00
byGalax 21376daf39 perf(P6B.T10): i18next tree-shake — prune dead locale keys
Audited all four i18next namespaces (common, auth, errors, app) against
static t() call-site grep across the entire desktop source tree.
Pruned 40 dead keys per locale, 80 total lines removed across 6 files.

Dead keys removed:
- app: call.{e2ee_active_hint,still_live,voice_connected},
       chats.{show_archived,show_active}, admin.nav,
       friends.confirm_unfriend, settings.{danger_zone,this_device,
       presence,section_ringtone}
- auth: signed_in.{title,session_active,user_id,admin,yes,no,sign_out,
        device_active,device_platform,device_registered_at},
        entire device.* section (old device-registration UI)
- common: loading, cancel, retry, online, offline, idle, dnd, invisible

Dynamic-key patterns were found and respected: t('errors:'+code)
keeps all errors keys; t('app:presence.'+val) keeps all presence keys;
t('app:annotator.tool.'+id) uses defaultValue so its locale entries
were not required.
2026-05-16 23:55:04 +02:00
byGalax 6d0e4fb1f0 perf(P6A.T5): pre-warm Supabase + avatar loading hints
Fire a no-await profiles query in AuthContext on session establish to absorb
cold-connection latency before the first user-triggered request. Add
loading='lazy' default to the central Avatar component so all off-screen
avatars (chat list, friends list, popovers, message senders) skip eager
Supabase Storage fetches; set loading='eager' on ConversationHeader (active
conv header) and CallParticipantTile inline imgs (both AudioContent and
VideoStub) which are always above-the-fold when visible.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
phase6a-done
2026-05-16 23:43:04 +02:00
byGalax 36ab7eca8a perf(P6A.T4): wrap all icon components in React.memo
All 57 exported SVG icon components in icons.tsx are now memoised via
React.memo, giving React permission to skip re-renders when props are
referentially equal.  Consumer icon-prop types updated from the legacy
SVGProps (includes string refs) to ComponentPropsWithoutRef<'svg'> so
the MemoExoticComponent return type satisfies TypeScript without casts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 23:39:47 +02:00
byGalax ddd696a790 perf(P6A.T3): memoize MessageBubble + stabilize parent callbacks 2026-05-16 23:30:50 +02:00
byGalax 53b3b5e1fc perf(P6A.T2): respect prefers-reduced-motion globally + gate confetti
- Update globals.css reduced-motion block: 0.01ms → 0.001ms durations
  and add scroll-behavior: auto to suppress all transitions/animations
  when OS reduced-motion preference is active.
- Gate canvas-confetti burst in GameModal behind matchMedia check so
  the particle effect is skipped entirely for users who opt out of motion.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 23:25:01 +02:00
byGalax a4573b315d perf(P6A.T1): lazy-load ImageAnnotator/Whiteboard/WatchTogether/GameModal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 23:22:52 +02:00
byGalax c271e95100 docs(P6): Phase 6 performance pack plan (Groups A/B/C + deferred channel pooling) 2026-05-16 23:16:42 +02:00
byGalax 888ed1b217 feat(P5C.T4): confetti burst on game win
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
pre-phase6-perf
2026-05-16 22:26:25 +02:00
byGalax a974e5b8ab feat(P5C.T3): recompute message_mentions on edit
After the ciphertext UPDATE succeeds in editEncryptedMessage, delete the
existing message_mentions rows for that message then re-resolve and
re-insert from the new plaintext — matching the best-effort pattern used
by insertMessage. No signature change needed; conversationId and
newPlaintext were already present in EditMessageParams.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-16 22:24:17 +02:00
byGalax 7ebc5f6c9d feat(P5C.T2): per-conv 'mentions only' toggle + notification gate 2026-05-16 22:22:04 +02:00
byGalax b8b451ef4f feat(P5C.T2-sql): conversation_members.mentions_only column 2026-05-16 22:17:04 +02:00
byGalax d7c0c3d0a2 feat(P5C.T1): empty-state for empty chat-list search results 2026-05-16 22:14:33 +02:00
byGalax 70ce824120 docs(P5C): Phase 5C polish plan (4 spec sub-items) 2026-05-16 22:12:27 +02:00