Skip to content

Latest commit

 

History

History
465 lines (350 loc) · 38.3 KB

File metadata and controls

465 lines (350 loc) · 38.3 KB

TUI Interface Guide

This guide explains the architecture and implementation of the Mostrix Text User Interface (TUI).

Core Technologies

Mostrix is built using:

  • Ratatui: For terminal rendering, layouts, and widgets.
  • Crossterm: For terminal manipulation (raw mode, alternate screen) and input event handling.

UI Architecture

1. AppState: The Single Source of Truth

The AppState struct in src/ui/app_state.rs manages the global state of the interface.

Source: src/ui/app_state.rs

pub struct AppState {
    pub user_role: UserRole,
    pub active_tab: Tab,
    pub selected_order_idx: usize,
    pub selected_dispute_idx: usize,
    pub selected_in_progress_idx: usize,
    pub active_chat_party: ChatParty,
    pub admin_chat_input: String,
    pub admin_chat_input_enabled: bool,
    pub admin_dispute_chats: HashMap<String, Vec<DisputeChatMessage>>,
    pub admin_chat_scrollview_state: tui_scrollview::ScrollViewState,
    pub admin_chat_selected_message_idx: Option<usize>,
    pub admin_chat_line_starts: Vec<usize>,
    pub admin_chat_scroll_tracker: Option<(String, ChatParty, usize)>,
    pub admin_chat_last_seen: HashMap<(String, ChatParty), AdminChatLastSeen>,
    pub selected_settings_option: usize,
    pub mode: UiMode,
    pub messages: Arc<Mutex<Vec<OrderMessage>>>,
    pub active_order_trade_indices: Arc<Mutex<HashMap<uuid::Uuid, i64>>>,
    pub selected_message_idx: usize,
    pub pending_notifications: Arc<Mutex<usize>>,
    // …see source for observer and attachment fields…
}

2. UI Layout

The screen is divided into three horizontal chunks using ratatui layouts. The available tabs and layout change dynamically based on the active UserRole.

Source: src/ui/mod.rs:523

    let chunks = Layout::new(
        Direction::Vertical,
        [
            Constraint::Length(3),
            Constraint::Min(0),
            Constraint::Length(1),
        ],
    )
    .split(f.area());
  1. Header (3 lines): Renders the navigation tabs. The tab list is determined by the UserRole (User vs Admin).
  2. Body (remaining space): Renders the active tab content or forms.
  3. Footer / Status bar (3 lines): Renders the multi-line status bar with settings + connection details.

Roles and Navigation

Mostrix supports two distinct roles, each with its own set of tabs and workflows.

User Role

Focused on trading and order management.

  • Orders: View the global order book.
  • My Trades: Manage active trades.
  • Messages: Direct messages for trade coordination.
  • Settings: Local configuration, including key rotation via Generate New Keys and mnemonic backup prompts. User mode only: Set Lightning Address (buyer) / Clear Lightning Address — optional user@domain.com stored in settings.toml; confirm-save fetches LNURL metadata (payRequest) before persisting (see src/util/ln_address.rs, spawn_verify_and_save_ln_address_task). The visible menu and Enter routing share ADMIN_SETTINGS / USER_SETTINGS in src/ui/tabs/settings_tab.rs (SettingsMenuAction + label per row; settings_action_for_index).
  • Create New Order: Form for publishing new orders.

Admin Role

Focused on dispute resolution and protocol management.

  • Disputes Pending: List of disputes waiting to be taken. Only displays disputes with Initiated status (filtering implemented in disputes_tab.rs). Admins can select and take ownership of these disputes.
  • Disputes in Progress: Complete workspace for managing taken disputes (state: InProgress), featuring:
    • Integrated chat system with buyer and seller
    • Comprehensive dispute information header
    • Dynamic message input with text wrapping
    • Chat history with scrolling (PageUp/PageDown)
    • Finalization popup for resolution actions
    • Empty state: When no disputes are available, displays helpful key hints footer (filter + ↑↓: Select Dispute | Ctrl+H: Help); footer is width-aware (narrow terminals show only Ctrl+H).
  • Observer: Read-only workspace for inspecting user-to-user encrypted chats via a shared key:
    • Single shared-key input (64-char hex secret, paste-friendly)
    • Fetches NIP-59 gift-wrap messages from relays for the last 7 days using the shared key's public key
    • Decrypts messages and maps sender pubkeys to Buyer/Seller/Admin roles automatically
    • Displays chat using the same formatting as the dispute chat (color-coded, right-aligned Buyer/Seller, left-aligned Admin)
    • Supports file/image attachments with Ctrl+S to save (same popup as dispute chat)
    • Keyboard hints: Enter to fetch chat, Ctrl+C to clear all, Ctrl+S to save attachment, Ctrl+H for help
  • Settings: Role-specific configuration including:
    • Add Dispute Solver
    • Generate New Keys (rotates the Admin keypair)
    • Manage relays and currency filters

For detailed information about admin dispute resolution workflows, see ADMIN_DISPUTES.md and FINALIZE_DISPUTES.md.

UI Modes & State Machine

The interface uses a nested state machine defined by UiMode, UserMode, and AdminMode.

Source: src/ui/app_state.rs

pub enum UiMode {
    // Shared modes (available to both user and admin)
    Normal,
    ViewingMessage(MessageViewState),
    RatingOrder(RatingOrderState), // 1–5 stars → RateUser DM (Messages tab, action `rate`)
    NewMessageNotification(MessageNotification, Action, InvoiceInputState),
    OperationResult(OperationResult), // Generic operation result popup (success/info/error)
    HelpPopup(Tab, Box<UiMode>),      // Context-aware keyboard shortcuts (Ctrl+H); Box<UiMode> = mode to restore on close
    SaveAttachmentPopup(usize),              // Dispute chat: list index of selected attachment (Ctrl+S opens, ↑↓/Enter/Esc in popup)
    ObserverSaveAttachmentPopup(usize),      // Observer tab: list index of selected attachment (Ctrl+S opens, ↑↓/Enter/Esc in popup)
    UserSaveAttachmentPopup(String, usize),  // My Trades: pinned order_id + list index (Ctrl+S; order_id pinned so sidebar changes do not retarget save)
    UserSendAttachmentPicker(String),        // My Trades: pinned order_id + ratatui-explorer (Ctrl+O; Enter on file enqueues send job)

    // User-specific modes
    UserMode(UserMode),

    // Admin-specific modes
    AdminMode(AdminMode),
}

Overlays (Popups)

Popups are implemented by rendering additional widgets on top of the main layout when the UiMode is not Normal. They are drawn at the end of the ui_draw function to ensure they appear as overlays.

The primary shared popup is the operation result modal, used for:

  • Order creation / take-order flows
  • Settings validation errors (invalid pubkey, relay, currency, Lightning address format, LNURL verification failure, etc.)
  • Admin actions (add solver, finalize disputes)
  • Blossom attachment downloads and Observer-mode shared-key errors

When the popup is closed (Esc or Enter) from the Disputes in Progress tab, the app stays on that tab and returns to ManagingDispute mode (it does not switch to the first tab).

Example: Rendering the OperationResult popup.

Source: src/ui/operation_result.rs (rendering), src/ui/orders.rs (OperationResult enum). Notable variants handled in handle_operation_result:

Variant Effect
Success My Trades placeholder + order_chat_static
PaymentRequestRequired Opens Pay / bond invoice popup (NewMessageNotification)
OpenInvoicePopup Opens Add Invoice or waiting popup from synchronous execute (bond payout reply); does not show the operation-result modal
InvoiceSubmitted Normalized to Info toast after optional buyer LN-address preference
TradeClosed / OrderHistoryDeleted Side effects on Messages list, then Info toast
OrderChatAttachmentSent Appends You chat row + JSON transcript save; clears pending_order_attachment_sends and sending_attachment_order_id when order_id matches; then normalized to Info (Attachment sent: …)
OrderChatAttachmentError Early send failure (validate / encrypt / upload before DM); clears sending_attachment_order_id when order_id matches; normalized to Error popup. Generic OperationResult::Error from other tasks does not clear the in-flight send guard.
OrderChatAttachmentSendFailed Stores PreparedOrderChatAttachment in AppState.pending_order_attachment_sends; clears sending_attachment_order_id when order_id matches; normalized to Error (Blossom URL + Ctrl+Shift+O retry hint)

OperationResult::Info / Error text: render_operation_result splits on newlines, wraps at word boundaries (avoids mid-word breaks on long UUIDs), and sizes the popup from line count. Admin dispute finalization success uses a structured multi-line body from BondSlashChoice::finalize_success_message (see FINALIZE_DISPUTES.md).

// Operation result popup overlay (shared)
if let UiMode::OperationResult(result) = &app.mode {
    operation_result::render_operation_result(f, result);
}

Help popup (Ctrl+H):

  • Open: Press Ctrl+H in normal or managing-dispute mode to show a context-aware shortcuts overlay for the current tab (Disputes in Progress, Observer, Settings, Orders, etc.).
  • Content: The popup lists all relevant key bindings for that tab; e.g. in Disputes in Progress it shows filter toggle, Tab/Enter/Shift+I/Shift+F, scroll keys, and Ctrl+S to open the save-attachment list when applicable. On My Trades it includes PgUp/PgDn/End chat scroll, Ctrl+S (save attachment list), Ctrl+O (send file picker), and Ctrl+Shift+O (retry DM after upload ok / send failed).
  • Close: Esc, Enter, or Ctrl+H close the popup; other keys are absorbed while it is open.
  • Source: src/ui/help_popup.rs (rendering), src/ui/key_handler/mod.rs (Ctrl+H and close handling).

Save attachment popup (Ctrl+S in Disputes in Progress, Observer tab, or My Trades):

  • Open: When managing a dispute, viewing an observer chat, or on My Trades with a selected active order, press Ctrl+S to open a centered popup listing all file/image attachments. In Disputes in Progress, attachments are scoped to the current dispute and active party (Buyer or Seller). In Observer mode, attachments are drawn from all observer messages. On My Trades, attachments come from the selected order’s chat transcript (get_order_attachment_messages). If there are no attachments, Ctrl+S does nothing.
  • In popup: ↑/↓ change selection, Enter enqueues the selected attachment on save_attachment_tx and closes the popup; Esc cancels. Other keys are absorbed. Footer shows "↑↓ Select, Enter Save, Esc Cancel".
  • Async download + popup: spawn_save_attachment runs on a Tokio task and sends OperationResult::Info ("Saved to …") or Error on order_result_tx. The main loop applies those results in apply_order_result and also try_recvs attachment and result channels before every frame (drain_save_attachment_queue, drain_send_order_attachment_queue, drain_order_result_queue in src/main.rs) so the operation-result modal appears without waiting for another key (My Trades may enqueue the job after a short async DB key lookup).
  • Saveable list: only attachments with a non-empty Blossom URL appear in the popup (attachment_is_saveable / get_order_attachment_messages in src/ui/helpers/chat_visibility.rs).
  • My Trades decrypt: when the sender did not embed a key in the attachment JSON, Mostrix derives the shared ChaCha20 key from order_chat_shared_key_hex or ECDH (order_chat_decryption_key_bytes in src/util/chat_utils.rs) before writing the file.
  • Source: src/ui/save_attachment_popup.rs (dispute, observer, and user order popups), src/ui/key_handler/mod.rs (open and popup key handling), src/main.rs (queue drain), src/ui/constants.rs (SAVE_ATTACHMENT_POPUP_HINT, FOOTER_CTRL_S_SAVE_FILE).

Send attachment picker (Ctrl+O on My Trades):

  • Open: On My Trades with a selected active order, press Ctrl+O while user_my_trades_interactive() is true. Opens UiMode::UserSendAttachmentPicker(order_id) with a ratatui-explorer modal (build_send_attachment_explorer in src/ui/send_attachment_picker.rs). Starts in dirs::document_dir() or $HOME. Does nothing while sending_attachment_order_id is set (send already in flight). Build failures show OperationResult::Error ("Could not open file picker: …").
  • Filter: Only directories and files whose extension passes attachment_extension_allowed (jpg/jpeg/png/pdf/mp4/mov/avi/doc/docx in src/util/file_validation.rs) appear in the list.
  • In picker: h/j/k/l (and other explorer keys routed via FileExplorer::handle) navigate; Enter on a regular file (not .. or a directory) enqueues SendOrderAttachmentJob::FromPath { order_id, path } on send_order_attachment_tx, sets sending_attachment_order_id, and closes the picker; Esc cancels. Footer hint: SEND_ATTACHMENT_PICKER_HINT ("Enter: Send file | Esc: Cancel | h/j/k/l: Navigate | …").
  • Retry: Ctrl+Shift+O on the same selected order enqueues SendOrderAttachmentJob::RetryPrepared when AppState.pending_order_attachment_sends holds that order (upload succeeded but shared-key DM failed). Same in-flight guard as Ctrl+O.
  • Async pipeline + popup: spawn_send_order_chat_attachment in src/util/send_attachment.rs validates (including PNG/JPEG dimensions for images), encrypts, uploads to Blossom, builds mobile-compatible wire JSON, and sends the DM. Results arrive on order_result_tx as OrderChatAttachmentSent, OrderChatAttachmentError (early failure), or OrderChatAttachmentSendFailed (upload ok / DM fail); handle_operation_result clears sending_attachment_order_id only for those attachment-specific variants (scoped by order_id). The main loop drains send_order_attachment_rx and order_result_rx before every draw (see STARTUP_AND_CONFIG.md).
  • Source: src/ui/send_attachment_picker.rs, src/ui/key_handler/mod.rs (Ctrl+O / Ctrl+Shift+O and picker keys), src/util/send_attachment.rs, src/ui/constants.rs (FOOTER_CTRL_O_SEND_FILE, FOOTER_CTRL_SHIFT_O_RETRY, FOOTER_SENDING_ATTACHMENT, HELP_MY_TRADES_CTRL_O_SEND, HELP_MY_TRADES_CTRL_SHIFT_O_RETRY).

Backup New Keys popup (first launch + key rotation):

  • Purpose: Displays the newly generated 12-word mnemonic so it can be backed up after Generate New Keys.
  • Where it appears:
    • On key rotation: shown as an overlay popup.
    • On very first launch: shown as an overlay on the initial Orders/Disputes tab (Mostrix does not force switching to the Settings tab).
  • Reminder: After saving the mnemonic, restart Mostrix so it uses the rotated keys everywhere.

UI constants

Shared copy (help titles, footer hints, filter labels) lives in src/ui/constants.rs so strings are defined once and reused by the help popup and by width-aware footers (e.g. Disputes in Progress). Use these constants when adding or changing UI text to avoid duplication.

Navigation & Input Handling

Input handling is centralized in src/ui/key_handler/ (mod.rs and submodules: enter_handlers, esc_handlers, navigation, etc.).

Tab Navigation

Users can switch between roles (User/Admin) and tabs using arrow keys.

  • Left/Right: Switch tabs.
  • Up/Down: Navigate within lists (Order book, Messages).

Mode-Specific Dispatch

The handle_key_event function dispatches keys based on the current UiMode.

Example: Handling the Enter key (dispatched from key_handler/mod.rs to enter_handlers::handle_enter_key). Async feedback uses purpose-specific channels from EnterKeyContext: order_result_tx for orders, takes, disputes, bulk history cleanup, attachments, etc., and ln_address_result_tx for Lightning address verify-and-save only (LnAddressVerifyResult → main loop maps to OperationResult for the shared popup).

Specialized Input

  • Forms: Character input and Backspace are handled by form_input::handle_char_input and form_input::handle_backspace for fields in FormState while UiMode::UserMode(UserMode::CreatingOrder(_)).
    • Create New Order (src/ui/order_form.rs, src/ui/orders.rsFormState / FormField): Tab / Shift+Tab cycle focus; Space toggles buy/sell on Order Type and single/range on Fiat Amount; Enter submits; Esc cancels.
    • Global shortcut guard: n / N (cancel) and c / C (copy invoice / observer clear) are handled before the generic Char(_) arm in key_handler/mod.rs. When a text field is focused (is_creating_order_text_input in form_input.rs — any field except Order Type), those keys are routed to form typing instead (fixes payment method labels like SEPA / Bizum). Outside the form, n still drives confirmation cancel (handle_cancel_key); c still copies PayInvoice / PayBondInvoice invoices.
  • Invoices: handle_invoice_input handles text entry for Lightning invoices, including support for bracketed paste mode.
  • Paste support: The event loop now centralizes paste routing for active inputs and supports:
    • Event::Paste(...) (bracketed paste)
    • mouse right-click paste (MouseEventKind::Down(MouseButton::Right)) using clipboard read fallback This applies to invoice input, admin key/solver inputs, and observer shared-key input.
  • Admin Chat: handle_admin_chat_input handles direct text input in the "Disputes in Progress" tab:
    • Takes priority over other input handling (except invoice and key input)
    • Supports direct character input and backspace
    • Dynamic input box that grows from 1 to 10 lines
    • Text wrapping with word boundary detection
    • Input toggle: Press Shift+I to enable/disable chat input (prevents accidental typing)
    • Visual feedback: Input title shows enabled/disabled state
  • Copy to Clipboard: Pressing C in a PayInvoice or PayBondInvoice notification uses the arboard crate to copy the invoice. On Linux, it uses the SetExtLinux::wait() method to properly wait until the clipboard is overwritten, ensuring reliable clipboard handling without arbitrary delays.
  • Exit Confirmation: Pressing Q or selecting the Exit tab shows a confirmation popup before exiting the application. Use Left/Right to select Yes/No, Enter to confirm, or Esc to cancel.
  • Help popup: Press Ctrl+H (in normal or managing-dispute mode) to open a centered overlay with all keyboard shortcuts for the current tab. Press Esc, Enter, or Ctrl+H to close.

UI Components

1. Orders Tab

Renders a table of pending orders from the Mostro network. Status and order kinds are color-coded for readability.

Source: src/ui/orders_tab.rs

2. Messages Tab

Displays a list of direct messages related to the user's trades. Messages are tracked as read or unread. The detail panel includes a trade timeline stepper (six columns): FlowStep from src/ui/orders.rs (message_trade_timeline_step), with per-column copy from src/ui/constants.rs (listing_timeline_labels). See buy order flow.md and sell order flow.md.

  • Sidebar labels: message_action_compact_label_for_message prefers order_status over raw action (e.g. Pending order, Trade Completed) so reboot replay does not show stale action text. Non-actionable rows opened with Enter use that compact label in an OperationResult::Info popup (enter_handlers.rs).

  • Pending / bond-pending timeline: Status::Pending, Status::WaitingTakerBond, and Status::WaitingMakerBond map to StepPendingOrder (discriminant 0). The stepper uses step_number() == 0, so no column is highlighted (all steps gray) until the trade advances — avoids falsely lighting step 1 while the order is still on the book or awaiting bond.

  • Post-success placeholder row: when OperationResult::Success arrives before any DM row exists, try_placeholder_order_message_from_success (orders.rs) inserts one synthetic OrderMessage for My Trades / Messages (action from status: maker → NewOrder or PayBondInvoice when WaitingMakerBond; taker → PayBondInvoice / WaitingSellerToPay / etc.; never synthetic take-buy / take-sell). main.rs also resyncs My Trades from DB after Success, not only after history delete.

  • Enter on a row: opens an invoice popup, a confirmation popup, the rating overlay (RatingOrder) when the daemon sent action: rate, or an info line for other actions (src/ui/key_handler/enter_handlers.rs).

  • Rating overlay: render_rating_order in src/ui/tabs/tab_content.rs; keys Left/Right or +/- adjust stars, Enter submits, Esc closes.

  • Invoice popups (NewMessageNotification):

    • AddInvoice / WaitingBuyerInvoice map to invoice-submit popup mode. If settings.toml ln_address is non-empty, UiMode::ConfirmSavedLnAddressForInvoice may appear first (YES / NO), listing the saved address; Left/Right moves the highlight; YES + Enter auto-submits AddInvoice via submit_add_invoice (message_handlers.rs) without opening the invoice input popup (UseSavedLnAddress is saved only after the send succeeds — OperationResult::InvoiceSubmitted in order_ch_mng.rs). Choose NO and press Enter to open the manual invoice UI (ManualInvoice). Esc closes the confirm popup without committing YES or NO (handle_esc_key in esc_handlers.rs). BuyerInvoicePreference per order_id (src/ui/app_state.rs, src/ui/orders.rs) remembers the choice for that trade until Cancel Order from the popup removes it (message_handlers.rs) or the trade row is torn down (order_ch_mng.rs).
    • PayInvoice / WaitingSellerToPay map to payment popup mode.
    • PayBondInvoice (Mostro Phase 1.5+ taker bond / Phase 5+ maker bond) maps to a dedicated bond popup mode (render_pay_bond_invoice in src/ui/message_notification.rs). It mirrors the PayInvoice layout but uses a 🛡️ title (Anti-abuse Bond Invoice), a taker or maker amount label (via MessageNotification.maker_bond_publish: "Bond invoice to pay" vs "Pay bond to publish your order"), and a yellow "Locked, not spent — refunded on normal completion" disclaimer. Primary button is Acknowledge (closes the popup; payment happens in the user's wallet); Cancel Order is still wired to Action::Cancel. The popup is gated on order_status ∈ {WaitingTakerBond, WaitingMakerBond, None} and role (local_user_must_act_on_invoice_popup — status-based for PayBondInvoice, not listing kind). Sync paths: take_order (taker) and send_new_order (maker) return PaymentRequestRequired. Bonds are configurable in mostrod — when not enabled, create/take flows skip this popup.
    • All three popups provide two actions (Primary + Cancel Order) via Left/Right selection; Enter confirms the selected action.
    • PayInvoice and PayBondInvoice keep copy (C) and scroll (Up/Down, PageUp/PageDown) behavior while adding cancel selection.

ViewingMessage (trade confirmations)render_message_view in src/ui/tabs/tab_content.rs:

  • Used for actionable events that need a YES/NO before sending a follow-up to Mostro: HoldInvoicePaymentAccepted, FiatSentOk, CooperativeCancelInitiatedByPeer, plus explicit user-triggered actions from My Trades (Cancel, FiatSent, Release).
  • Buttons are rendered with helpers::render_yes_no_buttons (same visual pattern as exit/settings confirms: ✓ YES / ✗ NO, Left/Right to move selection, Enter to confirm, Esc to dismiss).
  • Handler: handle_enter_viewing_message in src/ui/key_handler/message_handlers.rs (only proceeds when YES is selected). Cooperative cancel completion surfaces OperationResult::TradeClosed, which handle_operation_result resolves after removing the row from the Messages list (see MESSAGE_FLOW_AND_PROTOCOL.md).

Source: src/ui/tab_content.rs (render_messages_tab, render_message_view, render_rating_order)

3. Order Form

A stateful form for creating new orders. It supports both fixed amounts and fiat ranges.

Fields (FormField in src/ui/orders.rs): Order Type (toggle), Currency, Amount (sats), Fiat Amount (+ optional max when range), Payment Method, Premium (%), Invoice (optional), Expiration (days).

Source: src/ui/order_form.rs, src/ui/key_handler/form_input.rs, src/ui/key_handler/navigation.rs (Tab focus)

My Trades interactive mode (user_my_trades_interactive)

Source: src/ui/app_state.rsUiMode::default_for_role, UiMode::user_my_trades_interactive

  • On AppState::new and switch_role, user mode starts in UiMode::UserMode(UserMode::Normal) (not bare UiMode::Normal alone).
  • Chat input, Ctrl+S, Ctrl+O, Ctrl+Shift+O, PgUp/PgDn/End scroll, and related shortcuts are active only when app.mode.user_my_trades_interactive() is true: UiMode::Normal or UiMode::UserMode(UserMode::Normal).
  • Popups (save attachment, send attachment picker, operation result, viewing message, etc.) set other UiMode variants; My Trades shortcuts resume when the popup closes back to normal user mode.

My Trades (Order In Progress) updates

The My Trades workspace (src/ui/tabs/order_in_progress_tab.rs) now shows richer order context and cleaner chat readability:

  • Header metadata (two layers):
    • Stable (one-time): order id, kind, created-at, trade index, and initiator (role: Maker/Taker + truncated trade pubkey) come from OrderChatStaticHeader in AppState.order_chat_static. The map is filled when the user creates an order (maker) or takes an order (taker, including the PaymentRequestRequired path for both PayInvoice and PayBondInvoice responses — the variant carries the originating Action), and from sync_user_order_history_messages_from_db in src/ui/helpers/startup.rs (parses local orders rows so restarts do not lose the header). Entries are removed when a trade is closed or history cleanup deletes the row (handle_operation_result in src/util/dm_utils/order_ch_mng.rs).
    • Live (from DMs): status, amount / fiats, payment method, premium, and buyer/seller rating (when present) still come from the message projection (see below).
  • Privacy / ratings: there is no placeholder row for Privacy: / Buyer - / Seller - until trade privacy can be sourced from the same context as disputes (DM SmallOrder does not carry those flags). Buyer Rating: / Seller Rating: lines are shown only when reputation exists: helpers::build_active_order_chat_list merges Payload::Peer with UserInfo when peer.pubkey matches buyer_trade_pubkey / seller_trade_pubkey from Payload::Order, and the header uses helpers::format_user_rating for display.
  • Chat rendering: user/peer messages are wrapped to fit pane width (including splitting overlong tokens by Unicode character count so lines do not overflow); peer messages are right-aligned for better sender separation.
  • Chat scrolling: message history uses tui_scrollview::ScrollView with full content height (not viewport height) and an always-visible vertical scrollbar — same pattern as Disputes in Progress and Observer. PgUp/PgDn scroll the chat; End jumps to the latest messages. Auto-scroll-to-bottom runs when new messages arrive, when switching orders, or after sending (order_chat_scroll_tracker, scroll_order_chat_messages / scroll_order_chat_after_send in src/ui/key_handler/chat_helpers.rs).
  • Attachments (receive + transcript): encrypted file/image messages show as yellow lines with 🖼/📎 icons; the block title adds a file count; a yellow toast appears on new peer relay merges. Transcripts under ~/.mostrix/orders_chat/ persist attachment metadata as JSON so Ctrl+S works after restart (legacy placeholder lines hydrate from relay). Ctrl+S opens the save popup (see above).
  • Attachments (send): Ctrl+O opens UserSendAttachmentPicker (ratatui-explorer in src/ui/send_attachment_picker.rs); Enter on a file enqueues SendOrderAttachmentJob::FromPath on send_order_attachment_tx. Backend in src/util/send_attachment.rs: validate → encrypt (shared key) → Blossom upload (NIP-24242 auth signed with order trade key) → shared-key DM. Ctrl+Shift+O enqueues RetryPrepared when AppState.pending_order_attachment_sends has the selected order (upload ok / DM failed). sending_attachment_order_id blocks duplicate sends while in flight; cleared only by attachment-specific OperationResult variants in order_ch_mng.rs, not by unrelated errors on order_result_tx.
  • Empty states: sidebar/main panel copy is clearer ("No active orders yet"), and the help hint remains visible in the footer.
  • Footer shortcuts (width-aware):
    • Shift+I toggles chat input.
    • Shift+C cooperative cancel (YES/NO popup).
    • Shift+F mark fiat sent (YES/NO popup).
    • Shift+R release sats (YES/NO popup).
    • Shift+V rate counterparty (opens 1–5 star rating picker).
    • PgUp/PgDn scroll chat history; End jump to bottom.
    • Ctrl+S save attachment (when the selected order has attachments).
    • Ctrl+O send attachment (file picker); Ctrl+Shift+O retry DM when a prepared send is pending.
    • Sending attachment… (FOOTER_SENDING_ATTACHMENT) while sending_attachment_order_id matches the selected order.
    • Shift+H opens the shortcuts popup for the current tab.
  • Projection vs static: the DM-based list row (OrderChatListItem in src/ui/helpers/order_chat_projection.rs) holds live fields only: status, first-seen economic snapshot from Payload::Order (amount, fiat, payment, premium), trade_index (from any message in the order), and buyer/seller trade pubkeys plus reputation from Payload::Peer. It no longer carries kind, created_at, or initiator metadata (those are on order_chat_static above).
  • Selection correctness (shared projection): both the sidebar list and Enter/send handlers derive the selected order from the same projection (helpers::build_active_order_chat_list), with identical filtering and ordering. This prevents UI/action desync where selected_order_chat_idx could resolve a different trade than the highlighted row. Trade ID in the header still falls back to projection trade_index if a static entry is not yet in the map (e.g. race before the result handler runs).

Source: src/ui/tabs/order_in_progress_tab.rs

4. Color Coding

Mostrix uses a consistent color palette defined in src/ui/mod.rs:

  • PRIMARY_COLOR: #b1cc33 (Mostro green).
  • BACKGROUND_COLOR: #1D212C.
  • Status Colors: Yellow for pending, Green for active/success, Red for disputes/cancellation.
  • Chat Colors:
    • Cyan for Admin messages
    • Green for Buyer messages
    • Red for Seller messages

Source: src/ui/mod.rs (color constants), src/ui/orders_tab.rs and src/ui/disputes_in_progress_tab.rs (status colors), src/ui/disputes_in_progress_tab.rs (chat colors)

5. Admin Chat System

Status: ✅ Fully Implemented (NIP‑59 + Shared Keys)

The admin chat system in the "Disputes in Progress" tab provides real-time, Nostr-based communication using NIP‑59 gift-wrap events and per‑dispute shared keys derived between the admin key and each party’s trade pubkey.

Helper module organization (readability refactor)

The previous monolithic helper file was split into focused modules under src/ui/helpers/:

  • mod.rs: compatibility re-export layer (crate::ui::helpers::...) for existing call sites.
  • layout.rs: popup/help/button rendering helpers.
  • formatting.rs: UI formatting helpers (ratings, order id labels, finalized status check).
  • chat_visibility.rs: per-party visibility and selection helpers.
  • chat_render.rs: wrapped line formatting plus list/scrollview builders.
  • chat_storage.rs: transcript parse/load/save logic for disputes and user order chat.
  • attachments.rs: attachment JSON parse/serialize, outbound build_*_encrypted_json helpers, placeholders, and toast expiration/building.
  • order_chat_projection.rs: shared "My Trades" active-order projection (single source of truth for sidebar ordering and Enter/action resolution). Merges Payload::Order (economic first snapshot, buyer/seller trade pubkeys) and Payload::Peer (reputation when pubkeys match). Payload merge runs even when order_kind is missing on a message, so Peer reputation is not skipped for odd DM shapes.
  • startup.rs: startup hydration/recovery, applying chat updates to app state, and seeding order_chat_static from orders when syncing user history.

Data Structures

pub enum ChatSender {
    Admin,
    Buyer,
    Seller,
}

pub struct DisputeChatMessage {
    pub sender: ChatSender,
    pub content: String,
    pub timestamp: i64,
    pub target_party: Option<ChatParty>, // For Admin messages: which party this was sent to
}

pub struct AdminChatLastSeen {
    pub last_seen_timestamp: Option<u64>, // Last seen message timestamp for incremental fetches
}

Storage:

  • AppState.admin_dispute_chats: HashMap<String, Vec<DisputeChatMessage>> keyed by dispute ID.
  • AppState.admin_chat_last_seen: HashMap<(String, ChatParty), AdminChatLastSeen> keyed by (dispute_id, party).

UI Features

  • Direct input: Type immediately without mode switching (when input enabled).
  • Input toggle: Press Shift+I to enable/disable chat input.
  • Dynamic sizing: Input box grows from 1 to 10 lines based on content.
  • Text wrapping: Intelligent word-boundary wrapping with trim behavior.
  • Scrolling:
    • PageUp/PageDown: Navigate through message history.
    • End: Jump to bottom of chat (latest messages).
    • Visual scrollbar: Right-side scrollbar shows position (↑/↓/│/█ symbols).
  • Party filtering:
    • Admin messages are only shown in the chat view of the party they were sent to (based on target_party).
    • Buyer/Seller messages are only shown in their respective chat views.
  • Visual feedback: Focus indicators, color-coded messages, alignment prefixes, input state indicators.

Input Handling Priority

The key handler processes input in this order:

  1. Invoice input (highest priority, when in invoice mode).
  2. Key input (for settings popups).
  3. Shift+I toggle (for enabling/disabling admin chat input).
  4. Admin chat input (takes priority in Disputes in Progress tab, only when enabled).
  5. Other character/form input.

Source: src/ui/key_handler/mod.rs (handle_admin_chat_input, Shift+I toggle).

NIP‑59 Chat Internals (Shared Key Model)

  • Shared key derivation:

    • When a dispute is taken (AdminDispute::new), per-party shared keys are eagerly derived using ECDH: nostr_sdk::util::generate_shared_key(admin_secret, counterparty_pubkey).
    • Two shared keys are stored (as hex) in the admin_disputes table: buyer_shared_key_hex and seller_shared_key_hex.
    • The same derivation is used by mostro-chat so both the admin and the counterparty can independently derive the same shared key and subscribe to the same events.
  • Message addressing:

    • Admin chat messages are addressed to the shared key's public key (not the counterparty's trade pubkey directly).
    • The admin reads admin_privkey from settings.toml to sign the inner rumor; the gift wrap p tag targets the shared key pubkey.
  • Sending messages:

    • Admin messages are sent via send_admin_chat_message_via_shared_key (spawned as an async task to avoid blocking the UI):
      • Rumor content: Mostro protocol format (Message::Dm(SendDm, TextMessage(...)), None).
      • The gift wrap is built using EventBuilder::gift_wrap with the admin keys and the shared key public key as the recipient.
      • 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.
    • 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).
      • Decrypts each event using the shared key (standard NIP-59 or simplified mostro-chat format).
      • Uses last_seen_timestamp to skip already-processed events.
      • Skips events signed by the admin identity to avoid duplicating locally-sent messages.
  • Behavior on restart (Chat Restore at Startup):

    • Admin chat uses a hybrid persistence model to provide instant UI restore and incremental sync:
      • For each in‑progress dispute, chat transcripts are stored as human‑readable files under:

        ~/.mostrix/disputes_chat/<dispute_id>.txt
        
      • On startup, recover_admin_chat_from_files:

        • Reads each existing transcript file.
        • Rebuilds AppState.admin_dispute_chats so the Disputes in Progress tab immediately shows previous messages.
        • Computes the latest timestamps per party and updates AppState.admin_chat_last_seen.
      • The latest buyer/seller timestamps are also persisted in the admin_disputes table (buyer_chat_last_seen, seller_chat_last_seen) via update_chat_last_seen_by_dispute_id so that:

        • Background NIP‑59 fetches only request newer events (7-day rolling window).
        • Chat resumes from where it left off without replaying the full history.

Exit Confirmation

Mostrix includes a safety feature to prevent accidental exits:

  • Trigger: Navigate to the Exit tab (User or Admin) and confirm
  • Popup: Shows confirmation dialog with "Are you sure you want to exit Mostrix?"
  • Navigation: Use Left/Right arrows to select Yes/No buttons
  • Confirmation: Press Enter to confirm exit, or Esc to cancel
  • Visual: Green "✓ YES" button and red "✗ NO" button with clear styling

Source: src/ui/exit_confirm.rs, src/ui/key_handler/enter_handlers.rs