diff --git a/src/gui/colors.rs b/src/gui/colors.rs index f85a7e00..9f1cb3a2 100644 --- a/src/gui/colors.rs +++ b/src/gui/colors.rs @@ -26,6 +26,7 @@ const SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(100); const DARK_SEMI_TRANSPARENT: Color32 = Color32::from_black_alpha(170); const GOLD: Color32 = Color32::from_rgb(255, 215, 0); +const GOLD_DARK: Color32 = Color32::from_rgb(240, 203, 1); const YELLOW: Color32 = Color32::from_rgb(254, 241, 2); const YELLOW_DARK: Color32 = Color32::from_rgb(239, 229, 3); @@ -119,6 +120,14 @@ impl Colors { } } + pub fn gold_dark() -> Color32 { + if use_dark() { + GOLD_DARK.gamma_multiply(0.9) + } else { + GOLD_DARK + } + } + pub fn yellow() -> Color32 { YELLOW } diff --git a/src/gui/views/wallets/wallet/account/content.rs b/src/gui/views/wallets/wallet/account/content.rs index 2c644129..68f1a2de 100644 --- a/src/gui/views/wallets/wallet/account/content.rs +++ b/src/gui/views/wallets/wallet/account/content.rs @@ -142,10 +142,13 @@ impl AccountContent { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw button to show QR code scanner. - View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || { - self.qr_scan_content = Some(CameraContent::default()); - cb.start_camera(); - }); + let wallet_synced = wallet.synced_from_node(); + if wallet_synced { + View::item_button(ui, View::item_rounding(0, 2, true), SCAN, None, || { + self.qr_scan_content = Some(CameraContent::default()); + cb.start_camera(); + }); + } // Draw button to show list of accounts. let accounts = wallet.accounts(); @@ -154,7 +157,12 @@ impl AccountContent { } else { USER_PLUS }; - View::item_button(ui, View::item_rounding(1, 3, true), accounts_icon, None, || { + let rounding = if wallet_synced { + View::item_rounding(1, 3, true) + } else { + View::item_rounding(0, 2, true) + }; + View::item_button(ui, rounding, accounts_icon, None, || { if accounts.len() == 1 { self.create_account_content = CreateAccountContent::default(); Modal::new(CREATE_MODAL_ID) @@ -223,11 +231,8 @@ impl AccountContent { } } }; - View::animate_text(ui, - status_text, - 15.0, - Colors::gray(), - wallet.syncing() || wallet.message_opening()); + let animate = wallet.syncing() || wallet.message_opening(); + View::animate_text(ui, status_text, 15.0, Colors::gray(), animate); }) }); }); diff --git a/src/gui/views/wallets/wallet/content.rs b/src/gui/views/wallets/wallet/content.rs index 252cb912..2c520d22 100644 --- a/src/gui/views/wallets/wallet/content.rs +++ b/src/gui/views/wallets/wallet/content.rs @@ -16,15 +16,16 @@ use egui::scroll_area::ScrollBarVisibility; use egui::{Id, Margin, RichText, ScrollArea}; use grin_chain::SyncStatus; -use crate::gui::icons::{ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, GEAR_FINE, POWER, STACK}; +use crate::gui::icons::{ARROWS_CLOCKWISE, FILE_ARROW_DOWN, FILE_ARROW_UP, FILE_TEXT, GEAR_FINE, POWER, STACK}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::types::{LinePosition, ModalPosition}; use crate::gui::views::wallets::wallet::account::AccountContent; +use crate::gui::views::wallets::wallet::message::MessageInputContent; use crate::gui::views::wallets::wallet::request::{InvoiceRequestContent, SendRequestContent}; use crate::gui::views::wallets::wallet::transport::WalletTransportContent; use crate::gui::views::wallets::wallet::types::WalletContentContainer; use crate::gui::views::wallets::wallet::{WalletSettingsContent, WalletTransactionsContent}; -use crate::gui::views::{Content, FilePickContent, FilePickContentType, Modal, View}; +use crate::gui::views::{Content, Modal, View}; use crate::gui::Colors; use crate::node::Node; use crate::wallet::types::{ConnectionMethod, WalletTask}; @@ -48,21 +49,23 @@ pub struct WalletContent { invoice_content: Option, /// Send request creation [`Modal`] content. send_content: Option, - - /// Tab button to pick file for parsing. - file_pick_tab_button: FilePickContent, + /// Slatepack message input [`Modal`] content. + message_content: Option } /// Identifier for invoice creation [`Modal`]. const INVOICE_MODAL_ID: &'static str = "invoice_request_modal"; /// Identifier for sending request creation [`Modal`]. const SEND_MODAL_ID: &'static str = "send_request_modal"; +/// Identifier for Slatepack message input [`Modal`]. +pub const MESSAGE_MODAL_ID: &'static str = "input_message_modal"; impl WalletContentContainer for WalletContent { fn modal_ids(&self) -> Vec<&'static str> { vec![ INVOICE_MODAL_ID, - SEND_MODAL_ID + SEND_MODAL_ID, + MESSAGE_MODAL_ID ] } @@ -78,6 +81,11 @@ impl WalletContentContainer for WalletContent { c.modal_ui(ui, w, m, cb); } } + MESSAGE_MODAL_ID => { + if let Some(c) = self.message_content.as_mut() { + c.ui(ui, w, m, cb); + } + } _ => {} } } @@ -114,7 +122,7 @@ impl WalletContentContainer for WalletContent { .show_animated_inside(ui, !block_nav, |ui| { let r = ui.available_rect_before_wrap(); View::max_width_ui(ui, Content::SIDE_PANEL_WIDTH * 1.3, |ui| { - self.tabs_ui(wallet, ui, cb); + self.tabs_ui(wallet, ui); }); let rect = { let mut r = r.clone(); @@ -327,7 +335,7 @@ impl Default for WalletContent { transport_content: WalletTransportContent::default(), invoice_content: None, send_content: None, - file_pick_tab_button: FilePickContent::new(FilePickContentType::Tab), + message_content: None, } } } @@ -378,7 +386,7 @@ impl WalletContent { } /// Draw tab buttons at the bottom of the screen. - fn tabs_ui(&mut self, wallet: &Wallet, ui: &mut egui::Ui, cb: &dyn PlatformCallbacks) { + fn tabs_ui(&mut self, wallet: &Wallet, ui: &mut egui::Ui) { ui.scope(|ui| { // Setup spacing between tabs. ui.style_mut().spacing.item_spacing = egui::vec2(View::TAB_ITEMS_PADDING, 0.0); @@ -387,8 +395,7 @@ impl WalletContent { ui.style_mut().spacing.button_padding = egui::vec2(0.0, 4.0); let has_wallet_data = wallet.get_data().is_some(); - let can_send = has_wallet_data && - wallet.get_data().unwrap().info.amount_currently_spendable > 0; + let can_send = wallet.get_data().unwrap().info.amount_currently_spendable > 0; let tabs_amount = if can_send { 5 } else { 4 }; ui.columns(tabs_amount, |columns| { @@ -399,7 +406,8 @@ impl WalletContent { self.settings_content = None; }); }); - let active = if has_wallet_data { Some(false) } else { None }; + let active = if wallet.synced_from_node() && + has_wallet_data { Some(false) } else { None }; columns[1].vertical_centered_justified(|ui| { if wallet.invoice_creating() { ui.add_space(4.0); @@ -420,9 +428,13 @@ impl WalletContent { ui.add_space(4.0); View::small_loading_spinner(ui); } else { - self.file_pick_tab_button.set_active(active.is_some()); - self.file_pick_tab_button.ui(ui, cb, |m| { - wallet.task(WalletTask::OpenMessage(m)); + let (icon, color) = (FILE_TEXT, Some(Colors::gold_dark())); + View::tab_button(ui, icon, color, active, |_| { + self.message_content = Some(MessageInputContent::default()); + Modal::new(MESSAGE_MODAL_ID) + .position(ModalPosition::Center) + .title(t!("wallets.messages")) + .show(); }); } }); @@ -433,7 +445,7 @@ impl WalletContent { View::small_loading_spinner(ui); } else { let (icon, color) = (FILE_ARROW_UP, Some(Colors::red())); - View::tab_button(ui, icon, color, Some(false), |_| { + View::tab_button(ui, icon, color, active, |_| { self.send_content = Some(SendRequestContent::new(None)); Modal::new(SEND_MODAL_ID) .position(ModalPosition::CenterTop) diff --git a/src/gui/views/wallets/wallet/message.rs b/src/gui/views/wallets/wallet/message.rs new file mode 100644 index 00000000..7230171f --- /dev/null +++ b/src/gui/views/wallets/wallet/message.rs @@ -0,0 +1,203 @@ +// Copyright 2026 The Grim Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use egui::scroll_area::ScrollBarVisibility; +use egui::{Id, RichText, ScrollArea}; + +use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, SCAN}; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, View}; +use crate::gui::Colors; +use crate::wallet::types::WalletTask; +use crate::wallet::Wallet; + +pub struct MessageInputContent { + /// Slatepack input text. + message_edit: String, + /// Flag to check if error happened at Slatepack message parsing. + parse_error: bool, + + /// QR code scanner content. + scan_qr_content: Option, + + /// Button to parse picked file content. + file_pick_button: FilePickContent, +} + +/// Hint for Slatepack message input. +const SLATEPACK_MESSAGE_HINT: &'static str = "BEGINSLATEPACK.\n...\n...\n...\nENDSLATEPACK."; + +impl Default for MessageInputContent { + fn default() -> Self { + Self { + message_edit: "".to_string(), + parse_error: false, + scan_qr_content: None, + file_pick_button: FilePickContent::new(FilePickContentType::Button), + } + } +} + +impl MessageInputContent { + /// Draw [`Modal`] content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + wallet: &Wallet, + modal: &Modal, + cb: &dyn PlatformCallbacks) { + if let Some(scan_content) = self.scan_qr_content.as_mut() { + if let Some(result) = scan_content.qr_scan_result() { + cb.stop_camera(); + modal.enable_closing(); + self.scan_qr_content = None; + // Parse scan result. + self.on_message_input(result.text(), wallet); + } else { + scan_content.ui(ui, cb); + } + ui.add_space(8.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + // Show buttons to close modal or scanner. + ui.columns(2, |cols| { + cols[0].vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + cb.stop_camera(); + self.scan_qr_content = None; + Modal::close(); + }); + }); + cols[1].vertical_centered_justified(|ui| { + View::button(ui, t!("back"), Colors::white_or_black(false), || { + cb.stop_camera(); + self.scan_qr_content = None; + modal.enable_closing(); + }); + }); + }); + } else { + ui.add_space(6.0); + ui.vertical_centered(|ui| { + let (text, color) = if self.parse_error { + (t!("wallets.parse_slatepack_err"), Colors::red()) + } else { + (t!("wallets.input_slatepack_desc"), Colors::gray()) + }; + ui.label(RichText::new(text).size(16.0).color(color)); + }); + ui.add_space(6.0); + + // Draw slatepack message content. + ui.vertical_centered(|ui| { + let scroll_id = Id::from("message_input").with(wallet.identifier()); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("_input"); + egui::TextEdit::multiline(&mut self.message_edit) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(true) + .hint_text(SLATEPACK_MESSAGE_HINT) + .desired_width(f32::INFINITY) + .show(ui).response; + ui.add_space(6.0); + }); + }); + + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + if self.parse_error { + // Draw button to clear message input. + let clear_text = format!("{} {}", BROOM, t!("clear")); + View::button(ui, clear_text, Colors::white_or_black(false), || { + self.message_edit = "".to_string(); + self.parse_error = false; + }); + } else { + // Draw button to scan Slatepack message QR code. + let scan_text = format!("{} {}", SCAN, t!("scan")); + View::button(ui, scan_text, Colors::white_or_black(false), || { + self.scan_qr_content = Some(CameraContent::default()); + }); + } + }); + columns[1].vertical_centered_justified(|ui| { + // Draw paste button. + let paste_text = format!("{} {}", CLIPBOARD_TEXT, t!("paste")); + View::button(ui, paste_text, Colors::white_or_black(false), || { + self.on_message_input(cb.get_string_from_buffer(), wallet); + }); + }); + }); + + // Draw button to pick Slatepack message file. + ui.add_space(8.0); + ui.vertical_centered(|ui| { + let mut picked_data = None; + self.file_pick_button.ui(ui, cb, |data| { + picked_data = Some(data); + }); + if let Some(data) = picked_data { + self.on_message_input(data, wallet); + } + }); + + ui.add_space(8.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + + // Show button to close modal. + ui.vertical_centered_justified(|ui| { + View::button(ui, t!("close"), Colors::white_or_black(false), || { + self.message_edit = "".to_string(); + Modal::close(); + }); + }); + } + ui.add_space(6.0); + } + + /// Parse Slatepack message on input change. + fn on_message_input(&mut self, text: String, wallet: &Wallet) { + self.parse_error = false; + self.message_edit = text; + match wallet.parse_slatepack(&self.message_edit) { + Ok(_) => { + wallet.task(WalletTask::OpenMessage(self.message_edit.to_string())); + self.message_edit = "".to_string(); + Modal::close(); + } + Err(_) => { + self.parse_error = true; + } + } + } +} \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/mod.rs b/src/gui/views/wallets/wallet/mod.rs index 3ec02686..599a4516 100644 --- a/src/gui/views/wallets/wallet/mod.rs +++ b/src/gui/views/wallets/wallet/mod.rs @@ -25,4 +25,5 @@ pub use content::WalletContent; mod account; mod transport; -mod request; \ No newline at end of file +mod request; +mod message; \ No newline at end of file diff --git a/src/gui/views/wallets/wallet/request/send.rs b/src/gui/views/wallets/wallet/request/send.rs index 9900cd18..5c0102bd 100644 --- a/src/gui/views/wallets/wallet/request/send.rs +++ b/src/gui/views/wallets/wallet/request/send.rs @@ -16,6 +16,7 @@ use egui::{Id, RichText}; use grin_core::core::{amount_from_hr_string, amount_to_hr_string}; use grin_core::global::get_accept_fee_base; use grin_wallet_libwallet::SlatepackAddress; + use crate::gui::platform::PlatformCallbacks; use crate::gui::views::{CameraContent, Modal, TextEdit, View}; use crate::gui::Colors; diff --git a/src/gui/views/wallets/wallet/txs/content.rs b/src/gui/views/wallets/wallet/txs/content.rs index 0b8c744b..dc5fde03 100644 --- a/src/gui/views/wallets/wallet/txs/content.rs +++ b/src/gui/views/wallets/wallet/txs/content.rs @@ -52,7 +52,7 @@ impl WalletContentContainer for WalletTransactionsContent { match m.id { TX_INFO_MODAL => { if let Some(content) = self.tx_info_content.as_mut() { - content.ui(ui, w, m, cb); + content.ui(ui, w, cb); } } CANCEL_TX_CONFIRMATION_MODAL => { @@ -180,10 +180,10 @@ impl WalletTransactionsContent { } if !tx.cancelled() && !tx.cancelling() && !tx.posting() { - let rebroadcast = tx.broadcasting_timed_out(wallet); + let resend = tx.broadcasting_timed_out(wallet); // Draw button to cancel transaction. - if tx.can_cancel() || rebroadcast { + if tx.can_cancel() || resend { let (icon, color) = (PROHIBIT, Some(Colors::red())); View::item_button(ui, CornerRadius::default(), icon, color, || { self.confirm_cancel_tx_id = Some(tx.data.id); @@ -196,8 +196,8 @@ impl WalletTransactionsContent { } // Draw button to repeat transaction action. - if tx.can_repeat_action() || rebroadcast { - Self::tx_repeat_button_ui(ui, CornerRadius::default(), tx, wallet, rebroadcast); + if tx.can_repeat_action() || resend { + Self::tx_repeat_button_ui(ui, CornerRadius::default(), tx, wallet, resend); } } }); diff --git a/src/gui/views/wallets/wallet/txs/tx.rs b/src/gui/views/wallets/wallet/txs/tx.rs index ef436e0b..87dde034 100644 --- a/src/gui/views/wallets/wallet/txs/tx.rs +++ b/src/gui/views/wallets/wallet/txs/tx.rs @@ -12,15 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use egui::{Align, CornerRadius, Layout, RichText, StrokeKind}; +use egui::scroll_area::ScrollBarVisibility; +use egui::{Align, CornerRadius, Id, Layout, RichText, ScrollArea, StrokeKind}; use grin_core::core::amount_to_hr_string; use grin_util::ToHex; use grin_wallet_libwallet::TxLogEntryType; +use std::fs; -use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN}; +use crate::gui::icons::{CIRCLE_HALF, COPY, CUBE, FILE_ARCHIVE, FILE_TEXT, HASH_STRAIGHT, PROHIBIT, QR_CODE}; use crate::gui::platform::PlatformCallbacks; use crate::gui::views::wallets::wallet::txs::WalletTransactionsContent; -use crate::gui::views::{CameraContent, FilePickContent, FilePickContentType, Modal, QrCodeContent, View}; +use crate::gui::views::{Modal, QrCodeContent, View}; use crate::gui::Colors; use crate::wallet::types::{WalletTask, WalletTransaction}; use crate::wallet::Wallet; @@ -29,15 +31,11 @@ use crate::wallet::Wallet; pub struct WalletTransactionContent { /// Transaction identifier. tx_id: u32, + /// Slatepack message text. + message: Option, /// QR code Slatepack message image content. qr_code_content: Option, - - /// QR code scanner content. - scan_qr_content: Option, - - /// Button to parse picked file content. - file_pick_button: FilePickContent, } impl WalletTransactionContent { @@ -45,20 +43,13 @@ impl WalletTransactionContent { pub fn new(id: u32) -> Self { Self { tx_id: id, + message: None, qr_code_content: None, - scan_qr_content: None, - file_pick_button: FilePickContent::new( - FilePickContentType::ItemButton(View::item_rounding(0, 2, true)) - ), } } /// Draw [`Modal`] content. - pub fn ui(&mut self, - ui: &mut egui::Ui, - wallet: &Wallet, - modal: &Modal, - cb: &dyn PlatformCallbacks) { + pub fn ui(&mut self, ui: &mut egui::Ui, wallet: &Wallet, cb: &dyn PlatformCallbacks) { // Check values and setup transaction data. let wallet_data = wallet.get_data(); if wallet_data.is_none() { @@ -97,41 +88,9 @@ impl WalletTransactionContent { }); }); }); - } else if let Some(scan_content) = self.scan_qr_content.as_mut() { - if let Some(result) = scan_content.qr_scan_result() { - cb.stop_camera(); - modal.enable_closing(); - self.scan_qr_content = None; - // Provide scan result as Slatepack message. - wallet.task(WalletTask::OpenMessage(result.text())); - } else { - scan_content.ui(ui, cb); - } - ui.add_space(8.0); - - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); - - // Show buttons to close modal or scanner. - ui.columns(2, |cols| { - cols[0].vertical_centered_justified(|ui| { - View::button(ui, t!("close"), Colors::white_or_black(false), || { - cb.stop_camera(); - self.scan_qr_content = None; - Modal::close(); - }); - }); - cols[1].vertical_centered_justified(|ui| { - View::button(ui, t!("back"), Colors::white_or_black(false), || { - cb.stop_camera(); - self.scan_qr_content = None; - modal.enable_closing(); - }); - }); - }); } else { // Show transaction information. - self.info_ui(ui, modal, tx, wallet, cb); + self.info_ui(ui, tx, wallet, cb); // Show transaction sharing content when can cancel or finalized. if tx.can_cancel() && !tx.finalized() { @@ -158,62 +117,106 @@ impl WalletTransactionContent { wallet: &Wallet, tx: &WalletTransaction, cb: &dyn PlatformCallbacks) { - let amount = amount_to_hr_string(tx.amount, true); - let desc_text = if tx.can_finalize() { - if tx.data.tx_type == TxLogEntryType::TxSent { - t!("wallets.send_request_desc", "amount" => amount) - } else { - t!("wallets.invoice_desc", "amount" => amount) + if self.message.is_none() { + let slatepack_path = wallet.get_config().get_tx_slate_path(tx); + self.message = Some(fs::read_to_string(slatepack_path).unwrap_or("".to_string())); + } + if let Some(m) = &self.message { + if m.is_empty() { + return; } - } else { - if tx.data.tx_type == TxLogEntryType::TxSent { - t!("wallets.parse_i1_slatepack_desc", "amount" => amount) + let amount = amount_to_hr_string(tx.amount, true); + let desc_text = if tx.can_finalize() { + if tx.data.tx_type == TxLogEntryType::TxSent { + t!("wallets.send_request_desc", "amount" => amount) + } else { + t!("wallets.invoice_desc", "amount" => amount) + } } else { - t!("wallets.parse_s1_slatepack_desc", "amount" => amount) - } - }; - ui.add_space(6.0); - ui.vertical_centered(|ui| { - ui.label(RichText::new(desc_text).size(16.0).color(Colors::inactive_text())); - }); - ui.add_space(6.0); + if tx.data.tx_type == TxLogEntryType::TxSent { + t!("wallets.parse_i1_slatepack_desc", "amount" => amount) + } else { + t!("wallets.parse_s1_slatepack_desc", "amount" => amount) + } + }; + ui.add_space(6.0); + ui.vertical_centered(|ui| { + ui.label(RichText::new(desc_text).size(16.0).color(Colors::inactive_text())); + }); + ui.add_space(6.0); - // Setup spacing between buttons. - ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + let mut message = m.clone(); + // Draw slatepack message content. + ui.vertical_centered(|ui| { + let scroll_id = Id::from("tx_info_message_request").with(tx.data.id); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(3.0); + ScrollArea::vertical() + .id_salt(scroll_id) + .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden) + .max_height(128.0) + .auto_shrink([false; 2]) + .show(ui, |ui| { + ui.add_space(7.0); + let input_id = scroll_id.with("_input"); + egui::TextEdit::multiline(&mut message) + .id(input_id) + .font(egui::TextStyle::Small) + .desired_rows(5) + .interactive(false) + .desired_width(f32::INFINITY) + .show(ui).response; + ui.add_space(6.0); + }); + }); - ui.columns(2, |columns| { - columns[0].vertical_centered_justified(|ui| { - // Draw button to show Slatepack message as QR code. - let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); - View::button(ui, qr_text.clone(), Colors::white_or_black(false), || { - if let Some(c) = wallet.read_slatepack(tx) { - self.qr_code_content = Some(QrCodeContent::new(c, true)); - } + ui.add_space(2.0); + View::horizontal_line(ui, Colors::item_stroke()); + ui.add_space(8.0); + + // Setup spacing between buttons. + ui.spacing_mut().item_spacing = egui::Vec2::new(8.0, 0.0); + + ui.columns(2, |columns| { + columns[0].vertical_centered_justified(|ui| { + // Draw button to show Slatepack message as QR code. + let qr_text = format!("{} {}", QR_CODE, t!("qr_code")); + View::button(ui, qr_text, Colors::white_or_black(false), || { + self.qr_code_content = Some(QrCodeContent::new(message, true)); + }); + }); + columns[1].vertical_centered_justified(|ui| { + // Draw copy button. + let copy_text = format!("{} {}", COPY, t!("copy")); + View::button(ui, copy_text, Colors::white_or_black(false), || { + cb.copy_string_to_buffer(m.clone()); + Modal::close(); + }); }); }); - columns[1].vertical_centered_justified(|ui| { - // Show button to share response as file. - let share_text = format!("{} {}", FILE_TEXT, t!("file")); + + // Draw button to share response as file. + ui.add_space(8.0); + ui.vertical_centered(|ui| { + let share_text = format!("{} {}", FILE_TEXT, t!("share")); View::colored_text_button(ui, share_text, Colors::blue(), Colors::white_or_black(false), || { if let Some(slate_id) = tx.data.tx_slate_id { let name = format!("{}.{}.slatepack", slate_id, tx.state); - if let Some(c) = wallet.read_slatepack(tx) { - let data = c.as_bytes().to_vec(); - cb.share_data(name, data).unwrap_or_default(); - } + let data = m.as_bytes().to_vec(); + cb.share_data(name, data).unwrap_or_default(); + Modal::close(); } }); }); - }); + } } /// Draw transaction information content. fn info_ui(&mut self, ui: &mut egui::Ui, - modal: &Modal, tx: &WalletTransaction, wallet: &Wallet, cb: &dyn PlatformCallbacks) { @@ -244,30 +247,13 @@ impl WalletTransactionContent { } return; } - if tx.can_finalize() { - // Draw button to pick file. - self.file_pick_button.ui(ui, cb, |data| { - wallet.task(WalletTask::OpenMessage(data)); - }); - // Draw button to scan QR code. - let r = CornerRadius::default(); - View::item_button(ui, r, SCAN, Some(Colors::text_button()), || { - modal.disable_closing(); - cb.start_camera(); - self.scan_qr_content = Some(CameraContent::default()); - }); - } if !tx.cancelled() && !tx.cancelling() && !tx.posting() { let rebroadcast = tx.broadcasting_timed_out(&wallet); // Draw button to cancel transaction. if tx.can_cancel() || rebroadcast { - let r = if tx.can_finalize() { - CornerRadius::default() - } else { - View::item_rounding(0, 2, true) - }; + let r = View::item_rounding(0, 2, true); View::item_button(ui, r, PROHIBIT, Some(Colors::red()), || { wallet.task(WalletTask::Cancel(tx.clone())); Modal::close(); diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index 6f7ce808..60f01a95 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -79,6 +79,8 @@ pub struct Wallet { /// Wallet data. data: Arc>>, + /// Flag to check if wallet data was synced from node. + from_node: Arc, /// Flag to check if wallet reopening is needed. reopen: Arc, @@ -132,6 +134,7 @@ impl Wallet { info_sync_progress: Arc::from(AtomicU8::new(0)), accounts: Arc::new(RwLock::new(vec![])), data: Arc::new(RwLock::new(None)), + from_node: Arc::new(AtomicBool::new(false)), sync_attempts: Arc::new(AtomicU8::new(0)), syncing: Arc::new(AtomicBool::new(false)), repair_needed: Arc::new(AtomicBool::new(false)), @@ -567,6 +570,11 @@ impl Wallet { self.sync_error.store(error, Ordering::Relaxed); } + /// Check if last synchronization finished from node. + pub fn synced_from_node(&self) -> bool { + self.from_node.load(Ordering::Relaxed) + } + /// Get current wallet synchronization attempts before setting an error. fn get_sync_attempts(&self) -> u8 { self.sync_attempts.load(Ordering::Relaxed) @@ -721,7 +729,7 @@ impl Wallet { } /// Parse Slatepack message into [`Slate`]. - fn parse_slatepack(&self, text: &String) -> Result { + pub fn parse_slatepack(&self, text: &String) -> Result { let r_inst = self.instance.as_ref().read(); let instance = r_inst.clone().unwrap(); let mut api = Owner::new(instance, None); @@ -756,15 +764,6 @@ impl Wallet { fs::exists(slatepack_path).unwrap() } - /// Read Slatepack file content. - pub fn read_slatepack(&self, tx: &WalletTransaction) -> Option { - let slatepack_path = self.get_config().get_tx_slate_path(tx); - if let Ok(m) = fs::read_to_string(slatepack_path) { - return Some(m); - } - None - } - /// Calculate transaction fee for provided amount. fn calculate_fee(&self, a: u64) -> Result { let r_inst = self.instance.as_ref().read(); @@ -1574,6 +1573,7 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { // Update wallet transactions. if update_txs(wallet, instance.clone(), info).is_ok() { + wallet.from_node.store(from_node, Ordering::Relaxed); wallet.reset_sync_attempts(); return; }