Files
dchain/docs/ROADMAP.md
vsecoder 6b7cb1c5a9 feat(desktop): contact requests, auto-update banner, packaging polish (v2.2.0)
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.
2026-04-22 18:47:19 +03:00

233 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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].tsx``sendCore`: `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
```
### План работ
- [x] **v2.2.0-alpha4** — Boilerplate, 3-panel shell, safeStorage IPC,
Welcome / Create / Import auth, section stubs.
- [x] **v2.2.0-alpha5** — Messages section + pairing poll loop; chain
+ clients learn to attribute conversations by master Ed25519.
- [x] **v2.2.0-alpha6** — Feed (tabs + list + detail + compose) +
Wallet (history + detail + Send/Receive).
- [x] **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).
- [x] **v2.2.0** — Contact-request flow (New contact modal + Requests
inbox tab with Accept/Block), auto-update banner that polls
`/api/update-check` and 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.