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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 36 additions & 3 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ async fn drain_order_result_queue(
}
}

use crate::ui::orders::{OrderBookFilterField, OrderBookFilterState};
use crate::ui::{AppState, ChatAttachment, UiMode, UserRole};
use sqlx::SqlitePool;
use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender};
Expand Down Expand Up @@ -246,6 +247,13 @@ fn setup_logger(level: &str) -> Result<(), fern::InitError> {
}

fn apply_pasted_text_to_active_input(app: &mut AppState, pasted_text: &str) {
let filtered_text: String = pasted_text.chars().filter(|c| !c.is_control()).collect();

if let UiMode::OrderFilters(ref mut state) = app.mode {
append_paste_to_order_filter(state, &filtered_text);
return;
}

// Handle paste for invoice input
if let UiMode::NewMessageNotification(_, Action::AddInvoice, ref mut invoice_state) = app.mode {
if invoice_state.focused {
Expand All @@ -264,14 +272,30 @@ fn apply_pasted_text_to_active_input(app: &mut AppState, pasted_text: &str) {

// Handle paste for the Observer Shared key field
if app.observer_inputs_editable() {
let filtered_text: String = pasted_text.chars().filter(|c| !c.is_control()).collect();
app.observer_shared_key_input.push_str(&filtered_text);
}

// Disputes in Progress chatbox (admin) — keeps newlines for multi-line drafts
let _ = append_paste_to_admin_dispute_chat(app, pasted_text);
}

fn append_paste_to_order_filter(state: &mut OrderBookFilterState, text: &str) {
let target = match state.focused {
OrderBookFilterField::Kind => None,
OrderBookFilterField::FiatCurrency => Some(&mut state.filters.fiat_code),
OrderBookFilterField::FiatAmountMin => Some(&mut state.filters.fiat_amount_min),
OrderBookFilterField::FiatAmountMax => Some(&mut state.filters.fiat_amount_max),
OrderBookFilterField::PremiumMin => Some(&mut state.filters.premium_min),
OrderBookFilterField::PremiumMax => Some(&mut state.filters.premium_max),
OrderBookFilterField::PaymentMethod => Some(&mut state.filters.payment_method),
OrderBookFilterField::CreatedWithinDays => Some(&mut state.filters.created_within_days),
};

if let Some(target) = target {
target.push_str(text);
}
}

/// Draws the TUI interface with tabs and active content.
/// The "Orders" tab shows a table of pending orders and highlights the selected row.
use crate::ui::ui_draw;
Expand Down Expand Up @@ -960,6 +984,15 @@ async fn main() -> Result<(), anyhow::Error> {
true => "All currencies are accepted".to_string(),
false => current_settings.currencies_filter.join(", "),
};
let order_filter_shortcuts = if app.user_role == UserRole::User
&& matches!(
app.active_tab,
crate::ui::navigation::Tab::User(crate::ui::navigation::UserTab::Orders)
) {
" | Shift+F: Order filters | Shift+X: Clear order filters"
} else {
""
};
// Mostro name (Lightning node alias) from instance info
let mostro_alias = match app.mostro_info.as_ref() {
Some(info) => info
Expand All @@ -976,8 +1009,8 @@ async fn main() -> Result<(), anyhow::Error> {
),
format!("🔗 Relays: {}", relays_str),
format!(
"💱 Currencies: {} - Filters: {}",
mostro_instance_currencies, currencies_filter_str
"💱 Currencies: {} - Filters: {}{}",
mostro_instance_currencies, currencies_filter_str, order_filter_shortcuts
),
];
terminal.draw(|f| ui_draw(f, &mut app, &orders, &disputes, Some(&status_lines)))?;
Expand Down
13 changes: 10 additions & 3 deletions src/ui/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ use crate::ui::helpers::OrderChatListItem;
use crate::ui::navigation::{AdminTab, Tab, UserRole};
use crate::ui::orders::{
BuyerInvoicePreference, FormState, InvoiceInputState, KeyInputState, MessageNotification,
MessageViewState, OperationResult, OrderChatStaticHeader, OrderMessage, RatingOrderState,
MessageViewState, OperationResult, OrderBookFilterState, OrderBookFilters,
OrderChatStaticHeader, OrderMessage, RatingOrderState,
};
use crate::ui::user_state::UserMode;
use crate::util::{transport_from_instance, MostroInstanceInfo};
Expand Down Expand Up @@ -60,11 +61,13 @@ pub enum UiMode {
AddCurrency(KeyInputState),
ConfirmCurrency(String, bool), // (currency_string, selected_button: true=Yes, false=No)
ConfirmClearCurrencies(bool), // (selected_button: true=Yes, false=No)
/// Orders tab local filters. Enter applies, Esc cancels.
OrderFilters(OrderBookFilterState),
ConfirmDeleteHistoryOrder(uuid::Uuid, bool), // (order_id, selected_button)
ConfirmBulkDeleteHistory(bool), // (selected_button)
ConfirmBulkDeleteHistory(bool), // (selected_button)
/// User Settings: ask Mostro to restore this identity's orders and disputes.
ConfirmRestoreSession(bool), // (selected_button: true=Yes, false=No)
ConfirmExit(bool), // (selected_button: true=Yes, false=No)
ConfirmExit(bool), // (selected_button: true=Yes, false=No)

// Generate new keys flow (Settings tab)
ConfirmGenerateNewKeys(bool), // (selected_button: true=Yes, false=No)
Expand Down Expand Up @@ -161,6 +164,7 @@ impl Clone for UiMode {
UiMode::ConfirmCurrency(currency.clone(), *selected)
}
UiMode::ConfirmClearCurrencies(selected) => UiMode::ConfirmClearCurrencies(*selected),
UiMode::OrderFilters(state) => UiMode::OrderFilters(state.clone()),
UiMode::ConfirmDeleteHistoryOrder(order_id, selected) => {
UiMode::ConfirmDeleteHistoryOrder(*order_id, *selected)
}
Expand Down Expand Up @@ -291,6 +295,8 @@ pub struct AppState {
pub pending_admin_disputes_reload: bool,
/// Cached copy of currencies filter from settings (used for UI-side filtering).
pub currencies_filter: Vec<String>,
/// Orders-tab local filters, applied after the settings currency filter.
pub order_filters: OrderBookFilters,
/// Cached Mostro instance info (kind 38385), if available.
/// Populated only from client-authenticated fetches; apply via [`Self::set_mostro_info`].
pub mostro_info: Option<MostroInstanceInfo>,
Expand Down Expand Up @@ -391,6 +397,7 @@ impl AppState {
admin_keys: None,
pending_admin_disputes_reload: false,
currencies_filter: Vec::new(),
order_filters: OrderBookFilters::default(),
mostro_info: None,
transport: Transport::default(),
offline_overlay_message: None,
Expand Down
2 changes: 2 additions & 0 deletions src/ui/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ pub const HELP_EXIT_ENTER_CONFIRM: &str = "Enter: Confirm exit (then Yes/No)";
pub const HELP_ORDERS_ENTER_TAKE: &str =
"Enter: Take selected order (or cancel if it is your pending listing)";
pub const HELP_ORDERS_SELECT: &str = "↑↓: Select order";
pub const HELP_ORDERS_SHIFT_F_FILTERS: &str = "Shift+F: Edit order filters";
pub const HELP_ORDERS_SHIFT_X_CLEAR_FILTERS: &str = "Shift+X: Clear order filters";
/// Confirmation body when Enter on Orders targets a maker pending order we own.
pub const HELP_ORDERS_CANCEL_PENDING_MSG: &str =
"Cancel this pending order? It will be removed from the order book.";
Expand Down
3 changes: 3 additions & 0 deletions src/ui/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,9 @@ No: paste BOLT11 or Lightning address manually."
Some("Are you sure you want to clear all currencies filters?"),
);
}
if let UiMode::OrderFilters(state) = &app.mode {
tabs::orders_tab::render_order_filter_popup(f, state);
}
if let UiMode::ConfirmDeleteHistoryOrder(order_id, selected_button) = &app.mode {
admin_key_confirm::render_admin_key_confirm_with_message(
f,
Expand Down
139 changes: 134 additions & 5 deletions src/ui/help_popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ use crate::ui::navigation::{AdminTab, Tab, UserRole, UserTab};
// 13 shortcuts, intro, close hint, borders, and one row of margin above and below.
const MY_TRADES_FULL_HELP_MIN_HEIGHT: u16 = 19;
const MY_TRADES_FULL_HELP_MIN_WIDTH: u16 = 60;
const ORDERS_FULL_HELP_MIN_HEIGHT: u16 = 11;
const ORDERS_FULL_HELP_MIN_WIDTH: u16 = 48;

/// Renders the context-aware keyboard shortcuts popup (Ctrl+H, and Shift+H on My Trades).
pub fn render_help_popup(f: &mut ratatui::Frame, app: &AppState, tab: Tab) {
Expand All @@ -19,11 +21,17 @@ pub fn render_help_popup(f: &mut ratatui::Frame, app: &AppState, tab: Tab) {
matches!(tab, Tab::User(UserTab::MyTrades)) && area.width < MY_TRADES_FULL_HELP_MIN_WIDTH;
let compact_my_trades = matches!(tab, Tab::User(UserTab::MyTrades))
&& (area.height < MY_TRADES_FULL_HELP_MIN_HEIGHT || narrow_my_trades);
let narrow_orders =
matches!(tab, Tab::User(UserTab::Orders)) && area.width < ORDERS_FULL_HELP_MIN_WIDTH;
let compact_orders = matches!(tab, Tab::User(UserTab::Orders))
&& (area.height < ORDERS_FULL_HELP_MIN_HEIGHT || narrow_orders);

// Match Settings Shift+H: compact rows, styled shortcut + description, full viewport height.
let compact_chrome = matches!(
tab,
Tab::Admin(AdminTab::DisputesInProgress) | Tab::User(UserTab::MyTrades)
Tab::Admin(AdminTab::DisputesInProgress)
| Tab::User(UserTab::Orders)
| Tab::User(UserTab::MyTrades)
);

let (popup_width, popup_height) = if compact_chrome {
Expand Down Expand Up @@ -64,18 +72,33 @@ pub fn render_help_popup(f: &mut ratatui::Frame, app: &AppState, tab: Tab) {
let mut lines: Vec<Line<'static>> = Vec::new();
if matches!(tab, Tab::Admin(AdminTab::DisputesInProgress)) {
lines.push(help_disputes_in_progress_intro());
} else if compact_orders {
lines.extend(compact_orders_help(
narrow_orders,
inner.width,
inner.height,
));
} else if compact_my_trades {
lines.extend(compact_my_trades_help(narrow_my_trades));
} else if matches!(tab, Tab::User(UserTab::Orders)) {
for s in &plain_lines {
lines.push(help_shortcut_line(s));
}
} else {
lines.push(help_my_trades_intro());
}
if !compact_my_trades {
if !compact_my_trades && !matches!(tab, Tab::User(UserTab::Orders)) {
for s in plain_lines {
lines.push(help_shortcut_line(&s));
}
}
let close_hint = if compact_orders {
compact_orders_close_hint(inner.width)
} else {
HELP_CLOSE_HINT
};
lines.push(Line::from(Span::styled(
HELP_CLOSE_HINT,
close_hint,
Style::default().fg(Color::DarkGray),
)));
let paragraph = Paragraph::new(Text::from(lines)).wrap(Wrap { trim: true });
Expand Down Expand Up @@ -216,6 +239,40 @@ fn compact_my_trades_help(narrow: bool) -> Vec<Line<'static>> {
.collect()
}

fn compact_orders_help(narrow: bool, inner_width: u16, inner_height: u16) -> Vec<Line<'static>> {
if narrow {
let (title_style, _) = settings_instruction_block_style();
let close_rows = if inner_width <= 13 { 1 } else { 2 };
let available_shortcut_rows = inner_height.saturating_sub(close_rows);
if available_shortcut_rows <= 3 {
return ["Shift+F", "Shift+X"]
.into_iter()
.map(|row| Line::from(Span::styled(row, title_style)))
.collect();
}
return ["↑↓", "Enter", "Shift+F", "Shift+X"]
.into_iter()
.map(|row| Line::from(Span::styled(row, title_style)))
.collect();
}

[
"↑↓ / Enter: Select / take order",
"Shift+F / Shift+X: Edit / clear filters",
]
.into_iter()
.map(help_shortcut_line)
.collect()
}

fn compact_orders_close_hint(inner_width: u16) -> &'static str {
if inner_width <= 13 {
"Esc"
} else {
HELP_CLOSE_HINT
}
}

/// Split `Key: description` help strings into bold key + gray body (same as Settings Shift+H rows).
fn help_shortcut_line(s: &str) -> Line<'static> {
let (title_style, body_style) = settings_instruction_block_style();
Expand Down Expand Up @@ -408,6 +465,8 @@ fn help_content(app: &AppState, tab: Tab) -> (String, Vec<String>) {
vec![
HELP_ORDERS_ENTER_TAKE.to_string(),
HELP_ORDERS_SELECT.to_string(),
HELP_ORDERS_SHIFT_F_FILTERS.to_string(),
HELP_ORDERS_SHIFT_X_CLEAR_FILTERS.to_string(),
Comment thread
coderabbitai[bot] marked this conversation as resolved.
],
),
Tab::User(UserTab::MyTrades) => (
Expand Down Expand Up @@ -467,15 +526,19 @@ mod help_content_tests {
use ratatui::backend::TestBackend;
use ratatui::Terminal;

fn buffer_contains(buf: &ratatui::buffer::Buffer, needle: &str) -> bool {
fn buffer_text(buf: &ratatui::buffer::Buffer) -> String {
let mut flat = String::new();
for y in 0..buf.area.height {
for x in 0..buf.area.width {
flat.push_str(buf[(x, y)].symbol());
}
flat.push('\n');
}
flat.contains(needle)
flat
}

fn buffer_contains(buf: &ratatui::buffer::Buffer, needle: &str) -> bool {
buffer_text(buf).contains(needle)
}

#[test]
Expand Down Expand Up @@ -562,4 +625,70 @@ mod help_content_tests {
);
}
}

#[test]
fn short_narrow_orders_help_keeps_filter_shortcuts_and_close_hint_visible() {
let backend = TestBackend::new(20, 10);
let mut terminal = Terminal::new(backend).unwrap();
let app = AppState::new(UserRole::User);

terminal
.draw(|f| render_help_popup(f, &app, Tab::User(UserTab::Orders)))
.unwrap();

let buf = terminal.backend().buffer();
for expected in [
"Enter",
"Shift+F",
"Shift+X",
"Esc, Enter or",
"Ctrl+H to close",
] {
assert!(
buffer_contains(buf, expected),
"missing {expected:?} from compact Orders help"
);
}
}

#[test]
fn very_short_narrow_orders_help_keeps_filters_and_close_hint_visible() {
let backend = TestBackend::new(20, 6);
let mut terminal = Terminal::new(backend).unwrap();
let app = AppState::new(UserRole::User);

terminal
.draw(|f| render_help_popup(f, &app, Tab::User(UserTab::Orders)))
.unwrap();

let buf = terminal.backend().buffer();
for expected in ["Shift+F", "Shift+X", "Esc, Enter or", "Ctrl+H to close"] {
assert!(
buffer_contains(buf, expected),
"missing {expected:?} from very short Orders help"
);
}
}

#[test]
fn tiny_narrow_orders_help_keeps_filters_and_short_close_hint_visible() {
for (width, height) in [(15, 10), (15, 6)] {
let backend = TestBackend::new(width, height);
let mut terminal = Terminal::new(backend).unwrap();
let app = AppState::new(UserRole::User);

terminal
.draw(|f| render_help_popup(f, &app, Tab::User(UserTab::Orders)))
.unwrap();

let buf = terminal.backend().buffer();
for expected in ["Shift+F", "Shift+X", "Esc"] {
let rendered = buffer_text(buf);
assert!(
rendered.contains(expected),
"missing {expected:?} from {width}x{height} Orders help:\n{rendered}"
);
}
}
}
}
3 changes: 2 additions & 1 deletion src/ui/helpers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ pub use order_chat_projection::{
};
pub use order_selection::{
get_filtered_book_orders, move_book_order_selection, order_passes_currency_filter,
selected_book_display_idx, selected_filtered_book_order,
order_passes_order_filters, order_passes_order_filters_at, selected_book_display_idx,
selected_filtered_book_order,
};
pub use startup::{
active_peer_chat_order_ids_for_restore, admin_chat_keys_clone_for_role,
Expand Down
Loading