From be80e8272790df8087d7d579a3257853b5abe3d8 Mon Sep 17 00:00:00 2001 From: ardocrat Date: Tue, 4 Jun 2024 22:50:41 +0300 Subject: [PATCH] ui: file picker button for slatepacks messages (text parsing), check if tx was synced from node, do not show txs actions on wallet loading --- locales/en.yml | 1 + locales/ru.yml | 1 + locales/tr.yml | 1 + src/gui/colors.rs | 10 +++ src/gui/platform/android/mod.rs | 4 + src/gui/platform/desktop/mod.rs | 12 +++ src/gui/platform/mod.rs | 1 + src/gui/views/file.rs | 95 ++++++++++++++++++++++++ src/gui/views/mod.rs | 5 +- src/gui/views/modal.rs | 2 +- src/gui/views/wallets/wallet/messages.rs | 24 +++++- src/gui/views/wallets/wallet/txs.rs | 43 ++++++++--- src/wallet/types.rs | 8 +- src/wallet/wallet.rs | 19 +++-- 14 files changed, 199 insertions(+), 27 deletions(-) create mode 100644 src/gui/views/file.rs diff --git a/locales/en.yml b/locales/en.yml index 17f5544..94bc247 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -25,6 +25,7 @@ share: Share theme: 'Theme:' dark: Dark light: Light +choose_file: Choose file wallets: await_conf_amount: Awaiting confirmation await_fin_amount: Awaiting finalization diff --git a/locales/ru.yml b/locales/ru.yml index 228fb48..67beea1 100644 --- a/locales/ru.yml +++ b/locales/ru.yml @@ -25,6 +25,7 @@ share: Поделиться theme: 'Тема:' dark: Тёмная light: Светлая +choose_file: Выбрать файл wallets: await_conf_amount: Ожидает подтверждения await_fin_amount: Ожидает завершения diff --git a/locales/tr.yml b/locales/tr.yml index 9ec1d37..ac2d890 100644 --- a/locales/tr.yml +++ b/locales/tr.yml @@ -25,6 +25,7 @@ share: Paylasmak theme: 'Tema:' dark: Karanlik light: Isik +choose_file: Dosya seçin wallets: await_conf_amount: Onay bekleniyor await_fin_amount: Tamamlanma bekleniyor diff --git a/src/gui/colors.rs b/src/gui/colors.rs index 17e3a64..a2d054d 100644 --- a/src/gui/colors.rs +++ b/src/gui/colors.rs @@ -33,6 +33,8 @@ const GREEN: Color32 = Color32::from_rgb(0, 0x64, 0); const RED: Color32 = Color32::from_rgb(0x8B, 0, 0); +const BLUE: Color32 = Color32::from_rgb(0, 0x66, 0xE4); + const FILL: Color32 = Color32::from_gray(244); const FILL_DARK: Color32 = Color32::from_gray(24); @@ -132,6 +134,14 @@ impl Colors { } } + pub fn blue() -> Color32 { + if use_dark() { + BLUE.linear_multiply(1.3) + } else { + BLUE + } + } + pub fn fill() -> Color32 { if use_dark() { FILL_DARK diff --git a/src/gui/platform/android/mod.rs b/src/gui/platform/android/mod.rs index 553a669..6e2aa01 100644 --- a/src/gui/platform/android/mod.rs +++ b/src/gui/platform/android/mod.rs @@ -143,6 +143,10 @@ impl PlatformCallbacks for Android { &[JValue::Object(&JObject::from(arg_value))]).unwrap(); Ok(()) } + + fn pick_file(&self) -> Option { + None + } } lazy_static! { diff --git a/src/gui/platform/desktop/mod.rs b/src/gui/platform/desktop/mod.rs index 2d242f6..1890002 100644 --- a/src/gui/platform/desktop/mod.rs +++ b/src/gui/platform/desktop/mod.rs @@ -131,6 +131,7 @@ impl PlatformCallbacks for Desktop { fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error> { let folder = FileDialog::new() + .set_title(t!("share")) .set_directory(dirs::home_dir().unwrap()) .set_file_name(name.clone()) .save_file(); @@ -141,6 +142,17 @@ impl PlatformCallbacks for Desktop { } Ok(()) } + + fn pick_file(&self) -> Option { + let file = FileDialog::new() + .set_title(t!("choose_file")) + .set_directory(dirs::home_dir().unwrap()) + .pick_file(); + if let Some(file) = file { + return Some(file.to_str().unwrap_or_default().to_string()); + } + None + } } lazy_static! { diff --git a/src/gui/platform/mod.rs b/src/gui/platform/mod.rs index d8bf62a..c925973 100644 --- a/src/gui/platform/mod.rs +++ b/src/gui/platform/mod.rs @@ -32,4 +32,5 @@ pub trait PlatformCallbacks { fn can_switch_camera(&self) -> bool; fn switch_camera(&self); fn share_data(&self, name: String, data: Vec) -> Result<(), std::io::Error>; + fn pick_file(&self) -> Option; } \ No newline at end of file diff --git a/src/gui/views/file.rs b/src/gui/views/file.rs new file mode 100644 index 0000000..05d22fb --- /dev/null +++ b/src/gui/views/file.rs @@ -0,0 +1,95 @@ +// Copyright 2024 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 std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::{fs, thread}; +use parking_lot::RwLock; + +use crate::gui::Colors; +use crate::gui::icons::FILE_ARROW_UP; +use crate::gui::platform::PlatformCallbacks; +use crate::gui::views::View; + +/// Button to pick file and parse its data into text. +pub struct FilePickButton { + /// Flag to check if file is parsing. + pub file_parsing: Arc, + /// File parsing result. + pub file_parsing_result: Arc>> +} + +impl Default for FilePickButton { + fn default() -> Self { + Self { + file_parsing: Arc::new(AtomicBool::new(false)), + file_parsing_result: Arc::new(RwLock::new(None)) + } + } +} + +impl FilePickButton { + /// Draw button content. + pub fn ui(&mut self, + ui: &mut egui::Ui, + cb: &dyn PlatformCallbacks, + on_result: impl FnOnce(String)) { + if self.file_parsing.load(Ordering::Relaxed) { + // Draw loading spinner on file parsing. + View::small_loading_spinner(ui); + // Check file parsing result. + let has_result = { + let r_res = self.file_parsing_result.read(); + r_res.is_some() + }; + if has_result { + let text = { + let r_res = self.file_parsing_result.read(); + r_res.clone().unwrap() + }; + // Callback on result. + on_result(text); + // Clear result. + let mut w_res = self.file_parsing_result.write(); + *w_res = None; + self.file_parsing.store(false, Ordering::Relaxed); + } + } else { + // Draw button to pick file. + let file_text = format!("{} {}", FILE_ARROW_UP, t!("choose_file")); + View::colored_text_button(ui, file_text, Colors::blue(), Colors::button(), || { + if let Some(path) = cb.pick_file() { + // Parse file at new thread. + self.file_parsing.store(true, Ordering::Relaxed); + let result = self.file_parsing_result.clone(); + thread::spawn(move || { + if path.ends_with(".gif") { + //TODO: Detect QR codes on GIF file. + } else if path.ends_with(".jpeg") || path.ends_with(".jpg") || + path.ends_with(".png") { + //TODO: Detect QR codes on image files. + } else { + // Parse file as plain text. + if let Ok(text) = fs::read_to_string(path) { + let mut w_res = result.write(); + *w_res = Some(text); + } + } + }); + } + }); + } + } + +} \ No newline at end of file diff --git a/src/gui/views/mod.rs b/src/gui/views/mod.rs index f2ecf92..259d6cd 100644 --- a/src/gui/views/mod.rs +++ b/src/gui/views/mod.rs @@ -36,4 +36,7 @@ mod camera; pub use camera::*; mod qr; -pub use qr::*; \ No newline at end of file +pub use qr::*; + +mod file; +pub use file::*; \ No newline at end of file diff --git a/src/gui/views/modal.rs b/src/gui/views/modal.rs index 42daf79..7e61180 100644 --- a/src/gui/views/modal.rs +++ b/src/gui/views/modal.rs @@ -43,7 +43,7 @@ pub struct Modal { impl Modal { /// Margin from [`Modal`] window at top/left/right. - const DEFAULT_MARGIN: f32 = 8.0; + const DEFAULT_MARGIN: f32 = 4.0; /// Maximum width of the content. const DEFAULT_WIDTH: f32 = Root::SIDE_PANEL_WIDTH - (2.0 * Self::DEFAULT_MARGIN); diff --git a/src/gui/views/wallets/wallet/messages.rs b/src/gui/views/wallets/wallet/messages.rs index 380df97..685abf7 100644 --- a/src/gui/views/wallets/wallet/messages.rs +++ b/src/gui/views/wallets/wallet/messages.rs @@ -24,7 +24,7 @@ use parking_lot::RwLock; use crate::gui::Colors; use crate::gui::icons::{BROOM, CLIPBOARD_TEXT, COPY, DOWNLOAD_SIMPLE, PROHIBIT, QR_CODE, SCAN, UPLOAD_SIMPLE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, Modal, QrCodeContent, Root, View}; +use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, Root, View}; use crate::gui::views::types::{ModalPosition, QrScanResult, TextEditOptions}; use crate::gui::views::wallets::wallet::types::{SLATEPACK_MESSAGE_HINT, WalletTab, WalletTabType}; use crate::gui::views::wallets::wallet::WalletContent; @@ -72,6 +72,8 @@ pub struct WalletMessages { response_edit: String, /// Flag to check if Dandelion is needed to finalize transaction. dandelion: bool, + /// Button to parse picked file content. + file_pick_button: FilePickButton, /// Flag to check if invoice or sending request was opened for [`Modal`]. request_invoice: bool, @@ -168,6 +170,7 @@ impl WalletMessages { message_error: None, response_edit: "".to_string(), dandelion, + file_pick_button: FilePickButton::default(), request_amount_edit: "".to_string(), request_edit: "".to_string(), request_error: None, @@ -795,7 +798,9 @@ impl WalletMessages { ui.add_space(10.0); - // Draw clear button on message input or cancel and clear buttons on response. + // Draw clear button on message input, + // cancel and clear buttons on response + // or button to choose text or image file. if !self.message_loading { if self.message_slate.is_none() && !self.message_edit.is_empty() { // Draw button to clear message input. @@ -808,8 +813,8 @@ impl WalletMessages { }); } else if !self.response_edit.is_empty() && self.message_slate.is_some() { // Draw cancel button. - let cancel = format!("{} {}", PROHIBIT, t!("modal.cancel")); - View::colored_text_button(ui, cancel, Colors::red(), Colors::button(), || { + let cancel_text = format!("{} {}", PROHIBIT, t!("modal.cancel")); + View::colored_text_button(ui, cancel_text, Colors::red(), Colors::button(), || { let slate = self.message_slate.clone().unwrap(); if let Some(tx) = wallet.tx_by_slate(&slate) { wallet.cancel(tx.data.id); @@ -818,6 +823,17 @@ impl WalletMessages { self.message_slate = None; } }); + } else { + // Draw button to choose file. + let mut parsed_text = "".to_string(); + self.file_pick_button.ui(ui, cb, |text| { + parsed_text = text; + }); + if !parsed_text.is_empty() { + // Parse Slatepack message from file content. + self.message_edit = parsed_text; + self.parse_message(wallet); + } } } }); diff --git a/src/gui/views/wallets/wallet/txs.rs b/src/gui/views/wallets/wallet/txs.rs index 371ac28..6457f7c 100644 --- a/src/gui/views/wallets/wallet/txs.rs +++ b/src/gui/views/wallets/wallet/txs.rs @@ -24,9 +24,9 @@ use grin_wallet_libwallet::{Error, Slate, SlateState, TxLogEntryType}; use parking_lot::RwLock; use crate::gui::Colors; -use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, BRIDGE, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, CLIPBOARD_TEXT, COPY, DOTS_THREE_CIRCLE, FILE_ARCHIVE, FILE_TEXT, GEAR_FINE, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN, X_CIRCLE}; +use crate::gui::icons::{ARROW_CIRCLE_DOWN, ARROW_CIRCLE_UP, ARROW_CLOCKWISE, BRIDGE, BROOM, CALENDAR_CHECK, CHAT_CIRCLE_TEXT, CHECK, CHECK_CIRCLE, CLIPBOARD_TEXT, COPY, DOTS_THREE_CIRCLE, FILE_ARCHIVE, FILE_TEXT, GEAR_FINE, HASH_STRAIGHT, PROHIBIT, QR_CODE, SCAN, X_CIRCLE}; use crate::gui::platform::PlatformCallbacks; -use crate::gui::views::{CameraContent, Modal, QrCodeContent, Root, View}; +use crate::gui::views::{CameraContent, FilePickButton, Modal, QrCodeContent, Root, View}; use crate::gui::views::types::ModalPosition; use crate::gui::views::wallets::types::WalletTab; use crate::gui::views::wallets::wallet::types::{GRIN, SLATEPACK_MESSAGE_HINT, WalletTabType}; @@ -60,6 +60,8 @@ pub struct WalletTransactions { tx_info_show_scanner: bool, /// QR code scanner [`Modal`] content. tx_info_scanner_content: CameraContent, + /// Button to parse picked file content at [`Modal`]. + tx_info_file_pick_button: FilePickButton, /// Transaction identifier to use at confirmation [`Modal`]. confirm_cancel_tx_id: Option, @@ -83,6 +85,7 @@ impl Default for WalletTransactions { tx_info_qr_code_content: QrCodeContent::new("".to_string(), true), tx_info_show_scanner: false, tx_info_scanner_content: CameraContent::default(), + tx_info_file_pick_button: FilePickButton::default(), confirm_cancel_tx_id: None, manual_sync: None, } @@ -146,8 +149,6 @@ impl WalletTransactions { let amount_conf = data.info.amount_awaiting_confirmation; let amount_fin = data.info.amount_awaiting_finalization; let amount_locked = data.info.amount_locked; - - // Show transactions info. View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| { // Show non-zero awaiting confirmation amount. if amount_conf != 0 { @@ -228,10 +229,10 @@ impl WalletTransactions { View::max_width_ui(ui, Root::SIDE_PANEL_WIDTH * 1.3, |ui| { let padding = amount_conf != 0 || amount_fin != 0 || amount_locked != 0; for index in row_range { - // Show transaction item. let tx = txs.get(index).unwrap(); - let rounding = View::item_rounding(index, txs.len(), false); - self.tx_item_ui(ui, tx, rounding, padding, true, &data, wallet); + let r = View::item_rounding(index, txs.len(), false); + let show_info = tx.data.tx_slate_id.is_some(); + self.tx_item_ui(ui, tx, r, padding, show_info, &data, wallet); } }); }) @@ -318,7 +319,7 @@ impl WalletTransactions { ui.allocate_ui_with_layout(rect.size(), Layout::right_to_left(Align::Center), |ui| { // Draw button to show transaction info. - if can_show_info && tx.data.tx_slate_id.is_some() { + if can_show_info && tx.from_node { rounding.nw = 0.0; rounding.sw = 0.0; View::item_button(ui, rounding, FILE_TEXT, None, || { @@ -357,7 +358,7 @@ impl WalletTransactions { } // Draw cancel button for tx that can be reposted and canceled. - let wallet_loaded = wallet.foreign_api_port().is_some(); + let wallet_loaded = tx.from_node && wallet.foreign_api_port().is_some(); if wallet_loaded && ((!can_show_info && !self.tx_info_finalizing) || can_show_info) && (tx.can_repost(data) || tx.can_cancel()) { View::item_button(ui, Rounding::default(), PROHIBIT, Some(Colors::red()), || { @@ -828,7 +829,7 @@ impl WalletTransactions { ui.add_space(2.0); View::horizontal_line(ui, Colors::item_stroke()); - ui.add_space(8.0); + ui.add_space(10.0); // Do not show buttons on finalization. if self.tx_info_finalizing { @@ -862,6 +863,28 @@ impl WalletTransactions { } }); }); + ui.add_space(8.0); + ui.vertical_centered(|ui| { + if self.tx_info_finalize_error { + // Draw button to clear message input. + let clear_text = format!("{} {}", BROOM, t!("clear")); + View::button(ui, clear_text, Colors::button(), || { + self.tx_info_finalize_edit.clear(); + self.tx_info_finalize_error = false; + }); + } else { + // Draw button to choose file. + let mut parsed_text = "".to_string(); + self.tx_info_file_pick_button.ui(ui, cb, |text| { + parsed_text = text; + }); + if !parsed_text.is_empty() { + // Parse Slatepack message from file content. + self.tx_info_finalize_edit = parsed_text; + self.on_finalization_input_change(tx, wallet, modal, cb); + } + } + }); } else { ui.columns(2, |columns| { columns[0].vertical_centered_justified(|ui| { diff --git a/src/wallet/types.rs b/src/wallet/types.rs index 795862b..7dd594e 100644 --- a/src/wallet/types.rs +++ b/src/wallet/types.rs @@ -156,13 +156,15 @@ pub struct WalletTransaction { /// Block height when tx was confirmed. pub conf_height: Option, /// Block height when tx was reposted. - pub repost_height: Option + pub repost_height: Option, + /// Flag to check if tx was received after sync from node. + pub from_node: bool, } impl WalletTransaction { /// Check if transaction can be cancelled. pub fn can_cancel(&self) -> bool { - !self.cancelling && !self.posting && !self.data.confirmed && + self.from_node && !self.cancelling && !self.posting && !self.data.confirmed && self.data.tx_type != TxLogEntryType::TxReceivedCancelled && self.data.tx_type != TxLogEntryType::TxSentCancelled } @@ -171,7 +173,7 @@ impl WalletTransaction { pub fn can_repost(&self, data: &WalletData) -> bool { let last_height = data.info.last_confirmed_height; let min_conf = data.info.minimum_confirmations; - self.posting && self.repost_height.is_some() && + self.from_node && self.posting && self.repost_height.is_some() && last_height - self.repost_height.unwrap() > min_conf } } \ No newline at end of file diff --git a/src/wallet/wallet.rs b/src/wallet/wallet.rs index b3af133..fb0ea37 100644 --- a/src/wallet/wallet.rs +++ b/src/wallet/wallet.rs @@ -584,9 +584,9 @@ impl Wallet { } /// Parse Slatepack message into [`Slate`]. - pub fn parse_slatepack(&self, message: &String) -> Result { + pub fn parse_slatepack(&self, text: &String) -> Result { let mut api = Owner::new(self.instance.clone().unwrap(), None); - return match parse_slatepack(&mut api, None, None, Some(message.clone())) { + return match parse_slatepack(&mut api, None, None, Some(text.clone())) { Ok(s) => Ok(s.0), Err(e) => Err(e) } @@ -1168,7 +1168,9 @@ fn start_sync(wallet: Wallet) -> Thread { let failed_sync = wallet.sync_error() || wallet.get_sync_attempts() != 0; // Clear syncing status. - wallet.syncing.store(false, Ordering::Relaxed); + if !failed_sync { + wallet.syncing.store(false, Ordering::Relaxed); + } // Repeat after default or attempt delay if synchronization was not successful. let delay = if failed_sync { @@ -1288,12 +1290,12 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { tx.amount_credited - tx.amount_debited }; - let unconfirmed_sent_or_received = tx.tx_slate_id.is_some() && + let unc_sent_or_received = tx.tx_slate_id.is_some() && !tx.confirmed && (tx.tx_type == TxLogEntryType::TxSent || tx.tx_type == TxLogEntryType::TxReceived); // Setup transaction posting status based on slate state. - let posting = if unconfirmed_sent_or_received { + let posting = if unc_sent_or_received { // Create slate to check existing file. let is_invoice = tx.tx_type == TxLogEntryType::TxReceived; let mut slate = Slate::blank(0, is_invoice); @@ -1322,8 +1324,8 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { }; // Setup flag for ability to finalize transaction. - let can_finalize = if !posting && unconfirmed_sent_or_received { - // Create slate to check existing file. + let can_finalize = if from_node && !posting && unc_sent_or_received { + // Check existing file. let mut slate = Slate::blank(1, false); slate.id = tx.tx_slate_id.unwrap(); slate.state = match tx.tx_type { @@ -1400,7 +1402,8 @@ fn sync_wallet_data(wallet: &Wallet, from_node: bool) { posting, can_finalize, conf_height, - repost_height + repost_height, + from_node }); }