322 Commits

Author SHA1 Message Date
byGalax 3755a927c3 feat(mobile): real chat list with pull-to-refresh + logout 2026-05-14 00:00:24 +02:00
byGalax 0b3a6280fc feat(mobile): Avatar + ConversationRow + relative-time helper 2026-05-13 23:59:53 +02:00
byGalax 3313dd84f3 feat(mobile): auth callback screen consumes magic-link tokens 2026-05-13 23:58:52 +02:00
byGalax 2a5f836b86 feat(mobile): magic-link login screen 2026-05-13 23:58:15 +02:00
byGalax bf02380c19 feat(mobile): session-gated (app) layout redirects to login when signed out 2026-05-13 23:57:43 +02:00
byGalax 375d28efa5 feat(mobile): AuthProvider + crypto-backend registration; drop sharedSmoke canary 2026-05-13 23:57:18 +02:00
byGalax 1e88941796 feat(mobile): supabase client wired through async-storage session 2026-05-13 23:55:35 +02:00
byGalax 7ac93a95e6 feat(mobile): crypto + secret-store + session-storage adapters 2026-05-13 23:55:07 +02:00
byGalax a275703ba6 feat(mobile): add AsyncStorage dep + env reader + theme constants 2026-05-13 23:54:10 +02:00
byGalax 13c6ad68f0 docs(mobile): phase 0 quickstart + roadmap pointer 2026-05-13 23:38:05 +02:00
byGalax 7eb8224bb0 feat(mobile): Netralax-branded Chats placeholder screen 2026-05-13 23:36:58 +02:00
byGalax 574436ab71 feat(mobile): Netralax landing screen with runtime version + nav smoke test 2026-05-13 23:36:31 +02:00
byGalax 5e483368ec feat(mobile): wrap root layout in GestureHandler + SafeArea + ErrorBoundary 2026-05-13 23:35:51 +02:00
byGalax b4457b74f5 chore(mobile): add runtime canary import from @chat-app/shared 2026-05-13 23:35:17 +02:00
byGalax bb7aeec345 feat(mobile): add ErrorBoundary for render-error fallback 2026-05-13 23:34:46 +02:00
byGalax 1bf4f4bb83 chore(mobile): add expo-application + react-native-gesture-handler for phase 0 shell 2026-05-13 23:34:10 +02:00
byGalax 3049a0e39d chore(mobile): add eas.json with development/preview/production build profiles 2026-05-13 23:33:47 +02:00
byGalax 2003a26c23 chore(mobile): rebrand app.json to Netralax + cloud.netralax.app bundle ids 2026-05-13 23:33:24 +02:00
byGalax a560f24c45 chore(desktop): release v0.17.5 2026-05-13 22:35:43 +02:00
byGalax 87c2e45bb4 fix(preload): read appVersion from package.json instead of npm_package_version
`appVersion: process.env.npm_package_version ?? '0.0.0'` only worked in
dev — pnpm sets that env var while running its lifecycle scripts. In
the packaged Electron build npm_package_version is unset, so every
installed user saw `v0.0.0` on the Changelog page and the version badge
permanently flagged them as "Update verfügbar" against their own
actually-current version.

Replace with a static `import pkg from '../package.json'` so Vite
inlines the version string into the preload bundle at build time. The
release script bumps package.json before electron-builder runs, so the
inlined value always matches the freshly-released version.

resolveJsonModule + esModuleInterop are already on in tsconfig.node.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:33:18 +02:00
byGalax b65a3994f3 chore(desktop): release v0.17.4 2026-05-13 22:25:11 +02:00
byGalax aa609389fa feat(chat): render Media/Files + Group Info drawers inline, not overlaid
Both right-hand panels used absolute inset-y-0 right-0 and floated on
top of the conversation, hiding the messages directly underneath the
panel and looking unlike Discord's actual layout. Restructure:

* MediaFilesDrawer: drop absolute/z-index/shadow chrome, become a
  static flex column (w-[380px] shrink-0) with a left border. Internal
  layout unchanged.
* GroupInfoPanel: same treatment (w-[320px] shrink-0). Dropped the
  backdrop-blur and slide-up animation that only made sense as a modal.
* ConversationPage: wrap the chat content (voice rail, in-call panel,
  messages list, drag-overlay, input form) in a new
  `flex min-w-0 flex-1 flex-col` chat-column, and make that column a
  sibling of the drawers inside a new `flex flex-1 flex-row` row. The
  conversation header + search bar stay full-width above the row.

Result: opening a drawer narrows the chat column instead of covering
it, matching Discord's behaviour. The chat-column wrapper also carries
the `relative` anchor previously held by the outer wrapper so the
drag-and-drop overlay positions correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:21:05 +02:00
byGalax cb3cbd8827 feat(chat): show caller's name on call event pills in groups
In group conversations the "Outgoing/Incoming/Missed call" system pill
gave no clue WHO triggered the event — fine in a 1:1 where the only two
players are obvious, useless in a group with three+ members. Discord
puts the caller's name in the pill; mirror that.

CallEventRow now takes a senderDisplayName prop (plumbed through from
MessageBubble) and switches non-own labels to the name-aware variants:

* ended  + !mine + name → "{name} hat einen Anruf gestartet"
* missed + !mine + name → "Verpasster Anruf von {name}"
* declined + !mine + name → "Anruf von {name} abgelehnt"

Own events (mine) stay generic ("Outgoing call" / "No answer") since
the user already knows they were the initiator. Fallback path without
a name keeps the previous generic labels so nothing regresses if the
sender is unresolvable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:20:21 +02:00
byGalax d1f38ce313 chore(desktop): release v0.17.3 2026-05-12 22:59:44 +02:00
byGalax 0d65a134fd feat(settings): click own avatar in profile preview to view fullscreen
Lightbox was previously a file-private component inside AttachmentImage
(used for enlarging chat image attachments). Extracted to a standalone
components/Lightbox.tsx so other surfaces can reuse the same dialog
without duplicating Esc/backdrop/body-overflow plumbing.

In SettingsPage's profile live-preview, the round avatar overlapping the
banner is now wrapped in a transparent button that opens the Lightbox
with the full-resolution avatar URL on click. Cursor switches to
zoom-in. Disabled when the user only has the initial-letter placeholder
(nothing meaningful to enlarge). Native button chrome (border, padding,
button-face background) is reset to keep the avatar circle's appearance
unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:58:22 +02:00
byGalax 12c66d676a fix(chat): use useLayoutEffect for scroll restore + auto-bottom to avoid mount flicker
The scroll-position memory introduced in 0.17.2 still produced a visible
"chat appears at the top then jumps" frame when switching back into a
conversation. Cause: both scroll-affecting effects (auto-bottom on new
messages, restore on chat re-entry) used useEffect, which fires AFTER
the browser paints the freshly-committed DOM. So users saw scrollTop=0
for one frame before the effect ran and corrected it.

Switching both to useLayoutEffect moves the scroll write into the same
commit phase as the message-list DOM update, so the very first paint
already shows the correct position — single paint, no flicker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:58:07 +02:00
byGalax 0dde1dd1a3 chore(desktop): release v0.17.2 2026-05-12 22:24:32 +02:00
byGalax 81d3587a91 feat(chat): per-conversation scroll memory + version badge on changelog page
Two small UX polishes:

1. Switching between chats no longer slams you to the bottom. Each
   conversation's scroll position (pixel offset + stickToBottom flag)
   is remembered in a module-scoped Map for the lifetime of the
   renderer process. Discord-style: leave Chat A scrolled up, peek at
   another conversation, come back — same spot you were reading.
   Chats left at the bottom keep auto-following new messages on return.
   Reload resets everything (session-only, no localStorage).

   The restore runs once messages.length > 0 to avoid the browser
   clamping scrollTop to a near-zero scrollHeight before the message
   list has rendered. A small isRestoringRef guard prevents the
   programmatic scroll event from immediately overwriting the saved
   position with a clamped value.

2. Changelog page now shows a version badge in the header that compares
   the installed app version against entries[0].version from the
   server-side changelog feed. Three states:
   * `vX.Y.Z · aktuell` (emerald) — installed matches latest
   * `vX.Y.Z · Update verfügbar` + `neueste: vA.B.C` (amber) — outdated
   * `vX.Y.Z` neutral — installed is ahead of the published feed
     (dev/test builds)
   Semver compare is integer-major.minor.patch with a graceful
   garbage-fallback so a malformed version string doesn't false-flag
   a current install as outdated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 22:22:49 +02:00
byGalax 8be1105333 chore(desktop): release v0.17.1 2026-05-12 22:00:27 +02:00
byGalax f9e1d2f073 fix(secure-store): preserve original ciphertext on decrypt failure + startup path log
Critical hotfix for the 0.17.0 regression: users upgrading from 0.16.x
were logged out, and their next login wrote a fresh empty secure-store
on top of the original ciphertext — destroying device keys irrecoverably.

Why it happened: loadState used a blanket `catch {}` that conflated
"file doesn't exist (genuine new user)" with "file exists but can't be
decrypted (DPAPI / OSCrypt quirk after the install rename)". Both paths
returned an empty Map; the next scheduledSave then overwrote the
original .bin file with a fresh blob.

Fix:
* Separate ENOENT from decrypt/parse failures. ENOENT → empty Map. Any
  other read error → log, empty Map (no quarantine, matches old
  behaviour for transient lock issues).
* When decrypt/parse fails the original file is renamed to
  <file>.broken-<iso-ts> BEFORE returning empty Map. The next save
  writes to a fresh file; the original ciphertext is preserved on disk
  so a future build (or manual recovery) can still get at the bytes.
* Loud console.error around the failure so future regressions surface
  in main-process logs.

main.ts: move setPath('userData', appData/ChatApp) BEFORE setName so
any productName-derived path caching inside setName can't beat us to
it. Add a startup log of the resolved paths so future debugging has
hard evidence instead of guessing.

Affected users on 0.17.0 should still recover via Settings → Backup
Wiederherstellen (account-level keys are unchanged); this fix prevents
the data destruction for anyone who hasn't upgraded yet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:54:14 +02:00
byGalax 341f5f227d chore(desktop): release v0.17.0 2026-05-12 21:35:29 +02:00
byGalax 8baac2fd1e fix(call): tile sizing polish + cinema-mode chrome suppression + spec/plan docs
Equal-grid cells no longer set aspect-video — on wide chat panels this
forced cell height = width × 9/16 (~400px on a 700px panel) which pushed
the row past the section's max-h and ate the controls bar below. n>=2
cells now fill grid tracks normally via auto-rows-fr; the solo case
(n=1) keeps a 16:9 silhouette via aspect-video + max-w + justify-self-
center so a single-user-alone-calling view doesn't stretch into a
full-width slab. Same change applied to the fullscreen-grid path plus
+16px bottom-padding (pb-28) so audio-only avatars' name chip clears
the floating controls bar.

Docked stage strip thumbs (focus + bento) switch from aspect-video
shrink-0 to flex-1 min-w-[200px] max-w-[460px] so 2-3 thumbs share the
row width evenly under the share above, instead of clinging to the left
edge with dead space to the right. Fullscreen-cinema strip keeps the
small aspect-video thumbs the user explicitly approved.

ScreenShareViewer gains a hideFullscreenToggle prop; cinema mode passes
it via a new `cinema` prop on TileRender so the in-share fullscreen icon
doesn't visually collide with FullscreenCall's strip-hidden toggle at
the same top-right corner.

docs/superpowers/specs + plans for the Discord-style tile handling
workstream are committed alongside the implementation that completed it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:31:26 +02:00
byGalax 05870ef8fa feat(call): split resolution/fps in share picker + restore window state after fullscreen
ScreenSharePickerModal now exposes Auflösung (Auto · 720p · 1080p · 1440p
· 4K) and FPS (30 · 60) as separate pill rows instead of bundled quality
presets — users can pick "1440p · 30 fps" or "4K · 30 fps" which the old
preset list didn't surface. The underlying screenShareSettings framerateOverride
slot already existed; the modal just stopped resetting it to null on every
start and now plumbs the chosen FPS through to startScreenShare.

Cinema-mode fullscreen on Windows had two defects:

1. Maximized → fullscreen left the taskbar drawn on top of the window
   because DWM kept the maximized work-area constraints. We now unmaximize
   first so DWM recomposes cleanly and setFullScreen actually covers the
   whole monitor including the taskbar strip.

2. Esc out of cinema came back as a small floating window even when the
   user had been maximized before clicking the Vollbild button — the
   unmaximize from (1) was never undone. We now memo the pre-fullscreen
   maximized flag per window-id and call win.maximize() once the
   leave-full-screen event has fired.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:31:10 +02:00
byGalax f9e340dbec feat(branding): Netralax rebrand + Discord-style taskbar unread badge
App productName becomes Netralax (driving exe name and window title);
existing installs keep their %APPDATA%\ChatApp profile via an explicit
app.setPath('userData', appData/ChatApp) so no Login/Sounds/Secret store
data is lost.

The Windows taskbar overlay now renders a red bubble with the actual
unread count (Discord parity) instead of just a static red dot. Renderer
paints a 64×64 PNG via canvas — full-bleed red circle, white bold count
with a "99+" cap, no outer ring — and passes the data URL through the
existing setTrayUnread IPC. Main decodes via nativeImage and applies it
as the BrowserWindow overlay icon. Falls back to the static dot if the
renderer canvas pipeline is unavailable.

Also: app.setName('Netralax') + setAppUserModelId('cloud.netralax.desktop')
for Windows taskbar grouping and notification source attribution, and
release.mjs now reads productName dynamically from package.json so the
artifact lookup stays correct after the rename.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 21:30:54 +02:00
byGalax 91eedc0e8e fix(call): aspect-video for fullscreen grid+strip, switch section sizing to stageLayout 2026-05-12 19:03:23 +02:00
byGalax e56533918e feat(call): multi-share bento layout in stage + fullscreen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 18:54:23 +02:00
byGalax c87d4e82d3 feat(call): doubleclick on focused tile clears pin
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 18:50:29 +02:00
byGalax 187f8dc95a feat(call): left-click toggles pin, drop manual focus mode
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 18:47:10 +02:00
byGalax f612c1bb50 feat(call): introduce StageLayout discriminated union
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 18:43:55 +02:00
byGalax a9d5e2d430 feat(call): uniform 16:9 grid cells, drop grid-rows constraint 2026-05-12 18:40:54 +02:00
byGalax a065cc0a2c feat(call): drop hardcoded 16:9 on screen-share preview button 2026-05-12 18:39:06 +02:00
byGalax 005aebd60b feat(call): VideoStub accepts fit prop, contain when focused 2026-05-12 18:36:32 +02:00
byGalax 68bc1f76f6 chore(desktop): release v0.16.3 2026-05-07 17:25:56 +02:00
byGalax f1c7501807 fix(crypto): suppress approval banner for devices with existing wraps
After 0.16.2 some users saw the approval banner stack up to 6+ entries
on first launch — every old device they ever registered (Tauri-era,
test installs, dev builds) showed up because the only "already legit"
filter was `created_at <= ownDevice.created_at`. That fails when own
device is restored from Backup (older than every other entry) or when
the user accumulated installs around the migration window.

Add a semantic check: if a device already has at least one row in
`conversation_keys` (recipient_device_id), it has been wrapped before
and is by definition not awaiting approval. Treat as approved silently.
Bulk query against the candidate IDs, no N+1.

Plus UX: when more than one request is pending, render a sticky header
with a count and "Alle ablehnen" button so users with stale piles can
clear them in one click.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:24:17 +02:00
byGalax e16b248366 chore(desktop): release v0.16.2 2026-05-07 17:07:28 +02:00
byGalax 9b764053c4 feat(crypto): explicit device-approval flow + dev userData isolation
Disable the previous auto-share of conversation keys to newly-registered
devices: a stolen password / new device registered by an attacker no
longer automatically grants history access. Backup-Restore (which
restores the old device-id) still opens existing wraps as before.

Phase 1 of the approval replacement:
- New `lib/deviceApproval.ts`: realtime listener for `devices` INSERT,
  surfaces a pending list, persists approve/deny decisions in
  `chatapp.approvedDeviceIds` / `chatapp.dismissedDeviceIds`. Filters the
  initial fetch by created_at > own-device's created_at so a freshly
  installed client doesn't try to "approve" pre-existing devices.
- New `components/DeviceApprovalBanner.tsx`: bottom-right Discord-style
  banner per pending request with Genehmigen / Ablehnen actions; reuses
  `wrapForOneDevice` from conversationKeySync to fan out conv-keys.
- AppShell mounts both the listener and the banner.

Plus dev userData isolation in main.ts: when running unpackaged, append
`-Dev` to the userData path so `pnpm dev` runs side-by-side with the
installed packaged build instead of colliding on the single-instance
lock. Window title also distinguished as "ChatApp (Dev)".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 17:03:04 +02:00
byGalax cc84bb7ff6 chore(desktop): release v0.16.1 2026-05-06 23:37:34 +02:00
byGalax 825160ee46 feat(desktop): port v0.11.4-v0.15.2 from Tauri to Electron + Discord-parity audio (v0.16.0)
Chronological port of every Tauri release commit (v0.11.4 -> v0.15.2)
into the Electron rebuild, plus Discord-style audio handling that goes
beyond the Tauri original.

Highlights:
  - All 17 Tauri release commits ported (audio fixes, custom notification
    sound, Discord-style chat UX, profile banner, changelog page,
    Discord-parity call UX, screen-share echo + re-watch UX, audio-loop
    fix).
  - Native napi-rs audio-loopback addon with WASAPI process-loopback:
      * EXCLUDE_TARGET_PROCESS_TREE for full-screen shares -> peers
        never hear themselves echoed back through the capture.
      * INCLUDE_TARGET_PROCESS_TREE for window shares -> only the
        picked window's audio is captured, not the whole OS mixer
        (Discord parity).
      * HWND -> PID resolution via Win32 GetWindowThreadProcessId.
  - Discord-style screen-source picker (thumbnail grid, screens vs
    apps tabs, live-refreshing thumbnails).
  - Hash routing fix for packaged builds (file:// can't resolve
    BrowserRouter paths).
  - Tauri sources removed (apps/desktop/src-tauri).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:35:01 +02:00
byGalax 72d02e385f chore(desktop): release v0.11.3 2026-04-23 18:02:24 +02:00
byGalax c9b885b91c perf(call): screen-share hybrid — OS picker + WASAPI audio
Strip the sourceId-gated chromeMediaSource + xcap native capture paths
from startScreenShare and collapse to a single setScreenShareEnabled
call. Neither of the bypassed paths produced smooth frames in WebView2:
chromeMediaSource: 'desktop' is an extension-only Chromium constraint
and throws outside extension origins, and the xcap JPEG-over-IPC
fallback couldn't sustain 30fps at 1080p on a single main-thread.
setScreenShareEnabled goes through Chromium's native getDisplayMedia
capture, which is the only path that gets HW-accelerated frames into
the WebRTC encoder from WebView2.

Audio continues via the WASAPI loopback module — getDisplayMedia can't
grab system sound in WebView2 without desktop-capture entitlements
Chromium reserves for extensions. The audio track's teardown chains to
the ScreenShare video track's 'ended' event so the Windows stop-share
overlay kills both sides in lockstep.

ScreenSourcePicker is now a quality + audio chooser only; the
thumbnail grid disappears because custom source IDs don't round-trip
through WebView2, and a custom picker in front of the OS picker just
means the user picks twice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 17:59:41 +02:00