Skip to content

Conversation

@arkanoider
Copy link
Collaborator

@arkanoider arkanoider commented Jan 7, 2026

Add Settings Tab Features: Mostro Pubkey, Relay, and Add Solver

Overview

This PR introduces new settings management features that allow users and admins to configure Mostro instance, relays, and dispute solvers directly from the Settings tab. All features include input validation, confirmation popups, and proper error handling.

✨ 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
  • Validation:
    • Validates npub 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: Basic URL format checking
  • 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

➕ 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

🎨 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
  • Error messages: User-friendly error messages (e.g., "Invalid pubkey format")
  • Early validation: Errors shown before confirmation, preventing invalid saves

Tab Preservation

  • Settings tab is preserved after all operations
  • No unwanted navigation when closing result popups
  • Smooth workflow for multiple settings changes

🔧 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)
  • Admin solver list: Managed via Mostro network (not stored locally)

Admin Operations

  • Add Solver: Uses execute_admin_add_solver() 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

Code Organization

Settings-related handlers are organized in:

  • src/ui/key_handler/settings.rs - Settings save helpers
  • src/ui/key_handler/validation.rs - Input validation
  • src/ui/key_handler/enter_handlers.rs - Settings action handlers
  • src/util/order_utils/execute_admin_add_solver.rs - Network operation

📝 Files Changed

New Files

  • src/util/order_utils/execute_admin_add_solver.rs - Admin AddSolver implementation

Modified Files

  • src/ui/settings_tab.rs - Added new settings options
  • src/ui/admin_key_confirm.rs - Custom message support for AddSolver
  • src/ui/key_handler/settings.rs - Settings save functions
  • src/ui/key_handler/validation.rs - npub validation
  • src/ui/key_handler/enter_handlers.rs - Settings action handlers
  • src/ui/mod.rs - UI mode updates for new settings

🧪 Testing Checklist

  • Add Mostro Pubkey saves correctly to settings file
  • Add Relay appends to relays list (no duplicates)
  • Add Solver sends message to Mostro and shows success
  • npub 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

🎯 Use Cases

For Regular Users

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

For Admins

  • Manage Mostro instance configuration
  • Add custom relays
  • Onboard new dispute solvers to the network
  • All operations from a single Settings tab interface

Summary by CodeRabbit

  • New Features

    • Disputes tab; settings-based admin solver enrollment and admin key workflows; reusable key-input & confirmation popups; richer message notifications with invoice actions.
  • Improvements

    • Background scheduler auto-refreshes orders and disputes; settings reorganized with role-specific options; improved keyboard/input handling (paste behavior, NPUB validation); selection kept stable when disputes update.
  • Dependencies

    • Updated UI library version and a minor internal patch bump.

✏️ 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
Copy link
Contributor

coderabbitai bot commented Jan 7, 2026

Walkthrough

Refactors background fetching into a scheduler exposing shared orders and disputes; splits monolithic key handling into modular submodules; adds dispute parsing, disputes UI, new popups/confirmations, admin add-solver DM flow, helpers, tests, and related util exports.

Changes

Cohort / File(s) Summary
Dependency updates
Cargo.toml
ratatui pinned to "0.30.0"; mostro-core bumped 0.6.560.6.57.
Main & settings
src/main.rs, src/settings.rs
Replaced inline fetch loop with start_fetch_scheduler returning FetchSchedulerResult; ui_draw and key handling accept disputes; Settings now derives Clone.
Fetch scheduler & dispute plumbing
src/util/order_utils/fetch_scheduler.rs, src/util/order_utils/helper.rs, src/util/order_utils/mod.rs, src/util/mod.rs, src/util/filters.rs
New FetchSchedulerResult + start_fetch_scheduler that spawns periodic fetchers for orders and disputes; added dispute_from_tags, parse_disputes_events, get_disputes; exported helpers and added filter support for ListKind::Disputes.
Admin add-solver flow
src/util/order_utils/execute_admin_add_solver.rs, docs/ADMIN_DISPUTES.md
New async execute_admin_add_solver builds and sends AddSolver DM using admin private key from global settings; docs updated to reflect Settings-based flow.
Key handling modularization
src/ui/key_handler/*.rs, src/ui/key_handler/mod.rs
Replaced single key_handler.rs with modular submodules (confirmation, enter_handlers, esc_handlers, form_input, input_helpers, navigation, settings, validation); new central handle_key_event dispatcher and re-exports.
Enter/Esc/Confirm logic
src/ui/key_handler/enter_handlers.rs, src/ui/key_handler/esc_handlers.rs, src/ui/key_handler/confirmation.rs
Centralized Enter/Esc/confirmation flows; spawn async tasks for send/take/add-solver actions; validation and settings-save integration.
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 handling with paste guard (just_pasted), and validate_npub NPUB validator.
Navigation & settings helpers
src/ui/key_handler/navigation.rs, src/ui/key_handler/settings.rs
Directional/tab navigation across modes/tabs; settings persistence helpers and handle_mode_switch.
UI state, enums & root
src/ui/mod.rs, src/ui/tab_content.rs, src/lib.rs
Added KeyInputState; expanded UiMode/AdminMode variants; AppState gains selected_dispute_idx and selected_settings_option; ui_draw now accepts disputes; crate root exposes SETTINGS: OnceLock<Settings>.
New UI components
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/settings_tab.rs, src/ui/waiting.rs
Centered-popup helper, help-text renderer, order-id formatter, admin key confirmation popup, disputes tab renderer, key input popup, message notification popup, settings tab, and waiting UI.
UI cleanup / moved logic
src/ui/tab_content.rs
Removed inline render_message_notification and render_settings_tab; replaced centering/ID formatting with helpers and delegated rendering to new modules.
Tests and test helpers
tests/*, tests/common/mod.rs
DB test helper and multiple integration tests for models, parsing, validation, and helper utilities.

Sequence Diagram(s)

sequenceDiagram
    participant App as main.rs (App)
    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

    loop immediate + every 10s
        Scheduler->>Client: get_orders(Status::Pending)
        Client-->>Scheduler: Vec<SmallOrder>
        Scheduler->>Shared: replace orders vector
    end

    loop immediate + every 10s
        Scheduler->>Client: get_disputes()
        Client-->>Scheduler: Vec<Dispute>
        Scheduler->>Shared: replace disputes vector
    end

    Scheduler-->>App: return FetchSchedulerResult (orders, disputes)
    deactivate Scheduler

    App->>App: ui_draw(..., orders, disputes)
Loading
sequenceDiagram
    participant User as User Input
    participant KeyH as Key Handler
    participant Confirm as Confirmation Module
    participant Action as Async Action (DB / Nostr)
    participant Settings as SETTINGS (OnceLock)

    User->>KeyH: KeyEvent
    KeyH->>KeyH: dispatch by UiMode

    alt Confirmation flow
        KeyH->>Confirm: handle_confirm_key
        Confirm->>Confirm: YES?
        alt YES
            Confirm->>Action: spawn async (send/take/add-solver)
            Action->>Settings: read/update settings (if needed)
            Action-->>KeyH: send OrderResult on completion
        else NO
            Confirm->>KeyH: revert to input mode
        end
    else Form input
        KeyH->>KeyH: handle_char_input / handle_backspace
        KeyH->>KeyH: update form fields
    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 and store,

Disputes and orders tumbled in from yonder shore.
Key popups blink and confirmations charm the night,
Admins whisper solvers by DM under moonlight.
A rabbit cheers the tidy code — hop, patch, delight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Admin mode - Phase1 : Introducing Settings tab' directly reflects the main objective of adding a Settings tab for admin configuration, matching the primary feature described in the PR summary.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ 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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 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
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 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
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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 on lines +67 to +216
pub fn handle_confirm_key(
app: &mut AppState,
pool: &SqlitePool,
client: &Client,
mostro_pubkey: nostr_sdk::PublicKey,
order_result_tx: &UnboundedSender<crate::ui::OrderResult>,
) -> bool {
// Returns true if should continue (skip further processing)
let default_mode = match app.user_role {
UserRole::User => UiMode::UserMode(UserMode::Normal),
UserRole::Admin => UiMode::AdminMode(AdminMode::Normal),
};
match std::mem::replace(&mut app.mode, default_mode.clone()) {
UiMode::UserMode(UserMode::ConfirmingOrder(form)) => {
// User confirmed, send the order
let form_clone = form.clone();
app.mode = UiMode::UserMode(UserMode::WaitingForMostro(form_clone.clone()));

// Spawn async task to send order
let pool_clone = pool.clone();
let client_clone = client.clone();
let result_tx = order_result_tx.clone();

tokio::spawn(async move {
match crate::util::send_new_order(
&pool_clone,
&client_clone,
SETTINGS.get().unwrap(),
mostro_pubkey,
&form_clone,
)
.await
{
Ok(result) => {
let _ = result_tx.send(result);
}
Err(e) => {
log::error!("Failed to send order: {}", e);
let _ = result_tx.send(crate::ui::OrderResult::Error(e.to_string()));
}
}
});
true
}
UiMode::UserMode(UserMode::TakingOrder(take_state)) => {
// User confirmed taking the order (same as Enter key)
// Check validation first
if take_state.is_range_order {
if take_state.amount_input.is_empty() {
// Can't proceed without amount
app.mode = UiMode::UserMode(UserMode::TakingOrder(take_state));
return true;
}
if take_state.validation_error.is_some() {
// Can't proceed with invalid amount
app.mode = UiMode::UserMode(UserMode::TakingOrder(take_state));
return true;
}
}
// Proceed with taking the order
let take_state_clone = take_state.clone();
app.mode = UiMode::UserMode(UserMode::WaitingTakeOrder(take_state_clone.clone()));

// Parse amount if it's a range order
let amount = if take_state_clone.is_range_order {
take_state_clone.amount_input.trim().parse::<i64>().ok()
} else {
None
};

// For buy orders (taking sell), we'd need invoice, but for now we'll pass None
// TODO: Add invoice input for buy orders
let invoice = None;

// Spawn async task to take order
let pool_clone = pool.clone();
let client_clone = client.clone();
let result_tx = order_result_tx.clone();

tokio::spawn(async move {
match crate::util::take_order(
&pool_clone,
&client_clone,
SETTINGS.get().unwrap(),
mostro_pubkey,
&take_state_clone.order,
amount,
invoice,
)
.await
{
Ok(result) => {
let _ = result_tx.send(result);
}
Err(e) => {
log::error!("Failed to take order: {}", e);
let _ = result_tx.send(crate::ui::OrderResult::Error(e.to_string()));
}
}
});
true
}
UiMode::ConfirmMostroPubkey(key_string, _) => {
let default_mode = match app.user_role {
UserRole::User => UiMode::UserMode(UserMode::Normal),
UserRole::Admin => UiMode::AdminMode(AdminMode::Normal),
};
app.mode = handle_confirmation_enter(
true, // 'y' key means YES
&key_string,
default_mode,
save_mostro_pubkey_to_settings,
|input| UiMode::AddMostroPubkey(create_key_input_state(input)),
);
true
}
UiMode::ConfirmRelay(relay_string, _) => {
let default_mode = match app.user_role {
UserRole::User => UiMode::UserMode(UserMode::Normal),
UserRole::Admin => UiMode::AdminMode(AdminMode::Normal),
};
app.mode = handle_confirmation_enter(
true, // 'y' key means YES
&relay_string,
default_mode,
save_relay_to_settings,
|input| UiMode::AddRelay(create_key_input_state(input)),
);
true
}
UiMode::AdminMode(AdminMode::ConfirmAdminKey(key_string, _)) => {
let default_mode = match app.user_role {
UserRole::User => UiMode::UserMode(UserMode::Normal),
UserRole::Admin => UiMode::AdminMode(AdminMode::Normal),
};
app.mode = handle_confirmation_enter(
true, // 'y' key means YES
&key_string,
default_mode,
save_admin_key_to_settings,
|input| UiMode::AdminMode(AdminMode::SetupAdminKey(create_key_input_state(input))),
);
true
}
mode => {
app.mode = mode;
false
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing AdminMode::ConfirmAddSolver handling for 'y' key.

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

However, enter_handlers.rs (lines 114-149) handles ConfirmAddSolver for the Enter key. This creates an inconsistency where users can confirm with Enter but not with 'y' for the Add Solver flow, unlike other confirmation dialogs.

🔧 Suggested fix

Add handling for ConfirmAddSolver in the match statement:

         UiMode::AdminMode(AdminMode::ConfirmAdminKey(key_string, _)) => {
             let default_mode = match app.user_role {
                 UserRole::User => UiMode::UserMode(UserMode::Normal),
                 UserRole::Admin => UiMode::AdminMode(AdminMode::Normal),
             };
             app.mode = handle_confirmation_enter(
                 true, // 'y' key means YES
                 &key_string,
                 default_mode,
                 save_admin_key_to_settings,
                 |input| UiMode::AdminMode(AdminMode::SetupAdminKey(create_key_input_state(input))),
             );
             true
         }
+        UiMode::AdminMode(AdminMode::ConfirmAddSolver(solver_pubkey, _)) => {
+            // YES selected - send AddSolver message (same logic as enter_handlers.rs lines 115-143)
+            let solver_pubkey_clone = solver_pubkey.clone();
+            let client_clone = client.clone();
+            let result_tx = order_result_tx.clone();
+            let mostro_pubkey_clone = mostro_pubkey;
+
+            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_clone,
+                )
+                .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
         }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @src/ui/key_handler/confirmation.rs around lines 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.

Comment on lines +197 to +201
UiMode::AddRelay(key_state) => {
app.mode = handle_input_to_confirmation(&key_state.key_input, default_mode, |input| {
UiMode::ConfirmRelay(input, true)
});
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/ui/key_handler/enter_handlers.rs | head -220 | tail -121

Repository: MostroP2P/mostrix

Length of output: 6155


🏁 Script executed:

rg "validate" src/ui/key_handler/ -l

Repository: MostroP2P/mostrix

Length of output: 189


🏁 Script executed:

rg "relay.*url|relay.*ws|validate_relay" --type rust

Repository: MostroP2P/mostrix

Length of output: 118


🏁 Script executed:

rg "ConfirmRelay" src/ -B 2 -A 5 --type rust

Repository: MostroP2P/mostrix

Length of output: 6604


🏁 Script executed:

cat -n src/ui/key_handler/validation.rs

Repository: MostroP2P/mostrix

Length of output: 574


🏁 Script executed:

rg "save_relay_to_settings" src/ -B 2 -A 15 --type rust

Repository: MostroP2P/mostrix

Length of output: 6469


Add relay URL validation before confirmation.

Unlike the NPUB validation for Mostro pubkey (lines 173-186), Admin key (lines 150-163), and Add Solver (lines 99-113), the AddRelay flow at lines 197-201 proceeds directly to confirmation without validating the relay URL format. This allows invalid relay URLs to be saved to settings without format verification.

Consider adding a validation step matching the pattern used for npub inputs:

✨ Suggested validation approach
 UiMode::AddRelay(key_state) => {
-    app.mode = handle_input_to_confirmation(&key_state.key_input, default_mode, |input| {
-        UiMode::ConfirmRelay(input, true)
-    });
+    if key_state.key_input.trim().starts_with("ws://") || key_state.key_input.trim().starts_with("wss://") {
+        app.mode = handle_input_to_confirmation(&key_state.key_input, default_mode, |input| {
+            UiMode::ConfirmRelay(input, true)
+        });
+    } else {
+        app.mode = UiMode::OrderResult(crate::ui::OrderResult::Error(
+            "Invalid relay URL format. Must start with ws:// or wss://".to_string()
+        ));
+    }
 }

Alternatively, create a validate_relay_url function in the validation module for more comprehensive URL parsing.

📝 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::AddRelay(key_state) => {
app.mode = handle_input_to_confirmation(&key_state.key_input, default_mode, |input| {
UiMode::ConfirmRelay(input, true)
});
}
UiMode::AddRelay(key_state) => {
if key_state.key_input.trim().starts_with("ws://") || key_state.key_input.trim().starts_with("wss://") {
app.mode = handle_input_to_confirmation(&key_state.key_input, default_mode, |input| {
UiMode::ConfirmRelay(input, true)
});
} else {
app.mode = UiMode::OrderResult(crate::ui::OrderResult::Error(
"Invalid relay URL format. Must start with ws:// or wss://".to_string()
));
}
}
🤖 Prompt for AI Agents
In @src/ui/key_handler/enter_handlers.rs around lines 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.

}
}
}
Action::PayInvoice => {}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

PayInvoice action has no implementation.

The PayInvoice action branch is empty. Earlier in the file (line 234), both AddInvoice and PayInvoice are checked as actionable message types, but only AddInvoice has an implementation here.

If PayInvoice requires special handling (e.g., copying invoice to clipboard, displaying payment instructions), please implement it. If it's intentionally display-only and no action is needed on Enter, consider adding a comment explaining this, or filter it out earlier (line 234) to avoid showing it in the actionable messages flow.

Do you want me to help implement the PayInvoice handling logic?

🤖 Prompt for AI Agents
In @src/ui/key_handler/enter_handlers.rs at 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.

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.

2 participants