Files
dchain/docs/ROADMAP.md
vsecoder 96b347076e feat(desktop): Contacts + Settings→Devices + expanded Profile + QR + keybinds (v2.2.0-rc1)
Completes the desktop feature surface ahead of the v2.2.0 tag. Only
auto-update + packaging remain.

Settings — now two-paned (nav on the left, pages on the right):
  * NodePage — URL ping-on-commit + API token field.
  * IdentityPage — pub key / X25519 pub, Export (safe-save dialog) /
    Import (open dialog + wipe + replace) / Delete identity.
  * DevicesPage — full multi-device UI: list every active device with
    a THIS DEVICE badge; Unlink button on every other row submits
    UNLINK_DEVICE + optimistic local remove; Link new device modal
    takes {code, device key, name}, submits LINK_DEVICE, then ships
    the handshake envelope (master Ed25519 priv encrypted for the
    new X25519) — same protocol as mobile's primary-device modal.
  * AboutPage — version, platform, Gitea links.
  * store.settingsPage discriminated union keeps selection across
    section switches.

Contacts section (now real):
  * ContactsList — alphabetical, filter-as-you-type; each row shows
    avatar letter + name + short address.
  * ContactsDetail — profile card (username/alias/pub) + Open chat /
    View posts / Copy address actions + stats grid
    (Balance, Devices, Encryption, Added) + Identity card with
    DC address, username, published X25519, device_count.
  * store.selectedContact persists across navigation.

Profile section (expanded):
  * ProfileList — big avatar + pub key + contacts count.
  * ProfileDetail — balance hero, quick actions (My posts →
    feed author wall, Manage devices → Settings→Devices, Copy
    address), Identity card, inline Linked devices list with a
    THIS DEVICE badge matching the Settings page.

Receive modal — canvas QR via `qrcode` (new dep, ~5 KB gzipped),
white-on-transparent so it sits inside the same black modal chrome.

Global keybinds (useGlobalKeybinds hook mounted in Shell):
  * Ctrl/Cmd+W — close the current conversation (drops activeChat,
    keeps section). Does NOT close the window.
  * Ctrl/Cmd+K — jump to Contacts.
  * Ctrl/Cmd+, — Settings.
  Each guards against being in a text field so typing `k,` in a
  composer / search doesn't hijack.

docs/ROADMAP.md — rc1 row flipped to done; v2.2.0 narrows to
auto-update + packaging + optional attachments in Compose.
2026-04-22 18:39:39 +03:00

12 KiB
Raw Blame History

Roadmap

Living doc — «куда идём дальше». Два активных вектора: v2.2.0 multi-device и desktop Electron client. Всё что tentative — помечено ?; всё что решено и принято к работе — без знаков.


v2.2.0 — Multi-device (per-device X25519, on-chain device registry)

Проблема

Сейчас у пользователя одна пара X25519 (хранится в keyFile), и relay-mailbox — queue с DELETE-после-чтения. При двух устройствах:

  • Конверт забирает то устройство, что первое дёрнуло /relay/inbox — второе теряет сообщение навсегда.
  • История чатов per-device, не синхронизируется (AsyncStorage на каждом устройстве свой).

On-chain часть (posts, follows, tx, wallet) работает нормально — оба устройства читают один чейн и видят одно и то же. Чинить надо только мессенджер.

Решение — путь А (Signal-style): X25519 на устройство + device registry

  1. Master identity = Ed25519 (как сейчас). Подписывает tx, владеет балансом, — один на всю identity.
  2. X25519 — per-device. Каждое устройство при первом старте генерит свою пару. Relay-mailbox остаётся queue'ом, но адресуется по устройству, не по identity.
  3. Device registry on-chain: новые tx-типы LINK_DEVICE / UNLINK_DEVICE, подписываются master Ed25519, публикуют/отзывают связку (master_pub → device_x25519_pub, device_name).
  4. Sender-side fan-out: при отправке — один envelope на каждое активное устройство получателя. Мессадж → N конвертов.
  5. Revoke: master подписывает UNLINK_DEVICE, клиент, обнаруживший свой x25519_pub в revoked — wipe'ит локальную БД (zeroize master Ed25519 + delete chats).

План работ (PR-by-PR)

PR #1 — Chain-side (backend) — v2.2.0-alpha1

  • blockchain/types.go: EventLinkDevice, EventUnlinkDevice
    • payload structs LinkDevicePayload, UnlinkDevicePayload.
  • blockchain/chain.go: applyTx-ветки, state persistence:
    • prefixDevice + x25519_pub → DeviceRecord{owner, name, added_at, revoked_at?}.
    • Обратный индекс prefixDevicesByOwner + master_pub → [x25519_pub, …].
  • Константа MaxDevicesPerOwner = 10.
  • Validation:
    • Tx подписана master'ом; подпись matches payload.Owner.
    • x25519_pub уникален в registry (не занят другим master'ом).
    • device_name ≤ 64 символов; printable.
    • При UNLINK_DEVICE: запись существует, owner совпадает с signer'ом.
  • HTTP: GET /api/devices/{master_pub}[{x25519_pub, device_name, added_at}, …], фильтрует revoked.
  • В /api/identity/{pub} добавить поле device_count.
  • Unit-тесты:
    • Happy path link + list.
    • Unlink → list сократился.
    • Лимит MaxDevicesPerOwner.
    • Чужая подпись → reject.
    • Duplicate x25519_pub → reject.
  • Swagger + docs/api/devices.md.

Совместимость: старые клиенты, которые не обновили identity, продолжают работать по old-schema (envelope на published X25519 из identity). Sender fall-back'ит на identity.x25519, если device_count == 0.

PR #2 — Client fan-out (mobile) — v2.2.0-alpha2

  • lib/api.ts: fetchDevices(masterPub): Promise<DeviceRecord[]>, с кэшем в zustand store + инвалидацией по WS-ивенту tx:LINK_DEVICE|UNLINK_DEVICE.
  • chats/[id].tsxsendCore: Promise.all по devices[]; если devices.length == 0 — fall-back на identity.x25519 (legacy path).
  • При первом старте (онбординг): если identity ещё не зарегистрирована — PUBLISH_KEY tx + LINK_DEVICE tx (этот девайс как первый в списке).
  • UI: в chat-list tile бейдж 2/2 на своих сообщениях (доставлено на N устройств получателя).
  • Тест: два клиента на одну identity, отправляем на contact — оба получают.

PR #3 — Devices screen + pairing flow (mobile + desktop) — v2.2.0-alpha3

  • Settings → Devices: список активных, unlink-кнопка у каждого (кроме this device).
  • «Link new device» flow:
    • Новое устройство генерит свой X25519, показывает QR {x25519_pub, device_name, nonce, rendezvous_id}.
    • Старое сканирует → Confirm → подписывает LINK_DEVICE tx + шлёт через relay envelope {master_ed25519_priv, recent_history} — encrypted for new device's X25519, TTL ≤ 60 сек.
    • Новое читает envelope, сохраняет master + history, готово.
  • Self-wipe при обнаружении своего x25519_pub в revoked: zeroize master, clear AsyncStorage/keychain, redirect на onboarding.

PR #4 — Desktop Electron shell — v2.2.0-rc1

См. отдельный раздел ниже.

Нерешённые вопросы

  • QR-pairing UX на desktop'е: у ноутбука камеры часто нет. Вариант: master на phone шлёт invite-envelope по pre-shared secret (6-digit code, вводится в обе стороны), без QR. Подумать после PR #2.
  • History-sync backup формат: JSON export всей ChatStorage shreds'ов? Ограничение по размеру? TTL у backup-конверта? — в PR #3.
  • Revoked-device-wipe race: если устройство оффлайн 6 месяцев, потом поднимается — первое что видит это свой revoked. Должен успеть wipe'нуться до попытки послать tx с master-ключом. Продумать порядок bootstrap'а.

Desktop client (Electron)

Цель: 1:1 функциональный паритет с мобильным, десктопная эргономика, keyboard-first.

Архитектура

  • Electron + React (Vite) + TypeScript.
  • 80% lib/ переиспользуется из client-app/lib/ (api, feed, crypto, store, types).
  • Нативные модули: electron-store (+ safeStorage) для keys, clipboard, Notification, dialog.showOpenDialog.
  • Custom title bar (frame-less), 3-panel layout.

Экраны

Full design — docs/desktop-design.md (TODO). Sections:

  • Messages — список чатов + conversation panel. Saved Messages pinned top.
  • Feed — tabs (For You / Following / Trending 24h / 7d / Hashtag) + post list + thread panel.
  • Wallet — account overview + tx history + tx detail.
  • Contacts — grouped list (All / Online / Blocked / Requests) + contact profile.
  • Settings — grouped: Node, Identity, Devices, Privacy, Feed, Notifications, Advanced, About.
  • Profile (внизу nav) — self profile; чужой — в detail pane.
  • Auth/Onboarding — Welcome / Create / Import / Pair-with-phone (QR/code).

Стилистика (переносим)

Чёрный #000, карточки #0a0a0a, бордеры #1f1f1f, текст #fff/#8b8b8b. Синий accent #1d9bf0, оранж warning #f0b35a. Синий bookmark-avatar для Saved Messages.

Не переносим

Все React-Native-анимации (Pressable-animations, scaleY: -1, gesture-handler long-press). На десктопе — правый клик, hover-states, keyboard shortcuts, обычный scroll вместо inverted FlatList.

Global keybinds

Key Action
Ctrl/Cmd+1..5 Переключение секций
Ctrl/Cmd+N Новый пост / новый контакт (контекстно)
Ctrl/Cmd+K Global search
Ctrl/Cmd+F Search в текущей list pane
Ctrl/Cmd+, Settings
Ctrl/Cmd+Enter Send (chat / publish post)
Esc Close modal / cancel selection

Структура папок

desktop/
├── electron/
│   ├── main.ts          # BrowserWindow, IPC, auto-update
│   ├── preload.ts       # contextBridge — safe API
│   ├── menu.ts          # native menu bar
│   └── deep-link.ts     # dchain:// protocol handler
├── src/                 # renderer
│   ├── App.tsx
│   ├── shell/           # nav, status bar, title bar
│   ├── sections/
│   │   ├── messages/
│   │   ├── feed/
│   │   ├── wallet/
│   │   ├── contacts/
│   │   ├── settings/
│   │   └── profile/
│   ├── modals/
│   ├── auth/
│   ├── lib/             # reuse из client-app/lib
│   └── styles/
└── package.json

План работ

  • v2.2.0-alpha4 — Boilerplate, 3-panel shell, safeStorage IPC, Welcome / Create / Import auth, section stubs.
  • v2.2.0-alpha5 — Messages section + pairing poll loop; chain + clients learn to attribute conversations by master Ed25519.
  • v2.2.0-alpha6 — Feed (tabs + list + detail + compose) + Wallet (history + detail + Send/Receive).
  • v2.2.0-rc1 — Contacts section (list + profile detail + actions), Settings → Devices (list + unlink + link-new-device modal with the same protocol as mobile), expanded Profile, QR in Receive, global keybinds (Ctrl+W close chat / Ctrl+K jump to Contacts / Ctrl+, Settings).
  • v2.2.0 — Auto-update through the same /api/update-check pipeline nodes use; electron-builder.dmg, .exe, .AppImage, .deb; optional: attachments in Compose (file picker + client-side image resize + scrub).

Открытые вопросы (desktop)

  • Auto-update channel: гнать через Gitea releases (как node) или отдельный S3/Gitea-attachments?
  • Sandbox: desktop хранит master Ed25519 — обязательно safeStorage (macOS Keychain, Windows DPAPI, libsecret на Linux). На Linux без libsecret — fallback на plaintext + warning.
  • Deep-link: dchain://chat/<pub> для шаринга профилей/постов из браузера.

Меньшие хвосты (неприоритетно)

  • prev_hash mismatch при двойной gossip-доставке — понизить до Debug уровня, не пугать операторов (см. history с joiner-node bring-up).
  • /api/network-info.peers у seed'а возвращает [] если DCHAIN_ANNOUNCE не задан — добавить fallback: если announce пустой, публиковать первый listen-addr + свой peer_id (не 127.0.0.1), чтобы joiner'ы не упирались в peers:[].
  • Groups (3+ участников) в чатах — тип Contact.kind == 'group' в клиенте есть, а на backend'е не реализован. После v2.2.0, потому что pairwise messaging ↔ group messaging на multi-device — это другой математический слой (MLS или Sender Keys).
  • Repa как weight в selection-lider PBFT — сейчас только scoring; для настоящего proof-of-reputation нужно переписать leader-rotation.