ui: file picker button for slatepacks messages (text parsing), check if tx was synced from node, do not show txs actions on wallet loading

This commit is contained in:
ardocrat 2024-06-04 22:50:41 +03:00
parent 951886f5c7
commit be80e82727
14 changed files with 199 additions and 27 deletions

View file

@ -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

View file

@ -25,6 +25,7 @@ share: Поделиться
theme: 'Тема:'
dark: Тёмная
light: Светлая
choose_file: Выбрать файл
wallets:
await_conf_amount: Ожидает подтверждения
await_fin_amount: Ожидает завершения

View file

@ -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

View file

@ -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

View file

@ -143,6 +143,10 @@ impl PlatformCallbacks for Android {
&[JValue::Object(&JObject::from(arg_value))]).unwrap();
Ok(())
}
fn pick_file(&self) -> Option<String> {
None
}
}
lazy_static! {

View file

@ -131,6 +131,7 @@ impl PlatformCallbacks for Desktop {
fn share_data(&self, name: String, data: Vec<u8>) -> 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<String> {
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! {

View file

@ -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<u8>) -> Result<(), std::io::Error>;
fn pick_file(&self) -> Option<String>;
}

95
src/gui/views/file.rs Normal file
View file

@ -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<AtomicBool>,
/// File parsing result.
pub file_parsing_result: Arc<RwLock<Option<String>>>
}
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);
}
}
});
}
});
}
}
}

View file

@ -37,3 +37,6 @@ pub use camera::*;
mod qr;
pub use qr::*;
mod file;
pub use file::*;

View file

@ -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);

View file

@ -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);
}
}
}
});

View file

@ -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<u32>,
@ -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| {

View file

@ -156,13 +156,15 @@ pub struct WalletTransaction {
/// Block height when tx was confirmed.
pub conf_height: Option<u64>,
/// Block height when tx was reposted.
pub repost_height: Option<u64>
pub repost_height: Option<u64>,
/// 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
}
}

View file

@ -584,9 +584,9 @@ impl Wallet {
}
/// Parse Slatepack message into [`Slate`].
pub fn parse_slatepack(&self, message: &String) -> Result<Slate, grin_wallet_controller::Error> {
pub fn parse_slatepack(&self, text: &String) -> Result<Slate, grin_wallet_controller::Error> {
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.
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
});
}