From 1d9827beb08e105245b2ba279e8c4cad6d5b769c Mon Sep 17 00:00:00 2001 From: Fredrik Johansson Date: Mon, 29 Jun 2026 15:47:14 +0200 Subject: [PATCH] docs: mark resume UX and history gossip shipped; remove proposal doc - FUTURE.md: remove "not yet done" note on resume UX, update Native UI section to reflect tray/notifications shipped, add new roadmap entries - Delete PROPOSAL-history-gossip.md (implemented, documented in EXTENSIONS.md) Co-Authored-By: Claude Sonnet 4.6 --- FUTURE.md | 8 +- PROPOSAL-history-gossip.md | 154 ------------------------------------- 2 files changed, 6 insertions(+), 156 deletions(-) delete mode 100644 PROPOSAL-history-gossip.md diff --git a/FUTURE.md b/FUTURE.md index e45b8dc..70249a5 100644 --- a/FUTURE.md +++ b/FUTURE.md @@ -91,10 +91,10 @@ DM rooms (`dm:`) appear automatically in both interfaces when messages a - Live progress bar per active transfer - Push (📎) sends directly to a peer without them needing to share a folder -**Not yet done:** daemon-side resume UX (IPC event to surface resumable transfers to the UI on reconnect). +On daemon start, the download directory is scanned for `.tmp.meta` sidecars and a `resumable_transfers` IPC event is emitted so the UI can show pending transfers with a progress bar. ### Native UI -Web frontend (React, already built) + [Wails v2](https://wails.io) shell for native packaging. Wails is Go-native — no Rust toolchain required. The daemon runs embedded in the same process; the webview connects to the existing WebSocket IPC at `ws://127.0.0.1:17338`. Scaffolded in `cmd/app/`; build with `./build-app.sh`. Remaining work: system tray, OS notifications. +Web frontend (React, already built) + [Wails v2](https://wails.io) shell for native packaging. Wails is Go-native — no Rust toolchain required. The daemon runs embedded in the same process; the webview connects to the existing WebSocket IPC at `ws://127.0.0.1:17338`. Built in `cmd/app/` via `./build-app.sh`. System tray (Linux/Windows) and OS notifications are implemented. macOS menu-bar tray requires Cocoa main-thread integration — currently a stub. --- @@ -133,6 +133,10 @@ Web frontend (React, already built) + [Wails v2](https://wails.io) shell for nat | ✅ shipped | PWA manifest — installable via "Add to Home Screen" on iOS and Android | | ✅ shipped | Native desktop app (Wails 2) — system tray (Linux/Windows), OS notifications, single binary | | ✅ shipped | Gitea Actions CI — server binaries (all platforms via cross-compile) + desktop app (Linux amd64) | +| ✅ shipped | File transfer resume UX — resumable transfers surfaced in Transfers panel on reconnect | +| ✅ shipped | P2P message history gossip (EXT-007) — new peers receive recent history from first connected peer | +| ✅ shipped | Date-aware timestamps in TUI and web UI | +| ✅ shipped | Historical peer alias resolution in web UI | --- diff --git a/PROPOSAL-history-gossip.md b/PROPOSAL-history-gossip.md deleted file mode 100644 index 927194e..0000000 --- a/PROPOSAL-history-gossip.md +++ /dev/null @@ -1,154 +0,0 @@ -# Proposal: P2P Message History Gossip - -## Goals - -- New peers joining a network can retrieve recent message history from existing peers -- No central storage — history lives only in peer daemons (SQLite) -- No new trust requirements — history is shared only over already-established encrypted DataChannels -- No duplicates in the local store or UI -- No conflicts — history gossip is append-only and idempotent - ---- - -## Privacy Model - -History is shared **peer-to-peer over the encrypted mesh**, never via the anchor. The anchor remains dumb — it sees only signaling blobs. A peer only receives history from peers they have successfully completed a YAW/2 handshake with, so the same trust boundary as live messages applies. - -A peer can choose not to share history by ignoring `history_request` messages — the protocol is advisory, not mandatory. - ---- - -## Wire Protocol - -Two new YAW/2 extension messages (added to EXTENSIONS.md): - -### `history_request` - -Sent by a newly-connected peer to one or more existing peers shortly after the handshake completes. - -```json -{ - "type": "history_request", - "room": "general", - "since": "2026-01-01T00:00:00Z", - "limit": 200 -} -``` - -| Field | Type | Description | -|---------|-----------------|----------------------------------------------------------| -| `room` | string | Room name to request history for. One request per room. | -| `since` | ISO 8601 or "" | Only return messages newer than this timestamp. Empty = return up to `limit` most recent. | -| `limit` | int (max 500) | Maximum number of messages to return. Responder may return fewer. | - -### `history_chunk` - -Response from an existing peer. May be sent in multiple chunks if `limit` is large. - -```json -{ - "type": "history_chunk", - "room": "general", - "messages": [ - { - "id": "sha256:", - "from": "", - "from_id": "", - "body": "hello", - "ts": "2026-06-01T12:00:00Z", - "room": "general" - } - ], - "done": true -} -``` - -| Field | Type | Description | -|------------|---------|----------------------------------------------------------| -| `messages` | array | Ordered oldest-first. | -| `done` | bool | `true` on the final chunk. Receiver may display after this. | - ---- - -## Message Identity and Deduplication - -Each message has a **content-addressed ID**: - -``` -id = "sha256:" + hex(SHA-256(from_id || room || ts || body)) -``` - -- Computed by the original sender and included in every live message going forward -- The SQLite `messages` table gains an `id TEXT UNIQUE` column -- On insert, use `INSERT OR IGNORE` — receiving the same message twice (live or via gossip) is a no-op -- The UI sorts by `ts`, so late-arriving history slots in correctly without reordering visible messages - -Legacy messages (before this feature) have no `id`. They are assigned a local-only ID on migration and are never gossipped (they have no canonical ID the receiver could deduplicate against). - ---- - -## Daemon-Side Implementation - -### SQLite schema change - -```sql -ALTER TABLE messages ADD COLUMN msg_id TEXT; -CREATE UNIQUE INDEX IF NOT EXISTS idx_messages_msg_id ON messages(msg_id) WHERE msg_id IS NOT NULL; -``` - -### Handling `history_request` - -``` -peer sends history_request{room, since, limit} -→ query SQLite: SELECT * FROM messages WHERE room=? AND ts>? ORDER BY ts ASC LIMIT ? -→ send history_chunk{room, messages, done:true} -``` - -Responder enforces: -- `limit` capped at 500 -- Only messages the responder itself received or sent (no re-gossipping of gossipped history to avoid amplification) -- Rate limit: one `history_request` per peer per room per 60 seconds - -### Handling `history_chunk` - -``` -for each message in chunk: - INSERT OR IGNORE INTO messages (msg_id, room, from_alias, from_id, body, ts) VALUES (...) -emit IPC event: history_loaded{room, count} -``` - -### When to request - -- After handshake completes with the **first** peer in a network (only ask one peer — avoids fan-out) -- Request rooms the local peer knows about (from its own SQLite `rooms` table) -- If no rooms known yet: request `"general"` only; discover others from incoming live messages - ---- - -## IPC / UI Integration - -New IPC event emitted after history is loaded: - -```json -{ "type": "history_loaded", "network_id": "...", "room": "general", "count": 47 } -``` - -The web UI and TUI insert a visual separator above the first gossipped message: - -``` -── 47 earlier messages ───────────────────────────── -[12:03] alice: hey -[12:04] bob: yo -── live ───────────────────────────────────────────── -[14:22] you joined -``` - ---- - -## What This Does Not Do - -- **No conflict resolution** — messages are immutable append-only records; there is nothing to conflict -- **No ordering guarantee beyond timestamp** — if two peers sent messages at the same millisecond, both are stored; the UI sorts by `ts` then `msg_id` for a stable tiebreak -- **No full sync** — gossip is bounded by `limit` and `since`; it is not a replication protocol -- **No anchor involvement** — the anchor never sees history -- **No history from peers who were offline** — if no peer with history is online when you join, you get nothing (acceptable given the trust model)