diff --git a/locales/de.yml b/locales/de.yml index 3c1593e1..1cf8b25e 100644 --- a/locales/de.yml +++ b/locales/de.yml @@ -470,6 +470,7 @@ goblin: relays: "Relays" nostr_relays: "Nostr-Relays" node: "Node" + min_conf: "Mindestbestätigungen" integrated_node: "Einstellungen des integrierten Nodes" node_advanced: "Erweitert" slatepacks: "Slatepacks" diff --git a/locales/en.yml b/locales/en.yml index e4b3945e..dd0aee21 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -470,6 +470,7 @@ goblin: relays: "Relays" nostr_relays: "Nostr Relays" node: "Node" + min_conf: "Minimum confirmations" integrated_node: "Integrated node settings" node_advanced: "Advanced" slatepacks: "Slatepacks" diff --git a/locales/fr.yml b/locales/fr.yml index 041dc848..505c1f2c 100644 --- a/locales/fr.yml +++ b/locales/fr.yml @@ -470,6 +470,7 @@ goblin: relays: "Relais" nostr_relays: "Relais Nostr" node: "Nœud" + min_conf: "Confirmations minimales" integrated_node: "Paramètres du nœud intégré" node_advanced: "Avancé" slatepacks: "Slatepacks" diff --git a/locales/ru.yml b/locales/ru.yml index 5ed8b7c4..34cdc6eb 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -470,6 +470,7 @@ goblin: relays: "Реле" nostr_relays: "Реле Nostr" node: "Узел" + min_conf: "Минимум подтверждений" integrated_node: "Настройки встроенного узла" node_advanced: "Дополнительно" slatepacks: "Slatepacks" diff --git a/locales/tr.yml b/locales/tr.yml index 2abcc46e..c971f4ec 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -470,6 +470,7 @@ goblin: relays: "Relaylar" nostr_relays: "Nostr Relayları" node: "Düğüm" + min_conf: "Asgari onay sayısı" integrated_node: "Tümleşik düğüm ayarları" node_advanced: "Gelişmiş" slatepacks: "Slatepackler" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index edffc485..ee01cc04 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -470,6 +470,7 @@ goblin: relays: "中继" nostr_relays: "Nostr 中继" node: "节点" + min_conf: "最少确认数" integrated_node: "集成节点设置" node_advanced: "高级" slatepacks: "Slatepack" diff --git a/src/gui/views/goblin/mod.rs b/src/gui/views/goblin/mod.rs index 1d589da0..9de6057b 100644 --- a/src/gui/views/goblin/mod.rs +++ b/src/gui/views/goblin/mod.rs @@ -130,6 +130,8 @@ pub struct GoblinWalletView { /// "Wipe payment history" tap-twice confirm: armed after the first tap, /// wipes on the second (cleared once fired). wipe_confirm: bool, + /// Minimum-confirmations value being edited in its modal (GRIM parity). + min_conf_edit: String, } /// Whether the per-identity cue is drawn on activity rows (owner-approved). The @@ -259,6 +261,7 @@ impl Default for GoblinWalletView { cancel_msg: None, copy_flash: None, wipe_confirm: false, + min_conf_edit: String::new(), } } } @@ -330,6 +333,10 @@ const IDENTITY_MANAGE_MODAL: &str = "goblin_identity_manage_modal"; /// wallet-password modal executes the delete). Also background-locking. const IDENTITY_DELETE_MODAL: &str = "goblin_identity_delete_modal"; +/// Id of the minimum-confirmations edit modal (GRIM parity: numeric input, +/// Cancel/Save), opened from the Settings wallet group. +const MIN_CONF_MODAL: &str = "goblin_min_conf_modal"; + /// A password-gated identity action the modal executes. Switching no longer uses /// the modal (it is instant and local); only these need the wallet password. #[derive(Clone)] @@ -2608,6 +2615,13 @@ impl GoblinWalletView { SettingsPage::Identities => return self.identities_ui(ui, wallet, cb), SettingsPage::Main => {} } + // Minimum-confirmations edit modal (GRIM parity), opened from the + // wallet group below. + if Modal::opened() == Some(MIN_CONF_MODAL) { + Modal::ui(ui.ctx(), cb, |ui, modal, cb| { + self.min_conf_modal_content(ui, wallet, modal, cb); + }); + } ui.add_space(8.0); ui.label( RichText::new(t!("goblin.settings.title")) @@ -2902,22 +2916,26 @@ impl GoblinWalletView { ui.add_space(16.0); let mut open_node = false; - let mut open_integrated = false; let mut open_slatepack = false; settings_group(ui, &t!("goblin.settings.wallet"), |ui| { if settings_row_nav(ui, &t!("goblin.settings.node"), &node_summary(wallet)) { open_node = true; } - // GRIM's integrated-node tabs (info, metrics, mining, node - // settings), shown in Goblin chrome. Live sync status when - // the node runs, like the Node row above. - let node_value = if crate::node::Node::is_running() { - crate::node::Node::get_sync_status_text() - } else { - String::new() - }; - if settings_row_nav(ui, &t!("goblin.settings.integrated_node"), &node_value) { - open_integrated = true; + // Minimum confirmations before received funds are spendable + // (GRIM parity, default 10). Prominent, just below the node + // row; tapping opens the numeric edit modal. The value feeds + // the wallet's spendable/send logic via + // WalletConfig::min_confirmations. + if settings_row_nav( + ui, + &t!("goblin.settings.min_conf"), + &wallet.get_config().min_confirmations.to_string(), + ) { + self.min_conf_edit = wallet.get_config().min_confirmations.to_string(); + Modal::new(MIN_CONF_MODAL) + .position(ModalPosition::CenterTop) + .title(t!("goblin.settings.min_conf")) + .show(); } // GRIM's native by-hand slatepack exchange, for when a payment // can't go through a username. @@ -2948,11 +2966,6 @@ impl GoblinWalletView { self.node_secret_input.clear(); self.settings_page = SettingsPage::Node; } - if open_integrated { - self.node_tab = Box::new(crate::gui::views::network::NetworkNode); - self.node_tab_back = SettingsPage::Main; - self.settings_page = SettingsPage::IntegratedNode; - } ui.add_space(16.0); let mut open_pairing = false; @@ -3369,6 +3382,7 @@ impl GoblinWalletView { let progress = wallet.repairing_progress(); let mut leave = false; let mut open_node = false; + let mut open_integrated = false; { let adv = &mut self.advanced; ScrollArea::vertical() @@ -3405,6 +3419,23 @@ impl GoblinWalletView { if w::big_action_on_card(ui, &t!("goblin.advanced.manage_node")).clicked() { open_node = true; } + ui.add_space(10.0); + // GRIM's integrated-node tabs (info, metrics, mining with + // stratum, node settings) in Goblin chrome. Their ONE home + // (single-home rule): Goblin is the lighter client, so they + // live here under Advanced, not in main Settings. + let node_label = if crate::node::Node::is_running() { + format!( + "{} · {}", + t!("goblin.settings.integrated_node"), + crate::node::Node::get_sync_status_text() + ) + } else { + t!("goblin.settings.integrated_node").to_string() + }; + if w::big_action_on_card(ui, &node_label).clicked() { + open_integrated = true; + } }); ui.add_space(12.0); @@ -3660,12 +3691,18 @@ impl GoblinWalletView { self.settings_page = SettingsPage::Main; } if open_node { - // Advanced → "Manage node connection" opens Goblin's own Node screen - // (its Advanced button reaches the integrated-node tabs from there). + // Advanced → "Manage node connection" opens Goblin's own Node screen. self.node_url_input.clear(); self.node_secret_input.clear(); self.settings_page = SettingsPage::Node; } + if open_integrated { + // Advanced is the one home of the integrated-node tabs; back + // returns here. + self.node_tab = Box::new(crate::gui::views::network::NetworkNode); + self.node_tab_back = SettingsPage::Advanced; + self.settings_page = SettingsPage::IntegratedNode; + } } /// GRIM's four integrated-node tabs (Info / Metrics / Mining / Settings) @@ -3855,14 +3892,8 @@ impl GoblinWalletView { self.node_url_input.clear(); self.node_secret_input.clear(); } - ui.add_space(10.0); - // Advanced: GRIM's integrated-node tabs (info, metrics, mining - // with stratum, node settings) inside Goblin chrome. - if w::big_action(ui, &t!("goblin.settings.node_advanced"), true).clicked() { - self.node_tab = Box::new(crate::gui::views::network::NetworkNode); - self.node_tab_back = SettingsPage::Node; - self.settings_page = SettingsPage::IntegratedNode; - } + // (The integrated-node tabs' single home is Settings → Advanced; + // no duplicate entry here.) ui.add_space(16.0); }); } @@ -5561,6 +5592,69 @@ impl GoblinWalletView { } } + /// Content of the minimum-confirmations edit modal — a direct port of GRIM's + /// min_conf_modal_ui (numeric input, invalid-value error, Cancel/Save). The + /// saved value persists in WalletConfig::min_confirmations and feeds the + /// wallet's spendable/send logic on the next balance refresh. + fn min_conf_modal_content( + &mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks, + ) { + let on_save = |s: &mut GoblinWalletView| { + if let Ok(min_conf) = s.min_conf_edit.parse::() { + wallet.update_min_confirmations(min_conf); + Modal::close(); + } + }; + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label( + RichText::new(t!("wallets.min_tx_conf_count")) + .size(17.0) + .color(Colors::gray()), + ); + ui.add_space(8.0); + let mut edit = TextEdit::new(egui::Id::from(modal.id)).h_center().numeric(); + edit.ui(ui, &mut self.min_conf_edit, cb); + if edit.enter_pressed { + on_save(self); + } + if self.min_conf_edit.parse::().is_err() { + ui.add_space(12.0); + ui.label( + RichText::new(t!("network_settings.not_valid_value")) + .size(17.0) + .color(Colors::red()), + ); + } + ui.add_space(12.0); + }); + ui.scope(|ui| { + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + View::button( + ui, + t!("modal.cancel"), + Colors::white_or_black(false), + || { + Modal::close(); + }, + ); + }); + columns[1].vertical_centered_justified(|ui| { + View::button(ui, t!("modal.save"), Colors::white_or_black(false), || { + on_save(self); + }); + }); + }); + ui.add_space(6.0); + }); + } + /// Inline username-claim widget (availability check + registration). fn claim_ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { let t = theme::tokens();