Commit Graph

377 Commits

Author SHA1 Message Date
byGalax ca77214e2d fix(desktop): voice hotkeys are window-scoped unless Global is toggled
The old code registered every enabled hotkey through Electron globalShortcut
API, which captures system-wide. Setting M as mute meant m could not be
typed in any other app. Now the OS-level registration only happens when
binding.global === true; otherwise the existing window-keydown listener
handles it.
2026-05-16 16:41:20 +02:00
byGalax 8c27e8eafa feat(mobile): legacyDeviceVault helper for per-device key probing 2026-05-16 16:41:18 +02:00
byGalax 69733168ed feat(desktop): add 'global' flag to voice hotkey binding (default false) 2026-05-16 16:38:51 +02:00
byGalax bf87add18d feat(mobile): extend cryptoBackend with pwhash + scalarMultBase 2026-05-16 16:37:09 +02:00
byGalax c871687fd2 refactor(desktop): derivePublicKey via CryptoBackend.scalarMultBase 2026-05-16 16:35:36 +02:00
byGalax 61a1eb37ea refactor(shared): route userKey through CryptoBackend (no libsodium-wrappers-sumo) 2026-05-16 16:33:50 +02:00
byGalax 9b9802ff6e docs(plan): phase 1 — quality & fixes (hotkey-bug, tray-audit, empty-states, nicknames, memory-wipe) 2026-05-16 16:33:38 +02:00
byGalax 2463081949 feat(shared): extend CryptoBackend with pwhash + scalarMultBase 2026-05-16 16:30:52 +02:00
byGalax 1eea80c529 chore(mobile): check:env script lints .env.local against .env.example 2026-05-16 16:27:21 +02:00
byGalax 830dac4cdd fix(mobile): defer crypto backend init into AppBootstrap (prevents white-screen) 2026-05-16 16:25:10 +02:00
byGalax cb46483f5d feat(mobile): AppBootstrap boundary — defers crypto init, catches global throws 2026-05-16 16:24:34 +02:00
byGalax 87fed820dc feat(mobile): BootSplash + BootError fallback views for AppBootstrap 2026-05-16 16:23:26 +02:00
byGalax fca5211008 docs(spec): fifteen-features initiative (0.19.0) — 5 phases, single end-of-batch release 2026-05-16 16:21:32 +02:00
byGalax c430a590fc fix(mobile): lazy env proxy so missing EXPO_PUBLIC vars throw inside React 2026-05-16 16:21:02 +02:00
byGalax 3b884b0415 docs(mobile): document EAS Secrets contract for EXPO_PUBLIC_* 2026-05-16 16:14:23 +02:00
byGalax affff0b433 docs(plan): mobile encryption port + Android white-screen RCA plans
Two implementation plans for the 2026-05-16 specs.

- Android white-screen: 12 tasks across 7 phases. Phase 0 wires EAS Secrets,
  Phase 1-2 ship the lazy env proxy + AppBootstrap boundary + global JS
  error handler, Phase 3-4 validate against a real APK, Phase 5 has
  conditional hypothesis-specific fixes, Phase 6-7 close out.

- Mobile encryption port: 24 tasks across 8 phases. Extends shared
  CryptoBackend with pwhash + scalarMultBase (the change that lets mobile
  stop loading libsodium-wrappers-sumo in Hermes), refactors desktop
  derivePublicKey through the same backend, mirrors the desktop
  userIdentity orchestrator and Auth flow on RN with new PinInput, setup,
  unlock, and security-settings screens, updates every device-keyed call
  site, and ends with a manual Android smoke list.

Each plan ships with a spec-coverage checklist and explicit out-of-scope
list. White-screen plan must land first; mobile-encryption plan depends
on AppBootstrap deferring crypto init.
2026-05-16 16:01:51 +02:00
byGalax a5c0889b0a docs: mobile encryption-UX port + Android white-screen RCA specs (2026-05-16)
Two specs from the 2026-05-16 brainstorming session:

- mobile encryption-UX port: bring apps/mobile to feature parity with
  desktop v0.18.x user-key/PIN identity. Includes the shared CryptoBackend
  extension (pwhash + scalarMultBase) that removes the libsodium-wrappers-sumo
  Hermes blocker.

- Android white-screen RCA: five ranked hypotheses, ordered diagnostic
  playbook (env-missing, newArch, module-eval crypto init, shared sodium
  side-effects, asset paths), plus defense-in-depth (lazy env proxy,
  AppBootstrap boundary, global JS error handler) that ships regardless
  of which hypothesis confirms.

User decisions resolved at the review gate:
- EAS Secrets for EXPO_PUBLIC_* (not eas.json env block).
- newArchEnabled: false acceptable as a temporary rollback if H2 confirms.
2026-05-16 15:46:40 +02:00
byGalax 90fa0a7e95 chore(desktop): release v0.18.8 v0.18.8 2026-05-16 15:35:24 +02:00
byGalax 767db72847 fix(desktop): drop bright border on Settings SubSection cards
The inner cards (Nachrichten-Ton / Klingelton / Audio-Gerät / Hotkeys /
…) looked like they had a stark white outline. The cause: --color-line
is a semi-transparent white token, and applying the `/60` alpha modifier
in `border-line/60` overrides the original alpha — so the inner border
ended up brighter than the outer Section's normal `border-line`.

Drop the border entirely. Background tint + caps heading is enough
grouping signal in a tab-pattern panel.
2026-05-16 15:33:36 +02:00
byGalax 0288d7a476 chore(desktop): release v0.18.7 v0.18.7 2026-05-16 15:30:14 +02:00
byGalax 34b972ec2a refactor(desktop): Settings = tab pattern instead of scroll + observer
The previous build used IntersectionObserver to highlight whichever
section was in view. With nine sections of unequal heights and smooth-
scroll firing observer callbacks mid-scroll, the active highlight
drifted (clicking 'Konto' showed 'Soundboard' as active because the
last section never crossed the observer's 20%-30% band).

Switched to a tab pattern (macOS System Settings / Discord / GitHub
style): the sidebar selects ONE section, only that section renders.
`useState<TabId>` is the single source of truth; no observer, no
scrolling between sections, no anchor links to clash with HashRouter.
Mobile fallback (<lg) gets a `<select>` dropdown above the panel.

Drops the now-unused `id` prop from Section and removes the
IntersectionObserver effect.
2026-05-16 15:28:03 +02:00
byGalax c55173800b chore(desktop): release v0.18.6 v0.18.6 2026-05-16 01:13:54 +02:00
byGalax fc9f1ec143 fix(desktop): Settings sidebar redirected to /chats instead of scrolling
App uses HashRouter, so an <a href="#profile"> changes the routing
hash and the router can't find a match — it falls back to /chats.
Replace anchors with buttons that scroll the target section via
scrollIntoView and update the active highlight optimistically.
2026-05-16 01:12:50 +02:00
byGalax 12e4b597a0 chore(desktop): release v0.18.5 v0.18.5 2026-05-16 01:09:54 +02:00
byGalax 5c05afb009 feat(desktop): redesign Settings + fix unreadable bubble text
Two UI fixes:

1. MessageBubble: 'Nachricht nicht lesbar' was rendered with
   text-fg-muted on the blue 'mine' bubble — invisible. Now uses
   text-accent-fg/80 on mine, text-fg-muted on peer (still
   ≥4.5:1 contrast in both modes).

2. SettingsPage: redesigned from a long single-column scroll into a
   sticky-sidebar + content layout (lg+) with:
   - 9 anchor-linked sections with icons in the sidebar
   - IntersectionObserver highlights the active section
   - Each section has a description subtitle for context
   - Voice (the densest section) is now sub-grouped into Audio-Gerät /
     Qualität / PTT / Hotkeys / E2EE via SubSection cards
   - Notifications consolidates message-sound + ringtone
   - Danger-toned account section visually separated
   - Mobile fallback is the original single-column scroll
2026-05-16 01:08:28 +02:00
byGalax a636a3c1c1 chore(desktop): release v0.18.4 v0.18.4 2026-05-16 01:00:34 +02:00
byGalax c9fe4879e0 fix(shared): stop sending fake install-id as messages.sender_device_id
Task 12 (the AuthContext userKeyState refactor) replaced the per-device
DeviceRecord lookup with a localStorage UUID via ensureInstallId(). That
UUID was then passed straight through to messages.sender_device_id on
INSERT.

The messages_insert_member RLS policy requires sender_device_id to be
NULL OR to match a row in `devices` owned by the caller. The localStorage
UUID matches neither -> 403 -> outbox endlessly retries with "Wiederhole".

Fix: SendMessageParams.senderDeviceId becomes optional, and the message
INSERT coerces undefined to NULL. The column is pure telemetry post-conv-
keys so passing NULL is correct. Existing call sites that hand in
ensureInstallId() still typecheck (string is assignable to string|null|undefined)
but the row is written with NULL until those callers stop passing it.
2026-05-16 00:59:28 +02:00
byGalax 61462516d2 chore(desktop): release v0.18.3 v0.18.3 2026-05-16 00:49:32 +02:00
byGalax d39a0fb6dc fix: stop reset_user_key from wiping conv-key bundles + auto-rotate stuck convs
Root cause of "alle Nachrichten verschlüsselt + kann nicht schreiben":
uploadUserKeyBlob (called by setupNewUserIdentity, changePin and
regenerateRecoveryCode) routed through reset_user_key, which DELETES
every conversation_keys row addressed to the user or one of their
devices. So setting a PIN destroyed every legacy bundle BEFORE the
migration could re-wrap them. The user ended up with user_keys set,
zero un-migrated bundles, no decryption, no send.

Fixes shipped:

  * supabase/migrations/20260516000001_user_key_rpcs_v2.sql
    - upsert_user_key: same UPSERT, NO delete. Used everywhere except
      "Identität zurücksetzen" (which keeps reset_user_key on purpose).
    - rotate_conv_key: bumps active_key_version atomically and inserts
      a fresh batch of bundles (per-user + per-device fallback).
  * shared/auth/userKey.ts: uploadUserKeyBlob now calls upsert_user_key.
  * shared/chat/convKeys.ts: new rotateConvKey() that wraps the fresh
    conv-key for every member's user_keys (preferred) and falls back to
    each member's per-device public_key for peers still on 0.17.x.
  * shared/chat/convKeys.ts: getOrCreateConvKey auto-triggers rotate
    when the user has no recipient_user_id row at the active version
    but rows exist (the deadlock case). Existing outbox retries drain
    on their own once the rotate completes — no manual button.
  * desktop/MessageBubble.tsx: "...cannot decrypt" is now a softer,
    German "Nachricht nicht lesbar" so users don't think the app
    crashed when historical messages can't be unwrapped.
2026-05-16 00:48:28 +02:00
byGalax 9207f473cd chore(desktop): release v0.18.2 v0.18.2 2026-05-16 00:27:54 +02:00
byGalax 5367544b59 feat(desktop): diagnostic + manual retry for legacy key migration
The 0.18.1 fix relied on an existing-device + present-stronghold-key match.
That fails for users who:
  - had multiple device registrations and only retain the latest device's
    private key in the local vault
  - had a vault wipe / fresh OS install at some point
  - have device rows that vanished server-side but keys still locally

Migration now scans conversation_keys for distinct un-migrated
recipient_device_ids visible to the user (RLS-filtered) and probes the
stronghold for each, regardless of whether the server still lists that
device. Result struct surfaces attempted/migrated/noKey/decryptFail/rpcFail
counters; SecurityCenter shows them via a new "Migration erneut ausführen"
button so users can self-diagnose without DevTools.

Also adds [crypto-migration] console.info breadcrumbs at every decision
point so a single F12 shows what happened.
2026-05-16 00:25:48 +02:00
byGalax e6b698bf14 chore(desktop): release v0.18.1 v0.18.1 2026-05-16 00:16:53 +02:00
byGalax 6caa674c19 fix(shared): legacy conv-key migration query used .eq(null) instead of .is(null)
PostgREST translates .eq('col', null) to `col = NULL` which is always false
in SQL. The migration silently returned zero rows -> setupNewUserIdentity
fired but re-wrapped nothing -> users could set a PIN but every send threw
'Awaiting key'. Switching to .is('col', null) emits `col IS NULL` and the
migration finally finds its work.

Also makes the migration trigger idempotent and re-fires it on:
  - every successful loadOrUnlockUserKey
  - AuthContext startup when the user-key is already cached
so users stuck on 0.18.0 auto-recover the moment they install 0.18.1.

PinInput: focused + active-slot now show a brand-coloured ring, glow, and
a blinking caret so users see where the next keystroke lands.
2026-05-16 00:15:15 +02:00
byGalax d9377fc52a chore(desktop): release v0.18.0 v0.18.0 2026-05-16 00:04:59 +02:00
byGalax 1c18078b1f docs: encryption-UX spec + plan from 2026-05-15 brainstorming session 2026-05-16 00:02:27 +02:00
byGalax 0e05a2cd85 refactor(desktop): replace VoiceChannelRail with CallPreviewPanel
Drops the always-on 'Sprach-Channel' banner. The preview panel renders
only when peers are in the active call (1:1 and group identical).
Calls are still started via the topbar phone icon.
2026-05-15 23:59:17 +02:00
byGalax b55ccf899f feat(desktop): add CallPreviewPanel — Discord-DM-style join surface 2026-05-15 23:56:02 +02:00
byGalax b2eb214d9f docs(plan): call-preview-panel implementation plan 2026-05-15 23:51:01 +02:00
byGalax 010a810485 docs(spec): call-preview-panel — only show when peer is in active call 2026-05-15 23:43:50 +02:00
byGalax 6a9a0bb804 fix(db): swap conversation_keys natural PK for synthetic row_id
Migration 20260515000002 failed on prod because dropping NOT NULL on
recipient_device_id was rejected (column is part of the natural primary
key). This fix-up drops the PK, adds a synthetic row_id BIGSERIAL PK,
re-applies the NOT NULL drop, and re-runs the indexes/policies that
were skipped after the failure.
2026-05-15 23:25:49 +02:00
byGalax f7c60945d0 refactor(shared): strip cryptographic device provisioning (now telemetry-only)
devices rows no longer carry public_key for crypto purposes. The whole
per-device key API surface (provisionNewDevice, loadDevicePrivateKey,
saveDevicePrivateKey, forgetDevicePrivateKey, restoreDeviceFromServerRecord)
is removed; registerDevice now records {name, platform} only. SQL drops the
NOT NULL on devices.public_key so future telemetry rows can omit it.

Note: SQL not applied locally - push via pnpm prod:migrate when ready.
2026-05-15 23:19:26 +02:00
byGalax 15ef9ece66 feat(desktop): proactively rewrap conv-keys for un-migrated peers on open
When a conversation opens, the local client checks every accepted member
for a recipient_user_id bundle on the active key version. Members without
one get a best-effort wrap from the local conv-key handle. This closes
the legacy migration gap where peer B couldn't read because no one had
yet wrapped the new per-user conv-key for them.
2026-05-15 23:12:35 +02:00
byGalax b789f4b10d feat(desktop): Settings security center (PIN change / recovery / reset)
Drops the manual backup-string flow; replaces it with PIN change,
recovery-code regeneration, and identity reset (all sealed via the new
user_keys table).
2026-05-15 23:09:05 +02:00
byGalax 02e1af4517 feat(desktop): DevicePage routes to UserKeySetup or UserKeyUnlock 2026-05-15 23:03:02 +02:00
byGalax e292df82f0 feat(desktop): UserKeyUnlock screen (PIN entry + recovery fallback) 2026-05-15 23:00:18 +02:00
byGalax b3804d805b feat(desktop): UserKeySetup screen (PIN + optional recovery code) 2026-05-15 22:57:57 +02:00
byGalax 02c4bb1c9b feat(desktop): shared PinInput component 2026-05-15 22:54:58 +02:00
byGalax 20216b37c6 refactor(desktop): AuthContext exposes userKeyState instead of device record
Replaces the per-device DeviceRecord lookup with a per-user discriminated
union (loading | needs-setup | needs-unlock | unlocked). Heartbeat block
deleted (telemetry no longer device-bound); webPush keyed by install-id.
2026-05-15 22:52:41 +02:00
byGalax 2c586351fc fix(shared): expose ./crypto/testBackend in package exports
Removes the @shared Vite-alias workaround in userIdentity.test.ts so
tsc can resolve the import without an extra paths entry.
2026-05-15 22:43:24 +02:00
byGalax b89a7e2617 refactor: swap device-id contexts for user-id contexts at call sites
Renames DecryptParams.ownDeviceId to ownUserId so decryptMessages actually
looks up bundles by user. Sweeps remaining OwnDeviceCtx and
loadDevicePrivateKey consumers in the desktop app to use cachedUserKey
from userIdentity. Files scheduled for deletion in later tasks
(BackupExportDialog, DeviceRestore, BackupRestoreDialog, BackupPromptBanner,
deviceBackup, DeviceRegistration) are left untouched.
2026-05-15 22:41:44 +02:00