Closes the v2.2.0 roadmap. Desktop client is feature-complete and
ready for first installer builds.
Contact request flow (fills a real gap flagged by the user):
* lib/tx.ts grows buildContactRequestTx / buildAcceptContactTx /
buildBlockContactTx with canonical bytes matching mobile.
* lib/api.ts: fetchContactRequests + ContactRequestRaw.
* New contact modal — sections/contacts/NewContactModal.tsx — resolves
@username / DC-address / hex pub via resolveAccount, shows identity
preview (incl. "has encryption key / key not published" hint),
fee tier picker (5k / 10k / 50k µT), optional 280-char intro,
balance guard.
* Requests inbox — sections/contacts/RequestsList.tsx — polled every
15 s via /relay/contacts, filters pending, Accept submits
ACCEPT_CONTACT + adds the peer to local contacts with their
identity.x25519_pub pre-cached, Block submits BLOCK_CONTACT.
* ContactsList grows a two-tab header (Contacts / Requests with a
pending-count badge) + "+ New" button next to the filter input.
Auto-update:
* hooks/useUpdateCheck.ts — polls /api/update-check on mount and
every 6 hours; loose semver compares the Gitea release tag
against this build's app.version (from Electron IPC), ignores
the node's own update_available flag (it compares vs. the node,
not the desktop).
* shell/UpdateBanner.tsx — thin strip above the status bar with
the new tag, Download button (opens the release URL in the
default browser), and a dismiss-for-this-tag × so once-seen
updates don't nag.
Packaging — electron-builder config tightened:
* artifactName pattern includes version + os + arch.
* Mac: hardenedRuntime on, dmg + zip outputs, social-networking
category.
* Windows: NSIS (full installer, per-user or per-machine) +
portable exe.
* Linux: AppImage + deb.
* Strip source maps and test folders from the asar.
* publish: null — no auto-publisher yet; Gitea releases are
uploaded manually for now.
* directories.output = release/, directories.buildResources =
resources/ so icons land in a predictable place once we add them.
Version bumped to 2.2.0 in package.json. docs/ROADMAP.md marks
v2.2.0 row complete; remaining work (attachments, code signing,
group chats) moved to a post-v2.2.0 bucket.
12 KiB
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
- Master identity = Ed25519 (как сейчас). Подписывает tx, владеет балансом, — один на всю identity.
- X25519 — per-device. Каждое устройство при первом старте генерит свою пару. Relay-mailbox остаётся queue'ом, но адресуется по устройству, не по identity.
- Device registry on-chain: новые tx-типы
LINK_DEVICE/UNLINK_DEVICE, подписываются master Ed25519, публикуют/отзывают связку(master_pub → device_x25519_pub, device_name). - Sender-side fan-out: при отправке — один envelope на каждое активное устройство получателя. Мессадж → N конвертов.
- 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.
- payload structs
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'ом.
- Tx подписана master'ом; подпись matches
- 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].tsx→sendCore:Promise.allпоdevices[]; еслиdevices.length == 0— fall-back на identity.x25519 (legacy path).- При первом старте (онбординг): если identity ещё не зарегистрирована —
PUBLISH_KEYtx +LINK_DEVICEtx (этот девайс как первый в списке). - 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_DEVICEtx + шлёт через relay envelope{master_ed25519_priv, recent_history}— encrypted for new device's X25519, TTL ≤ 60 сек. - Новое читает envelope, сохраняет master + history, готово.
- Новое устройство генерит свой X25519, показывает QR
- 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 — Contact-request flow (New contact modal + Requests
inbox tab with Accept/Block), auto-update banner that polls
/api/update-checkand offers the latest Gitea release, electron-builder config ready for.dmg/.exe/.AppImage/.deb+ NSIS installer + macOS hardenedRuntime. - post-v2.2.0 — Attachments in Compose (file picker + client-side image resize + metadata scrub), code signing certificates, draft group chats (multi-recipient envelopes or MLS integration).
Открытые вопросы (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.