Files
dchain/desktop
vsecoder 7e6fe2c2a0 fix(desktop): infinite render loop opening a chat (Maximum update depth)
The Conversation selector `useStore(s => s.messages[address] ?? [])`
allocates a fresh empty array on every call when the chat has no cached
messages. Zustand compares selector results with `===`, so the new []
is different from the previous [], marking the slice as "changed",
which re-renders Conversation, which calls the selector again, which
produces another new []... "Maximum update depth exceeded" inside
seconds.

Fix: module-level `const EMPTY_MESSAGES: Message[] = []` returned as the
fallback. Same object reference every render, zustand's === bails
early, no re-render.

This crash only showed up after opening a chat whose messages hadn't
been cached yet — picking any entry in ChatList that hadn't received
an envelope would hang the renderer. PaneBoundary (added in the prior
commit) now catches it visibly instead of blacking out the whole
window, but we still want the real fix.
2026-04-22 18:58:57 +03:00
..

DChain Desktop

Electron shell for the DChain messenger and social feed.

Same functionality as the mobile client-app, re-imagined with a keyboard-first, 3-panel desktop layout:

┌──────────────────────────────────────────────────────────┐
│  DChain                                                   │ titlebar (drag)
├──────┬───────────────────┬────────────────────────────────┤
│ nav  │      list         │             detail             │
│ 72px │   340px fixed     │            flex 1              │
├──────┴───────────────────┴────────────────────────────────┤
│  ● online  ·  node.example:8080  ·  height 10942          │ status bar
└──────────────────────────────────────────────────────────┘

Sections (left rail): Messages · Feed · Wallet · Contacts · Settings · Profile.

Quick start

cd desktop
npm install
npm run dev          # concurrently: Vite dev server + Electron

The first boot will show the Welcome screen. Pick Create to generate fresh keys, or Import a node.json exported from the mobile client.

Build

npm run build        # produces dist/ (renderer) + dist-electron/ (main) + installers

Default installers are built with electron-builder: .dmg on macOS, NSIS .exe on Windows, AppImage + .deb on Linux. Adjust build.* in package.json for signing / notarisation.

Layout

  • electron/ — main + preload. TypeScript, compiled to dist-electron/ by tsc -p electron/tsconfig.json.
  • src/ — renderer. React + Vite. @/ aliases to src/.
  • src/shell/ — 3-panel chrome.
  • src/sections/ — one folder per nav section, each exports { List, Detail }.
  • src/auth/Welcome.tsx — shown when no key is loaded.
  • src/lib/ — api, storage, store, types. Mirrors (without React-Native deps) the relevant pieces of ../client-app/lib/.

Security model

Master Ed25519 priv lives in the OS keychain via Electron safeStorage (macOS Keychain / Windows DPAPI / libsecret). A renderer compromise cannot read or exfiltrate the key — it always travels through window.dchain.keyfile.* IPC, which main.ts validates and mediates.

contextIsolation: true, nodeIntegration: false. CSP in index.html pins script sources to 'self' while allowing connect-src * so the renderer can hit any node the user configures.

Pairing (v2.2.0-alpha5+)

Desktop will reuse the same 6-digit-code + relay-envelope handshake as the mobile client. The scaffold in src/auth/Welcome.tsx stubs the button until the polling loop lands.

Multi-device fan-out

When the node is at v2.2.0-alpha1+, lib/api.ts:fetchDevices returns every linked X25519 pub for a given identity; the sender then encrypts one envelope per device. Legacy nodes return an empty array and the client falls back to IdentityInfo.x25519_pub, preserving the pre-multi-device behaviour.