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>
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>
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.
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>
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>
- 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>
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>
Adds conversation_games table type + game_make_move RPC to db-types, GamePayload variant and parseMessagePayload branch to shared/attachments, games.ts with createGame/getGame/makeGameMove wrappers plus pure tttWinningLine/c4WinningCells/c4DropRow/isBoardFull helpers, 11 tests covering all winner helpers, and re-export from chat/index.ts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mount useSoundboardSync in SoundboardManagerDialog, thread badges map through
SoundboardCategoryGroup/SoundboardRow, render a cloud-state glyph badge inline
with each row's size/mime metadata, and wrap handleDelete to attempt a
best-effort remote delete via deleteRemoteSound before the local deleteSound call.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a whiteboard branch to the MessageBubble ternary chain that renders
a card with an icon, label, and Öffnen button dispatching the
chatapp:open-whiteboard CustomEvent. ConversationPage now listens for
that event via a useEffect and calls setOpenWhiteboardId to open the
modal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds WhiteboardPayload wire-format variant to MessagePayload/ParsedMessagePayload,
creates the whiteboards.ts CRUD wrapper (createWhiteboard, listWhiteboardStrokes,
insertWhiteboardStroke, clearWhiteboardStrokes), writes 5 unit tests (38/38 pass),
re-exports from chat/index.ts, and registers conversation_whiteboards +
whiteboard_strokes in packages/db-types so typecheck passes cleanly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Hover an image attachment in the composer to reveal a pencil-edit button;
clicking it opens ImageAnnotator and Save replaces the File in attachments[].
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Shrink the StripToggleIcon SVG from 18x18 to 14x14 px so the 2-people
icon sits centred with ~11 px of breathing room inside its 36x36 button,
matching the h-4/w-4 small-icon convention used elsewhere in InCallPanel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>