Admin mode - Phase1 : Introducing Settings tab #24
Conversation
- 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.
WalkthroughRefactors background fetching into a scheduler that supplies shared orders and disputes; fully rewrites key handling into modular submodules; adds dispute parsing, disputes UI, admin add-solver DM flow, multiple popup helpers, settings load/save improvements, validation utilities, tests, and docs updates. Changes
Sequence Diagram(s)sequenceDiagram
participant App as main.rs
participant Scheduler as FetchScheduler
participant Client as Nostr Client
participant Shared as Shared State (Arc<Mutex>)
App->>Scheduler: start_fetch_scheduler(client, mostro_pubkey)
activate Scheduler
Scheduler->>Client: get_orders(Status::Pending, currencies) [immediate + every 10s]
Client-->>Scheduler: Vec<SmallOrder>
Scheduler->>Shared: replace orders vector
Scheduler->>Client: get_disputes() [immediate + every 10s]
Client-->>Scheduler: Vec<Dispute>
Scheduler->>Shared: replace disputes vector
Scheduler-->>App: return FetchSchedulerResult(orders, disputes)
deactivate Scheduler
App->>App: ui_draw(..., orders, disputes)
sequenceDiagram
participant User as Terminal
participant KeyH as handle_key_event
participant Confirm as confirmation module
participant Async as Async actions (DB / Nostr)
participant Settings as SETTINGS
User->>KeyH: KeyEvent
KeyH->>KeyH: dispatch by UiMode (navigation/form/confirm)
alt Confirmation flow
KeyH->>Confirm: handle_confirm_key
Confirm-->Confirm: YES?
alt YES
Confirm->>Async: spawn(send/take/add-solver)
Async->>Settings: read/update as needed
Async-->>KeyH: send OrderResult when done
else NO
Confirm->>KeyH: revert mode
end
else Form input
KeyH->>KeyH: handle_char_input / handle_backspace
else Navigation
KeyH->>KeyH: handle_navigation (up/down/left/right/tab)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
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. Comment |
There was a problem hiding this comment.
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 usinglock().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_styleis also set on the list (line 81). Since the highlighting is already applied to individual items, thehighlight_styleon 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()returnsNone, 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_settingsparameter.The
_settingsparameter is not used inhandle_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.
.unwrap()onlock()will panic if a thread previously panicked while holding the lock. Consider using.lock().expect("mutex poisoned")for clearer panic messages, or handle thePoisonError.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_optionsvalues (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 onlock()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 reusingrender_order_id_headerfor 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 (missingbg(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 thehelpers::format_order_idfunction 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 - usesUiMode::Normalinstead of role-baseddefault_mode.When the user selects NO in the TakingOrder confirmation, the mode is set to
UiMode::Normalinstead of the role-baseddefault_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
PayInvoicearm 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'sstd::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 withenter_handlers.rs.The TakingOrder handling logic (validation, parsing, async task spawning) is nearly identical to
handle_enter_taking_orderinenter_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_orderandhandle_confirm_key.src/ui/key_handler/mod.rs (1)
185-191: Double handling of Backspace key.Both
handle_char_inputandhandle_backspaceare called when Backspace is pressed. Looking at the referenced code inform_input.rs,handle_char_inputonly matches onChar(' ')andChar(c), soBackspacefalls 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:KeyInputStatestructure mirrorsInvoiceInputState- consider unifying.
KeyInputStatehas nearly identical fields toInvoiceInputState(lines 412-417), differing only in thecopied_to_clipboardfield. 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
⛔ Files ignored due to path filters (1)
Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (27)
Cargo.tomlsrc/main.rssrc/settings.rssrc/ui/admin_key_confirm.rssrc/ui/disputes_tab.rssrc/ui/helpers.rssrc/ui/key_handler.rssrc/ui/key_handler/confirmation.rssrc/ui/key_handler/enter_handlers.rssrc/ui/key_handler/esc_handlers.rssrc/ui/key_handler/form_input.rssrc/ui/key_handler/input_helpers.rssrc/ui/key_handler/mod.rssrc/ui/key_handler/navigation.rssrc/ui/key_handler/settings.rssrc/ui/key_handler/validation.rssrc/ui/key_input_popup.rssrc/ui/message_notification.rssrc/ui/mod.rssrc/ui/settings_tab.rssrc/ui/tab_content.rssrc/util/filters.rssrc/util/mod.rssrc/util/order_utils/execute_admin_add_solver.rssrc/util/order_utils/fetch_scheduler.rssrc/util/order_utils/helper.rssrc/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_ordersand 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 usesFlex::Center(available in ratatui 0.30.0) to center the popup and properly clamps dimensions usingmin().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
Cloneto 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_disputesto 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_colorand 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_disputewithAction::AdminAddSolverandPayload::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 thenew_disputemethod name seems incongruent with theAdminAddSolveraction 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_messagefunction.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
FetchSchedulerResultstruct 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: Theunreachable!()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 ensurefocusedstays 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_notificationfunction 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_popupis 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_keyfunction usesstd::mem::replacecorrectly 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
FetchSchedulerResultis 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_idxmirrors the existing pattern forselected_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_keyfunction 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, andhandle_confirmation_escfunctions provide a clean, reusable pattern for confirmation flows using closures for customization. Thecreate_key_input_statefactory 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 letblocks 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::newandswitch_rolemethods properly initialize and reset the newselected_dispute_idxandselected_settings_optionfields.
| UiMode::NewMessageNotification(_, _, _) => { | ||
| // Dismiss notification | ||
| app.mode = UiMode::Normal; | ||
| true | ||
| } | ||
| UiMode::ViewingMessage(_) => { | ||
| // Dismiss message view popup | ||
| app.mode = UiMode::Normal; | ||
| true | ||
| } |
There was a problem hiding this comment.
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.
| 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().
| /// 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) | ||
| } |
There was a problem hiding this comment.
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.
| /// 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.
There was a problem hiding this comment.
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
ListItembased onselected_option(lines 68-75), then also setshighlight_styleandhighlight_symbolon theListwidget (lines 82-83). This is redundant—typically you either style items manually or rely on theList'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
📒 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: Theselected_settings_optionstate is properly managed by the caller. When the user role changes,AppState::switch_role()explicitly resetsselected_settings_optionto0(line 503 inui/mod.rs). Since both User mode (2 options) and Admin mode (4 options) have index0as valid, no out-of-bounds condition can occur. No action needed.Likely an incorrect or invalid review comment.
There was a problem hiding this comment.
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
CantDoReasonvariants is brittle—if new variants are added to the enum inmostro-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'sCantDoReasonderivesstrum::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
strumis 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_dbmay diverge from the production schema over time, causing tests to pass while production code fails. Consider one of the following approaches to maintain consistency:
- Shared schema definition: Extract the schema to a common location used by both tests and migrations.
- Run production migrations: Apply the same migrations used in production to the test database.
- 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 aHashMap<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
📒 Files selected for processing (10)
docs/ADMIN_DISPUTES.mdsrc/lib.rssrc/ui/key_handler/mod.rssrc/util/order_utils/helper.rssrc/util/order_utils/mod.rstests/common/mod.rstests/db_tests.rstests/helper_tests.rstests/parsing_tests.rstests/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_tagsfor 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
Useris 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 viaget_or_init()insrc/settings.rs:42rather than requiring manualSETTINGS.set()calls. Usingget_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_tagspublic is consistent with the newdispute_from_tagsfunction and allows external modules to reuse the order parsing logic.
215-220: LGTM: Consistent implementation for dispute fetching.The
ListKind::Disputesbranch follows the same pattern as theOrdersbranch, ensuring consistent behavior across different event types.
257-283: LGTM: Clean API for dispute retrieval.The
get_disputesfunction provides a straightforward wrapper that mirrors theget_orderspattern, 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>
There was a problem hiding this comment.
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
📒 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
There was a problem hiding this comment.
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
WaitingAddInvoicemode but then callsexecute_send_msg(notexecute_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.,
WaitingForMostroor a newWaitingSendMessagemode) 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_roleis repeated throughout this file (lines 31-34, 388-392, 402-405) and inconfirmation.rs(lines 75-78, 170-173, 184-187, 198-201, 220-223).♻️ Suggested approach
Add a method to
AppStateor 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
📒 Files selected for processing (8)
docs/ADMIN_DISPUTES.mddocs/TUI_INTERFACE.mdsrc/main.rssrc/ui/key_handler/confirmation.rssrc/ui/key_handler/enter_handlers.rssrc/ui/key_handler/mod.rssrc/ui/key_handler/settings.rssrc/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
Mutexandinterval_at/Instantimports aligns with moving the fetch scheduling logic tostart_fetch_scheduler, and the addition ofFetchSchedulerResultandAdminModesupports 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_schedulerimproves 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_pastedflag to prevent immediate Enter key processing.
237-247: Disputes parameter correctly passed to key handler.The addition of the
&disputesparameter 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.rsline 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)
grunch
left a comment
There was a problem hiding this comment.
I tried to run it and I got an error message that the nsec was missing, so I deleted the whole .mostrix directory and run cargo run again, then now I get a message Error: Invalid user role: user and nothing happens, can you check if the instructions in the readme are updated with this changes?
|
I had the same error |
|
Going to fix both issues! |
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Fix all issues with AI agents
In `@README.md`:
- Line 60: Replace the incorrect relay URL string "wss://damus.realy.io" with
the correct "wss://damus.relay.io" in the README example so users copy a working
URL; locate the URL literal in the README and update the host from "realy" to
"relay".
- Line 52: Update the README example to mark the nsec_privkey as an explicit
placeholder (e.g., replace the shown key or append "EXAMPLE ONLY - REPLACE THIS"
to the nsec_privkey entry) so users know not to use the plaintext value; ensure
you reference the nsec_privkey symbol in the example. Also correct the relay URL
typo by changing any instances of "wss://damus.realy.io" to
"wss://damus.relay.io" (occurrences noted in README.md and settings.toml) so the
relay URL is accurate.
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (1)
README.md
🧰 Additional context used
🪛 Gitleaks (8.30.0)
README.md
[high] 52-52: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🔇 Additional comments (4)
README.md (4)
31-44: LGTM: Clear documentation of Settings behavior.The expanded Settings section provides helpful guidance on the first-run vs. subsequent-run behavior and makes it clear when users should edit which settings file.
76-111: LGTM: Comprehensive field documentation.The field explanations are clear, well-structured, and include appropriate security warnings for sensitive fields like
nsec_privkey.
125-142: LGTM: TODO checklist accurately reflects completed features.The checklist updates align with the PR objectives, particularly marking "Create Settings tab" as complete.
143-143: LGTM: Appropriate disclaimer for production readiness.The testing note appropriately sets expectations about the maturity level of the codebase, especially given the reported first-run issues mentioned in PR comments.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
…ar, navigation fixes, and Enter handler refactor. - Add currency filter management with confirmation popup - Dynamic relay/currency updates with immediate status bar refresh - Fix navigation from order form to settings (single arrow press) - Refactor handle_enter_key into smaller focused functions - Update validation: hex pubkey, wss:// relay, currency codes - Move Settings tab to last position in user mode
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@docs/ADMIN_DISPUTES.md`:
- Around line 583-585: The docs say validate_relay accepts both "wss://" and
"ws://" but the validate_relay function only checks for "wss://"; update the
validation logic in validate_relay (src/validation.rs) to accept either protocol
by changing the condition to allow relay.starts_with("wss://") ||
relay.starts_with("ws://") and update the returned error message to "Relay URL
must start with \"wss://\" or \"ws://\"" so code and docs match.
In `@README.md`:
- Line 64: The example TOML uses an inline comment without a '#' causing parse
errors; update the configuration lines containing the key log_level (and the
similar line at 73) to move the explanatory text into a proper TOML comment
using '#' after the value (e.g., keep the value "info" and add a '#' before the
comment) so the example is valid TOML and parsable when copied.
- Line 102: Fix the minor typo in the README example string by adding a missing
space between the closing backtick and "or": change the fragment '["USD",
"EUR"]`or' to '["USD", "EUR"]` or' so the sentence reads correctly.
In `@src/ui/key_handler/confirmation.rs`:
- Around line 254-281: handle_cancel_key is missing handling for
UiMode::UserMode(UserMode::ConfirmCurrency(...)) and
UiMode::UserMode(UserMode::ConfirmClearCurrencies): add branches in
handle_cancel_key that mirror the 'n' behavior of other confirmations—when
encountering ConfirmCurrency(form) set app.mode =
UiMode::UserMode(UserMode::CreatingOrder(form.clone())); when encountering
ConfirmClearCurrencies set app.mode = default_mode (same as other canceled
actions); use the same pattern and enum variants used elsewhere in this function
to locate the correct match arms.
In `@src/ui/key_handler/navigation.rs`:
- Around line 226-233: The navigation check uses max_options as a max index
causing an off-by-one: update the logic so max_options represents the number of
options (not the last index) and compare with app.selected_settings_option
accordingly; e.g., set max_options = 6 for UserRole::Admin and 4 for non-admin,
or better compute max_options from the actual options count in settings_tab.rs
(e.g., options.len()) and then use if app.selected_settings_option + 1 <
max_options or use saturating_sub to avoid underflow—adjust the condition around
app.selected_settings_option and max_options so the last option (indices 0..=5
for admin, 0..=3 for user) is reachable.
In `@src/ui/status.rs`:
- Line 23: The expression uses the unstable/stabilized-at-1.87.0 method
is_multiple_of on the integer result of (now / 500) which breaks compatibility;
change the check to use the modulo operator instead (replace the
is_multiple_of(2) call with a modulo comparison so blink_on is true when (now /
500) % 2 == 0), keeping the same variable name blink_on and preserving integer
types to avoid needing newer Rust features.
♻️ Duplicate comments (5)
src/util/order_utils/helper.rs (1)
68-99: Validate required fields before returning a Dispute.The function can return a
Disputewith default (potentially invalid) values if the "d" or "s" tags are missing entirely. This concern was previously raised and remains unaddressed.src/ui/key_handler/esc_handlers.rs (1)
44-53: Inconsistent mode reset for notification dismissal.Lines 46 and 51 set
app.modetoUiMode::Normaldirectly, while other Esc handlers use the role-specificdefault_mode. This could lead to inconsistent UI state after dismissing notifications vs. escaping from other modes.Suggested fix
UiMode::NewMessageNotification(_, _, _) => { // Dismiss notification - app.mode = UiMode::Normal; + app.mode = default_mode.clone(); true } UiMode::ViewingMessage(_) => { // Dismiss message view popup - app.mode = UiMode::Normal; + app.mode = default_mode.clone(); true }README.md (1)
52-52: Clarify that the example nsec key is a placeholder.The nsec key in the example configuration should be clearly marked as a placeholder to prevent accidental use. Consider adding explicit "EXAMPLE ONLY - REPLACE THIS" marking or using an obviously invalid format.
src/ui/key_handler/enter_handlers.rs (1)
610-610: PayInvoice action has no implementation.The
PayInvoicebranch is empty whileAddInvoice(lines 573-608) has a complete implementation. At line 346, bothAddInvoiceandPayInvoiceare checked as actionable message types, but onlyAddInvoiceis handled here.If
PayInvoicerequires special handling (e.g., copying invoice to clipboard, displaying payment instructions), please implement it. Otherwise, consider adding a comment explaining intentional no-op behavior.src/ui/key_handler/confirmation.rs (1)
232-250: MissingAdminMode::ConfirmAddSolverhandling for 'y' key.The
handle_confirm_keyfunction handles 'y' key confirmations forConfirmMostroPubkey,ConfirmRelay,ConfirmCurrency,ConfirmClearCurrencies, andConfirmAdminKey, but not forAdminMode::ConfirmAddSolver.Meanwhile,
handle_cancel_key(lines 273-276) does handleConfirmAddSolverfor the 'n' key, andenter_handlers.rshandles it for Enter. This creates an inconsistency where users can confirm with Enter and cancel with 'n', but cannot confirm with 'y'.🔧 Suggested fix
Add handling for
ConfirmAddSolverafter line 244:UiMode::AdminMode(AdminMode::ConfirmAdminKey(key_string, _)) => { // ... existing code ... true } + UiMode::AdminMode(AdminMode::ConfirmAddSolver(solver_pubkey, _)) => { + // YES selected - send AddSolver message + let solver_pubkey_clone = solver_pubkey.clone(); + let client_clone = client.clone(); + let result_tx = order_result_tx.clone(); + + app.mode = UiMode::AdminMode(AdminMode::Normal); + + tokio::spawn(async move { + match crate::util::order_utils::execute_admin_add_solver( + &solver_pubkey_clone, + &client_clone, + mostro_pubkey, + ) + .await + { + Ok(_) => { + let _ = result_tx.send(crate::ui::OrderResult::Info( + "Solver added successfully".to_string(), + )); + } + Err(e) => { + log::error!("Failed to add solver: {}", e); + let _ = result_tx.send(crate::ui::OrderResult::Error(e.to_string())); + } + } + }); + true + } mode => { app.mode = mode; false }
🧹 Nitpick comments (17)
settings.toml (1)
2-2: Placeholder format inconsistency with validation expectations.The
mostro_pubkeyplaceholder suggests hex format, butnsec_privkeyusesnsec1_prefix which is bech32 (not hex). Since the code supports both formats for pubkey validation, consider making placeholder names format-agnostic or documenting both accepted formats clearly.Suggested clarification
# Mostro pubkey, it can be hex or npub -mostro_pubkey = "mostro_pubkey_hex_format" +mostro_pubkey = "your_mostro_pubkey_here" # Admin private key - leave empty for user mode admin_privkey="" # Relays to connect to relays = ["wss://relay.mostro.network"] log_level = "info" currencies = ["VES", "ARS", "USD"] -nsec_privkey='nsec1_privkey_format' +nsec_privkey='your_nsec_privkey_here'Also applies to: 9-9
src/settings.rs (2)
46-51: Consider propagating the home directory error instead of panicking.Line 48 uses
expect()which will panic if the home directory cannot be determined. Since this function already returns aResult, it would be more consistent to propagate this error.Suggested fix
fn init_or_load_settings_from_disk() -> Result<Settings, anyhow::Error> { // HOME and package name at compile time - let home_dir = dirs::home_dir().expect("Could not find home directory"); + let home_dir = dirs::home_dir() + .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?; let package_name = env!("CARGO_PKG_NAME"); let hidden_dir = home_dir.join(format!(".{package_name}")); let hidden_file = hidden_dir.join("settings.toml");
85-90: Duplicate path construction logic.The path construction for
hidden_fileis duplicated betweeninit_or_load_settings_from_disk()andsave_settings(). Consider extracting a helper function to reduce duplication and ensure consistency.Suggested helper function
/// Get the path to the settings file fn get_settings_file_path() -> Result<PathBuf, anyhow::Error> { let home_dir = dirs::home_dir() .ok_or_else(|| anyhow::anyhow!("Could not find home directory"))?; let package_name = env!("CARGO_PKG_NAME"); Ok(home_dir.join(format!(".{package_name}")).join("settings.toml")) }src/ui/key_handler/settings.rs (1)
43-53: Minor optimization: avoid duplicate string allocation.The
relay_string.to_string()is called twice when the relay is not already present. Consider using a local variable to avoid the extra allocation.Suggested fix
pub fn save_relay_to_settings(relay_string: &str) { + let relay = relay_string.to_string(); save_settings_with( |s| { - if !s.relays.contains(&relay_string.to_string()) { - s.relays.push(relay_string.to_string()); + if !s.relays.contains(&relay) { + s.relays.push(relay.clone()); } }, "Failed to save relay to settings", "Relay added to settings file", ); }src/util/order_utils/helper.rs (1)
119-130: Consider a more efficient deduplication strategy.The current approach clones the entire
disputes_listtobuffer_dispute_listfor comparison. For large dispute sets, this is O(n²) with an extra O(n) memory overhead. Consider using aHashMapkeyed byidto track the latestcreated_atper dispute, similar to the approach inparse_orders_events.Suggested optimization
pub fn parse_disputes_events(events: Events) -> Vec<Dispute> { - let mut disputes_list = Vec::<Dispute>::new(); + use std::collections::HashMap; + let mut latest_by_id: HashMap<Uuid, Dispute> = HashMap::new(); // Scan events to extract all disputes for event in events.iter() { let mut dispute = match dispute_from_tags(event.tags.clone()) { Ok(d) => d, Err(e) => { log::warn!("Failed to parse dispute from tags: {:?}", e); continue; } }; // Get created at field from Nostr event dispute.created_at = event.created_at.as_u64() as i64; - disputes_list.push(dispute); + + latest_by_id + .entry(dispute.id) + .and_modify(|existing| { + if dispute.created_at > existing.created_at { + *existing = dispute.clone(); + } + }) + .or_insert(dispute); } - let buffer_dispute_list = disputes_list.clone(); - // Order all elements (disputes) received to filter - discard disaligned messages - // if a dispute has an older message with the state we received is discarded for the latest one - disputes_list.retain(|keep| { - !buffer_dispute_list - .iter() - .any(|x| x.id == keep.id && x.created_at > keep.created_at) - }); - - // Sort by id to remove duplicates - disputes_list.sort_by(|a, b| b.id.cmp(&a.id)); - disputes_list.dedup_by(|a, b| a.id == b.id); - - // Finally sort list by creation time (newest first) - disputes_list.sort_by(|a, b| b.created_at.cmp(&a.created_at)); - disputes_list + let mut disputes: Vec<Dispute> = latest_by_id.into_values().collect(); + disputes.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + disputes }src/ui/settings_tab.rs (1)
76-95: Redundanthighlight_styleconfiguration.The list items are pre-styled with selection highlighting at lines 80-87, making the
highlight_styleon line 94 ineffective. Thehighlight_symbolon line 95 will still work, buthighlight_stylewon't override the already-applied inline styles.Consider removing the redundant
highlight_stylefor clarity, or removing the inline selection styling and relying solely onhighlight_style.♻️ Option 1: Remove redundant highlight_style
let list = List::new(items) .block(Block::default().borders(Borders::NONE)) - .highlight_style(Style::default().bg(PRIMARY_COLOR).fg(Color::Black)) .highlight_symbol(">> ");src/util/order_utils/fetch_scheduler.rs (2)
56-67: Silent failure on fetch errors hinders debugging.Failed fetches are silently ignored with
if let Ok(...). Consider logging errors to help diagnose connectivity or protocol issues.♻️ Proposed improvement for orders fetch
- if let Ok(fetched_orders) = get_orders( + match get_orders( &client_for_orders, mostro_pubkey_for_orders, Some(Status::Pending), currencies, ) .await { - let mut orders_lock = orders_clone.lock().unwrap(); - orders_lock.clear(); - orders_lock.extend(fetched_orders); + Ok(fetched_orders) => { + let mut orders_lock = orders_clone.lock().unwrap(); + orders_lock.clear(); + orders_lock.extend(fetched_orders); + } + Err(e) => { + tracing::warn!("Failed to fetch orders: {}", e); + } }Also applies to: 80-86
46-54: Disk I/O on every fetch cycle.
load_settings_from_disk()is called every 10 seconds to reload currencies. While acceptable, consider caching settings with a file watcher or longer refresh interval if this becomes a performance concern.src/ui/key_handler/enter_handlers.rs (2)
219-220: Remove duplicate doc comment.Line 220 is an exact duplicate of line 219.
🧹 Suggested fix
/// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.) -/// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.) fn handle_enter_settings_mode(
161-182: Consider adding a waiting state for AddSolver operation.Unlike order operations that set
WaitingForMostroorWaitingTakeOrderstates before spawning async tasks (e.g., line 441), the AddSolver flow immediately returns toNormalmode at line 162. If the network call is slow, users may be confused about whether the action was triggered.This is a minor UX consideration—the current implementation works correctly, and the result popup will eventually show success/error feedback.
src/ui/key_handler/mod.rs (2)
127-131: Clarify return value semantics fromhandle_confirm_key.The
handle_confirm_keyfunction returnsboolindicating whether the key was handled (per its doc comment "Returns true if should continue"), but the caller wraps it inSome(should_continue). This is confusing because:
truefromhandle_confirm_keymeans "key was handled, skip further processing"- But it's returned as
Some(true)which means "continue the main loop"The current logic works correctly, but a comment clarifying the semantics would improve maintainability.
187-192: Restructure Backspace handling for clarity.When
KeyCode::Backspaceis pressed, the code callshandle_char_input(code, ...)even though that function doesn't handle Backspace (it falls through to the default case). While Backspace is ultimately handled only once byhandle_backspace, separating this into different match arms improves code clarity and avoids the unnecessary function call:🔧 Suggested fix
- KeyCode::Char(_) | KeyCode::Backspace => { - handle_char_input(code, app, validate_range_amount); - if code == KeyCode::Backspace { - handle_backspace(app, validate_range_amount); - } + KeyCode::Char(_) => { + handle_char_input(code, app, validate_range_amount); + Some(true) + } + KeyCode::Backspace => { + handle_backspace(app, validate_range_amount); Some(true) }docs/ADMIN_DISPUTES.md (1)
383-393: Minor discrepancy in error message between documentation and code.The error message shown in the documentation (line 389) is
"Invalid key format", but the actual code insrc/validation.rsreturns"Invalid npub key format". Consider syncing these for accuracy.src/main.rs (1)
278-293: Settings reloaded from disk on every draw cycle.
load_settings_from_disk()is called every 500ms (perrefresh_interval). While this ensures newly added settings are reflected immediately, it introduces disk I/O on every refresh cycle.For a TUI application this trade-off is likely acceptable, but consider caching with a file modification timestamp check if performance becomes a concern.
src/ui/mod.rs (3)
285-291: Consider using direct string literals for clarity.The current pattern using
to_string()in match guards works but is indirect. Using string literals directly would be clearer and avoid the runtimeto_string()allocation.♻️ Suggested refactor
fn from_str(s: &str) -> Result<Self, Self::Err> { - match s.to_ascii_lowercase() { - val if val == UserRole::User.to_string() => Ok(UserRole::User), - val if val == UserRole::Admin.to_string() => Ok(UserRole::Admin), - _ => Err(anyhow::anyhow!("Invalid user role: {s}")), - } + match s.to_ascii_lowercase().as_str() { + "user" => Ok(UserRole::User), + "admin" => Ok(UserRole::Admin), + _ => Err(anyhow::anyhow!("Invalid user role: {s}")), + } }
502-507: Consider resettingselected_dispute_idxwhen switching roles.
selected_settings_optionis reset to 0 inswitch_role(), butselected_dispute_idxis not. For consistency, consider resetting it as well to avoid a stale index when switching to admin mode.♻️ Suggested fix
pub fn switch_role(&mut self, new_role: UserRole) { self.user_role = new_role; self.active_tab = Tab::first(new_role); self.mode = UiMode::Normal; + self.selected_dispute_idx = 0; self.selected_settings_option = 0; }
708-717: Consider adding an emoji to the "Add Solver" title for consistency.Other popup titles use emojis (🔐, 🌐, 📡, 💱), but "Add Solver" on line 711 doesn't. For visual consistency, consider adding an appropriate emoji.
♻️ Suggested change
if let UiMode::AdminMode(AdminMode::AddSolver(key_state)) = &app.mode { key_input_popup::render_key_input_popup( f, - "Add Solver", + "👤 Add Solver", "Enter solver public key (npub...):", "npub...", key_state, false, ); }
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (19)
README.mddocs/ADMIN_DISPUTES.mddocs/SETTINGS_ANALYSIS.mdsettings.tomlsrc/main.rssrc/settings.rssrc/ui/key_handler/confirmation.rssrc/ui/key_handler/enter_handlers.rssrc/ui/key_handler/esc_handlers.rssrc/ui/key_handler/mod.rssrc/ui/key_handler/navigation.rssrc/ui/key_handler/settings.rssrc/ui/key_handler/validation.rssrc/ui/mod.rssrc/ui/settings_tab.rssrc/ui/status.rssrc/util/order_utils/fetch_scheduler.rssrc/util/order_utils/helper.rstests/validation_tests.rs
✅ Files skipped from review due to trivial changes (1)
- docs/SETTINGS_ANALYSIS.md
🚧 Files skipped from review as they are similar to previous changes (2)
- src/ui/key_handler/validation.rs
- tests/validation_tests.rs
🧰 Additional context used
🧬 Code graph analysis (5)
src/ui/key_handler/enter_handlers.rs (4)
src/util/order_utils/execute_add_invoice.rs (1)
execute_add_invoice(25-110)src/util/order_utils/execute_admin_add_solver.rs (1)
execute_admin_add_solver(12-53)src/ui/key_handler/settings.rs (5)
save_admin_key_to_settings(25-31)save_currency_to_settings(56-67)save_mostro_pubkey_to_settings(34-40)save_relay_to_settings(43-53)clear_currency_filters(70-78)src/ui/mod.rs (5)
first(83-85)first(155-157)first(221-226)new(486-500)order_message_to_notification(453-483)
src/ui/key_handler/esc_handlers.rs (1)
src/ui/mod.rs (3)
first(83-85)first(155-157)first(221-226)
src/ui/settings_tab.rs (1)
src/ui/mod.rs (1)
new(486-500)
src/util/order_utils/helper.rs (1)
src/util/filters.rs (1)
create_filter(28-38)
src/util/order_utils/fetch_scheduler.rs (1)
src/util/order_utils/helper.rs (2)
get_disputes(267-291)get_orders(234-263)
🪛 Gitleaks (8.30.0)
README.md
[high] 52-52: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.
(generic-api-key)
🪛 LanguageTool
docs/ADMIN_DISPUTES.md
[style] ~785-~785: Consider replacing this word to strengthen your wording.
Context: ...vation is a critical security component and is thoroughly tested: - Same mnemonic ...
(AND_THAT)
🔇 Additional comments (23)
settings.toml (1)
10-10: LGTM!The
user_modefield with default value "user" aligns with thedefault_user_mode()function insrc/settings.rsand provides a sensible default for first-time users.src/settings.rs (1)
8-18: LGTM!Adding
Clonederive enables the settings struct to be cloned for UI state management, and theuser_modefield with serde default provides backward compatibility with existing config files.src/ui/key_handler/settings.rs (2)
4-22: LGTM!The generic
save_settings_withhelper provides a clean read-modify-write pattern with proper error handling and logging. This reduces boilerplate across the various settings update functions.
80-96: LGTM!The mode switch handler correctly toggles between User and Admin roles, updates the app state, and persists the change to disk. The use of
new_role.to_string()ensures the persisted value matches the enum's display representation.src/util/order_utils/helper.rs (2)
186-193: LGTM!The multi-currency filter correctly handles all cases:
None(no filter), empty list (show all), and non-empty list (filter by any matching currency). The logic is clear and the comment explains the behavior well.
265-291: LGTM!The
get_disputesfunction follows the same clean pattern asget_orders, providing a convenient API for fetching disputes from the Mostro network.src/ui/key_handler/esc_handlers.rs (2)
5-10: LGTM!The default mode determination based on user role provides a clean foundation for consistent mode transitions throughout the Esc handler.
63-93: LGTM!The confirmation escape handlers cleanly delegate to
handle_confirmation_escwith appropriate closures to reconstruct the input state. This pattern ensures users can go back to editing their input rather than losing it.src/ui/settings_tab.rs (1)
13-145: Well-structured Settings tab implementation.The layout correctly separates the footer into its own chunk (addressing the previous overlap issue), the role-based options are cleanly handled, and the responsive width logic for narrow terminals is a good UX consideration.
src/ui/status.rs (1)
26-56: Multi-line status bar implementation looks good.The dynamic layout generation based on input lines, area clearing to prevent artifacts, and notification indicator placement on the last line are well-implemented.
src/util/order_utils/fetch_scheduler.rs (1)
32-91: Scheduler structure is well-designed.The use of
interval_at(Instant::now(), ...)for immediate-first-then-periodic fetching, cloning client/pubkey per task, and returning shared state handles is a clean pattern for background data refresh.src/ui/key_handler/navigation.rs (2)
269-284: Good side-effect handling on tab switch.The logic to clear pending notifications and mark messages as read when entering the Messages tab is well-structured, with proper guarding against redundant operations when already on the Messages tab.
9-22: Clean navigation dispatch pattern.The modular structure separating navigation by key direction and delegating to mode-specific handlers is maintainable and extensible.
src/ui/key_handler/enter_handlers.rs (2)
252-266: LGTM - Relay validation is now properly implemented.The relay URL validation using
validate_relaybefore proceeding to confirmation addresses the concern from the previous review. This ensures invalid relay URLs are rejected with a proper error message.
326-340: LGTM - Normal mode Enter handling is well-structured.The
handle_enter_normal_modefunction properly checks the active tab and selected order/message before transitioning to appropriate states. TheTakeOrderStateinitialization with range order detection is correct.src/ui/key_handler/confirmation.rs (1)
12-65: LGTM - Well-designed helper functions.The helper functions (
handle_input_to_confirmation,handle_confirmation_enter,handle_confirmation_esc,create_key_input_state) are cleanly designed with good separation of concerns. The generic function signatures enable reuse across different confirmation types.src/ui/key_handler/mod.rs (1)
156-172: LGTM - Clipboard handling follows arboard best practices.The platform-specific clipboard handling using
SetExtLinux::wait()on Linux and directset_text()on other platforms correctly addresses the "dropped too quickly" issue. This is the recommended approach from arboard documentation.docs/ADMIN_DISPUTES.md (1)
32-73: LGTM - Comprehensive Settings tab documentation.The Settings tab documentation clearly describes User and Admin mode options, features, validation rules, and keyboard support. The separation of common and admin-only features is well-documented.
src/main.rs (3)
208-222: LGTM - Paste handling extended for admin key inputs.The paste handling for admin key inputs (
AddSolver,SetupAdminKey) correctly mirrors the invoice input pattern: filtering control characters, appending to input, and settingjust_pastedflag to prevent immediate Enter handling.
270-276: LGTM - Dispute index bounds check is consistent with orders pattern.The bounds check for
selected_dispute_idxcorrectly mirrors the existing pattern forselected_order_idx, preventing index out of bounds when the disputes list shrinks.
130-133: LGTM - Clean integration with new fetch scheduler.The
FetchSchedulerResultdestructuring cleanly provides bothordersanddisputesfrom the background scheduler, and these are properly threaded through tohandle_key_eventandui_draw.src/ui/mod.rs (2)
422-428: LGTM!The
KeyInputStatestruct is well-designed for tracking key input state. Thejust_pastedflag to prevent accidental Enter-key handling after paste is a good UX consideration.
730-740: The pubkey is correctly hidden for the Add Solver confirmation. Therender_admin_key_confirm_with_messagefunction only displays the key whencustom_message.is_none(). Since the Add Solver confirmation provides a custom message ("Are you sure you want to add this pubkey as dispute solver?"), the key display is skipped entirely, satisfying the PR objective.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
…strix into admin-list-disputes
…mits from constants - Add 'n' key handling for ConfirmCurrency and ConfirmClearCurrencies modes - Replace hardcoded max_options with constants (ADMIN_SETTINGS_OPTIONS_COUNT, USER_SETTINGS_OPTIONS_COUNT) - Derive max_index dynamically to prevent off-by-one errors
…ation, optimize dispute parsing - Add 'n' key handling for ConfirmCurrency and ConfirmClearCurrencies modes- Replace hardcoded max_options with constants (ADMIN_SETTINGS_OPTIONS_COUNT, USER_SETTINGS_OPTIONS_COUNT)- Optimize parse_disputes_events: use HashMap approach (O(n) instead of O(n²))- Apply DRY principle: reuse same deduplication strategy as parse_orders_events
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/ui/key_handler/enter_handlers.rs`:
- Around line 482-486: The NO-selection branch currently resets app.mode to
UiMode::Normal which is inconsistent with other handlers that use a
role-appropriate default_mode (e.g., UserMode::Normal or AdminMode::Normal);
change the branch to set app.mode = default_mode for the current user role
instead of UiMode::Normal — either add a UserRole parameter to this handler or
derive the role from app, then call the same logic used elsewhere to compute
default_mode and assign it to app.mode so the behavior matches other enter
handlers.
♻️ Duplicate comments (1)
src/ui/key_handler/enter_handlers.rs (1)
612-612: PayInvoice action remains unimplemented.As noted in the previous review, the
PayInvoicebranch is empty. The developer acknowledged this will be completed when switching back to user mode work.
🧹 Nitpick comments (2)
src/ui/key_handler/enter_handlers.rs (2)
221-222: Duplicate doc comment.There's a duplicate doc comment on lines 221-222.
♻️ Suggested fix
/// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.) -/// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.) fn handle_enter_settings_mode(
328-407: Consider handling poisoned mutex locks.Lines 331 and 344 use
.unwrap()on mutex locks which will panic if the mutex is poisoned. While mutex poisoning is rare and often indicates a bigger problem, you may want to handle it gracefully.♻️ Suggested approach
- let orders_lock = orders.lock().unwrap(); + let Ok(orders_lock) = orders.lock() else { + log::error!("Orders mutex poisoned"); + return; + };- let messages_lock = app.messages.lock().unwrap(); + let Ok(messages_lock) = app.messages.lock() else { + log::error!("Messages mutex poisoned"); + return; + };
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/ui/key_handler/confirmation.rssrc/ui/key_handler/enter_handlers.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/ui/key_handler/enter_handlers.rs (4)
src/main.rs (3)
tokio(142-142)tokio(146-146)settings(116-118)src/ui/key_handler/confirmation.rs (3)
create_key_input_state(59-65)handle_confirmation_enter(29-48)handle_input_to_confirmation(13-26)src/ui/key_handler/settings.rs (5)
save_admin_key_to_settings(25-31)save_currency_to_settings(56-67)save_mostro_pubkey_to_settings(34-40)save_relay_to_settings(43-53)clear_currency_filters(70-78)src/ui/key_handler/validation.rs (4)
validate_currency(47-60)validate_mostro_pubkey(18-30)validate_npub(5-14)validate_relay(33-44)
🔇 Additional comments (9)
src/ui/key_handler/confirmation.rs (5)
12-65: LGTM!The helper functions are well-structured with clear responsibilities:
handle_input_to_confirmationcleanly gates empty inputhandle_confirmation_enterproperly separates YES/NO paths with a callback patternhandle_confirmation_escandcreate_key_input_stateare simple and correct
81-111: Minor: Redundant clone on line 84.
form_cloneis created on line 83, thenform_clone.clone()is called again on line 84 when setting the mode. Consider reusing the same clone.♻️ Suggested fix
UiMode::UserMode(UserMode::ConfirmingOrder(form)) => { // User confirmed, send the order let form_clone = form.clone(); - app.mode = UiMode::UserMode(UserMode::WaitingForMostro(form_clone.clone())); + app.mode = UiMode::UserMode(UserMode::WaitingForMostro(form_clone.clone())); + // Note: One clone is used for the mode, the other for the async taskActually, both clones are needed - one for the mode and one for the async task. The current code is correct.
170-231: LGTM!The settings confirmation flows are correctly implemented:
- Each confirmation mode uses
handle_confirmation_enterwithtrue(since 'y' means YES)- The relay confirmation properly spawns an async task to add the relay at runtime
- Currency filter operations are handled appropriately
232-244: Good DRY approach for ConfirmAddSolver.The delegation to
handle_enter_admin_modewith a synthesized YES selection avoids duplicating the AddSolver execution logic. This addresses the previous review comment about missing 'y' key handling forConfirmAddSolver.
267-304: LGTM!All confirmation modes have proper 'n' key cancellation handling, including the previously missing
ConfirmCurrencyandConfirmClearCurrenciescases. The implementation correctly returns users to their respective input modes or the default mode.src/ui/key_handler/enter_handlers.rs (4)
25-126: LGTM!The main dispatch function is well-organized with clear mode-specific routing. The use of
std::mem::replaceto take ownership of the current mode is appropriate, and allUiModevariants appear to be handled.
128-219: LGTM!The admin mode handler properly validates npub inputs before transitioning to confirmation dialogs, and the async task for adding solvers is correctly spawned with appropriate error handling.
409-416: LGTM!Simple and correct logic for transitioning to the order confirmation flow.
488-561: LGTM!The message viewing handler correctly maps actions to outgoing messages and handles the async send operation with appropriate error handling.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Fix all issues with AI agents
In `@src/ui/key_handler/enter_handlers.rs`:
- Around line 536-537: The code unconditionally sets app.mode =
UiMode::UserMode(UserMode::WaitingAddInvoice), which forces admins into a
user-only state; update the logic to set the waiting mode based on app.user_role
instead: if the role is User, set UiMode::UserMode(UserMode::WaitingAddInvoice);
if the role is Admin either create and use an AdminMode::WaitingAddInvoice
variant or map to the appropriate AdminMode waiting state (e.g.,
UiMode::AdminMode(AdminMode::WaitingAddInvoice)) so the mode matches the
caller's role and preserves the role-aware pattern used elsewhere.
♻️ Duplicate comments (2)
src/ui/key_handler/enter_handlers.rs (2)
616-616: PayInvoice action has no implementation.This was noted in a previous review and acknowledged as a planned future addition. Consider adding a TODO comment to make the intent clearer.
500-504: Inconsistent mode reset when NO is selected.When NO is selected, the mode is set to
UiMode::Normaldirectly (line 502), but elsewhere in this file the role-appropriate default mode is used. This could cause inconsistent behavior.🔧 Suggested fix
// Only proceed if YES is selected if !view_state.selected_button { - app.mode = UiMode::Normal; + let default_mode = match app.user_role { + UserRole::User => UiMode::UserMode(UserMode::Normal), + UserRole::Admin => UiMode::AdminMode(AdminMode::Normal), + }; + app.mode = default_mode; return; }
🧹 Nitpick comments (2)
src/ui/key_handler/validation.rs (1)
45-57: Docstring is inconsistent with implementation.The doc comment states the URL "must start with wss://" but the implementation accepts both
wss://andws://. Consider updating the comment to reflect the actual behavior.📝 Suggested doc fix
-/// Validate if a relay URL has a valid format (must start with wss://) +/// Validate if a relay URL has a valid format (must start with wss:// or ws://) pub fn validate_relay(relay_str: &str) -> Result<(), String> {src/ui/key_handler/enter_handlers.rs (1)
221-222: Duplicate comment line.📝 Remove duplicate
/// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.) -/// Handle Enter key for settings-related modes (Mostro pubkey, relay, currency, etc.) fn handle_enter_settings_mode(
📜 Review details
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
src/ui/key_handler/enter_handlers.rssrc/ui/key_handler/validation.rs
🧰 Additional context used
🧬 Code graph analysis (1)
src/ui/key_handler/enter_handlers.rs (6)
src/util/order_utils/execute_admin_add_solver.rs (1)
execute_admin_add_solver(12-53)src/util/order_utils/execute_send_msg.rs (1)
execute_send_msg(44-141)src/ui/key_handler/confirmation.rs (3)
create_key_input_state(59-65)handle_confirmation_enter(29-48)handle_input_to_confirmation(13-26)src/ui/key_handler/settings.rs (5)
save_admin_key_to_settings(25-31)save_currency_to_settings(56-67)save_mostro_pubkey_to_settings(34-40)save_relay_to_settings(43-53)clear_currency_filters(70-78)src/ui/key_handler/validation.rs (5)
validate_currency(60-73)validate_mostro_pubkey(31-43)validate_npub(5-14)validate_nsec(18-27)validate_relay(46-57)src/util/order_utils/take_order.rs (1)
take_order(36-195)
🔇 Additional comments (10)
src/ui/key_handler/validation.rs (4)
5-14: LGTM!The validation logic correctly uses
PublicKey::from_bech32from nostr-sdk to validate npub format. Trimming input and checking for empty strings before parsing is a good defensive pattern.
18-27: LGTM!Consistent validation pattern with
validate_npub, correctly usingSecretKey::from_bech32for nsec validation.
29-43: LGTM!Good use of
PublicKey::from_hexto validate the hex-encoded Mostro pubkey. The error message clearly indicates the expected format.
59-73: LGTM!The intentionally lenient validation is documented and appropriate for supporting non-standard currency codes while still preventing unreasonable input.
src/ui/key_handler/enter_handlers.rs (6)
26-126: LGTM!The main dispatcher is well-structured with clear mode matching and appropriate delegation to specialized handlers. Using
std::mem::replaceto take ownership of the current mode is a good pattern for avoiding borrow checker issues.
131-219: LGTM!Admin mode handling correctly validates inputs before confirmation and properly spawns async tasks for network operations. The pattern of returning to input mode on NO selection (line 187-188) maintains good UX.
254-268: LGTM - Relay validation now implemented.Good to see the validation step is now in place, matching the pattern used for other input types.
328-407: LGTM!Normal mode handling covers all the appropriate tabs and settings options with clear index-based routing. The mutex lock pattern is consistent with the rest of the codebase.
418-490: LGTM!The taking order handler properly validates range order amounts, handles the async flow correctly, and uses the role-appropriate default mode for NO selection (lines 484-488) - addressing the pattern noted in past reviews.
567-622: LGTM overall, but note the empty PayInvoice branch.The AddInvoice handling is well-implemented with proper async flow. The empty PayInvoice branch is acknowledged as pending work.
✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.
Catrya
left a comment
There was a problem hiding this comment.
tACK great job, and the UI is very pretty!!!
grunch
left a comment
There was a problem hiding this comment.
tACK, great job! this is a piece of art 🔥
Admin Mode - Phase 1: Settings Tab & Dispute Management
Overview
This PR introduces comprehensive admin mode functionality, including settings management features, dispute resolution interface, and improved user experience. All features include input validation, confirmation popups, proper error handling, and role-aware UI behavior.
✨ New Features
🌐 Add Mostro Pubkey (User & Admin)
Allows users to change the Mostro instance used by the client.
settings.tomlfileUse 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.
wss://relay.example.com)wss://orws://)settings.toml(no duplicates)Use Case: Add custom relays for better connectivity or privacy
💱 Currency Filters (User & Admin)
Allows users to filter orders by fiat currency codes.
settings.tomlas arrayUse Case: Focus on specific fiat currencies when viewing orders
➕ Add Solver (Admin Only)
Allows admins to add new dispute solvers to the Mostro network.
AdminAddSolvermessage to Mostro daemonUse Case: Onboard new dispute solvers to the network
🔑 Admin Key Setup (Admin Only)
Allows admins to configure their admin private key.
settings.tomlasadmin_privkeyUse Case: Configure admin private key for dispute resolution
🛡️ Admin Dispute Management UI
Comprehensive dispute resolution interface for admins.
Disputes in Progress Tab
Two-pane layout for active dispute management:
Left Sidebar:
Main Area (when dispute selected):
State Management:
Use Case: Admins can take multiple disputes and manage them with dedicated chat interfaces
📊 Multi-line Status Bar
Enhanced status bar displaying comprehensive configuration information.
🎨 User Experience Improvements
Consistent Confirmation Flow
All settings operations follow the same user-friendly pattern:
Input Validation
wss://orws://Clipboard Management
arboard::SetExtLinux::wait()for proper clipboard synchronizationstd::thread::sleepwith proper synchronizationRole-Aware UI Behavior
🔧 Technical Implementation
Settings Persistence
All settings are saved to
settings.toml:mostro_pubkey: Mostro instance public keyrelays: Array of relay URLs (appended, no duplicates)currencies: Array of currency codes (uppercase, no duplicates)admin_privkey: Admin private key (nsec format)Dispute Status Management
AdminDispute::set_status_in_progress()methodCode Organization & DRY Principles
mod.rs,enter_handlers.rs,navigation.rs,esc_handlers.rs,confirmation.rs,settings.rs,validation.rs,form_input.rs,input_helpers.rs)execute_take_order_action()- Extracted common take order logicexecute_add_solver_action()- Extracted common add solver logicAdmin Operations
execute_admin_add_solver()functionexecute_take_dispute()functionTest Infrastructure
tests/directorytests/common/mod.rsfor test helperstests/db_tests.rs)tests/parsing_tests.rs)tests/validation_tests.rs)tests/helper_tests.rs)src/lib.rsto enable integration tests📝 Files Changed
New Files
src/util/order_utils/execute_admin_add_solver.rs- Admin AddSolver implementationsrc/util/order_utils/execute_take_dispute.rs- Admin take dispute implementationsrc/ui/disputes_tab.rs- Disputes pending tab renderingsrc/ui/key_handler/- Modular key handler structuremod.rs- Main dispatcherenter_handlers.rs- Enter key handlingnavigation.rs- Navigation and tab switchingesc_handlers.rs- Escape key handlingconfirmation.rs- Confirmation popup logicsettings.rs- Settings persistence functionsvalidation.rs- Input validation utilitiesform_input.rs- Character input and backspaceinput_helpers.rs- Generic text input handlingsrc/lib.rs- Library crate for integration teststests/common/mod.rs- Shared test utilitiestests/db_tests.rs- Database integration teststests/parsing_tests.rs- Parsing logic teststests/validation_tests.rs- Validation function teststests/helper_tests.rs- Helper function testsModified Files
src/ui/settings_tab.rs- Added new settings optionssrc/ui/tab_content.rs- Added disputes in progress renderingsrc/ui/admin_key_confirm.rs- Custom message supportsrc/ui/key_handler/settings.rs- Settings save functionssrc/ui/key_handler/validation.rs- Input validation (added nsec validation)src/ui/key_handler/enter_handlers.rs- Settings action handlers, DRY refactoringsrc/ui/key_handler/mod.rs- Clipboard improvementssrc/ui/mod.rs- UI mode updates, admin dispute state managementsrc/models.rs- Addedset_status_in_progress()methodsrc/main.rs- Dispute list loading and refresh logicsrc/util/order_utils/mod.rs- Module exports🧪 Testing Checklist
🎯 Use Cases
For Regular Users
For Admins
📚 Documentation Updates
docs/ADMIN_DISPUTES.mdwith latest featuresdocs/TUI_INTERFACE.mdwith clipboard improvementsdocs/CODING_STANDARDS.mdwith DRY principlesSummary by CodeRabbit
New Features
Improvements
Tests
Chores
Documentation
✏️ Tip: You can customize this high-level summary in your review settings.