Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/ADMIN_DISPUTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ Buyers and sellers can send encrypted file or image attachments in dispute chat.
- Rebuilds `admin_dispute_chats` so existing disputes immediately show their chat history in the UI.
- Computes per‑party max timestamps and updates `AppState.admin_chat_last_seen`.
- These timestamps are also stored in the `admin_disputes` table as `buyer_chat_last_seen` and `seller_chat_last_seen`.
- The background listener uses these DB fields as cursors for `fetch_admin_chat_updates`, so only newer NIP‑59 events are fetched after restart. A single-flight guard ensures only one admin chat fetch runs at a time (see `src/util/order_utils/fetch_scheduler.rs`).
- The shared-key chat subscription router (`listen_for_chat_messages` in `src/util/chat_listener.rs`) uses these DB fields as cursors to hydrate history once per key on track (`fetch_gift_wraps_for_shared_key`), then receives newer NIP‑59 events live over one batched `kind: 1059` subscription. Disputes are tracked via `track_dispute_chat` when taken and re-tracked by `track_startup_chats` at startup/reconnect.
- This hybrid approach keeps the protocol stateless while giving admins a smooth, restart-safe chat experience across application restarts.

#### Keyboard Shortcuts
Expand Down
20 changes: 7 additions & 13 deletions docs/MESSAGE_FLOW_AND_PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ In addition to relay-driven trade DMs, Mostrix keeps a lightweight local transcr

- **Path**: `~/.mostrix/orders_chat/<order_id>.txt`
- **Startup restore**: `load_user_order_chats_at_startup` restores cached chat into `AppState.order_chats`, seeds `order_chat_last_seen`, then runs an initial `fetch_user_order_chat_updates` relay backfill.
- **Periodic relay sync (User role)**: on the shared **`admin_chat_interval`** timer (every **2 seconds** in `src/main.rs`), the User-role branch calls `spawn_user_order_chat_fetch`, which polls active orders via `fetch_user_order_chat_updates`. Uses the same single-flight `CHAT_MESSAGES_SEMAPHORE` as admin chat so overlapping fetches are skipped. Shared keys come from persisted `order_chat_shared_key_hex` when set, otherwise ECDH from local `trade_keys` + `counterparty_pubkey` (`src/util/chat_utils.rs`).
- **Live relay sync (User role)**: the **shared-key chat subscription router** (`listen_for_chat_messages` in `src/util/chat_listener.rs`) maintains one batched `kind: 1059` subscription over all active order shared pubkeys and routes incoming gift wraps by `p` tag. `track_startup_chats` seeds the active-order set at startup; the DM router tracks/untracks orders when the shared key becomes resolvable or the order hits a chat-terminal status ([`TERMINAL_DM_STATUSES`](../src/models.rs) — **`success` keeps chat live**). Shared keys come from persisted `order_chat_shared_key_hex` when set, otherwise ECDH from local `trade_keys` + `counterparty_pubkey` (`src/util/chat_utils.rs`).
- **Incremental merge**: `apply_user_order_chat_updates` in `src/ui/helpers/startup.rs`:
- **Skip own relay echoes**: each `OrderChatUpdate` carries `local_trade_pubkey`; messages whose decrypted `sender_pubkey` matches are ignored (same rule as admin chat and Mostro Mobile — avoids showing your send on both **You** and **Peer** after the optimistic local append on Enter).
- **Dedup**: relay self-echoes are skipped by `sender_pubkey == local_trade_pubkey`; peer dedup matches only existing **Peer** rows at the same `(timestamp, content)` (or same attachment / legacy placeholder) so an optimistic **You** line cannot hide a real counterparty message in the same second.
Expand Down Expand Up @@ -588,26 +588,20 @@ User-facing strings for `Payload::CantDo(Some(reason))` come from [`get_cant_do_

Trade DMs carrying `CantDo` are not upserted into the Messages list ([DM_LISTENER_FLOW.md](DM_LISTENER_FLOW.md)); they surface via waiters / `OperationResult` popups.

## Admin Chat Fetch (Single-Flight, Shared-Key Based)
## Admin Chat (Shared-Key Subscription Router)

When the user is in **Admin** mode, the main event loop runs a periodic admin chat sync so the "Disputes in Progress" tab stays up to date with NIP‑59 gift-wrap messages exchanged over **per‑dispute shared keys**.
When the user is in **Admin** mode, the shared-key chat subscription router keeps the "Disputes in Progress" tab up to date with NIP‑59 gift-wrap messages exchanged over **per‑dispute shared keys** — live via a relay subscription, not a timer.

- **Trigger**: Every **2 seconds** on the shared **`admin_chat_interval`** timer in `src/main.rs` when `app.user_role == UserRole::Admin` (the same timer’s User-role branch calls `spawn_user_order_chat_fetch` instead).
- **Shared keys**: For each `AdminDispute` in `InProgress` state, the database may hold `buyer_shared_key_hex` / `seller_shared_key_hex`. At runtime these are converted back to `Keys` via `keys_from_shared_hex` in `src/util/chat_utils.rs`.
- **Entry point**: `spawn_admin_chat_fetch` in `src/util/order_utils/fetch_scheduler.rs` is called with the Nostr client, the current disputes, `admin_chat_last_seen`, and the channel to send results.
- **Single-flight guard**: A shared `AtomicBool` (`CHAT_MESSAGES_SEMAPHORE`) ensures that only one admin chat fetch runs concurrently. If a previous fetch is still running, subsequent ticks are skipped until the flag is cleared.
- **Fetch work**: The spawned task calls `fetch_admin_chat_updates`, which, for every dispute+party that has a stored shared key:
- Rebuilds the shared `Keys` from hex.
- Fetches `Kind::GiftWrap` events **addressed to the shared key’s public key** over a 7‑day rolling window.
- Decrypts each event with the shared key (first trying standard NIP‑59 `from_gift_wrap`, then falling back to the simplified Mostro‑chat format).
- Applies per‑(dispute, party) `last_seen_timestamp` filtering so only newer messages are returned.
- **Trigger**: `execute_take_dispute` calls `track_dispute_chat` for the buyer and seller when a dispute is taken; `track_startup_chats` re-tracks all `InProgress` disputes at startup and on reconnect. Untracked when the dispute leaves `InProgress`.
- **Shared keys**: For each `AdminDispute` in `InProgress` state, the database holds `buyer_shared_key_hex` / `seller_shared_key_hex`, converted back to `Keys` via `keys_from_shared_hex` in `src/util/chat_utils.rs`.
- **Router**: `listen_for_chat_messages` (`src/util/chat_listener.rs`) subscribes one batched `kind: 1059` filter over all tracked shared pubkeys, hydrates history once per key on track (`fetch_gift_wraps_for_shared_key`, 7‑day window, filtered by `last_seen`), and routes live gift wraps by `p` tag.
- **Application**: The main loop receives results on `admin_chat_updates_rx` and applies them via `apply_admin_chat_updates`, which:
- Appends new `DisputeChatMessage` items into `AppState.admin_dispute_chats`.
- Updates in‑memory `admin_chat_last_seen` entries.
- Persists cursors to the `admin_disputes` table (`buyer_chat_last_seen`, `seller_chat_last_seen`) via `update_chat_last_seen_by_dispute_id`.
- **Attachments**: Attachment messages (Mostro Mobile Encrypted File Messaging: `image_encrypted` / `file_encrypted`) are parsed into structured attachment entries. From the dispute chat, the admin presses **Ctrl+S** to open a **Save attachment** popup listing all attachments for the current dispute/party; they select one with ↑/↓ and press Enter to download from Blossom (`blossom://` → `https://`), optionally decrypt with ChaCha20‑Poly1305 (nonce + ciphertext + tag), and save to `~/.mostrix/downloads/<dispute_id>_<filename>`. See `src/util/blossom.rs` and the "Receiving and saving file attachments" section in [ADMIN_DISPUTES.md](ADMIN_DISPUTES.md).

This avoids overlapping relay queries and duplicate work when the 2‑second tick fires before a previous fetch has finished, while ensuring admin chat is driven entirely by the per‑dispute shared keys stored in the database.
Admin chat is driven entirely by the per‑dispute shared keys stored in the database, delivered live over one batched subscription (same model as Mostro Mobile's `SubscriptionManager`).

### Database Errors
Database operations (saving orders, updating trade indices) log errors but don't necessarily fail the entire operation, allowing the user to continue using the client.
Expand Down
24 changes: 11 additions & 13 deletions docs/STARTUP_AND_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,18 +169,16 @@ Several background tasks are spawned to keep the UI and data in sync:
- On `Offline`, startup overlay text indicates automatic retry.
- On `Online`, `main.rs` triggers `reload_runtime_session_after_reconnect(...)` to reconnect
and reload runtime background tasks.
5. **Shared chat relay poll** (`admin_chat_interval`, 2 seconds in `src/main.rs`):
- **Admin role**: triggers `spawn_admin_chat_fetch` → `fetch_admin_chat_updates` (see `src/util/order_utils/fetch_scheduler.rs`).
- For each in-progress dispute, rebuilds per-party shared `Keys` from `buyer_shared_key_hex` / `seller_shared_key_hex` stored in the `admin_disputes` table.
- Fetches NIP‑59 `GiftWrap` events addressed to each shared key's public key (ECDH-derived, same model as `mostro-chat`).
- Uses per‑party `last_seen_timestamp` values to request only new events.
- Delegates application of updates to `ui::helpers::apply_admin_chat_updates` (implemented in `src/ui/helpers/startup.rs`), which:
- Appends new `DisputeChatMessage` items into `AppState.admin_dispute_chats`.
- Persists updated buyer/seller chat cursors in the `admin_disputes` table (`buyer_chat_last_seen`, `seller_chat_last_seen`).
- **User role**: triggers `spawn_user_order_chat_fetch` → `fetch_user_order_chat_updates` on the same timer (shared keys from `order_chat_shared_key_hex` or `trade_keys` + `counterparty_pubkey`; applied via `apply_user_order_chat_updates`).
- A **single-flight guard** (`CHAT_MESSAGES_SEMAPHORE`: `AtomicBool`) ensures only one shared-key chat fetch runs at a time; overlapping ticks skip spawning a new fetch until the current one completes.

**Source**: `src/main.rs` (background task setup), `src/util/order_utils/fetch_scheduler.rs` (admin chat scheduler), `src/ui/helpers/startup.rs` (`apply_admin_chat_updates`)
5. **Shared-key chat subscription router** (`listen_for_chat_messages` in `src/util/chat_listener.rs`):
- A **single long-lived task** (spawned in `src/startup.rs`, respawned on reconnect/key reload) maintains **one** relay subscription whose filter batches **all** active shared-key pubkeys (`kind: 1059`, `#p: [shared pubkeys]`) — the same model as Mostro Mobile's `SubscriptionManager`. No timed polling.
- **Track/untrack** via the global command channel (`ChatRouterCmd`, published by `set_chat_router_cmd_tx`). Helpers: `track_order_chat` / `untrack_order_chat` / `track_dispute_chat` / `untrack_dispute_chat`.
- On `TrackChatKey`, the router hydrates history once (`fetch_gift_wraps_for_shared_key`, filtered by the last-seen cursor), then rebuilds the batched subscription (idempotent: re-tracking an already-tracked key is a no-op).
- Live `kind: 1059` gift wraps are routed by the event's `p` tag to the owning shared key, decrypted with `unwrap_giftwrap_with_shared_key`, and emitted on the existing `admin_chat_updates` / `user_order_chat_updates` channels.
- **Application is unchanged**: `ui::helpers::apply_admin_chat_updates` / `apply_user_order_chat_updates` (in `src/ui/helpers/startup.rs`) still merge updates, persist cursors (`buyer_chat_last_seen` / `seller_chat_last_seen`, order transcripts), and dedupe.
- **Startup track set (option B)**: `track_startup_chats` emits tracks for `Order::get_startup_active_orders` rows (active + `success`) with a resolvable shared key (User), and each `InProgress` dispute's buyer/seller shared key (Admin).
- **Untrack** when an order reaches `TERMINAL_DM_STATUSES` or its row is removed/reverted (DM router hooks), or a dispute leaves `InProgress`. Chat is **not** untracked on `success`; on-disk transcripts preserve history for untracked terminal orders.

**Source**: `src/util/chat_listener.rs` (router), `src/startup.rs` + `src/ui/helpers/startup.rs` (`track_startup_chats`), `src/util/dm_utils/mod.rs` (track/untrack hooks), `src/ui/helpers/startup.rs` (`apply_admin_chat_updates`)

6. **DM Router Wiring (trade messages)**:
- App channel creation includes `dm_subscription_tx` / `dm_subscription_rx`.
Expand Down Expand Up @@ -215,7 +213,7 @@ For **User** role, Mostrix restores peer-to-peer order chat alongside trade DMs:

- Cached transcripts live under `~/.mostrix/orders_chat/<order_id>.txt` and are loaded into `AppState.order_chats` by `load_user_order_chats_at_startup`.
- **Attachment rows in transcripts** are stored as **JSON** (`image_encrypted` / `file_encrypted` via `serialize_attachment_for_transcript`) so **Ctrl+S** and file counts work immediately after restart; legacy `[Image: … - Ctrl+S to save]` lines are hydrated in memory when relay returns the same attachment at the same timestamp.
- An immediate relay fetch (`fetch_user_order_chat_updates`) merges any newer gift-wrap messages; subsequent polls run every **2 seconds** on the shared `admin_chat_interval` timer via `spawn_user_order_chat_fetch` in `src/util/order_utils/fetch_scheduler.rs`.
- An immediate relay fetch (`fetch_user_order_chat_updates`) merges any newer gift-wrap messages; live updates then arrive via the **shared-key chat subscription router** (`listen_for_chat_messages`), which `track_startup_chats` seeds with the active-order set — no timed polling.
- `apply_user_order_chat_updates` skips relay echoes of the local trade pubkey; peer dedup is scoped to existing **Peer** rows so optimistic **You** sends are not mirrored as **Peer** and do not suppress unrelated peer text at the same timestamp. See [MESSAGE_FLOW_AND_PROTOCOL.md](MESSAGE_FLOW_AND_PROTOCOL.md) — "User order chat local cache".

## Main Event Loop
Expand Down
2 changes: 1 addition & 1 deletion docs/TUI_INTERFACE.md
Original file line number Diff line number Diff line change
Expand Up @@ -428,7 +428,7 @@ The key handler processes input in this order:
- Published to relays without blocking the main UI thread.

- **Receiving messages**:
- The main loop calls `spawn_admin_chat_fetch` every **2 seconds** on the shared `admin_chat_interval` timer when in Admin mode (User mode uses the same timer for `spawn_user_order_chat_fetch`). Each spawn runs `fetch_admin_chat_updates` in a one-off task. A single-flight guard (`CHAT_MESSAGES_SEMAPHORE`) ensures only one shared-key chat fetch runs at a time; overlapping interval ticks skip spawning until the current fetch completes.
- The shared-key chat subscription router (`listen_for_chat_messages`) delivers messages live over one batched `kind: 1059` subscription; disputes are tracked via `track_dispute_chat` when taken and re-tracked by `track_startup_chats` at startup/reconnect. History is hydrated once per key on track.
- For each in-progress dispute, the fetch:
- Rebuilds buyer/seller shared `Keys` from the stored hex.
- Fetches `GiftWrap` events addressed to each shared key's public key (7-day rolling window).
Expand Down
Loading