Files
dchain/client-app/lib/types.ts
vsecoder ce11a13874 feat: desktop messaging + pairing + cross-client master-pub attribution (v2.2.0-alpha5)
Two coordinated changes:

1. Desktop client gets a functional Messages section and working pairing
flow, putting it at feature parity with mobile for the v2.2.0 line.

2. Server + both clients teach each other to use the sender's master
Ed25519 (not just their X25519) to address conversations, so a peer
writing from a different linked device still rolls into the same chat.
This is the "new API logic" the desktop scaffold was waiting on.

Server (node/api_relay.go, cmd/node/main.go):
  * /relay/inbox items now carry `sender_ed25519_pub` alongside the
    per-device `sender_pub`. Empty string for pre-v2.2.0 senders.
  * WS `inbox` push summary also includes `sender_ed25519_pub`, so the
    client can skip the refetch when the envelope plainly isn't for
    the chat they're watching.
  * Both existing tests pass.

Mobile client:
  * lib/types.ts Envelope grew `sender_ed25519_pub`; fetchInbox normalises
    it (default '') for older nodes.
  * hooks/useGlobalInbox matches contacts by (master Ed25519 OR legacy
    X25519) so an incoming message from a peer's desktop reuses the
    existing chat instead of creating a duplicate placeholder.
  * hooks/useMessages now takes an optional `contactMasterEd25519` and
    exposes a matchesChat() predicate; WS inbox handler uses it too to
    avoid spurious refetches.
  * chats/[id].tsx passes `contact.address` (master) along with x25519.

Desktop client — all new:
  * src/lib/crypto.ts — tweetnacl hex/base64 helpers, generateKeyFile,
    encryptMessage/decryptMessage, signBase64, shortAddr. Same signatures
    as the mobile lib; uses Chromium's window.crypto, no expo-crypto dep.
  * src/lib/tx.ts — buildTransferTx / buildLinkDeviceTx / buildUnlinkDeviceTx
    + submitTx + humanizeTxError, canonical-bytes identical to mobile.
  * src/lib/relay.ts — fetchInbox, sendEnvelope, resolveRecipientKeys
    (multi-device fan-out with legacy identity.x25519 fallback).
  * src/lib/store.ts — zustand state gets messages{}, unread{},
    activeChat.
  * src/lib/storage.ts — per-chat cache via localStorage (500-msg cap).
  * src/hooks/useInboxPoll — 4s polling loop, addresses conversations
    by master Ed25519, bumps unread unless chat is active.
  * src/sections/messages/* — ChatList (sorted tiles, unread badges),
    Conversation (auto-scroll messages + composer + fan-out send,
    Enter-to-send / Shift+Enter for newline), EmptyConversation.
  * src/auth/Pair.tsx — 6-digit code + device key screen, polls inbox
    for a handshake envelope, assembles the KeyFile on arrival.
  * Welcome.tsx: Pair button now actually routes to <Pair>; imports
    generateKeyFile from lib/crypto (was inlined).

docs/ROADMAP.md delta: alpha5 row flipped to done inline. Alpha6
(feed + wallet) and rc1 (contacts + devices UI + profile) still
pending.
2026-04-22 17:43:18 +03:00

178 lines
8.4 KiB
TypeScript
Raw 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.

// ─── Key material ────────────────────────────────────────────────────────────
export interface KeyFile {
pub_key: string; // hex Ed25519 public key (32 bytes)
priv_key: string; // hex Ed25519 private key (64 bytes)
x25519_pub: string; // hex X25519 public key (32 bytes)
x25519_priv: string; // hex X25519 private key (32 bytes)
}
// ─── Contact ─────────────────────────────────────────────────────────────────
/**
* Тип беседы в v2.0.0 — только direct (1:1 E2E чат). Каналы убраны в
* пользу публичной ленты (см. lib/feed.ts). Поле `kind` осталось ради
* обратной совместимости со старыми записями в AsyncStorage; новые
* контакты не пишут его.
*/
export type ContactKind = 'direct' | 'group';
export interface Contact {
address: string; // Ed25519 pubkey hex — blockchain address
x25519Pub: string; // X25519 pubkey hex — encryption key
username?: string; // @name from registry contract
alias?: string; // local nickname
addedAt: number; // unix ms
/** Legacy field (kept for backward compat with existing AsyncStorage). */
kind?: ContactKind;
/** Количество непрочитанных — опционально, проставляется WS read-receipt'ами. */
unread?: number;
}
// ─── Messages ─────────────────────────────────────────────────────────────────
export interface Envelope {
/** sha256(nonce||ciphertext)[:16] hex — stable server-assigned id. */
id: string;
sender_pub: string; // X25519 hex (this envelope's per-device sender key)
/**
* sender_ed25519_pub (v2.2.0+): the sender's master Ed25519 identity.
* Multiple X25519 pubs under the same identity all share one master —
* clients use THIS to group messages into a single conversation even
* when the sender replies from different devices.
*
* Empty string on legacy envelopes from pre-v2.2.0 senders. Consumers
* should fall back to `sender_pub` in that case (keeps old clients'
* messages visible, even if attribution is per-X25519 rather than
* per-identity).
*/
sender_ed25519_pub: string;
recipient_pub: string; // X25519 hex
nonce: string; // hex 24 bytes
ciphertext: string; // hex NaCl box
timestamp: number; // unix seconds (server's sent_at, normalised client-side)
}
/**
* Вложение к сообщению. MVP — хранится как URI на локальной файловой
* системе клиента (expo-image-picker / expo-document-picker / expo-av
* возвращают именно такие URI). Wire-формат для передачи attachment'ов
* через relay-envelope ещё не финализирован — пока этот тип для UI'а и
* локального отображения.
*
* Формат по kind:
* image — width/height опциональны (image-picker их отдаёт)
* video — same + duration в секундах
* voice — duration в секундах, нет дизайна превью кроме waveform-stub
* file — name + size в байтах, тип через mime
*/
export type AttachmentKind = 'image' | 'video' | 'voice' | 'file';
export interface Attachment {
kind: AttachmentKind;
uri: string; // локальный file:// URI или https:// (incoming decoded)
mime?: string; // 'image/jpeg', 'application/pdf', …
name?: string; // имя файла (для file)
size?: number; // байты (для file)
width?: number; // image/video
height?: number; // image/video
duration?: number; // seconds (video/voice)
/** Для kind='video' — рендерить как круглое видео-сообщение (Telegram-style). */
circle?: boolean;
}
export interface Message {
id: string;
from: string; // X25519 pubkey of sender
text: string;
timestamp: number;
mine: boolean;
/** true если сообщение было отредактировано. Показываем "Edited" в углу. */
edited?: boolean;
/**
* Для mine=true — true если получатель его прочитал.
* UI: пустая галочка = отправлено, filled = прочитано.
* Для mine=false не используется.
*/
read?: boolean;
/** Одно вложение. Multi-attach пока не поддерживается — будет массивом. */
attachment?: Attachment;
/**
* Если сообщение — ответ на другое, здесь лежит ссылка + short preview
* того оригинала. id используется для scroll-to + highlight; text/author
* — для рендера "quoted"-блока внутри текущего bubble'а без запроса
* исходного сообщения (копия замороженная в момент ответа).
*/
replyTo?: {
id: string;
text: string;
author: string; // @username / alias / "you"
};
/**
* Ссылка на пост из ленты. Если присутствует — сообщение рендерится как
* карточка-превью поста (аватар автора, хэндл, текст-excerpt, картинка
* если есть). Тап на карточку → открывается полный пост. Сценарий — юзер
* нажал Share в ленте и отправил пост в этот чат/ЛС.
*
* Содержимое (автор, excerpt) дублируется тут, чтобы карточку можно было
* рендерить оффлайн / когда у хостящей релей-ноды пропал пост — чат
* остаётся читаемым независимо от жизни ленты.
*/
postRef?: {
postID: string;
author: string; // Ed25519 hex — для чипа имени в карточке
excerpt: string; // первые 120 символов тела поста
hasImage?: boolean;
};
}
// ─── Chat ────────────────────────────────────────────────────────────────────
export interface Chat {
contactAddress: string; // Ed25519 pubkey hex
contactX25519: string; // X25519 pubkey hex
username?: string;
alias?: string;
lastMessage?: string;
lastTime?: number;
unread: number;
}
// ─── Contact request ─────────────────────────────────────────────────────────
export interface ContactRequest {
from: string; // Ed25519 pubkey hex
x25519Pub: string; // X25519 pubkey hex; empty until fetched from identity
username?: string;
intro: string; // plaintext intro (stored on-chain)
timestamp: number;
txHash: string;
}
// ─── Transaction ─────────────────────────────────────────────────────────────
export interface TxRecord {
hash: string;
type: string;
from: string;
to?: string;
amount?: number;
fee: number;
timestamp: number;
status: 'confirmed' | 'pending';
}
// ─── Node info ───────────────────────────────────────────────────────────────
export interface NetStats {
total_blocks: number;
total_txs: number;
peer_count: number;
chain_id: string;
}
export interface NodeSettings {
nodeUrl: string;
contractId: string; // username_registry contract
}