Skip to content

Admin mode - Phase1 : Introducing Settings tab #24

Merged
grunch merged 18 commits into
mainfrom
admin-list-disputes
Jan 15, 2026
Merged

Admin mode - Phase1 : Introducing Settings tab #24
grunch merged 18 commits into
mainfrom
admin-list-disputes

Conversation

@arkanoider

@arkanoider arkanoider commented Jan 7, 2026

Copy link
Copy Markdown
Collaborator

Admin Mode - Phase 1: Settings Tab & Dispute Management

Overview

This PR introduces comprehensive admin mode functionality, including settings management features, dispute resolution interface, and improved user experience. All features include input validation, confirmation popups, proper error handling, and role-aware UI behavior.

✨ New Features

🌐 Add Mostro Pubkey (User & Admin)

Allows users to change the Mostro instance used by the client.

  • Location: Settings tab (available to both User and Admin roles)
  • Input: npub format public key or hex format
  • Validation:
    • Validates npub/hex format before confirmation
    • Shows error popup with clear message on invalid input
  • Persistence: Saves to settings.toml file
  • UX:
    • Keyboard input support (not just paste)
    • Confirmation popup before saving
    • Clear success/error feedback

Use Case: Switch between different Mostro instances (mainnet, testnet, custom instances)

📡 Add Relay (User & Admin)

Allows users to add new Nostr relays to the client configuration.

  • Location: Settings tab (available to both User and Admin roles)
  • Input: Relay URL (e.g., wss://relay.example.com)
  • Validation: URL format checking (wss:// or ws://)
  • Persistence: Appends to relays list in settings.toml (no duplicates)
  • UX:
    • Keyboard input support
    • Confirmation popup before saving
    • Clear success/error feedback

Use Case: Add custom relays for better connectivity or privacy

💱 Currency Filters (User & Admin)

Allows users to filter orders by fiat currency codes.

  • Location: Settings tab (available to both User and Admin roles)
  • Input: Currency code (e.g., USD, EUR, ARS)
  • Validation: Non-empty, max 10 characters
  • Persistence: Saves to settings.toml as array
  • Features:
    • Automatically converts to uppercase
    • Prevents duplicate currencies
    • Applied in real-time to order filtering
    • Clear all filters option with confirmation
  • UX:
    • Keyboard input support
    • Confirmation popup before saving
    • Clear success/error feedback

Use Case: Focus on specific fiat currencies when viewing orders

➕ Add Solver (Admin Only)

Allows admins to add new dispute solvers to the Mostro network.

  • Location: Settings tab (Admin role only)
  • Input: Solver's npub public key
  • Validation:
    • Validates npub format before confirmation
    • Shows error popup on invalid input
  • Network Operation:
    • Sends AdminAddSolver message to Mostro daemon
    • Uses admin keys for authentication (NIP-59 gift wrap)
    • Shows success/error feedback
  • UX:
    • Custom confirmation message: "Are you sure you want to add this pubkey as dispute solver?"
    • Hides pubkey display in confirmation (cleaner interface)
    • Preserves Settings tab after operation (no unwanted navigation)
    • Keyboard input support

Use Case: Onboard new dispute solvers to the network

🔑 Admin Key Setup (Admin Only)

Allows admins to configure their admin private key.

  • Location: Settings tab (Admin role only)
  • Input: nsec format secret key
  • Validation:
    • Validates nsec format before confirmation
    • Shows error popup on invalid input
  • Persistence: Saves to settings.toml as admin_privkey
  • UX:
    • Keyboard input support
    • Confirmation popup before saving
    • Clear success/error feedback

Use Case: Configure admin private key for dispute resolution

🛡️ Admin Dispute Management UI

Comprehensive dispute resolution interface for admins.

  • Location: Admin tabs
  • Tabs:
    • Disputes Pending: Lists all new disputes not yet taken
    • Disputes in Progress: Full dispute management interface for taken disputes
    • Settings: Configuration options

Disputes in Progress Tab

Two-pane layout for active dispute management:

  • Left Sidebar:

    • List of disputes taken by admin (status InProgress)
    • Arrow key navigation
    • Auto-refreshes after taking a dispute
  • Main Area (when dispute selected):

    • Header: Dispute ID, type, and status
    • Party Tabs: Buyer/Seller chat tabs with color coding
      • Tab key to switch between parties
      • Distinct colors for clear visual distinction
    • Chat History: Filtered messages for selected dispute and active party
    • Input Field: Message composition for admin chat
    • Footer: Action hints for dispute resolution (Settle/Cancel)
  • State Management:

    • Tracks selected dispute index
    • Tracks active chat party (Buyer/Seller)
    • Tracks message input
    • Auto-refreshes disputes list after taking a dispute

Use Case: Admins can take multiple disputes and manage them with dedicated chat interfaces

📊 Multi-line Status Bar

Enhanced status bar displaying comprehensive configuration information.

  • Display: 3 separate lines
    • Mostro Pubkey (truncated if long)
    • Relays List (comma-separated, truncated if many)
    • Currencies List (comma-separated, or "All" if none)
  • Dynamic Updates:
    • Reloads settings from disk on each draw cycle
    • Ensures displayed information is always current
    • Updates immediately when settings change

🎨 User Experience Improvements

Consistent Confirmation Flow

All settings operations follow the same user-friendly pattern:

  1. Input Phase: User enters data via keyboard or paste
  2. Validation: Input is validated before proceeding
  3. Confirmation: Clear confirmation popup with YES/NO buttons
  4. Feedback: Success or error popup with actionable messages
  5. Navigation: Stays on Settings tab for continued operations

Input Validation

  • npub validation: Ensures public keys are in correct bech32 format
  • nsec validation: Ensures secret keys are in correct bech32 format
  • Relay validation: Ensures URLs start with wss:// or ws://
  • Currency validation: Ensures non-empty, reasonable length
  • Error messages: User-friendly error messages (e.g., "Invalid key format")
  • Early validation: Errors shown before confirmation, preventing invalid saves

Clipboard Management

  • Linux: Uses arboard::SetExtLinux::wait() for proper clipboard synchronization
  • Cross-platform: Platform-specific clipboard handling
  • Reliability: Replaced arbitrary std::thread::sleep with proper synchronization

Role-Aware UI Behavior

  • Order Actions: FiatSent, Release, AddInvoice are user-only (admins manage disputes, not orders)
  • Mode Handling: UI modes respect user role (User vs Admin)
  • Tab Preservation: Settings tab preserved after all operations
  • No Unwanted Navigation: Smooth workflow for multiple operations

🔧 Technical Implementation

Settings Persistence

All settings are saved to settings.toml:

  • mostro_pubkey: Mostro instance public key
  • relays: Array of relay URLs (appended, no duplicates)
  • currencies: Array of currency codes (uppercase, no duplicates)
  • admin_privkey: Admin private key (nsec format)
  • Admin solver list: Managed via Mostro network (not stored locally)

Dispute Status Management

  • Status Tracking: Disputes marked as "InProgress" when taken by admin
  • Database Persistence: AdminDispute::set_status_in_progress() method
  • Multiple Disputes: Admins can take multiple disputes concurrently
  • Status Synchronization: UI and database stay in sync

Code Organization & DRY Principles

  • Modular Key Handler: Refactored into separate modules (mod.rs, enter_handlers.rs, navigation.rs, esc_handlers.rs, confirmation.rs, settings.rs, validation.rs, form_input.rs, input_helpers.rs)
  • Helper Functions:
    • execute_take_order_action() - Extracted common take order logic
    • execute_add_solver_action() - Extracted common add solver logic
  • Reduced Duplication: Eliminated code duplication in order and admin flows

Admin Operations

  • Add Solver: Uses execute_admin_add_solver() function
  • Take Dispute: Uses execute_take_dispute() function
  • Authentication: Admin private key required (from settings)
  • Protocol: NIP-59 encrypted direct messages to Mostro daemon
  • Error Handling: Comprehensive error messages for network failures

Test Infrastructure

  • Integration Tests: Comprehensive test suite in tests/ directory
  • Shared Utilities: tests/common/mod.rs for test helpers
  • Test Coverage:
    • Database operations (tests/db_tests.rs)
    • Parsing logic (tests/parsing_tests.rs)
    • Validation functions (tests/validation_tests.rs)
    • Helper functions (tests/helper_tests.rs)
  • Library Crate: Created src/lib.rs to enable integration tests

📝 Files Changed

New Files

  • src/util/order_utils/execute_admin_add_solver.rs - Admin AddSolver implementation
  • src/util/order_utils/execute_take_dispute.rs - Admin take dispute implementation
  • src/ui/disputes_tab.rs - Disputes pending tab rendering
  • src/ui/key_handler/ - Modular key handler structure
    • mod.rs - Main dispatcher
    • enter_handlers.rs - Enter key handling
    • navigation.rs - Navigation and tab switching
    • esc_handlers.rs - Escape key handling
    • confirmation.rs - Confirmation popup logic
    • settings.rs - Settings persistence functions
    • validation.rs - Input validation utilities
    • form_input.rs - Character input and backspace
    • input_helpers.rs - Generic text input handling
  • src/lib.rs - Library crate for integration tests
  • tests/common/mod.rs - Shared test utilities
  • tests/db_tests.rs - Database integration tests
  • tests/parsing_tests.rs - Parsing logic tests
  • tests/validation_tests.rs - Validation function tests
  • tests/helper_tests.rs - Helper function tests

Modified Files

  • src/ui/settings_tab.rs - Added new settings options
  • src/ui/tab_content.rs - Added disputes in progress rendering
  • src/ui/admin_key_confirm.rs - Custom message support
  • src/ui/key_handler/settings.rs - Settings save functions
  • src/ui/key_handler/validation.rs - Input validation (added nsec validation)
  • src/ui/key_handler/enter_handlers.rs - Settings action handlers, DRY refactoring
  • src/ui/key_handler/mod.rs - Clipboard improvements
  • src/ui/mod.rs - UI mode updates, admin dispute state management
  • src/models.rs - Added set_status_in_progress() method
  • src/main.rs - Dispute list loading and refresh logic
  • src/util/order_utils/mod.rs - Module exports

🧪 Testing Checklist

  • Add Mostro Pubkey saves correctly to settings file
  • Add Relay appends to relays list (no duplicates)
  • Add Currency Filter saves correctly and filters orders
  • Clear Currency Filters removes all filters
  • Add Solver sends message to Mostro and shows success
  • Admin Key Setup validates nsec format and saves correctly
  • npub/nsec validation catches invalid formats
  • Error popups display correctly
  • Settings tab preserved after all operations
  • Keyboard input works for all fields
  • Confirmation popups display correctly
  • Admin-only features hidden from User role
  • Disputes list displays correctly
  • Taking a dispute updates status to InProgress
  • Disputes in Progress tab renders correctly
  • Chat party switching works (Tab key)
  • Clipboard copy works reliably on Linux
  • Integration tests pass
  • Role-aware UI behavior works correctly

🎯 Use Cases

For Regular Users

  • Switch between Mostro instances (mainnet/testnet)
  • Add custom relays for better connectivity
  • Filter orders by currency
  • Configure client without editing config files manually

For Admins

  • Manage Mostro instance configuration
  • Add custom relays
  • Filter orders by currency
  • Onboard new dispute solvers to the network
  • View and take disputes
  • Manage multiple disputes concurrently
  • Chat with buyers and sellers in disputes
  • All operations from a single Settings tab interface

📚 Documentation Updates

  • Updated docs/ADMIN_DISPUTES.md with latest features
  • Updated docs/TUI_INTERFACE.md with clipboard improvements
  • Updated docs/CODING_STANDARDS.md with DRY principles
  • All documentation reflects current implementation status

Summary by CodeRabbit

  • New Features

    • Disputes tab, settings-driven admin solver flow, admin-key confirmation popup, reusable key-input popup, richer message/invoice popups, new Settings UI, admin dispute management UI with chat interface, currency filters, multi-line status bar.
  • Improvements

    • Background scheduler refreshes orders and disputes, multi-line status bar, improved paste/clipboard behavior (Linux-specific wait), preserved dispute selection, unified input/confirmation flows and validation for keys/relays/currencies, DRY refactoring (execute_take_order_action, execute_add_solver_action), role-aware UI mode handling.
  • Tests

    • New integration tests for DB models, parsing, validation, and helper functions. Comprehensive test infrastructure with shared utilities.
  • Chores

    • Dependency version updates, code organization improvements.
  • Documentation

    • Expanded settings and admin/disputes docs, updated with latest features.

✏️ Tip: You can customize this high-level summary in your review settings.

- Add dispute fetching and parsing from Nostr events
- Create disputes tab UI component (admin mode only)
- Add Up/Down navigation for disputes list
- Refactor orders/disputes fetching into fetch_scheduler module
- Update AppState with selected_dispute_idx tracking

Disputes are fetched every 10 seconds and displayed in a dedicated
admin tab following the same patterns as orders for consistency.
- Add confirmation popup when saving admin key to settings
- Implement Yes/No button navigation with Left/Right keys
- Save admin key to settings.toml file on confirmation
- Refactor message notification rendering into separate module
- Extract common UI patterns into helpers module
- Optimize code organization per coding standards
…gs UX

- Refactor key_handler.rs (1439 lines) into focused modules:
  * input_helpers: unified text input handling (eliminates duplication)
  * navigation: arrow keys and tab navigation
  * enter_handlers: Enter key logic
  * esc_handlers: Esc key handling
  * form_input: form character input
  * confirmation: confirmation/cancel handlers
  * settings: settings save helpers
  * validation: npub format validation
  * mod: main dispatcher and public API

- Add npub validation for AddSolver and AddMostroPubkey inputs
  * Shows error popup with clear message on invalid format

- Improve AddSolver confirmation UX:
  * Custom message: "Are you sure you want to add this pubkey as dispute solver?"
  * Hide pubkey display in confirmation popup
  * Preserve Settings tab after confirmation (no tab switching)

- Fix tab navigation to preserve Settings tab when closing OrderResult popups

All modules are under 500 lines, following coding standards. Functionality
remains unchanged while improving maintainability and code organization.
@coderabbitai

coderabbitai Bot commented Jan 7, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Refactors background fetching into a scheduler that supplies shared orders and disputes; fully rewrites key handling into modular submodules; adds dispute parsing, disputes UI, admin add-solver DM flow, multiple popup helpers, settings load/save improvements, validation utilities, tests, and docs updates.

Changes

Cohort / File(s) Summary
Deps
Cargo.toml
Pin ratatui to 0.30.0; bump mostro-core 0.6.560.6.57.
Scheduler & entry
src/main.rs, src/util/order_utils/fetch_scheduler.rs
Replace inline fetch loop with start_fetch_scheduler returning FetchSchedulerResult (shared orders + disputes), spawning periodic fetch tasks.
Order/Dispute parsing & util exports
src/util/order_utils/helper.rs, src/util/order_utils/mod.rs, src/util/mod.rs, src/util/filters.rs
Expose order_from_tags; add dispute_from_tags, parse_disputes_events, get_disputes; extend parse_orders_events to accept multi-currency vector; add ListKind::Disputes.
Admin add-solver
src/util/order_utils/execute_admin_add_solver.rs, docs/ADMIN_DISPUTES.md
New execute_admin_add_solver to send AddSolver DM using admin privkey from SETTINGS; docs updated to Settings-driven flow.
Settings loader & API
src/settings.rs, settings.toml, src/lib.rs
Add Clone to Settings; introduce init_or_load_settings_from_disk and load_settings_from_disk; replace unwraps with Result; expose SETTINGS: OnceLock<Settings>; settings TOML values updated.
UI root, state & enums
src/ui/mod.rs, src/ui/tab_content.rs, src/ui/status.rs, src/ui/settings_tab.rs
Add selected_dispute_idx, selected_settings_option to AppState; add KeyInputState; extend UiMode/AdminMode; ui_draw now accepts disputes and multi-line status.
Key handling rewrite
src/ui/key_handler/*, src/ui/key_handler/mod.rs
Replace monolithic key handler with modular submodules (confirmation, enter_handlers, esc_handlers, navigation, form_input, input_helpers, settings, validation); central handle_key_event now accepts orders, disputes, DB/client context, and validation callback.
Enter/Esc/Confirm flows
src/ui/key_handler/enter_handlers.rs, src/ui/key_handler/confirmation.rs, src/ui/key_handler/esc_handlers.rs
Centralized Enter/Esc/confirmation logic, validation, and async action spawning (send/take/add-solver); mode transition helpers added.
Form & text input helpers
src/ui/key_handler/form_input.rs, src/ui/key_handler/input_helpers.rs, src/ui/key_handler/validation.rs
Char/backspace handlers for forms, generic text input with paste-guard (just_pasted), and validators (validate_npub, validate_mostro_pubkey, validate_relay, validate_currency).
Navigation & settings persistence
src/ui/key_handler/navigation.rs, src/ui/key_handler/settings.rs
Directional/tab navigation across modes/tabs using shared orders/disputes; settings save helpers and handle_mode_switch persisting user_mode.
UI components & helpers
src/ui/helpers.rs, src/ui/admin_key_confirm.rs, src/ui/disputes_tab.rs, src/ui/key_input_popup.rs, src/ui/message_notification.rs, src/ui/waiting.rs
New centered-popup helper, help-text renderer, order-id formatter, admin key confirmation popup, disputes tab, key input popup, message notification popup, waiting UI.
Tab content & wiring
src/ui/tab_content.rs, src/main.rs, src/ui/*
Remove inline message/settings rendering, delegate to new modules; main loop consumes FetchSchedulerResult and passes disputes into UI and key handlers; clamp dispute selection on changes.
Tests & test helpers
tests/*, tests/common/mod.rs
Add DB test helper create_test_db, integration tests for models, parsing, validation, and helper tests.
Docs & README
docs/*, README.md, docs/SETTINGS_ANALYSIS.md
Update Admin Disputes docs, TUI docs, README settings section, and add SETTINGS_ANALYSIS.

Sequence Diagram(s)

sequenceDiagram
    participant App as main.rs
    participant Scheduler as FetchScheduler
    participant Client as Nostr Client
    participant Shared as Shared State (Arc<Mutex>)

    App->>Scheduler: start_fetch_scheduler(client, mostro_pubkey)
    activate Scheduler
    Scheduler->>Client: get_orders(Status::Pending, currencies) [immediate + every 10s]
    Client-->>Scheduler: Vec<SmallOrder>
    Scheduler->>Shared: replace orders vector
    Scheduler->>Client: get_disputes() [immediate + every 10s]
    Client-->>Scheduler: Vec<Dispute>
    Scheduler->>Shared: replace disputes vector
    Scheduler-->>App: return FetchSchedulerResult(orders, disputes)
    deactivate Scheduler
    App->>App: ui_draw(..., orders, disputes)
Loading
sequenceDiagram
    participant User as Terminal
    participant KeyH as handle_key_event
    participant Confirm as confirmation module
    participant Async as Async actions (DB / Nostr)
    participant Settings as SETTINGS

    User->>KeyH: KeyEvent
    KeyH->>KeyH: dispatch by UiMode (navigation/form/confirm)
    alt Confirmation flow
        KeyH->>Confirm: handle_confirm_key
        Confirm-->Confirm: YES?
        alt YES
            Confirm->>Async: spawn(send/take/add-solver)
            Async->>Settings: read/update as needed
            Async-->>KeyH: send OrderResult when done
        else NO
            Confirm->>KeyH: revert mode
        end
    else Form input
        KeyH->>KeyH: handle_char_input / handle_backspace
    else Navigation
        KeyH->>KeyH: handle_navigation (up/down/left/right/tab)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through schedulers at dawn to fetch each score,

Disputes and orders came in from the network shore.
Popups blink and keys confirm, an admin rabbit sends a DM,
I stash the settings, trim the keys, then nibble on a gem.
Tests pass, docs hum — I twitch my whiskers and grin again.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately reflects the main objective of this PR: introducing Settings tab functionality for admin mode Phase 1, which includes configuration of Mostro pubkey, relays, and dispute solvers.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In @src/ui/admin_key_confirm.rs:
- Around line 65-73: Update the misleading comment to match the actual
truncation logic: change the comment that says "show first 20 chars" to indicate
it shows the first 30 characters (or "first 30 chars + ...") for the display_key
calculation that uses key_string and the conditional truncation (custom_message,
key_string, display_key).

In @src/ui/key_handler/esc_handlers.rs:
- Around line 44-53: The Esc handlers for UiMode::NewMessageNotification and
UiMode::ViewingMessage currently set app.mode = UiMode::Normal directly; change
both to set app.mode = default_mode.clone() instead so dismissal uses the
role-specific default (e.g., UiMode::UserMode(UserMode::Normal) or
UiMode::AdminMode(AdminMode::Normal)) consistent with other Esc handlers; locate
the branches matching UiMode::NewMessageNotification(_, _, _) and
UiMode::ViewingMessage(_) and replace UiMode::Normal assignments with
default_mode.clone().

In @src/ui/key_handler/mod.rs:
- Around line 159-170: The current clipboard write uses
arboard::Clipboard::new() and a blocking
std::thread::sleep(Duration::from_millis(100)) to avoid "dropped too quickly";
remove this sleep and instead adopt arboard's recommended approach: either store
a long-lived Clipboard instance on your app state (create Clipboard once and
reuse it when handling the Copy action, replacing the local
arboard::Clipboard::new() and clipboard variable), or after set_text call on
platforms that support it call the SetExtLinux::wait() method to hold ownership
until the selection is served; update the copy logic that uses
clipboard.set_text(invoice.clone()) to use the shared Clipboard (or call the
wait extension) and remove the blocking sleep to avoid blocking the event loop.

In @src/ui/settings_tab.rs:
- Around line 90-106: The footer Paragraph is being rendered into the same area
as the settings list (both using chunks[3]), causing overlap; update the layout
to create a dedicated chunk for the footer (e.g., extend the layout split to
produce chunks[4] or rename the footer chunk variable) and render the footer
Paragraph there instead of chunks[3]; adjust the call that currently checks
user_role == UserRole::User and the f.render_widget(... Paragraph::new(...))
invocation to target the new footer chunk so the list widget and footer do not
overlap.

In @src/util/order_utils/helper.rs:
- Around line 68-99: The dispute_from_tags function can return a default Dispute
when required tags are absent; update it to validate that Dispute.id and
Dispute.status were populated before returning: after the loop check if
dispute.id is not the zero/default UUID (or track a boolean like saw_id) and
dispute.status is non-empty (or track saw_status), and if either is missing
return Err(anyhow::anyhow!("Missing dispute id") or similar for status) instead
of Ok(dispute); reference the dispute_from_tags function and the Dispute struct
fields id and status when adding these checks and error messages.
🧹 Nitpick comments (21)
src/util/order_utils/helper.rs (1)

101-135: Optimize duplicate handling using HashMap pattern.

The current approach clones the entire list (line 119) and uses nested iteration (O(n²) complexity) to filter duplicates. For consistency and efficiency, consider adopting the same HashMap-based deduplication pattern used in parse_orders_events (lines 144-196).

♻️ Proposed optimization using HashMap
 pub fn parse_disputes_events(events: Events) -> Vec<Dispute> {
-    let mut disputes_list = Vec::<Dispute>::new();
+    let mut latest_by_id: std::collections::HashMap<uuid::Uuid, Dispute> = std::collections::HashMap::new();
 
     // Scan events to extract all disputes
     for event in events.iter() {
         let mut dispute = match dispute_from_tags(event.tags.clone()) {
             Ok(d) => d,
             Err(e) => {
                 log::warn!("Failed to parse dispute from tags: {:?}", e);
                 continue;
             }
         };
         // Get created at field from Nostr event
         dispute.created_at = event.created_at.as_u64() as i64;
-        disputes_list.push(dispute);
+        
+        // Keep only the latest dispute for each id
+        latest_by_id
+            .entry(dispute.id)
+            .and_modify(|existing| {
+                if dispute.created_at > existing.created_at {
+                    *existing = dispute.clone();
+                }
+            })
+            .or_insert(dispute);
     }
 
-    let buffer_dispute_list = disputes_list.clone();
-    // Order all elements (disputes) received to filter - discard disaligned messages
-    // if a dispute has an older message with the state we received is discarded for the latest one
-    disputes_list.retain(|keep| {
-        !buffer_dispute_list
-            .iter()
-            .any(|x| x.id == keep.id && x.created_at > keep.created_at)
-    });
-
-    // Sort by id to remove duplicates
-    disputes_list.sort_by(|a, b| b.id.cmp(&a.id));
-    disputes_list.dedup_by(|a, b| a.id == b.id);
-
+    let mut disputes_list: Vec<Dispute> = latest_by_id.into_values().collect();
+    
     // Finally sort list by creation time (newest first)
     disputes_list.sort_by(|a, b| b.created_at.cmp(&a.created_at));
     disputes_list
 }
src/ui/disputes_tab.rs (1)

20-20: Consider handling mutex poisoning gracefully.

Using .unwrap() on a mutex lock will panic if the mutex is poisoned (i.e., a thread panicked while holding the lock). While this is often acceptable in single-threaded UI contexts, you could consider using lock().expect("disputes mutex poisoned") for a clearer panic message, or handle the error to avoid crashing the UI.

src/ui/settings_tab.rs (1)

63-88: Redundant highlight styling.

The items are manually styled based on selection (lines 67-74), and highlight_style is also set on the list (line 81). Since the highlighting is already applied to individual items, the highlight_style on the List widget is redundant and could be removed for clarity.

src/util/order_utils/execute_admin_add_solver.rs (1)

39-52: Fire-and-forget operation lacks server confirmation.

The DM is sent without waiting for a response from Mostro. While the comment acknowledges this, consider adding logging for debugging purposes and ensure the calling UI code provides appropriate feedback that the operation was sent but not confirmed.

Suggested improvement
     // Send the DM using admin keys (signed gift wrap)
     // Note: Following the example pattern, we don't wait for a response
+    log::info!("Sending AdminAddSolver request for solver: {}", solver_pubkey);
     send_dm(
         client,
         Some(&admin_keys),
         &admin_keys,
         &mostro_pubkey,
         add_solver_message,
         None,
         false,
     )
     .await?;
 
+    log::info!("AdminAddSolver request sent successfully");
     Ok(())
 }
src/ui/key_input_popup.rs (1)

31-43: Layout constraint comments could be clearer.

The layout works correctly, but the inline comments are slightly misleading. When is_sensitive=true, chunks[2] (labeled "spacer") is actually used for the label, while chunks[1] (labeled "warning or label") gets the warning. Consider updating comments to reflect the actual usage pattern.

📝 Suggested clarification
     let chunks = Layout::new(
         Direction::Vertical,
         [
             Constraint::Length(1),                                // spacer
-            Constraint::Length(if is_sensitive { 2 } else { 1 }), // warning (if sensitive) or label
-            Constraint::Length(1),                                // spacer
+            Constraint::Length(if is_sensitive { 2 } else { 1 }), // warning (if sensitive) or label (if not)
+            Constraint::Length(1),                                // label (if sensitive) or spacer
             Constraint::Length(3),                                // input field
             Constraint::Length(1),                                // spacer
             Constraint::Length(1),                                // help text
             Constraint::Length(1),                                // help text
         ],
     )
src/ui/key_handler/settings.rs (2)

5-18: Consider returning a Result or logging when SETTINGS is not initialized.

When SETTINGS.get() returns None, the function silently does nothing. While this may be acceptable for a TUI app where settings should always be initialized, consider at minimum logging a warning for debugging purposes.

📝 Suggested improvement
 pub fn save_settings_with<F>(update_fn: F, error_msg: &str, success_msg: &str)
 where
     F: FnOnce(&mut crate::settings::Settings),
 {
     if let Some(settings) = SETTINGS.get() {
         let mut new_settings = settings.clone();
         update_fn(&mut new_settings);
         if let Err(e) = crate::settings::save_settings(&new_settings) {
             log::error!("{}: {}", error_msg, e);
         } else {
             log::info!("{}", success_msg);
         }
+    } else {
+        log::warn!("Settings not initialized, cannot save");
     }
 }

51-67: Remove unused _settings parameter.

The _settings parameter is not used in handle_mode_switch. If it's intentionally reserved for future use, consider documenting that. Otherwise, remove it to clean up the API.

♻️ Suggested fix
-/// Handle mode switching (M key in Settings tab)
-pub fn handle_mode_switch(app: &mut AppState, _settings: &crate::settings::Settings) {
+/// Handle mode switching (M key in Settings tab)
+pub fn handle_mode_switch(app: &mut AppState) {
     let new_role = match app.user_role {
         UserRole::User => UserRole::Admin,
         UserRole::Admin => UserRole::User,
     };
src/util/order_utils/fetch_scheduler.rs (2)

40-56: Consider handling mutex poison and logging fetch errors.

  1. .unwrap() on lock() will panic if a thread previously panicked while holding the lock. Consider using .lock().expect("mutex poisoned") for clearer panic messages, or handle the PoisonError.

  2. Fetch failures are silently ignored. Consider logging errors for debugging network issues.

♻️ Suggested improvements
             if let Ok(fetched_orders) = get_orders(
                 &client_for_orders,
                 mostro_pubkey_for_orders,
                 Some(Status::Pending),
             )
             .await
             {
-                let mut orders_lock = orders_clone.lock().unwrap();
+                let mut orders_lock = orders_clone.lock().expect("orders mutex poisoned");
                 orders_lock.clear();
                 orders_lock.extend(fetched_orders);
+            } else {
+                log::debug!("Failed to fetch orders, will retry");
             }

63-76: Same improvements apply to disputes fetcher.

Apply consistent error logging and mutex handling as suggested for the orders fetcher.

♻️ Suggested improvements
             if let Ok(fetched_disputes) =
                 get_disputes(&client_for_disputes, mostro_pubkey_for_disputes).await
             {
-                let mut disputes_lock = disputes_clone.lock().unwrap();
+                let mut disputes_lock = disputes_clone.lock().expect("disputes mutex poisoned");
                 disputes_lock.clear();
                 disputes_lock.extend(fetched_disputes);
+            } else {
+                log::debug!("Failed to fetch disputes, will retry");
             }
src/ui/key_handler/navigation.rs (2)

204-216: Extract magic numbers for settings options count.

The max_options values (3 for Admin, 1 for User) are hardcoded. If the settings UI changes, this will be a source of bugs. Consider defining these as constants or deriving from the actual settings options.

♻️ Suggested improvement
+// At module level or in a constants module
+const ADMIN_SETTINGS_OPTIONS_COUNT: usize = 3;
+const USER_SETTINGS_OPTIONS_COUNT: usize = 1;

 // In handle_down_key:
-                let max_options = if app.user_role == UserRole::Admin {
-                    3
-                } else {
-                    1
-                };
+                let max_options = if app.user_role == UserRole::Admin {
+                    ADMIN_SETTINGS_OPTIONS_COUNT
+                } else {
+                    USER_SETTINGS_OPTIONS_COUNT
+                };

117-126: Consider consistent mutex handling pattern.

Multiple .unwrap() calls on lock() throughout the navigation handlers. While mutex poisoning is unlikely in a TUI app, using .expect() with descriptive messages would aid debugging if issues arise.

src/ui/key_handler/form_input.rs (1)

44-54: Consider validating decimal point input to prevent multiple dots.

The current logic allows multiple decimal points (e.g., "12.34.56"), which would fail parsing later. Consider checking if a decimal already exists before allowing another.

♻️ Suggested fix
             } else if let UiMode::UserMode(UserMode::TakingOrder(ref mut take_state)) = app.mode {
                 // Allow typing in the amount input field for range orders
                 if take_state.is_range_order {
                     // Only allow digits and decimal point
-                    if c.is_ascii_digit() || c == '.' {
+                    if c.is_ascii_digit() || (c == '.' && !take_state.amount_input.contains('.')) {
                         take_state.amount_input.push(c);
                         // Validate after typing
                         validate_range_amount(take_state);
                     }
                 }
             }
src/ui/message_notification.rs (1)

288-316: Consider reusing render_order_id_header for consistency.

The order ID rendering at lines 307-316 duplicates the logic in render_order_id_header (lines 9-21) but with slightly different styling (missing bg(BACKGROUND_COLOR)). Consider using the existing helper for consistency.

♻️ Suggested refactor
 fn render_default_notification(
     f: &mut ratatui::Frame,
     popup: Rect,
     notification: &MessageNotification,
 ) {
     let chunks = Layout::new(
         Direction::Vertical,
         [
             Constraint::Length(1), // spacer
             Constraint::Length(1), // order id
             Constraint::Length(1), // message preview
             Constraint::Length(1), // spacer
             Constraint::Length(1), // help text
         ],
     )
     .split(popup);

     let order_id_str = helpers::format_order_id(notification.order_id);
-    f.render_widget(
-        Paragraph::new(Line::from(vec![Span::styled(
-            order_id_str,
-            Style::default()
-                .fg(PRIMARY_COLOR)
-                .add_modifier(Modifier::BOLD),
-        )]))
-        .alignment(ratatui::layout::Alignment::Center),
-        chunks[1],
-    );
+    render_order_id_header(f, chunks[1], &order_id_str);
src/ui/tab_content.rs (1)

45-56: Redundant character collection - .take() with full length has no effect.

The code collects characters up to the full length of the string, which is equivalent to just using .to_string(). This appears to be leftover from a truncation that was removed. Consider using the helpers::format_order_id function for consistency with other parts of the codebase.

♻️ Suggested fix
             let order_id_str = if let Some(order_id) = msg.order_id {
-                format!(
-                    "Order: {}",
-                    order_id
-                        .to_string()
-                        .chars()
-                        .take(order_id.to_string().len())
-                        .collect::<String>()
-                )
+                helpers::format_order_id(Some(order_id))
             } else {
-                "Order: Unknown".to_string()
+                helpers::format_order_id(None)
             };
src/ui/key_handler/enter_handlers.rs (3)

354-358: Inconsistent mode reset - uses UiMode::Normal instead of role-based default_mode.

When the user selects NO in the TakingOrder confirmation, the mode is set to UiMode::Normal instead of the role-based default_mode. This could cause issues if an admin is taking an order (though currently TakingOrder is user-mode only, this inconsistency could cause problems if the code evolves).

♻️ Suggested fix
     } else {
         // NO selected - cancel
-        app.mode = UiMode::Normal;
+        let default_mode = match app.user_role {
+            UserRole::User => UiMode::UserMode(UserMode::Normal),
+            UserRole::Admin => UiMode::AdminMode(AdminMode::Normal),
+        };
+        app.mode = default_mode;
     }

484-489: Empty handler for PayInvoice and silent error for other actions.

The PayInvoice arm is empty (Enter does nothing), which seems intentional since PayInvoice is display-only. However, the error case for other actions (lines 485-488) sends an error but the mode transition was already handled by the caller's std::mem::replace. Consider adding a comment to clarify this is intentional.

📝 Consider adding clarifying comments
         Action::PayInvoice => {
+            // Enter does nothing on PayInvoice - it's display-only
+            // User must press Esc to dismiss or C to copy
         }
         _ => {
+            // Unexpected action in notification context - mode already reset by caller
             let _ =
                 order_result_tx.send(crate::ui::OrderResult::Error("Invalid action".to_string()));
         }

258-276: Magic numbers for settings options could be fragile.

The settings option indices (0, 1, 2, 3) are hardcoded and must stay in sync with the rendering order in settings_tab.rs. Consider using an enum or constants for these options to prevent drift.

src/main.rs (1)

194-223: Paste handling logic is duplicated for invoice and admin key inputs.

The paste handling for invoice input (lines 196-206) and admin key input (lines 209-221) share identical filtering logic. Consider extracting a helper function to reduce duplication.

♻️ Example helper extraction
fn filter_pasted_text(text: &str) -> String {
    text.chars()
        .filter(|c| !c.is_control() || *c == '\t')
        .collect()
}

Then use it in both paste handlers:

let filtered_text = filter_pasted_text(&pasted_text);
invoice_state.invoice_input.push_str(&filtered_text);
src/ui/key_handler/confirmation.rs (1)

112-169: Significant code duplication with enter_handlers.rs.

The TakingOrder handling logic (validation, parsing, async task spawning) is nearly identical to handle_enter_taking_order in enter_handlers.rs (lines 289-358). Consider extracting a shared helper to avoid maintaining two copies of this logic.

♻️ Suggested approach

Extract the core order-taking logic into a shared function in a common module:

// In a shared module or within enter_handlers.rs
pub fn execute_take_order(
    app: &mut AppState,
    take_state: TakeOrderState,
    pool: &SqlitePool,
    client: &Client,
    mostro_pubkey: nostr_sdk::PublicKey,
    order_result_tx: &UnboundedSender<crate::ui::OrderResult>,
) -> bool {
    // ... shared validation and execution logic
}

Then call this from both handle_enter_taking_order and handle_confirm_key.

src/ui/key_handler/mod.rs (1)

185-191: Double handling of Backspace key.

Both handle_char_input and handle_backspace are called when Backspace is pressed. Looking at the referenced code in form_input.rs, handle_char_input only matches on Char(' ') and Char(c), so Backspace falls through to _ => {}. This works but is confusing - the code structure suggests both handle the same key.

♻️ Clearer structure
-        KeyCode::Char(_) | KeyCode::Backspace => {
-            handle_char_input(code, app, validate_range_amount);
-            if code == KeyCode::Backspace {
-                handle_backspace(app, validate_range_amount);
-            }
-            Some(true)
-        }
+        KeyCode::Char(_) => {
+            handle_char_input(code, app, validate_range_amount);
+            Some(true)
+        }
+        KeyCode::Backspace => {
+            handle_backspace(app, validate_range_amount);
+            Some(true)
+        }
src/ui/mod.rs (1)

419-425: KeyInputState structure mirrors InvoiceInputState - consider unifying.

KeyInputState has nearly identical fields to InvoiceInputState (lines 412-417), differing only in the copied_to_clipboard field. Consider whether these could share a common base or trait to reduce duplication.

♻️ Potential unified structure
#[derive(Clone, Debug)]
pub struct TextInputState {
    pub input: String,
    pub focused: bool,
    pub just_pasted: bool,
}

// Then InvoiceInputState could wrap this with additional fields:
#[derive(Clone, Debug)]
pub struct InvoiceInputState {
    pub base: TextInputState,
    pub copied_to_clipboard: bool,
}
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee2e901 and 0aee5fa.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (27)
  • Cargo.toml
  • src/main.rs
  • src/settings.rs
  • src/ui/admin_key_confirm.rs
  • src/ui/disputes_tab.rs
  • src/ui/helpers.rs
  • src/ui/key_handler.rs
  • src/ui/key_handler/confirmation.rs
  • src/ui/key_handler/enter_handlers.rs
  • src/ui/key_handler/esc_handlers.rs
  • src/ui/key_handler/form_input.rs
  • src/ui/key_handler/input_helpers.rs
  • src/ui/key_handler/mod.rs
  • src/ui/key_handler/navigation.rs
  • src/ui/key_handler/settings.rs
  • src/ui/key_handler/validation.rs
  • src/ui/key_input_popup.rs
  • src/ui/message_notification.rs
  • src/ui/mod.rs
  • src/ui/settings_tab.rs
  • src/ui/tab_content.rs
  • src/util/filters.rs
  • src/util/mod.rs
  • src/util/order_utils/execute_admin_add_solver.rs
  • src/util/order_utils/fetch_scheduler.rs
  • src/util/order_utils/helper.rs
  • src/util/order_utils/mod.rs
💤 Files with no reviewable changes (1)
  • src/ui/key_handler.rs
🧰 Additional context used
🧬 Code graph analysis (16)
src/ui/key_handler/esc_handlers.rs (2)
src/ui/key_handler/confirmation.rs (2)
  • create_key_input_state (58-64)
  • handle_confirmation_esc (50-55)
src/ui/mod.rs (3)
  • first (83-85)
  • first (155-157)
  • first (221-226)
src/util/order_utils/fetch_scheduler.rs (3)
src/main.rs (2)
  • tokio (142-142)
  • tokio (146-146)
src/util/order_utils/helper.rs (2)
  • get_disputes (259-283)
  • get_orders (227-255)
src/ui/mod.rs (1)
  • new (483-497)
src/ui/key_input_popup.rs (3)
src/ui/helpers.rs (2)
  • create_centered_popup (9-18)
  • render_help_text (21-36)
src/settings.rs (1)
  • default (26-37)
src/ui/mod.rs (1)
  • new (483-497)
src/util/mod.rs (3)
src/util/order_utils/helper.rs (3)
  • fetch_events_list (199-223)
  • get_disputes (259-283)
  • get_orders (227-255)
src/util/order_utils/send_new_order.rs (1)
  • send_new_order (17-236)
src/util/order_utils/take_order.rs (1)
  • take_order (36-195)
src/ui/settings_tab.rs (2)
src/settings.rs (1)
  • default (26-37)
src/ui/mod.rs (1)
  • new (483-497)
src/ui/admin_key_confirm.rs (1)
src/ui/helpers.rs (1)
  • create_centered_popup (9-18)
src/ui/key_handler/settings.rs (1)
src/settings.rs (1)
  • save_settings (76-90)
src/util/order_utils/execute_admin_add_solver.rs (2)
src/util/dm_utils/mod.rs (1)
  • send_dm (25-69)
src/main.rs (1)
  • settings (116-118)
src/ui/message_notification.rs (1)
src/ui/helpers.rs (3)
  • format_order_id (39-48)
  • render_help_text (21-36)
  • create_centered_popup (9-18)
src/ui/key_handler/navigation.rs (2)
src/main.rs (1)
  • take_state (73-73)
src/settings.rs (1)
  • default (26-37)
src/ui/key_handler/form_input.rs (1)
src/main.rs (2)
  • validate_range_amount (67-92)
  • take_state (73-73)
src/ui/disputes_tab.rs (1)
src/ui/mod.rs (2)
  • apply_status_color (508-532)
  • new (483-497)
src/util/order_utils/mod.rs (4)
src/util/order_utils/execute_admin_add_solver.rs (1)
  • execute_admin_add_solver (12-53)
src/util/order_utils/send_new_order.rs (1)
  • send_new_order (17-236)
src/util/order_utils/fetch_scheduler.rs (1)
  • start_fetch_scheduler (32-79)
src/util/order_utils/helper.rs (5)
  • fetch_events_list (199-223)
  • get_disputes (259-283)
  • get_orders (227-255)
  • parse_disputes_events (102-135)
  • parse_orders_events (138-196)
src/ui/key_handler/confirmation.rs (1)
src/ui/key_handler/settings.rs (3)
  • save_admin_key_to_settings (21-27)
  • save_mostro_pubkey_to_settings (30-36)
  • save_relay_to_settings (39-49)
src/ui/key_handler/mod.rs (8)
src/models.rs (1)
  • sqlx (240-240)
src/ui/key_handler/confirmation.rs (2)
  • handle_cancel_key (220-248)
  • handle_confirm_key (67-217)
src/ui/key_handler/enter_handlers.rs (1)
  • handle_enter_key (23-205)
src/ui/key_handler/esc_handlers.rs (1)
  • handle_esc_key (5-89)
src/ui/key_handler/form_input.rs (2)
  • handle_backspace (61-87)
  • handle_char_input (5-58)
src/ui/key_handler/input_helpers.rs (2)
  • handle_invoice_input (57-62)
  • handle_key_input (66-68)
src/ui/key_handler/navigation.rs (1)
  • handle_navigation (9-22)
src/ui/key_handler/settings.rs (1)
  • handle_mode_switch (52-68)
src/main.rs (3)
src/util/dm_utils/notifications_ch_mng.rs (1)
  • handle_message_notification (32-56)
src/util/dm_utils/order_ch_mng.rs (1)
  • handle_order_result (6-82)
src/ui/mod.rs (2)
  • new (483-497)
  • ui_draw (542-738)
🔇 Additional comments (33)
src/util/order_utils/helper.rs (2)

215-220: LGTM!

The Disputes handling follows the same pattern as Orders, maintaining consistency in the codebase.


257-283: LGTM!

The implementation is consistent with get_orders and correctly extracts Dispute items from the Event enum.

src/ui/helpers.rs (3)

21-36: LGTM!

Clean implementation for consistent help text rendering across the UI.


39-48: LGTM!

The use of chars().take(8).collect() is safer than string slicing for displaying the UUID prefix.


9-18: Code looks good. The function correctly uses Flex::Center (available in ratatui 0.30.0) to center the popup and properly clamps dimensions using min().

src/util/filters.rs (1)

35-35: LGTM!

The Disputes filter follows the same pattern as Orders, maintaining consistency.

src/settings.rs (1)

8-8: LGTM!

Adding Clone to Settings is appropriate for the settings persistence and UI update flows described in the PR. All fields are Clone-compatible.

Cargo.toml (1)

12-14: Ratatui 0.30.0 compatibility verified; mostro-core 0.6.57 cannot be verified due to lack of public release notes.

The ratatui 0.30.0 breaking changes (Backend trait changes, TestBackend refactoring, Bar widget API updates) do not affect this codebase. The code uses only CrosstermBackend (concrete type, not generic), does not use TestBackend, and does not use the Bar widget. All UI components (disputes_tab, key_input_popup, admin_key_confirm) rely on standard ratatui widgets (Block, Borders, Paragraph, List, Table, etc.) that remain stable. No public changelog is available for mostro-core 0.6.57, but the codebase's simple type usage (Dispute, DisputeStatus, Action) suggests compatibility is likely. Consider running integration tests to confirm.

src/util/mod.rs (1)

14-14: LGTM!

The addition of get_disputes to the re-exports is consistent with the existing pattern and properly exposes the new dispute-fetching functionality.

src/ui/key_handler/validation.rs (1)

5-13: LGTM!

The validation function correctly validates npub format using PublicKey::from_bech32. The error messages are clear and user-friendly.

src/ui/key_handler/input_helpers.rs (1)

27-53: LGTM!

The generic text input handler cleanly abstracts the paste-aware input handling logic. The just_pasted flag handling correctly prevents spurious Enter keypresses immediately after paste operations.

src/ui/disputes_tab.rs (1)

42-67: LGTM!

The dispute row rendering logic is clean. The status styling via apply_status_color and date formatting with fallback handling are well implemented.

src/util/order_utils/execute_admin_add_solver.rs (1)

29-37: Verify the message construction method for AdminAddSolver against mostro-core specification.

The code uses Message::new_dispute with Action::AdminAddSolver and Payload::TextMessage(solver_pubkey.to_string()). Confirm this is the correct Message builder and payload type for AdminAddSolver actions in mostro-core v0.6.57, as the new_dispute method name seems incongruent with the AdminAddSolver action semantics.

src/util/order_utils/mod.rs (1)

1-19: LGTM!

The module organization is clean and follows the existing patterns. The new modules (execute_admin_add_solver, fetch_scheduler) are properly declared and their public APIs are correctly re-exported alongside the expanded helper functions.

src/ui/admin_key_confirm.rs (1)

9-16: LGTM!

Clean wrapper pattern delegating to the more flexible render_admin_key_confirm_with_message function.

src/ui/key_input_popup.rs (1)

8-126: LGTM!

The key input popup implementation is well-structured with clear visual feedback for focus state, appropriate sensitive data warnings, and helpful user guidance.

src/util/order_utils/fetch_scheduler.rs (1)

9-14: LGTM!

The FetchSchedulerResult struct provides a clean API for sharing fetched data with the UI layer.

src/ui/key_handler/navigation.rs (2)

248-263: LGTM!

The tab switch handler correctly clears pending notifications and marks messages as read when entering the Messages tab, with proper guard to avoid redundant operations.


265-292: LGTM!

The Tab/BackTab handling correctly cycles through form fields with modular arithmetic and properly skips the range field when not in range mode.

src/ui/key_handler/form_input.rs (2)

1-87: Overall structure is clean.

The form input handling logic is well-organized with clear separation between character input and backspace handling. The callback pattern for validation is appropriate.


31-41: The unreachable!() is justified given the guard check and navigation bounds.

The code is sound: the guard on line 28 (if form.focused == 0) prevents value 0 from reaching the match statement, and navigation.rs uses modulo 9 (form.focused = (form.focused + 1) % 9) to ensure focused stays within 0-8. The match arms cover all values (1-8) that can reach the unreachable case, making the panic branch unreachable under normal operation.

Likely an incorrect or invalid review comment.

src/ui/message_notification.rs (2)

1-6: LGTM - Clean imports and module structure.

The imports are well-organized, using the super module pattern for local dependencies and explicit imports from ratatui.


350-391: Well-structured public API with clean dispatch logic.

The render_message_notification function provides a clean entry point that properly delegates to specialized rendering functions based on the action type. The popup sizing adapts appropriately for different notification types.

src/ui/tab_content.rs (1)

124-125: LGTM - Helper function integration.

The refactoring to use helpers::create_centered_popup is correctly applied, improving code consistency with other popup rendering.

src/ui/key_handler/enter_handlers.rs (1)

22-205: Well-structured Enter key dispatcher with proper async handling.

The handle_enter_key function uses std::mem::replace correctly to take ownership of the mode while avoiding borrowing issues. The async task spawning for order operations is properly implemented with cloned resources and channel-based result reporting.

src/main.rs (2)

130-132: Clean integration of the background fetch scheduler.

The destructuring pattern for FetchSchedulerResult is clean and the removal of the manual order-fetching loop in favor of the scheduler improves code organization.


271-277: Good addition of dispute index bounds checking.

The bounds checking for selected_dispute_idx mirrors the existing pattern for selected_order_idx, maintaining consistency when the disputes list shrinks.

src/ui/key_handler/confirmation.rs (2)

220-248: Correct cancellation logic with appropriate state transitions.

The handle_cancel_key function properly handles cancellation for different confirmation modes, returning users to the input state while preserving their entered data.


12-64: Well-designed generic confirmation helpers.

The handle_input_to_confirmation, handle_confirmation_enter, and handle_confirmation_esc functions provide a clean, reusable pattern for confirmation flows using closures for customization. The create_key_input_state factory function ensures consistent initialization.

src/ui/key_handler/mod.rs (1)

27-40: Function signature with many parameters is acceptable given the complexity.

The #[allow(clippy::too_many_arguments)] annotation is appropriate here. The function needs access to multiple pieces of state to properly dispatch key events. Consider if a context struct might improve ergonomics in the future, but this is fine for now.

src/ui/mod.rs (3)

638-717: Well-organized popup overlay rendering.

The popup overlay rendering is cleanly organized with appropriate calls to specialized rendering functions. Using separate if let blocks ensures only the active popup is rendered, and the ordering is correct (confirmations render after input popups).


725-731: Refactored message notification rendering using dedicated module.

The message notification rendering now correctly delegates to message_notification::render_message_notification, improving code organization by moving the complex rendering logic to a dedicated module.


482-504: Clean AppState initialization with new fields.

The AppState::new and switch_role methods properly initialize and reset the new selected_dispute_idx and selected_settings_option fields.

Comment thread src/ui/admin_key_confirm.rs Outdated
Comment on lines +44 to +53
UiMode::NewMessageNotification(_, _, _) => {
// Dismiss notification
app.mode = UiMode::Normal;
true
}
UiMode::ViewingMessage(_) => {
// Dismiss message view popup
app.mode = UiMode::Normal;
true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent mode reset for notification dismissal.

Lines 46 and 51 set app.mode to UiMode::Normal directly, while other Esc handlers use the role-specific default_mode (which is UiMode::UserMode(UserMode::Normal) or UiMode::AdminMode(AdminMode::Normal)). This could lead to inconsistent UI state after dismissing notifications vs. escaping from other modes.

Consider using default_mode.clone() here for consistency:

Suggested fix
         UiMode::NewMessageNotification(_, _, _) => {
             // Dismiss notification
-            app.mode = UiMode::Normal;
+            app.mode = default_mode.clone();
             true
         }
         UiMode::ViewingMessage(_) => {
             // Dismiss message view popup
-            app.mode = UiMode::Normal;
+            app.mode = default_mode.clone();
             true
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
UiMode::NewMessageNotification(_, _, _) => {
// Dismiss notification
app.mode = UiMode::Normal;
true
}
UiMode::ViewingMessage(_) => {
// Dismiss message view popup
app.mode = UiMode::Normal;
true
}
UiMode::NewMessageNotification(_, _, _) => {
// Dismiss notification
app.mode = default_mode.clone();
true
}
UiMode::ViewingMessage(_) => {
// Dismiss message view popup
app.mode = default_mode.clone();
true
}
🤖 Prompt for AI Agents
In @src/ui/key_handler/esc_handlers.rs around lines 44 - 53, The Esc handlers
for UiMode::NewMessageNotification and UiMode::ViewingMessage currently set
app.mode = UiMode::Normal directly; change both to set app.mode =
default_mode.clone() instead so dismissal uses the role-specific default (e.g.,
UiMode::UserMode(UserMode::Normal) or UiMode::AdminMode(AdminMode::Normal))
consistent with other Esc handlers; locate the branches matching
UiMode::NewMessageNotification(_, _, _) and UiMode::ViewingMessage(_) and
replace UiMode::Normal assignments with default_mode.clone().

Comment thread src/ui/key_handler/mod.rs Outdated
Comment thread src/ui/settings_tab.rs Outdated
Comment on lines +68 to +99
/// Parse dispute from nostr tags
pub(super) fn dispute_from_tags(tags: Tags) -> Result<Dispute> {
let mut dispute = Dispute::default();
for tag in tags {
let t = tag.to_vec();

// Check if tag has at least 2 elements
if t.len() < 2 {
continue;
}

let key = t.first().map(|s| s.as_str()).unwrap_or("");
let value = t.get(1).map(|s| s.as_str()).unwrap_or("");

match key {
"d" => {
let id = value
.parse::<Uuid>()
.map_err(|_| anyhow::anyhow!("Invalid dispute id"))?;
dispute.id = id;
}
"s" => {
let status = DisputeStatus::from_str(value)
.map_err(|_| anyhow::anyhow!("Invalid dispute status"))?;
dispute.status = status.to_string();
}
_ => {}
}
}

Ok(dispute)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate required fields before returning a Dispute.

The function can return a Dispute with default (potentially invalid) values if the "d" or "s" tags are missing entirely. Unlike parsing errors which return Err, missing tags silently produce a default Dispute that may not represent valid data.

Consider validating that required fields (id and status) were actually populated before returning Ok.

🛡️ Proposed fix to validate required fields
 pub(super) fn dispute_from_tags(tags: Tags) -> Result<Dispute> {
     let mut dispute = Dispute::default();
+    let mut has_id = false;
+    let mut has_status = false;
+
     for tag in tags {
         let t = tag.to_vec();
 
         // Check if tag has at least 2 elements
         if t.len() < 2 {
             continue;
         }
 
         let key = t.first().map(|s| s.as_str()).unwrap_or("");
         let value = t.get(1).map(|s| s.as_str()).unwrap_or("");
 
         match key {
             "d" => {
                 let id = value
                     .parse::<Uuid>()
                     .map_err(|_| anyhow::anyhow!("Invalid dispute id"))?;
                 dispute.id = id;
+                has_id = true;
             }
             "s" => {
                 let status = DisputeStatus::from_str(value)
                     .map_err(|_| anyhow::anyhow!("Invalid dispute status"))?;
                 dispute.status = status.to_string();
+                has_status = true;
             }
             _ => {}
         }
     }
 
+    if !has_id {
+        return Err(anyhow::anyhow!("Missing required dispute id tag"));
+    }
+    if !has_status {
+        return Err(anyhow::anyhow!("Missing required dispute status tag"));
+    }
+
     Ok(dispute)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Parse dispute from nostr tags
pub(super) fn dispute_from_tags(tags: Tags) -> Result<Dispute> {
let mut dispute = Dispute::default();
for tag in tags {
let t = tag.to_vec();
// Check if tag has at least 2 elements
if t.len() < 2 {
continue;
}
let key = t.first().map(|s| s.as_str()).unwrap_or("");
let value = t.get(1).map(|s| s.as_str()).unwrap_or("");
match key {
"d" => {
let id = value
.parse::<Uuid>()
.map_err(|_| anyhow::anyhow!("Invalid dispute id"))?;
dispute.id = id;
}
"s" => {
let status = DisputeStatus::from_str(value)
.map_err(|_| anyhow::anyhow!("Invalid dispute status"))?;
dispute.status = status.to_string();
}
_ => {}
}
}
Ok(dispute)
}
/// Parse dispute from nostr tags
pub(super) fn dispute_from_tags(tags: Tags) -> Result<Dispute> {
let mut dispute = Dispute::default();
let mut has_id = false;
let mut has_status = false;
for tag in tags {
let t = tag.to_vec();
// Check if tag has at least 2 elements
if t.len() < 2 {
continue;
}
let key = t.first().map(|s| s.as_str()).unwrap_or("");
let value = t.get(1).map(|s| s.as_str()).unwrap_or("");
match key {
"d" => {
let id = value
.parse::<Uuid>()
.map_err(|_| anyhow::anyhow!("Invalid dispute id"))?;
dispute.id = id;
has_id = true;
}
"s" => {
let status = DisputeStatus::from_str(value)
.map_err(|_| anyhow::anyhow!("Invalid dispute status"))?;
dispute.status = status.to_string();
has_status = true;
}
_ => {}
}
}
if !has_id {
return Err(anyhow::anyhow!("Missing required dispute id tag"));
}
if !has_status {
return Err(anyhow::anyhow!("Missing required dispute status tag"));
}
Ok(dispute)
}
🤖 Prompt for AI Agents
In @src/util/order_utils/helper.rs around lines 68 - 99, The dispute_from_tags
function can return a default Dispute when required tags are absent; update it
to validate that Dispute.id and Dispute.status were populated before returning:
after the loop check if dispute.id is not the zero/default UUID (or track a
boolean like saw_id) and dispute.status is non-empty (or track saw_status), and
if either is missing return Err(anyhow::anyhow!("Missing dispute id") or similar
for status) instead of Ok(dispute); reference the dispute_from_tags function and
the Dispute struct fields id and status when adding these checks and error
messages.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/ui/settings_tab.rs (1)

64-89: Consider consolidating the highlighting approach.

The code manually applies styling to each ListItem based on selected_option (lines 68-75), then also sets highlight_style and highlight_symbol on the List widget (lines 82-83). This is redundant—typically you either style items manually or rely on the List's built-in highlighting, not both. Simplifying to one approach would improve maintainability.

♻️ Option 1: Use only manual item styling
 let list = List::new(items)
-    .block(Block::default().borders(Borders::NONE))
-    .highlight_style(Style::default().bg(PRIMARY_COLOR).fg(Color::Black))
-    .highlight_symbol(">> ");
+    .block(Block::default().borders(Borders::NONE));
 
 f.render_stateful_widget(
     list,
     chunks[3],
-    &mut ratatui::widgets::ListState::default().with_selected(Some(selected_option)),
+    &mut ratatui::widgets::ListState::default(),
 );
♻️ Option 2: Use only List highlighting
 let items: Vec<ListItem> = options
     .iter()
-    .enumerate()
-    .map(|(idx, opt)| {
-        let style = if idx == selected_option {
-            Style::default()
-                .bg(PRIMARY_COLOR)
-                .fg(Color::Black)
-                .add_modifier(Modifier::BOLD)
-        } else {
-            Style::default().fg(Color::White)
-        };
-        ListItem::new(Line::from(vec![Span::styled(*opt, style)]))
-    })
+    .map(|opt| ListItem::new(Line::from(*opt)))
     .collect();
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0aee5fa and c42fab8.

📒 Files selected for processing (1)
  • src/ui/settings_tab.rs
🔇 Additional comments (2)
src/ui/settings_tab.rs (2)

22-32: Footer overlap issue resolved.

The layout now correctly allocates separate chunks for the list (chunks[3]) and footer (chunks[4]), addressing the previous review comment about overlapping rendering areas.

Also applies to: 116-119


8-13: The selected_settings_option state is properly managed by the caller. When the user role changes, AppState::switch_role() explicitly resets selected_settings_option to 0 (line 503 in ui/mod.rs). Since both User mode (2 options) and Admin mode (4 options) have index 0 as valid, no out-of-bounds condition can occur. No action needed.

Likely an incorrect or invalid review comment.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (6)
tests/helper_tests.rs (1)

5-46: Consider using an exhaustive match or macro to ensure all variants are tested.

The manual vector of CantDoReason variants is brittle—if new variants are added to the enum in mostro-core, this test won't fail to remind developers to add coverage. Consider refactoring to use a match statement with #[deny(non_exhaustive_omitted_patterns)] or a macro that iterates all variants automatically, ensuring compile-time completeness.

♻️ Alternative approach using strum

If mostro-core's CantDoReason derives strum::IntoEnumIterator, you could iterate all variants automatically:

+use strum::IntoEnumIterator;
+
 #[test]
 fn test_get_cant_do_description_all_reasons() {
-    // Test all CantDoReason variants return non-empty descriptions
-    let reasons = vec![
-        CantDoReason::InvalidSignature,
-        CantDoReason::InvalidTradeIndex,
-        // ... (all variants)
-        CantDoReason::TooManyRequests,
-    ];
-
-    for reason in reasons {
+    for reason in CantDoReason::iter() {
         let description = get_cant_do_description(&reason);
         assert!(
             !description.is_empty(),
             "Description should not be empty for {:?}",
             reason
         );
     }
 }

Alternatively, if strum is not available, consider a compile-time exhaustive match that ensures all variants are covered.

tests/common/mod.rs (1)

6-43: Monitor for schema drift between test and production databases.

The hardcoded SQL schema in create_test_db may diverge from the production schema over time, causing tests to pass while production code fails. Consider one of the following approaches to maintain consistency:

  1. Shared schema definition: Extract the schema to a common location used by both tests and migrations.
  2. Run production migrations: Apply the same migrations used in production to the test database.
  3. Schema validation tests: Add a test that compares the test schema against the production schema.
tests/db_tests.rs (1)

92-92: Consider using deterministic keys for more reliable tests.

Using Keys::generate() introduces non-determinism into the tests. While the current tests may not be sensitive to specific key values, deterministic keys (derived from a fixed seed or mnemonic) improve debuggability and ensure consistent test behavior across runs.

♻️ Use deterministic key generation

You could create a helper similar to test_mnemonic():

// In tests/common/mod.rs
pub fn test_trade_keys() -> Keys {
    Keys::from_mnemonic_advanced(
        "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
        None,
        Some(1), // Different account than identity keys
        Some(0),
        Some(0)
    ).unwrap()
}

Then use it in tests:

-let trade_keys = Keys::generate();
+let trade_keys = common::test_trade_keys();

Also applies to: 117-117, 152-152

src/util/order_utils/helper.rs (2)

68-99: Validate required dispute fields.

The function allows disputes with missing "d" tags to succeed with a default UUID value. This could lead to issues if the dispute ID is used as a unique key downstream or if multiple disputes end up with the same default ID.

Consider validating that required fields are present before returning Ok:

💡 Suggested validation enhancement
 pub fn dispute_from_tags(tags: Tags) -> Result<Dispute> {
     let mut dispute = Dispute::default();
+    let mut has_id = false;
+    let mut has_status = false;
+
     for tag in tags {
         let t = tag.to_vec();
 
         // Check if tag has at least 2 elements
         if t.len() < 2 {
             continue;
         }
 
         let key = t.first().map(|s| s.as_str()).unwrap_or("");
         let value = t.get(1).map(|s| s.as_str()).unwrap_or("");
 
         match key {
             "d" => {
                 let id = value
                     .parse::<Uuid>()
                     .map_err(|_| anyhow::anyhow!("Invalid dispute id"))?;
                 dispute.id = id;
+                has_id = true;
             }
             "s" => {
                 let status = DisputeStatus::from_str(value)
                     .map_err(|_| anyhow::anyhow!("Invalid dispute status"))?;
                 dispute.status = status.to_string();
+                has_status = true;
             }
             _ => {}
         }
     }
+
+    if !has_id {
+        return Err(anyhow::anyhow!("Missing required dispute id tag"));
+    }
+    if !has_status {
+        return Err(anyhow::anyhow!("Missing required dispute status tag"));
+    }
 
     Ok(dispute)
 }

101-135: Refactor to use HashMap for better performance.

The current implementation clones the entire dispute list (line 119) for deduplication, which is inefficient for large event sets. The similar function parse_orders_events (lines 138-196 in this file) uses a HashMap<Uuid, T> to track the latest entry per ID without cloning, which is more efficient.

♻️ Refactor to use HashMap approach
 pub fn parse_disputes_events(events: Events) -> Vec<Dispute> {
-    let mut disputes_list = Vec::<Dispute>::new();
+    let mut latest_by_id: HashMap<Uuid, Dispute> = HashMap::new();
 
     // Scan events to extract all disputes
     for event in events.iter() {
         let mut dispute = match dispute_from_tags(event.tags.clone()) {
             Ok(d) => d,
             Err(e) => {
                 log::warn!("Failed to parse dispute from tags: {:?}", e);
                 continue;
             }
         };
         // Get created at field from Nostr event
         dispute.created_at = event.created_at.as_u64() as i64;
-        disputes_list.push(dispute);
+        
+        // Update latest dispute by id
+        latest_by_id
+            .entry(dispute.id)
+            .and_modify(|existing| {
+                if dispute.created_at > existing.created_at {
+                    *existing = dispute.clone();
+                }
+            })
+            .or_insert(dispute);
     }
 
-    let buffer_dispute_list = disputes_list.clone();
-    // Order all elements (disputes) received to filter - discard disaligned messages
-    // if a dispute has an older message with the state we received is discarded for the latest one
-    disputes_list.retain(|keep| {
-        !buffer_dispute_list
-            .iter()
-            .any(|x| x.id == keep.id && x.created_at > keep.created_at)
-    });
-
-    // Sort by id to remove duplicates
-    disputes_list.sort_by(|a, b| b.id.cmp(&a.id));
-    disputes_list.dedup_by(|a, b| a.id == b.id);
+    let mut disputes_list: Vec<Dispute> = latest_by_id.into_values().collect();
 
     // Finally sort list by creation time (newest first)
     disputes_list.sort_by(|a, b| b.created_at.cmp(&a.created_at));
     disputes_list
 }
tests/validation_tests.rs (1)

1-34: LGTM: Comprehensive test coverage for npub validation.

The test suite effectively covers the key validation scenarios:

  • Valid npub generation and validation
  • Empty and whitespace inputs
  • Various invalid format patterns
  • Whitespace trimming behavior

The tests use nostr-sdk's key generation to ensure realistic test data.

Optional: Verify error messages for better diagnostic coverage

You could optionally enhance the tests to verify specific error messages:

#[test]
fn test_validate_npub_error_messages() {
    let err = validate_npub("").unwrap_err();
    assert_eq!(err, "Public key cannot be empty");
    
    let err = validate_npub("invalid").unwrap_err();
    assert_eq!(err, "Invalid pubkey format");
}

This ensures the error messages remain stable and helpful.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c42fab8 and e3ecefb.

📒 Files selected for processing (10)
  • docs/ADMIN_DISPUTES.md
  • src/lib.rs
  • src/ui/key_handler/mod.rs
  • src/util/order_utils/helper.rs
  • src/util/order_utils/mod.rs
  • tests/common/mod.rs
  • tests/db_tests.rs
  • tests/helper_tests.rs
  • tests/parsing_tests.rs
  • tests/validation_tests.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/util/order_utils/mod.rs
  • src/ui/key_handler/mod.rs
🧰 Additional context used
🧬 Code graph analysis (7)
tests/helper_tests.rs (1)
src/util/types.rs (1)
  • get_cant_do_description (29-77)
tests/validation_tests.rs (1)
src/ui/key_handler/validation.rs (1)
  • validate_npub (5-13)
tests/parsing_tests.rs (1)
src/util/order_utils/helper.rs (1)
  • order_from_tags (14-66)
src/util/order_utils/helper.rs (1)
src/util/filters.rs (1)
  • create_filter (28-38)
src/lib.rs (1)
src/main.rs (1)
  • settings (116-118)
tests/common/mod.rs (1)
src/models.rs (1)
  • sqlx (240-240)
tests/db_tests.rs (2)
tests/common/mod.rs (2)
  • create_test_db (6-43)
  • test_mnemonic (46-49)
src/models.rs (4)
  • get (58-65)
  • update_last_trade_index (67-75)
  • get_identity_keys (77-84)
  • get_by_id (239-255)
🪛 LanguageTool
docs/ADMIN_DISPUTES.md

[style] ~692-~692: Consider replacing this word to strengthen your wording.
Context: ...vation is a critical security component and is thoroughly tested: - Same mnemonic ...

(AND_THAT)

🔇 Additional comments (8)
tests/parsing_tests.rs (1)

1-41: LGTM! Well-structured parsing tests.

The tests comprehensively validate order_from_tags for both basic orders and range-based orders. The assertions cover all relevant fields, and the test data is appropriate.

tests/db_tests.rs (2)

9-87: User model tests look comprehensive.

The test coverage for User is thorough, validating creation, retrieval, updates, identity key derivation, trade key derivation consistency, and persistence. The assertions appropriately verify expected behavior.


89-180: Order model tests provide good coverage.

The tests validate order creation, retrieval, error handling for missing records, and updates. The test data is realistic and the assertions are appropriate.

src/lib.rs (1)

11-12: The code is correct as written. OnceLock::new() properly initializes the lock itself, and the settings are safely initialized via get_or_init() in src/settings.rs:42 rather than requiring manual SETTINGS.set() calls. Using get_or_init() with a closure is the idiomatic and safer pattern for lazy initialization with OnceLock.

src/util/order_utils/helper.rs (3)

14-14: LGTM: Visibility change exposes order parsing helper.

Making order_from_tags public is consistent with the new dispute_from_tags function and allows external modules to reuse the order parsing logic.


215-220: LGTM: Consistent implementation for dispute fetching.

The ListKind::Disputes branch follows the same pattern as the Orders branch, ensuring consistent behavior across different event types.


257-283: LGTM: Clean API for dispute retrieval.

The get_disputes function provides a straightforward wrapper that mirrors the get_orders pattern, maintaining consistency across the codebase.

docs/ADMIN_DISPUTES.md (1)

1-713: LGTM: Comprehensive and accurate documentation.

The documentation updates accurately reflect the implementation changes:

  • ✅ Settings tab integration for Add Solver is properly documented
  • ✅ Code references and snippets are accurate
  • ✅ Sequence diagrams clearly illustrate the flows
  • ✅ Security considerations are well-covered
  • ✅ Implementation status is clearly marked with checkmarks
  • ✅ Test coverage section provides clear guidance

The documentation will serve as an excellent guide for users and future developers working with the admin dispute resolution features.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
src/ui/admin_key_confirm.rs (3)

65-87: Clarify the comment to match the simple logic.

The comment on lines 66-67 suggests complex conditional logic ("only show key if no custom message... or if custom message is provided but we still want to show it"), but the actual implementation is straightforward: the key is displayed only when custom_message.is_none(). Additionally, mentioning "AddSolver" specifically in a generic rendering function reduces clarity.

📝 Suggested comment simplification
-    // Display truncated key (show first 30 chars + ...)
-    // Only show key if no custom message (for settings saves) or if custom message is provided but we still want to show it
-    // For AddSolver, we hide the key display
+    // Display truncated key only when using the default message
+    // When a custom message is provided, the key is hidden
     if custom_message.is_none() {

69-73: Consider showing more characters or using prefix+suffix pattern for better key identification.

Truncating npub keys to 30 characters may not provide enough visual distinction for users to confidently verify which key they're confirming. Consider either:

  • Showing more characters (e.g., 50) if the 80-width popup allows
  • Using a prefix+suffix pattern (e.g., first 20 chars + "..." + last 8 chars) to show both ends of the key
🔍 Example prefix+suffix pattern
-        let display_key = if key_string.len() > 30 {
-            format!("{}...", &key_string[..30])
+        let display_key = if key_string.len() > 50 {
+            format!("{}...{}", &key_string[..20], &key_string[key_string.len()-8..])
         } else {
             key_string.to_string()
         };

113-177: Consider extracting a helper function to reduce button rendering duplication.

The YES and NO button rendering logic (lines 113-146 and 147-177) follows the same pattern with only minor variations (selection condition, color, label). Extracting a helper function would improve maintainability and reduce ~30 lines of duplication.

♻️ Example helper extraction
fn render_confirmation_button(
    f: &mut ratatui::Frame,
    area: Rect,
    label: &str,
    color: Color,
    is_selected: bool,
) {
    let style = if is_selected {
        Style::default()
            .bg(color)
            .fg(Color::Black)
            .add_modifier(Modifier::BOLD)
    } else {
        Style::default()
            .fg(color)
            .add_modifier(Modifier::BOLD)
    };

    let block = Block::default().borders(Borders::ALL).style(style);
    f.render_widget(block, area);

    let inner = Layout::new(Direction::Vertical, [Constraint::Min(0)])
        .margin(1)
        .split(area);

    f.render_widget(
        Paragraph::new(Line::from(vec![Span::styled(
            label,
            Style::default()
                .fg(if is_selected { Color::Black } else { color })
                .add_modifier(Modifier::BOLD),
        )]))
        .alignment(ratatui::layout::Alignment::Center),
        inner[0],
    );
}

Then use it:

render_confirmation_button(f, button_chunks[0], "✓ YES", Color::Green, selected_button);
render_confirmation_button(f, button_chunks[2], "✗ NO", Color::Red, !selected_button);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e3ecefb and ede3a18.

📒 Files selected for processing (1)
  • src/ui/admin_key_confirm.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/ui/admin_key_confirm.rs (1)
src/ui/helpers.rs (1)
  • create_centered_popup (9-18)
🔇 Additional comments (1)
src/ui/admin_key_confirm.rs (1)

8-16: LGTM! Clean API design.

The wrapper function provides a convenient interface for the common case while delegating to the more flexible implementation.

feat: improve clipboard handling with arboard wait() method  Replace arbitrary 100ms sleep with arboard SetExtLinux::wait() method for reliable clipboard operations on Linux. The wait() method ensures the clipboard instance remains alive until actually overwritten.  - Use builder pattern with wait() on Linux, fallback to set_text() elsewhere - Remove unused settings parameter from key handler functions - Update documentation to reflect improved implementation - Fix documentation to match code changes (error messages, function signatures)  Files changed: - src/ui/key_handler/mod.rs: Clipboard implementation with wait() - docs/TUI_INTERFACE.md: Updated clipboard docs and function signature example - docs/ADMIN_DISPUTES.md: Updated error message examples to match code - src/ui/key_handler/*.rs: Remove unused settings parameters - src/main.rs: Code cleanup

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Fix all issues with AI agents
In @src/ui/key_handler/confirmation.rs:
- Around line 67-216: Add a match arm for AdminMode::ConfirmAddSolver in
handle_confirm_key: when encountering
UiMode::AdminMode(AdminMode::ConfirmAddSolver(key_string, _)) call
handle_confirmation_enter(true, &key_string, default_mode,
save_solver_to_settings, |input|
UiMode::AdminMode(AdminMode::AddSolver(create_key_input_state(input)))) (use the
same default_mode computation as the other Admin cases), assign the result to
app.mode and return true so 'y' behaves like Enter for the Add Solver flow.

In @src/ui/key_handler/enter_handlers.rs:
- Line 490: The Action::PayInvoice branch in enter_handlers.rs is empty; either
implement the intended behavior (e.g., invoke the clipboard helper to copy the
invoice payload, open/display payment instructions, or send a PayInvoice
command) inside the handler where other actions are handled, or explicitly mark
it as no-op with a comment if no Enter action is required; alternatively remove
PayInvoice from the actionable-message check near the earlier filter (where
AddInvoice and PayInvoice are considered) so it never reaches this handler.
Locate the match arm for Action::PayInvoice and add the concrete call(s) to the
existing helpers used by similar cases (see how AddInvoice is handled) or add a
clear comment explaining intentional emptiness and update the filter logic
accordingly.
- Around line 197-201: The AddRelay flow currently jumps straight to
confirmation in the UiMode::AddRelay match arm; change it to validate the relay
string before creating UiMode::ConfirmRelay by reusing the same pattern as the
npub/admin/solver handlers: call a validation routine (create or use
validate_relay_url in the validation module) on key_state.key_input and only
invoke handle_input_to_confirmation(...) -> UiMode::ConfirmRelay(input, true)
when validation passes; otherwise keep the mode and surface the validation error
(e.g., set the same error handling path used by validate_npub flows) so invalid
relay URLs are rejected before saving.
🧹 Nitpick comments (4)
src/ui/key_handler/enter_handlers.rs (4)

296-364: Consider extracting duplicated take_order logic.

The take order validation and async task spawning logic (lines 296-364) is duplicated in confirmation.rs (lines 111-168). Both implementations validate range orders, parse amounts, and spawn the same async task.

♻️ Potential refactor approach

Consider extracting a shared helper function:

fn spawn_take_order_task(
    app: &mut AppState,
    take_state: TakeOrderState,
    pool: &SqlitePool,
    client: &Client,
    mostro_pubkey: nostr_sdk::PublicKey,
    order_result_tx: &UnboundedSender<crate::ui::OrderResult>,
) {
    // Validation
    if take_state.is_range_order {
        if take_state.amount_input.is_empty() || take_state.validation_error.is_some() {
            app.mode = UiMode::UserMode(UserMode::TakingOrder(take_state));
            return;
        }
    }
    
    // Set waiting mode and spawn task
    // ... (rest of the logic)
}

This would reduce duplication and ensure consistency between Enter and 'y' key handlers.


410-411: Misleading mode name for send message operation.

The code sets WaitingAddInvoice mode but then calls execute_send_msg (not execute_add_invoice). This mode name is misleading since it's used for sending FiatSent/Release messages, not adding invoices.

Consider using a more generic waiting mode (e.g., WaitingForMostro or a new WaitingSendMessage mode) to better represent the actual operation being performed.


265-283: Consider using named constants for settings option indices.

The settings option selection uses magic numbers (0, 1, 2, 3) which reduces readability and maintainability. If the settings menu order changes, these indices need manual updates.

♻️ Suggested approach

Define constants at the module level or use an enum:

const SETTINGS_MOSTRO_PUBKEY: usize = 0;
const SETTINGS_RELAY: usize = 1;
const SETTINGS_ADD_SOLVER: usize = 2;  // Admin only
const SETTINGS_ADMIN_KEY: usize = 3;    // Admin only

// Then use in the match:
match app.selected_settings_option {
    SETTINGS_MOSTRO_PUBKEY => {
        app.mode = UiMode::AddMostroPubkey(key_state);
    }
    SETTINGS_RELAY => {
        app.mode = UiMode::AddRelay(key_state);
    }
    // ...
}

This makes the code more self-documenting and easier to maintain.


31-34: Consider extracting default mode calculation to a helper.

The default mode calculation based on user_role is repeated throughout this file (lines 31-34, 388-392, 402-405) and in confirmation.rs (lines 75-78, 170-173, 184-187, 198-201, 220-223).

♻️ Suggested approach

Add a method to AppState or a helper function:

// In AppState impl
impl AppState {
    pub fn default_mode(&self) -> UiMode {
        match self.user_role {
            UserRole::User => UiMode::UserMode(UserMode::Normal),
            UserRole::Admin => UiMode::AdminMode(AdminMode::Normal),
        }
    }
}

// Usage:
let default_mode = app.default_mode();

This eliminates duplication and ensures consistency across all default mode calculations.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ede3a18 and a2b00d9.

📒 Files selected for processing (8)
  • docs/ADMIN_DISPUTES.md
  • docs/TUI_INTERFACE.md
  • src/main.rs
  • src/ui/key_handler/confirmation.rs
  • src/ui/key_handler/enter_handlers.rs
  • src/ui/key_handler/mod.rs
  • src/ui/key_handler/settings.rs
  • src/ui/key_handler/validation.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/ui/key_handler/settings.rs
🧰 Additional context used
🧬 Code graph analysis (3)
src/ui/key_handler/enter_handlers.rs (7)
src/util/order_utils/execute_add_invoice.rs (1)
  • execute_add_invoice (25-110)
src/util/order_utils/execute_send_msg.rs (1)
  • execute_send_msg (44-141)
src/main.rs (3)
  • tokio (142-142)
  • tokio (146-146)
  • settings (116-118)
src/ui/key_handler/confirmation.rs (3)
  • create_key_input_state (58-64)
  • handle_confirmation_enter (28-47)
  • handle_input_to_confirmation (12-25)
src/ui/key_handler/settings.rs (3)
  • save_admin_key_to_settings (21-27)
  • save_mostro_pubkey_to_settings (30-36)
  • save_relay_to_settings (39-49)
src/ui/key_handler/validation.rs (1)
  • validate_npub (5-13)
src/util/order_utils/take_order.rs (1)
  • take_order (36-195)
src/ui/key_handler/mod.rs (7)
src/ui/key_handler/confirmation.rs (2)
  • handle_cancel_key (219-247)
  • handle_confirm_key (67-216)
src/ui/key_handler/enter_handlers.rs (1)
  • handle_enter_key (23-212)
src/ui/key_handler/esc_handlers.rs (1)
  • handle_esc_key (5-89)
src/ui/key_handler/form_input.rs (2)
  • handle_backspace (61-87)
  • handle_char_input (5-58)
src/ui/key_handler/input_helpers.rs (2)
  • handle_invoice_input (57-62)
  • handle_key_input (66-68)
src/ui/key_handler/navigation.rs (2)
  • handle_navigation (9-22)
  • handle_tab_navigation (266-292)
src/ui/key_handler/validation.rs (1)
  • validate_npub (5-13)
src/main.rs (3)
src/util/dm_utils/notifications_ch_mng.rs (1)
  • handle_message_notification (32-56)
src/util/order_utils/fetch_scheduler.rs (1)
  • start_fetch_scheduler (32-79)
src/ui/mod.rs (1)
  • new (483-497)
🪛 LanguageTool
docs/ADMIN_DISPUTES.md

[style] ~692-~692: Consider replacing this word to strengthen your wording.
Context: ...vation is a critical security component and is thoroughly tested: - Same mnemonic ...

(AND_THAT)

🔇 Additional comments (16)
docs/ADMIN_DISPUTES.md (3)

27-101: Documentation accurately reflects the Settings tab implementation.

The reorganization of the Add Solver functionality from a dedicated Admin tab to the Settings tab is clearly documented with comprehensive feature lists and source references.


321-435: LGTM! Comprehensive documentation of the Add Solver workflow.

The sequence diagram and implementation details clearly document the complete flow from Settings tab navigation through NPUB validation, confirmation, and network communication. The code snippets and key points provide excellent reference material.


555-708: Excellent implementation status tracking and testing documentation.

The completed features section provides clear traceability with source file references, and the testing documentation offers practical guidance for running and expanding the test suite.

docs/TUI_INTERFACE.md (1)

132-132: Documentation accurately reflects the clipboard handling improvement.

The update correctly documents the Linux-specific SetExtLinux::wait() method for reliable clipboard operations, replacing the previous arbitrary delay approach.

src/ui/key_handler/validation.rs (1)

5-13: LGTM! Clean and straightforward NPUB validation.

The function correctly trims input, validates emptiness, and uses the standard nostr-sdk validation. The generic error message is appropriate for UI-facing validation.

src/ui/key_handler/mod.rs (3)

1-26: Excellent modular organization of key handling logic.

The separation into focused submodules (confirmation, enter_handlers, esc_handlers, form_input, input_helpers, navigation, settings, validation) improves maintainability and provides a clean public API through selective re-exports.


28-84: Clean early input handling with proper focus management.

The function correctly prioritizes focused input fields (invoice and key inputs) before general key handling, and properly manages the clipboard copy indicator state.


86-192: Comprehensive key event handling with proper control flow.

The match structure covers all key inputs with appropriate delegation to specialized handlers. The clipboard handling correctly uses platform-specific methods (SetExtLinux::wait() on Linux), and the mode switch is properly scoped to the Settings tab.

src/main.rs (6)

9-10: Import changes correctly reflect the fetch scheduler refactoring.

The removal of Mutex and interval_at/Instant imports aligns with moving the fetch scheduling logic to start_fetch_scheduler, and the addition of FetchSchedulerResult and AdminMode supports the new functionality.

Also applies to: 16-16, 34-34, 39-39


130-132: LGTM! Clean refactoring to background fetch scheduler.

Moving the fetch logic to start_fetch_scheduler improves separation of concerns and makes the main loop more focused on UI event handling.


208-222: Paste handling correctly implemented for admin key inputs.

The implementation follows the same safe pattern as invoice paste handling: filtering control characters and setting the just_pasted flag to prevent immediate Enter key processing.


237-247: Disputes parameter correctly passed to key handler.

The addition of the &disputes parameter enables the key handler to navigate and manage disputes in the UI.


270-277: Proper bounds checking for dispute selection index.

This validation mirrors the orders index validation and correctly prevents out-of-bounds access when the disputes list changes.


284-284: Disputes data correctly passed to UI drawing function.

This enables the UI to render disputes-related tabs and content as part of the admin workflow.

src/ui/key_handler/enter_handlers.rs (1)

330-332: Documented limitation: Invoice input for buy orders.

The TODO indicates that invoice input is needed for buy orders. This same limitation is noted in confirmation.rs line 138.

Is invoice input for buy orders planned for a future PR, or should it be addressed in this phase? If it's acceptable to defer, consider creating an issue to track this work to ensure it's not forgotten.

src/ui/key_handler/confirmation.rs (1)

11-64: LGTM! Clean helper functions.

The confirmation flow helpers are well-designed with good use of generics and callbacks. They provide a consistent pattern for:

  • Transitioning from input to confirmation (handle_input_to_confirmation)
  • Handling YES/NO selections (handle_confirmation_enter)
  • Escaping back to input (handle_confirmation_esc)
  • Creating input states (create_key_input_state)

Comment thread src/ui/key_handler/confirmation.rs
Comment thread src/ui/key_handler/enter_handlers.rs
Comment thread src/ui/key_handler/enter_handlers.rs

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to run it and I got an error message that the nsec was missing, so I deleted the whole .mostrix directory and run cargo run again, then now I get a message Error: Invalid user role: user and nothing happens, can you check if the instructions in the readme are updated with this changes?

@Catrya

Catrya commented Jan 13, 2026

Copy link
Copy Markdown
Member

I had the same error Error: Invalid user role: user but it was my first time using mostrix and I didn't modify anything in .mostrix before

@arkanoider

Copy link
Copy Markdown
Collaborator Author

Going to fix both issues!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@README.md`:
- Line 60: Replace the incorrect relay URL string "wss://damus.realy.io" with
the correct "wss://damus.relay.io" in the README example so users copy a working
URL; locate the URL literal in the README and update the host from "realy" to
"relay".
- Line 52: Update the README example to mark the nsec_privkey as an explicit
placeholder (e.g., replace the shown key or append "EXAMPLE ONLY - REPLACE THIS"
to the nsec_privkey entry) so users know not to use the plaintext value; ensure
you reference the nsec_privkey symbol in the example. Also correct the relay URL
typo by changing any instances of "wss://damus.realy.io" to
"wss://damus.relay.io" (occurrences noted in README.md and settings.toml) so the
relay URL is accurate.
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2b00d9 and fe0503f.

📒 Files selected for processing (1)
  • README.md
🧰 Additional context used
🪛 Gitleaks (8.30.0)
README.md

[high] 52-52: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (4)
README.md (4)

31-44: LGTM: Clear documentation of Settings behavior.

The expanded Settings section provides helpful guidance on the first-run vs. subsequent-run behavior and makes it clear when users should edit which settings file.


76-111: LGTM: Comprehensive field documentation.

The field explanations are clear, well-structured, and include appropriate security warnings for sensitive fields like nsec_privkey.


125-142: LGTM: TODO checklist accurately reflects completed features.

The checklist updates align with the PR objectives, particularly marking "Create Settings tab" as complete.


143-143: LGTM: Appropriate disclaimer for production readiness.

The testing note appropriately sets expectations about the maturity level of the codebase, especially given the reported first-run issues mentioned in PR comments.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread README.md
Comment thread README.md Outdated
arkanoider and others added 2 commits January 14, 2026 14:55
…ar, navigation fixes, and Enter handler refactor.

-  Add currency filter management with confirmation popup - Dynamic relay/currency updates with immediate status bar refresh - Fix navigation from order form to settings (single arrow press) - Refactor handle_enter_key into smaller focused functions - Update validation: hex pubkey, wss:// relay, currency codes - Move Settings tab to last position in user mode

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@docs/ADMIN_DISPUTES.md`:
- Around line 583-585: The docs say validate_relay accepts both "wss://" and
"ws://" but the validate_relay function only checks for "wss://"; update the
validation logic in validate_relay (src/validation.rs) to accept either protocol
by changing the condition to allow relay.starts_with("wss://") ||
relay.starts_with("ws://") and update the returned error message to "Relay URL
must start with \"wss://\" or \"ws://\"" so code and docs match.

In `@README.md`:
- Line 64: The example TOML uses an inline comment without a '#' causing parse
errors; update the configuration lines containing the key log_level (and the
similar line at 73) to move the explanatory text into a proper TOML comment
using '#' after the value (e.g., keep the value "info" and add a '#' before the
comment) so the example is valid TOML and parsable when copied.
- Line 102: Fix the minor typo in the README example string by adding a missing
space between the closing backtick and "or": change the fragment '["USD",
"EUR"]`or' to '["USD", "EUR"]` or' so the sentence reads correctly.

In `@src/ui/key_handler/confirmation.rs`:
- Around line 254-281: handle_cancel_key is missing handling for
UiMode::UserMode(UserMode::ConfirmCurrency(...)) and
UiMode::UserMode(UserMode::ConfirmClearCurrencies): add branches in
handle_cancel_key that mirror the 'n' behavior of other confirmations—when
encountering ConfirmCurrency(form) set app.mode =
UiMode::UserMode(UserMode::CreatingOrder(form.clone())); when encountering
ConfirmClearCurrencies set app.mode = default_mode (same as other canceled
actions); use the same pattern and enum variants used elsewhere in this function
to locate the correct match arms.

In `@src/ui/key_handler/navigation.rs`:
- Around line 226-233: The navigation check uses max_options as a max index
causing an off-by-one: update the logic so max_options represents the number of
options (not the last index) and compare with app.selected_settings_option
accordingly; e.g., set max_options = 6 for UserRole::Admin and 4 for non-admin,
or better compute max_options from the actual options count in settings_tab.rs
(e.g., options.len()) and then use if app.selected_settings_option + 1 <
max_options or use saturating_sub to avoid underflow—adjust the condition around
app.selected_settings_option and max_options so the last option (indices 0..=5
for admin, 0..=3 for user) is reachable.

In `@src/ui/status.rs`:
- Line 23: The expression uses the unstable/stabilized-at-1.87.0 method
is_multiple_of on the integer result of (now / 500) which breaks compatibility;
change the check to use the modulo operator instead (replace the
is_multiple_of(2) call with a modulo comparison so blink_on is true when (now /
500) % 2 == 0), keeping the same variable name blink_on and preserving integer
types to avoid needing newer Rust features.
♻️ Duplicate comments (5)
src/util/order_utils/helper.rs (1)

68-99: Validate required fields before returning a Dispute.

The function can return a Dispute with default (potentially invalid) values if the "d" or "s" tags are missing entirely. This concern was previously raised and remains unaddressed.

src/ui/key_handler/esc_handlers.rs (1)

44-53: Inconsistent mode reset for notification dismissal.

Lines 46 and 51 set app.mode to UiMode::Normal directly, while other Esc handlers use the role-specific default_mode. This could lead to inconsistent UI state after dismissing notifications vs. escaping from other modes.

Suggested fix
         UiMode::NewMessageNotification(_, _, _) => {
             // Dismiss notification
-            app.mode = UiMode::Normal;
+            app.mode = default_mode.clone();
             true
         }
         UiMode::ViewingMessage(_) => {
             // Dismiss message view popup
-            app.mode = UiMode::Normal;
+            app.mode = default_mode.clone();
             true
         }
README.md (1)

52-52: Clarify that the example nsec key is a placeholder.

The nsec key in the example configuration should be clearly marked as a placeholder to prevent accidental use. Consider adding explicit "EXAMPLE ONLY - REPLACE THIS" marking or using an obviously invalid format.

src/ui/key_handler/enter_handlers.rs (1)

610-610: PayInvoice action has no implementation.

The PayInvoice branch is empty while AddInvoice (lines 573-608) has a complete implementation. At line 346, both AddInvoice and PayInvoice are checked as actionable message types, but only AddInvoice is handled here.

If PayInvoice requires special handling (e.g., copying invoice to clipboard, displaying payment instructions), please implement it. Otherwise, consider adding a comment explaining intentional no-op behavior.

src/ui/key_handler/confirmation.rs (1)

232-250: Missing AdminMode::ConfirmAddSolver handling for 'y' key.

The handle_confirm_key function handles 'y' key confirmations for ConfirmMostroPubkey, ConfirmRelay, ConfirmCurrency, ConfirmClearCurrencies, and ConfirmAdminKey, but not for AdminMode::ConfirmAddSolver.

Meanwhile, handle_cancel_key (lines 273-276) does handle ConfirmAddSolver for the 'n' key, and enter_handlers.rs handles it for Enter. This creates an inconsistency where users can confirm with Enter and cancel with 'n', but cannot confirm with 'y'.

🔧 Suggested fix

Add handling for ConfirmAddSolver after line 244:

         UiMode::AdminMode(AdminMode::ConfirmAdminKey(key_string, _)) => {
             // ... existing code ...
             true
         }
+        UiMode::AdminMode(AdminMode::ConfirmAddSolver(solver_pubkey, _)) => {
+            // YES selected - send AddSolver message
+            let solver_pubkey_clone = solver_pubkey.clone();
+            let client_clone = client.clone();
+            let result_tx = order_result_tx.clone();
+
+            app.mode = UiMode::AdminMode(AdminMode::Normal);
+
+            tokio::spawn(async move {
+                match crate::util::order_utils::execute_admin_add_solver(
+                    &solver_pubkey_clone,
+                    &client_clone,
+                    mostro_pubkey,
+                )
+                .await
+                {
+                    Ok(_) => {
+                        let _ = result_tx.send(crate::ui::OrderResult::Info(
+                            "Solver added successfully".to_string(),
+                        ));
+                    }
+                    Err(e) => {
+                        log::error!("Failed to add solver: {}", e);
+                        let _ = result_tx.send(crate::ui::OrderResult::Error(e.to_string()));
+                    }
+                }
+            });
+            true
+        }
         mode => {
             app.mode = mode;
             false
         }
🧹 Nitpick comments (17)
settings.toml (1)

2-2: Placeholder format inconsistency with validation expectations.

The mostro_pubkey placeholder suggests hex format, but nsec_privkey uses nsec1_ prefix which is bech32 (not hex). Since the code supports both formats for pubkey validation, consider making placeholder names format-agnostic or documenting both accepted formats clearly.

Suggested clarification
 # Mostro pubkey, it can be hex or npub
-mostro_pubkey = "mostro_pubkey_hex_format"
+mostro_pubkey = "your_mostro_pubkey_here"
 # Admin private key - leave empty for user mode
 admin_privkey=""
 # Relays to connect to
 relays = ["wss://relay.mostro.network"]
 log_level = "info"
 currencies = ["VES", "ARS", "USD"]
-nsec_privkey='nsec1_privkey_format'
+nsec_privkey='your_nsec_privkey_here'

Also applies to: 9-9

src/settings.rs (2)

46-51: Consider propagating the home directory error instead of panicking.

Line 48 uses expect() which will panic if the home directory cannot be determined. Since this function already returns a Result, it would be more consistent to propagate this error.

Suggested fix
 fn init_or_load_settings_from_disk() -> Result<Settings, anyhow::Error> {
     // HOME and package name at compile time
-    let home_dir = dirs::home_dir().expect("Could not find home directory");
+    let home_dir = dirs::home_dir()
+        .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
     let package_name = env!("CARGO_PKG_NAME");
     let hidden_dir = home_dir.join(format!(".{package_name}"));
     let hidden_file = hidden_dir.join("settings.toml");

85-90: Duplicate path construction logic.

The path construction for hidden_file is duplicated between init_or_load_settings_from_disk() and save_settings(). Consider extracting a helper function to reduce duplication and ensure consistency.

Suggested helper function
/// Get the path to the settings file
fn get_settings_file_path() -> Result<PathBuf, anyhow::Error> {
    let home_dir = dirs::home_dir()
        .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?;
    let package_name = env!("CARGO_PKG_NAME");
    Ok(home_dir.join(format!(".{package_name}")).join("settings.toml"))
}
src/ui/key_handler/settings.rs (1)

43-53: Minor optimization: avoid duplicate string allocation.

The relay_string.to_string() is called twice when the relay is not already present. Consider using a local variable to avoid the extra allocation.

Suggested fix
 pub fn save_relay_to_settings(relay_string: &str) {
+    let relay = relay_string.to_string();
     save_settings_with(
         |s| {
-            if !s.relays.contains(&relay_string.to_string()) {
-                s.relays.push(relay_string.to_string());
+            if !s.relays.contains(&relay) {
+                s.relays.push(relay.clone());
             }
         },
         "Failed to save relay to settings",
         "Relay added to settings file",
     );
 }
src/util/order_utils/helper.rs (1)

119-130: Consider a more efficient deduplication strategy.

The current approach clones the entire disputes_list to buffer_dispute_list for comparison. For large dispute sets, this is O(n²) with an extra O(n) memory overhead. Consider using a HashMap keyed by id to track the latest created_at per dispute, similar to the approach in parse_orders_events.

Suggested optimization
 pub fn parse_disputes_events(events: Events) -> Vec<Dispute> {
-    let mut disputes_list = Vec::<Dispute>::new();
+    use std::collections::HashMap;
+    let mut latest_by_id: HashMap<Uuid, Dispute> = HashMap::new();

     // Scan events to extract all disputes
     for event in events.iter() {
         let mut dispute = match dispute_from_tags(event.tags.clone()) {
             Ok(d) => d,
             Err(e) => {
                 log::warn!("Failed to parse dispute from tags: {:?}", e);
                 continue;
             }
         };
         // Get created at field from Nostr event
         dispute.created_at = event.created_at.as_u64() as i64;
-        disputes_list.push(dispute);
+        
+        latest_by_id
+            .entry(dispute.id)
+            .and_modify(|existing| {
+                if dispute.created_at > existing.created_at {
+                    *existing = dispute.clone();
+                }
+            })
+            .or_insert(dispute);
     }

-    let buffer_dispute_list = disputes_list.clone();
-    // Order all elements (disputes) received to filter - discard disaligned messages
-    // if a dispute has an older message with the state we received is discarded for the latest one
-    disputes_list.retain(|keep| {
-        !buffer_dispute_list
-            .iter()
-            .any(|x| x.id == keep.id && x.created_at > keep.created_at)
-    });
-
-    // Sort by id to remove duplicates
-    disputes_list.sort_by(|a, b| b.id.cmp(&a.id));
-    disputes_list.dedup_by(|a, b| a.id == b.id);
-
-    // Finally sort list by creation time (newest first)
-    disputes_list.sort_by(|a, b| b.created_at.cmp(&a.created_at));
-    disputes_list
+    let mut disputes: Vec<Dispute> = latest_by_id.into_values().collect();
+    disputes.sort_by(|a, b| b.created_at.cmp(&a.created_at));
+    disputes
 }
src/ui/settings_tab.rs (1)

76-95: Redundant highlight_style configuration.

The list items are pre-styled with selection highlighting at lines 80-87, making the highlight_style on line 94 ineffective. The highlight_symbol on line 95 will still work, but highlight_style won't override the already-applied inline styles.

Consider removing the redundant highlight_style for clarity, or removing the inline selection styling and relying solely on highlight_style.

♻️ Option 1: Remove redundant highlight_style
     let list = List::new(items)
         .block(Block::default().borders(Borders::NONE))
-        .highlight_style(Style::default().bg(PRIMARY_COLOR).fg(Color::Black))
         .highlight_symbol(">> ");
src/util/order_utils/fetch_scheduler.rs (2)

56-67: Silent failure on fetch errors hinders debugging.

Failed fetches are silently ignored with if let Ok(...). Consider logging errors to help diagnose connectivity or protocol issues.

♻️ Proposed improvement for orders fetch
-            if let Ok(fetched_orders) = get_orders(
+            match get_orders(
                 &client_for_orders,
                 mostro_pubkey_for_orders,
                 Some(Status::Pending),
                 currencies,
             )
             .await
             {
-                let mut orders_lock = orders_clone.lock().unwrap();
-                orders_lock.clear();
-                orders_lock.extend(fetched_orders);
+                Ok(fetched_orders) => {
+                    let mut orders_lock = orders_clone.lock().unwrap();
+                    orders_lock.clear();
+                    orders_lock.extend(fetched_orders);
+                }
+                Err(e) => {
+                    tracing::warn!("Failed to fetch orders: {}", e);
+                }
             }

Also applies to: 80-86


46-54: Disk I/O on every fetch cycle.

load_settings_from_disk() is called every 10 seconds to reload currencies. While acceptable, consider caching settings with a file watcher or longer refresh interval if this becomes a performance concern.

src/ui/key_handler/enter_handlers.rs (2)

219-220: Remove duplicate doc comment.

Line 220 is an exact duplicate of line 219.

🧹 Suggested fix
 /// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.)
-/// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.)
 fn handle_enter_settings_mode(

161-182: Consider adding a waiting state for AddSolver operation.

Unlike order operations that set WaitingForMostro or WaitingTakeOrder states before spawning async tasks (e.g., line 441), the AddSolver flow immediately returns to Normal mode at line 162. If the network call is slow, users may be confused about whether the action was triggered.

This is a minor UX consideration—the current implementation works correctly, and the result popup will eventually show success/error feedback.

src/ui/key_handler/mod.rs (2)

127-131: Clarify return value semantics from handle_confirm_key.

The handle_confirm_key function returns bool indicating whether the key was handled (per its doc comment "Returns true if should continue"), but the caller wraps it in Some(should_continue). This is confusing because:

  • true from handle_confirm_key means "key was handled, skip further processing"
  • But it's returned as Some(true) which means "continue the main loop"

The current logic works correctly, but a comment clarifying the semantics would improve maintainability.


187-192: Restructure Backspace handling for clarity.

When KeyCode::Backspace is pressed, the code calls handle_char_input(code, ...) even though that function doesn't handle Backspace (it falls through to the default case). While Backspace is ultimately handled only once by handle_backspace, separating this into different match arms improves code clarity and avoids the unnecessary function call:

🔧 Suggested fix
-        KeyCode::Char(_) | KeyCode::Backspace => {
-            handle_char_input(code, app, validate_range_amount);
-            if code == KeyCode::Backspace {
-                handle_backspace(app, validate_range_amount);
-            }
+        KeyCode::Char(_) => {
+            handle_char_input(code, app, validate_range_amount);
+            Some(true)
+        }
+        KeyCode::Backspace => {
+            handle_backspace(app, validate_range_amount);
             Some(true)
         }
docs/ADMIN_DISPUTES.md (1)

383-393: Minor discrepancy in error message between documentation and code.

The error message shown in the documentation (line 389) is "Invalid key format", but the actual code in src/validation.rs returns "Invalid npub key format". Consider syncing these for accuracy.

src/main.rs (1)

278-293: Settings reloaded from disk on every draw cycle.

load_settings_from_disk() is called every 500ms (per refresh_interval). While this ensures newly added settings are reflected immediately, it introduces disk I/O on every refresh cycle.

For a TUI application this trade-off is likely acceptable, but consider caching with a file modification timestamp check if performance becomes a concern.

src/ui/mod.rs (3)

285-291: Consider using direct string literals for clarity.

The current pattern using to_string() in match guards works but is indirect. Using string literals directly would be clearer and avoid the runtime to_string() allocation.

♻️ Suggested refactor
     fn from_str(s: &str) -> Result<Self, Self::Err> {
-        match s.to_ascii_lowercase() {
-            val if val == UserRole::User.to_string() => Ok(UserRole::User),
-            val if val == UserRole::Admin.to_string() => Ok(UserRole::Admin),
-            _ => Err(anyhow::anyhow!("Invalid user role: {s}")),
-        }
+        match s.to_ascii_lowercase().as_str() {
+            "user" => Ok(UserRole::User),
+            "admin" => Ok(UserRole::Admin),
+            _ => Err(anyhow::anyhow!("Invalid user role: {s}")),
+        }
     }

502-507: Consider resetting selected_dispute_idx when switching roles.

selected_settings_option is reset to 0 in switch_role(), but selected_dispute_idx is not. For consistency, consider resetting it as well to avoid a stale index when switching to admin mode.

♻️ Suggested fix
     pub fn switch_role(&mut self, new_role: UserRole) {
         self.user_role = new_role;
         self.active_tab = Tab::first(new_role);
         self.mode = UiMode::Normal;
+        self.selected_dispute_idx = 0;
         self.selected_settings_option = 0;
     }

708-717: Consider adding an emoji to the "Add Solver" title for consistency.

Other popup titles use emojis (🔐, 🌐, 📡, 💱), but "Add Solver" on line 711 doesn't. For visual consistency, consider adding an appropriate emoji.

♻️ Suggested change
     if let UiMode::AdminMode(AdminMode::AddSolver(key_state)) = &app.mode {
         key_input_popup::render_key_input_popup(
             f,
-            "Add Solver",
+            "👤 Add Solver",
             "Enter solver public key (npub...):",
             "npub...",
             key_state,
             false,
         );
     }
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe0503f and 3d0b9e4.

📒 Files selected for processing (19)
  • README.md
  • docs/ADMIN_DISPUTES.md
  • docs/SETTINGS_ANALYSIS.md
  • settings.toml
  • src/main.rs
  • src/settings.rs
  • src/ui/key_handler/confirmation.rs
  • src/ui/key_handler/enter_handlers.rs
  • src/ui/key_handler/esc_handlers.rs
  • src/ui/key_handler/mod.rs
  • src/ui/key_handler/navigation.rs
  • src/ui/key_handler/settings.rs
  • src/ui/key_handler/validation.rs
  • src/ui/mod.rs
  • src/ui/settings_tab.rs
  • src/ui/status.rs
  • src/util/order_utils/fetch_scheduler.rs
  • src/util/order_utils/helper.rs
  • tests/validation_tests.rs
✅ Files skipped from review due to trivial changes (1)
  • docs/SETTINGS_ANALYSIS.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/ui/key_handler/validation.rs
  • tests/validation_tests.rs
🧰 Additional context used
🧬 Code graph analysis (5)
src/ui/key_handler/enter_handlers.rs (4)
src/util/order_utils/execute_add_invoice.rs (1)
  • execute_add_invoice (25-110)
src/util/order_utils/execute_admin_add_solver.rs (1)
  • execute_admin_add_solver (12-53)
src/ui/key_handler/settings.rs (5)
  • save_admin_key_to_settings (25-31)
  • save_currency_to_settings (56-67)
  • save_mostro_pubkey_to_settings (34-40)
  • save_relay_to_settings (43-53)
  • clear_currency_filters (70-78)
src/ui/mod.rs (5)
  • first (83-85)
  • first (155-157)
  • first (221-226)
  • new (486-500)
  • order_message_to_notification (453-483)
src/ui/key_handler/esc_handlers.rs (1)
src/ui/mod.rs (3)
  • first (83-85)
  • first (155-157)
  • first (221-226)
src/ui/settings_tab.rs (1)
src/ui/mod.rs (1)
  • new (486-500)
src/util/order_utils/helper.rs (1)
src/util/filters.rs (1)
  • create_filter (28-38)
src/util/order_utils/fetch_scheduler.rs (1)
src/util/order_utils/helper.rs (2)
  • get_disputes (267-291)
  • get_orders (234-263)
🪛 Gitleaks (8.30.0)
README.md

[high] 52-52: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🪛 LanguageTool
docs/ADMIN_DISPUTES.md

[style] ~785-~785: Consider replacing this word to strengthen your wording.
Context: ...vation is a critical security component and is thoroughly tested: - Same mnemonic ...

(AND_THAT)

🔇 Additional comments (23)
settings.toml (1)

10-10: LGTM!

The user_mode field with default value "user" aligns with the default_user_mode() function in src/settings.rs and provides a sensible default for first-time users.

src/settings.rs (1)

8-18: LGTM!

Adding Clone derive enables the settings struct to be cloned for UI state management, and the user_mode field with serde default provides backward compatibility with existing config files.

src/ui/key_handler/settings.rs (2)

4-22: LGTM!

The generic save_settings_with helper provides a clean read-modify-write pattern with proper error handling and logging. This reduces boilerplate across the various settings update functions.


80-96: LGTM!

The mode switch handler correctly toggles between User and Admin roles, updates the app state, and persists the change to disk. The use of new_role.to_string() ensures the persisted value matches the enum's display representation.

src/util/order_utils/helper.rs (2)

186-193: LGTM!

The multi-currency filter correctly handles all cases: None (no filter), empty list (show all), and non-empty list (filter by any matching currency). The logic is clear and the comment explains the behavior well.


265-291: LGTM!

The get_disputes function follows the same clean pattern as get_orders, providing a convenient API for fetching disputes from the Mostro network.

src/ui/key_handler/esc_handlers.rs (2)

5-10: LGTM!

The default mode determination based on user role provides a clean foundation for consistent mode transitions throughout the Esc handler.


63-93: LGTM!

The confirmation escape handlers cleanly delegate to handle_confirmation_esc with appropriate closures to reconstruct the input state. This pattern ensures users can go back to editing their input rather than losing it.

src/ui/settings_tab.rs (1)

13-145: Well-structured Settings tab implementation.

The layout correctly separates the footer into its own chunk (addressing the previous overlap issue), the role-based options are cleanly handled, and the responsive width logic for narrow terminals is a good UX consideration.

src/ui/status.rs (1)

26-56: Multi-line status bar implementation looks good.

The dynamic layout generation based on input lines, area clearing to prevent artifacts, and notification indicator placement on the last line are well-implemented.

src/util/order_utils/fetch_scheduler.rs (1)

32-91: Scheduler structure is well-designed.

The use of interval_at(Instant::now(), ...) for immediate-first-then-periodic fetching, cloning client/pubkey per task, and returning shared state handles is a clean pattern for background data refresh.

src/ui/key_handler/navigation.rs (2)

269-284: Good side-effect handling on tab switch.

The logic to clear pending notifications and mark messages as read when entering the Messages tab is well-structured, with proper guarding against redundant operations when already on the Messages tab.


9-22: Clean navigation dispatch pattern.

The modular structure separating navigation by key direction and delegating to mode-specific handlers is maintainable and extensible.

src/ui/key_handler/enter_handlers.rs (2)

252-266: LGTM - Relay validation is now properly implemented.

The relay URL validation using validate_relay before proceeding to confirmation addresses the concern from the previous review. This ensures invalid relay URLs are rejected with a proper error message.


326-340: LGTM - Normal mode Enter handling is well-structured.

The handle_enter_normal_mode function properly checks the active tab and selected order/message before transitioning to appropriate states. The TakeOrderState initialization with range order detection is correct.

src/ui/key_handler/confirmation.rs (1)

12-65: LGTM - Well-designed helper functions.

The helper functions (handle_input_to_confirmation, handle_confirmation_enter, handle_confirmation_esc, create_key_input_state) are cleanly designed with good separation of concerns. The generic function signatures enable reuse across different confirmation types.

src/ui/key_handler/mod.rs (1)

156-172: LGTM - Clipboard handling follows arboard best practices.

The platform-specific clipboard handling using SetExtLinux::wait() on Linux and direct set_text() on other platforms correctly addresses the "dropped too quickly" issue. This is the recommended approach from arboard documentation.

docs/ADMIN_DISPUTES.md (1)

32-73: LGTM - Comprehensive Settings tab documentation.

The Settings tab documentation clearly describes User and Admin mode options, features, validation rules, and keyboard support. The separation of common and admin-only features is well-documented.

src/main.rs (3)

208-222: LGTM - Paste handling extended for admin key inputs.

The paste handling for admin key inputs (AddSolver, SetupAdminKey) correctly mirrors the invoice input pattern: filtering control characters, appending to input, and setting just_pasted flag to prevent immediate Enter handling.


270-276: LGTM - Dispute index bounds check is consistent with orders pattern.

The bounds check for selected_dispute_idx correctly mirrors the existing pattern for selected_order_idx, preventing index out of bounds when the disputes list shrinks.


130-133: LGTM - Clean integration with new fetch scheduler.

The FetchSchedulerResult destructuring cleanly provides both orders and disputes from the background scheduler, and these are properly threaded through to handle_key_event and ui_draw.

src/ui/mod.rs (2)

422-428: LGTM!

The KeyInputState struct is well-designed for tracking key input state. The just_pasted flag to prevent accidental Enter-key handling after paste is a good UX consideration.


730-740: The pubkey is correctly hidden for the Add Solver confirmation. The render_admin_key_confirm_with_message function only displays the key when custom_message.is_none(). Since the Add Solver confirmation provides a custom message ("Are you sure you want to add this pubkey as dispute solver?"), the key display is skipped entirely, satisfying the PR objective.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread docs/ADMIN_DISPUTES.md
Comment thread README.md Outdated
Comment thread README.md Outdated
Comment thread src/ui/key_handler/confirmation.rs
Comment thread src/ui/key_handler/navigation.rs Outdated
Comment thread src/ui/status.rs
…mits from constants - Add 'n' key handling for ConfirmCurrency and ConfirmClearCurrencies modes - Replace hardcoded max_options with constants (ADMIN_SETTINGS_OPTIONS_COUNT, USER_SETTINGS_OPTIONS_COUNT) - Derive max_index dynamically to prevent off-by-one errors
…ation, optimize dispute parsing

- Add 'n' key handling for ConfirmCurrency and ConfirmClearCurrencies modes- Replace hardcoded max_options with constants (ADMIN_SETTINGS_OPTIONS_COUNT, USER_SETTINGS_OPTIONS_COUNT)- Optimize parse_disputes_events: use HashMap approach (O(n) instead of O(n²))- Apply DRY principle: reuse same deduplication strategy as parse_orders_events
@arkanoider arkanoider requested a review from grunch January 14, 2026 14:52
@arkanoider

Copy link
Copy Markdown
Collaborator Author

@Catrya @grunch I introduce many fixes, now you should just start with clear situation. Delete your .mostrix folder and try again.
Focus on admin part of settings, you just need to run mostrix which will start in user mode by default, go to settings switch to Admin mode with M key and let me know.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/ui/key_handler/enter_handlers.rs`:
- Around line 482-486: The NO-selection branch currently resets app.mode to
UiMode::Normal which is inconsistent with other handlers that use a
role-appropriate default_mode (e.g., UserMode::Normal or AdminMode::Normal);
change the branch to set app.mode = default_mode for the current user role
instead of UiMode::Normal — either add a UserRole parameter to this handler or
derive the role from app, then call the same logic used elsewhere to compute
default_mode and assign it to app.mode so the behavior matches other enter
handlers.
♻️ Duplicate comments (1)
src/ui/key_handler/enter_handlers.rs (1)

612-612: PayInvoice action remains unimplemented.

As noted in the previous review, the PayInvoice branch is empty. The developer acknowledged this will be completed when switching back to user mode work.

🧹 Nitpick comments (2)
src/ui/key_handler/enter_handlers.rs (2)

221-222: Duplicate doc comment.

There's a duplicate doc comment on lines 221-222.

♻️ Suggested fix
 /// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.)
-/// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.)
 fn handle_enter_settings_mode(

328-407: Consider handling poisoned mutex locks.

Lines 331 and 344 use .unwrap() on mutex locks which will panic if the mutex is poisoned. While mutex poisoning is rare and often indicates a bigger problem, you may want to handle it gracefully.

♻️ Suggested approach
-        let orders_lock = orders.lock().unwrap();
+        let Ok(orders_lock) = orders.lock() else {
+            log::error!("Orders mutex poisoned");
+            return;
+        };
-        let messages_lock = app.messages.lock().unwrap();
+        let Ok(messages_lock) = app.messages.lock() else {
+            log::error!("Messages mutex poisoned");
+            return;
+        };
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1861f45 and 97f779a.

📒 Files selected for processing (2)
  • src/ui/key_handler/confirmation.rs
  • src/ui/key_handler/enter_handlers.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/ui/key_handler/enter_handlers.rs (4)
src/main.rs (3)
  • tokio (142-142)
  • tokio (146-146)
  • settings (116-118)
src/ui/key_handler/confirmation.rs (3)
  • create_key_input_state (59-65)
  • handle_confirmation_enter (29-48)
  • handle_input_to_confirmation (13-26)
src/ui/key_handler/settings.rs (5)
  • save_admin_key_to_settings (25-31)
  • save_currency_to_settings (56-67)
  • save_mostro_pubkey_to_settings (34-40)
  • save_relay_to_settings (43-53)
  • clear_currency_filters (70-78)
src/ui/key_handler/validation.rs (4)
  • validate_currency (47-60)
  • validate_mostro_pubkey (18-30)
  • validate_npub (5-14)
  • validate_relay (33-44)
🔇 Additional comments (9)
src/ui/key_handler/confirmation.rs (5)

12-65: LGTM!

The helper functions are well-structured with clear responsibilities:

  • handle_input_to_confirmation cleanly gates empty input
  • handle_confirmation_enter properly separates YES/NO paths with a callback pattern
  • handle_confirmation_esc and create_key_input_state are simple and correct

81-111: Minor: Redundant clone on line 84.

form_clone is created on line 83, then form_clone.clone() is called again on line 84 when setting the mode. Consider reusing the same clone.

♻️ Suggested fix
         UiMode::UserMode(UserMode::ConfirmingOrder(form)) => {
             // User confirmed, send the order
             let form_clone = form.clone();
-            app.mode = UiMode::UserMode(UserMode::WaitingForMostro(form_clone.clone()));
+            app.mode = UiMode::UserMode(UserMode::WaitingForMostro(form_clone.clone()));
+            // Note: One clone is used for the mode, the other for the async task

Actually, both clones are needed - one for the mode and one for the async task. The current code is correct.


170-231: LGTM!

The settings confirmation flows are correctly implemented:

  • Each confirmation mode uses handle_confirmation_enter with true (since 'y' means YES)
  • The relay confirmation properly spawns an async task to add the relay at runtime
  • Currency filter operations are handled appropriately

232-244: Good DRY approach for ConfirmAddSolver.

The delegation to handle_enter_admin_mode with a synthesized YES selection avoids duplicating the AddSolver execution logic. This addresses the previous review comment about missing 'y' key handling for ConfirmAddSolver.


267-304: LGTM!

All confirmation modes have proper 'n' key cancellation handling, including the previously missing ConfirmCurrency and ConfirmClearCurrencies cases. The implementation correctly returns users to their respective input modes or the default mode.

src/ui/key_handler/enter_handlers.rs (4)

25-126: LGTM!

The main dispatch function is well-organized with clear mode-specific routing. The use of std::mem::replace to take ownership of the current mode is appropriate, and all UiMode variants appear to be handled.


128-219: LGTM!

The admin mode handler properly validates npub inputs before transitioning to confirmation dialogs, and the async task for adding solvers is correctly spawned with appropriate error handling.


409-416: LGTM!

Simple and correct logic for transitioning to the order confirmation flow.


488-561: LGTM!

The message viewing handler correctly maps actions to outgoing messages and handles the async send operation with appropriate error handling.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/ui/key_handler/enter_handlers.rs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/ui/key_handler/enter_handlers.rs`:
- Around line 536-537: The code unconditionally sets app.mode =
UiMode::UserMode(UserMode::WaitingAddInvoice), which forces admins into a
user-only state; update the logic to set the waiting mode based on app.user_role
instead: if the role is User, set UiMode::UserMode(UserMode::WaitingAddInvoice);
if the role is Admin either create and use an AdminMode::WaitingAddInvoice
variant or map to the appropriate AdminMode waiting state (e.g.,
UiMode::AdminMode(AdminMode::WaitingAddInvoice)) so the mode matches the
caller's role and preserves the role-aware pattern used elsewhere.
♻️ Duplicate comments (2)
src/ui/key_handler/enter_handlers.rs (2)

616-616: PayInvoice action has no implementation.

This was noted in a previous review and acknowledged as a planned future addition. Consider adding a TODO comment to make the intent clearer.


500-504: Inconsistent mode reset when NO is selected.

When NO is selected, the mode is set to UiMode::Normal directly (line 502), but elsewhere in this file the role-appropriate default mode is used. This could cause inconsistent behavior.

🔧 Suggested fix
     // Only proceed if YES is selected
     if !view_state.selected_button {
-        app.mode = UiMode::Normal;
+        let default_mode = match app.user_role {
+            UserRole::User => UiMode::UserMode(UserMode::Normal),
+            UserRole::Admin => UiMode::AdminMode(AdminMode::Normal),
+        };
+        app.mode = default_mode;
         return;
     }
🧹 Nitpick comments (2)
src/ui/key_handler/validation.rs (1)

45-57: Docstring is inconsistent with implementation.

The doc comment states the URL "must start with wss://" but the implementation accepts both wss:// and ws://. Consider updating the comment to reflect the actual behavior.

📝 Suggested doc fix
-/// Validate if a relay URL has a valid format (must start with wss://)
+/// Validate if a relay URL has a valid format (must start with wss:// or ws://)
 pub fn validate_relay(relay_str: &str) -> Result<(), String> {
src/ui/key_handler/enter_handlers.rs (1)

221-222: Duplicate comment line.

📝 Remove duplicate
 /// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.)
-/// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.)
 fn handle_enter_settings_mode(
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 97f779a and 1cbd266.

📒 Files selected for processing (2)
  • src/ui/key_handler/enter_handlers.rs
  • src/ui/key_handler/validation.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/ui/key_handler/enter_handlers.rs (6)
src/util/order_utils/execute_admin_add_solver.rs (1)
  • execute_admin_add_solver (12-53)
src/util/order_utils/execute_send_msg.rs (1)
  • execute_send_msg (44-141)
src/ui/key_handler/confirmation.rs (3)
  • create_key_input_state (59-65)
  • handle_confirmation_enter (29-48)
  • handle_input_to_confirmation (13-26)
src/ui/key_handler/settings.rs (5)
  • save_admin_key_to_settings (25-31)
  • save_currency_to_settings (56-67)
  • save_mostro_pubkey_to_settings (34-40)
  • save_relay_to_settings (43-53)
  • clear_currency_filters (70-78)
src/ui/key_handler/validation.rs (5)
  • validate_currency (60-73)
  • validate_mostro_pubkey (31-43)
  • validate_npub (5-14)
  • validate_nsec (18-27)
  • validate_relay (46-57)
src/util/order_utils/take_order.rs (1)
  • take_order (36-195)
🔇 Additional comments (10)
src/ui/key_handler/validation.rs (4)

5-14: LGTM!

The validation logic correctly uses PublicKey::from_bech32 from nostr-sdk to validate npub format. Trimming input and checking for empty strings before parsing is a good defensive pattern.


18-27: LGTM!

Consistent validation pattern with validate_npub, correctly using SecretKey::from_bech32 for nsec validation.


29-43: LGTM!

Good use of PublicKey::from_hex to validate the hex-encoded Mostro pubkey. The error message clearly indicates the expected format.


59-73: LGTM!

The intentionally lenient validation is documented and appropriate for supporting non-standard currency codes while still preventing unreasonable input.

src/ui/key_handler/enter_handlers.rs (6)

26-126: LGTM!

The main dispatcher is well-structured with clear mode matching and appropriate delegation to specialized handlers. Using std::mem::replace to take ownership of the current mode is a good pattern for avoiding borrow checker issues.


131-219: LGTM!

Admin mode handling correctly validates inputs before confirmation and properly spawns async tasks for network operations. The pattern of returning to input mode on NO selection (line 187-188) maintains good UX.


254-268: LGTM - Relay validation now implemented.

Good to see the validation step is now in place, matching the pattern used for other input types.


328-407: LGTM!

Normal mode handling covers all the appropriate tabs and settings options with clear index-based routing. The mutex lock pattern is consistent with the rest of the codebase.


418-490: LGTM!

The taking order handler properly validates range order amounts, handles the async flow correctly, and uses the role-appropriate default mode for NO selection (lines 484-488) - addressing the pattern noted in past reviews.


567-622: LGTM overall, but note the empty PayInvoice branch.

The AddInvoice handling is well-implemented with proper async flow. The empty PayInvoice branch is acknowledged as pending work.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/ui/key_handler/enter_handlers.rs

@Catrya Catrya left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK great job, and the UI is very pretty!!!

@grunch grunch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tACK, great job! this is a piece of art 🔥

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants