From 956b7901c1aea72f2311dfb22fca2304fb0b582f Mon Sep 17 00:00:00 2001 From: edouard Date: Thu, 20 Apr 2023 17:19:36 +0200 Subject: [PATCH 1/5] gui: change coins remaining time label --- gui/src/app/view/coins.rs | 246 ++++++++++++++----------- gui/src/app/view/mod.rs | 6 +- gui/src/app/view/psbts.rs | 6 +- gui/src/app/view/spend/step.rs | 34 +--- gui/src/app/view/transactions.rs | 22 ++- gui/ui/src/component/badge.rs | 24 ++- gui/ui/src/image.rs | 18 ++ gui/ui/src/theme.rs | 8 + gui/ui/static/icons/clock-icon.svg | 6 + gui/ui/static/icons/clock-red-icon.svg | 6 + gui/ui/static/icons/coins-icon.svg | 3 + 11 files changed, 218 insertions(+), 161 deletions(-) create mode 100644 gui/ui/static/icons/clock-icon.svg create mode 100644 gui/ui/static/icons/clock-red-icon.svg create mode 100644 gui/ui/static/icons/coins-icon.svg diff --git a/gui/src/app/view/coins.rs b/gui/src/app/view/coins.rs index 4e0ccf55..790bea90 100644 --- a/gui/src/app/view/coins.rs +++ b/gui/src/app/view/coins.rs @@ -2,8 +2,10 @@ use iced::{Alignment, Length}; use liana_ui::{ color, - component::{amount::*, badge, separation, text::*}, - icon, theme, + component::{amount::*, badge, text::*}, + icon, + image::*, + theme, util::Collection, widget::*, }; @@ -20,14 +22,7 @@ pub fn coins_view<'a>( selected: &[usize], ) -> Element<'a, Message> { Column::new() - .push( - Container::new( - Row::new() - .push(text(format!(" {}", coins.len()))) - .push(text(" coins")), - ) - .width(Length::Fill), - ) + .push(Container::new(h3("Coins")).width(Length::Fill)) .push( Column::new() .spacing(10) @@ -45,7 +40,7 @@ pub fn coins_view<'a>( )), ) .align_items(Alignment::Center) - .spacing(20) + .spacing(30) .into() } @@ -65,47 +60,11 @@ fn coin_list_view( .push( Row::new() .push(badge::coin()) - .push_maybe(if coin.spend_info.is_some() { - Some(badge::spent()) + .push(if coin.spend_info.is_some() { + badge::spent() } else { let seq = remaining_sequence(coin, blockheight, timelock); - if seq == 0 { - Some(Container::new( - Row::new() - .spacing(5) - .push(text(" 0").small().style(color::RED)) - .push( - icon::hourglass_done_icon() - .small() - .style(color::RED), - ) - .align_items(Alignment::Center), - )) - } else if seq < timelock as u32 * 10 / 100 { - Some(Container::new( - Row::new() - .spacing(5) - .push( - text(format!(" {}", seq)) - .small() - .style(color::ORANGE), - ) - .push( - icon::hourglass_icon() - .small() - .style(color::ORANGE), - ) - .align_items(Alignment::Center), - )) - } else { - Some(Container::new( - Row::new() - .spacing(5) - .push(text(format!(" {}", seq)).small()) - .push(icon::hourglass_icon().small()) - .align_items(Alignment::Center), - )) - } + coin_sequence_label(seq, timelock as u32) }) .push_maybe(if coin.block_height.is_none() { Some(badge::unconfirmed()) @@ -127,74 +86,83 @@ fn coin_list_view( .push_maybe(if collapsed { Some( Column::new() - .spacing(10) - .push(separation().width(Length::Fill)) + .padding(10) + .spacing(5) + .push_maybe(if coin.spend_info.is_none() { + if let Some(b) = coin.block_height { + if blockheight > b as u32 + timelock as u32 { + Some(Container::new( + p1_bold("One of the recovery path is available") + .style(color::RED), + )) + } else { + Some(Container::new(p1_bold(format!( + "One of the recovery path will be available in {} blocks", + b as u32 + timelock as u32 - blockheight + )))) + } + } else { + None + } + } else { + None + }) .push( Column::new() - .padding(10) - .spacing(5) - .push_maybe(if coin.spend_info.is_none() { - if let Some(b) = coin.block_height { - if blockheight > b as u32 + timelock as u32 { - Some(Container::new( - text("One of the recovery path is available") - .bold() - .small() - .style(color::RED), - )) - } else { - Some(Container::new( - text(format!("One of the recovery path will be available in {} blocks", b as u32 + timelock as u32 - blockheight)) - .bold() - .small(), - )) - } - } else { - None - } - } else { - None - }) .push( - Column::new() + Row::new() + .align_items(Alignment::Center) + .push(p2_regular("Outpoint:").bold().style(color::GREY_2)) .push( Row::new() .align_items(Alignment::Center) - .push(text("Outpoint:").small().bold()) - .push(Row::new().align_items(Alignment::Center) - .push(text(format!("{}", coin.outpoint)).small()) - .push(Button::new(icon::clipboard_icon()) - .on_press(Message::Clipboard(coin.outpoint.to_string())) - .style(theme::Button::TransparentBorder) - )) - .spacing(5), + .push( + p2_regular(format!("{}", coin.outpoint)) + .style(color::GREY_2), + ) + .push( + Button::new(icon::clipboard_icon()) + .on_press(Message::Clipboard( + coin.outpoint.to_string(), + )) + .style(theme::Button::TransparentBorder), + ), ) - .push_maybe(coin.block_height.map(|b| { - Row::new() - .push(text("Block height:").small().bold()) - .push(text(format!("{}", b)).small()) - .spacing(5) - })), + .spacing(5), ) - .push_maybe(coin.spend_info.map(|info| { - Column::new() + .push_maybe(coin.block_height.map(|b| { + Row::new() .push( - Row::new() - .push(text("Spend txid:").small().bold()) - .push(text(format!("{}", info.txid)).small()) - .spacing(5), + p2_regular("Block height:").bold().style(color::GREY_2), ) - .push(if let Some(height) = info.height { - Row::new() - .push(text("Spend block height:").small().bold()) - .push(text(format!("{}", height)).small()) - .spacing(5) - } else { - Row::new().push(text("Not in a block").bold().small()) - }) + .push(p2_regular(format!("{}", b)).style(color::GREY_2)) .spacing(5) })), - ), + ) + .push_maybe(coin.spend_info.map(|info| { + Column::new() + .push( + Row::new() + .push(p2_regular("Spend txid:").bold().style(color::GREY_2)) + .push(p2_regular(format!("{}", info.txid))) + .spacing(5), + ) + .push(if let Some(height) = info.height { + Row::new() + .push( + p2_regular("Spend block height:") + .bold() + .style(color::GREY_2), + ) + .push(p2_regular(format!("{}", height))) + .spacing(5) + } else { + Row::new().push( + p2_regular("Not in a block").bold().style(color::GREY_2), + ) + }) + .spacing(5) + })), ) } else { None @@ -202,3 +170,69 @@ fn coin_list_view( ) .style(theme::Container::Card(theme::Card::Simple)) } + +pub fn coin_sequence_label<'a, T: 'a>(seq: u32, timelock: u32) -> Container<'a, T> { + if seq == 0 { + Container::new( + Row::new() + .spacing(5) + .push(clock_red_icon().width(Length::Units(20))) + .push(p2_regular("Expired")) + .align_items(Alignment::Center), + ) + .padding(10) + .style(theme::Container::Pill(theme::Pill::Warning)) + } else if seq < timelock as u32 * 10 / 100 { + Container::new( + Row::new() + .spacing(5) + .push(clock_red_icon().width(Length::Units(20))) + .push(p2_regular(expire_message(seq))) + .align_items(Alignment::Center), + ) + .padding(10) + .style(theme::Container::Pill(theme::Pill::Simple)) + } else { + Container::new( + Row::new() + .spacing(5) + .push(clock_icon().width(Length::Units(20))) + .push(p2_regular(expire_message(seq)).style(color::GREY_3)) + .align_items(Alignment::Center), + ) + .padding(10) + .style(theme::Container::Pill(theme::Pill::Simple)) + } +} + +/// returns y,m,d,h,m +pub fn expire_message(sequence: u32) -> String { + let mut n_minutes = sequence * 10; + let n_years = n_minutes / 525960; + n_minutes -= n_years * 525960; + let n_months = n_minutes / 43830; + n_minutes -= n_months * 43830; + let n_days = n_minutes / 1440; + n_minutes -= n_days * 1440; + let n_hours = n_minutes / 60; + n_minutes -= n_hours * 60; + + let units: Vec = [ + (n_years, "year"), + (n_months, "month"), + (n_days, "day"), + (n_hours, "hour"), + (n_minutes, "minute"), + ] + .iter() + .filter_map(|(n, u)| { + if *n != 0 { + Some(format!("{} {}{}", n, u, if *n > 1 { "s" } else { "" })) + } else { + None + } + }) + .collect(); + + format!("Expires in {}", units.join(",")) +} diff --git a/gui/src/app/view/mod.rs b/gui/src/app/view/mod.rs index 4210afb7..ad9c11fd 100644 --- a/gui/src/app/view/mod.rs +++ b/gui/src/app/view/mod.rs @@ -21,7 +21,7 @@ use iced::{ use liana_ui::{ component::{button, text::*}, - icon::{coin_icon, cross_icon, home_icon, receive_icon, send_icon, settings_icon}, + icon::{cross_icon, home_icon, receive_icon, send_icon, settings_icon}, image::*, theme, util::Collection, @@ -75,7 +75,7 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { Row::new() .push( Row::new() - .push(coin_icon()) + .push(coins_icon().width(Length::Units(20))) .push(text("Coins")) .spacing(10) .width(iced::Length::Fill) @@ -114,7 +114,7 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { Row::new() .push( Row::new() - .push(coin_icon()) + .push(coins_icon().width(Length::Units(20))) .push(text("Coins")) .spacing(10) .width(iced::Length::Fill) diff --git a/gui/src/app/view/psbts.rs b/gui/src/app/view/psbts.rs index b7c6ea6c..6c187fb1 100644 --- a/gui/src/app/view/psbts.rs +++ b/gui/src/app/view/psbts.rs @@ -86,7 +86,7 @@ pub fn psbts_view<'a>(spend_txs: &[SpendTx]) -> Element<'a, Message> { ), ) .align_items(Alignment::Center) - .spacing(20) + .spacing(25) .into() } @@ -99,8 +99,8 @@ fn spend_tx_list_view<'a>(i: usize, tx: &SpendTx) -> Element<'a, Message> { .push(badge::spend()) .push(if !tx.sigs.recovery_paths().is_empty() { Row::new().push( - Container::new(text(" Recovery ").small()) - .padding(3) + Container::new(p2_regular(" Recovery ")) + .padding(10) .style(theme::Container::Pill(theme::Pill::Simple)), ) } else { diff --git a/gui/src/app/view/spend/step.rs b/gui/src/app/view/spend/step.rs index cd4288ac..df606985 100644 --- a/gui/src/app/view/spend/step.rs +++ b/gui/src/app/view/spend/step.rs @@ -18,7 +18,7 @@ use crate::{ app::{ cache::Cache, error::Error, - view::{message::*, modal}, + view::{coins, message::*, modal}, }, daemon::model::{remaining_sequence, Coin}, }; @@ -207,37 +207,11 @@ fn coin_list_view<'a>( icon::square_icon() }) .push(badge::coin()) - .push_maybe(if coin.spend_info.is_some() { - Some(badge::spent()) + .push(if coin.spend_info.is_some() { + badge::spent() } else { let seq = remaining_sequence(coin, blockheight, timelock); - if seq == 0 { - Some(Container::new( - Row::new() - .spacing(5) - .push(text(" 0").small().style(color::RED)) - .push(icon::hourglass_done_icon().small().style(color::RED)) - .align_items(Alignment::Center), - )) - } else if seq < timelock as u32 * 10 / 100 { - Some(Container::new( - Row::new() - .spacing(5) - .push( - text(format!(" {}", seq)).small().style(color::ORANGE), - ) - .push(icon::hourglass_icon().small().style(color::ORANGE)) - .align_items(Alignment::Center), - )) - } else { - Some(Container::new( - Row::new() - .spacing(5) - .push(text(format!(" {}", seq)).small()) - .push(icon::hourglass_icon().small()) - .align_items(Alignment::Center), - )) - } + coins::coin_sequence_label(seq, timelock as u32) }) .push_maybe(if coin.block_height.is_none() { Some(badge::unconfirmed()) diff --git a/gui/src/app/view/transactions.rs b/gui/src/app/view/transactions.rs index 102f64a5..454dfb43 100644 --- a/gui/src/app/view/transactions.rs +++ b/gui/src/app/view/transactions.rs @@ -25,14 +25,18 @@ pub fn transactions_view<'a>( .push( Column::new() .spacing(10) - .push( - pending_txs - .iter() - .enumerate() - .fold(Column::new().spacing(10), |col, (i, tx)| { - col.push(tx_list_view(i, tx)) - }), - ) + .push_maybe(if !pending_txs.is_empty() { + Some( + pending_txs + .iter() + .enumerate() + .fold(Column::new().spacing(10), |col, (i, tx)| { + col.push(tx_list_view(i, tx)) + }), + ) + } else { + None + }) .push( txs.iter() .enumerate() @@ -63,7 +67,7 @@ pub fn transactions_view<'a>( ), ) .align_items(Alignment::Center) - .spacing(20) + .spacing(30) .into() } diff --git a/gui/ui/src/component/badge.rs b/gui/ui/src/component/badge.rs index afa653f9..41e09a53 100644 --- a/gui/ui/src/component/badge.rs +++ b/gui/ui/src/component/badge.rs @@ -1,6 +1,6 @@ use iced::{widget::tooltip, Length}; -use crate::{component::text, icon, theme, widget::*}; +use crate::{component::text, icon, image, theme, widget::*}; pub struct Badge { icon: crate::widget::Text<'static>, @@ -53,19 +53,23 @@ pub fn spend() -> Container<'static, T> { } pub fn coin() -> Container<'static, T> { - Container::new(icon::coin_icon().width(Length::Units(20))) - .width(Length::Units(40)) - .height(Length::Units(40)) - .style(theme::Container::Badge(theme::Badge::Standard)) - .center_x() - .center_y() + Container::new( + image::liana_grey_logo() + .height(Length::Units(25)) + .width(Length::Units(25)), + ) + .width(Length::Units(40)) + .height(Length::Units(40)) + .style(theme::Container::Badge(theme::Badge::Standard)) + .center_x() + .center_y() } pub fn unconfirmed<'a, T: 'a>() -> Container<'a, T> { Container::new( tooltip::Tooltip::new( Container::new(text::p2_regular(" Unconfirmed ")) - .padding(3) + .padding(10) .style(theme::Container::Pill(theme::Pill::Simple)), "Do not treat this as a payment until it is confirmed", tooltip::Position::Top, @@ -78,7 +82,7 @@ pub fn deprecated<'a, T: 'a>() -> Container<'a, T> { Container::new( tooltip::Tooltip::new( Container::new(text::p2_regular(" Deprecated ")) - .padding(3) + .padding(10) .style(theme::Container::Pill(theme::Pill::Simple)), "This spend cannot be included anymore in the blockchain", tooltip::Position::Top, @@ -91,7 +95,7 @@ pub fn spent<'a, T: 'a>() -> Container<'a, T> { Container::new( tooltip::Tooltip::new( Container::new(text::p2_regular(" Spent ")) - .padding(3) + .padding(10) .style(theme::Container::Pill(theme::Pill::Simple)), "The spend transaction was included in the blockchain", tooltip::Position::Top, diff --git a/gui/ui/src/image.rs b/gui/ui/src/image.rs index dde177a3..0403e7e8 100644 --- a/gui/ui/src/image.rs +++ b/gui/ui/src/image.rs @@ -18,3 +18,21 @@ pub fn history_icon() -> Svg { let h = Handle::from_memory(HISTORY_ICON.to_vec()); Svg::new(h) } + +const COINS_ICON: &[u8] = include_bytes!("../static/icons/coins-icon.svg"); +pub fn coins_icon() -> Svg { + let h = Handle::from_memory(COINS_ICON.to_vec()); + Svg::new(h) +} + +const CLOCK_ICON: &[u8] = include_bytes!("../static/icons/clock-icon.svg"); +pub fn clock_icon() -> Svg { + let h = Handle::from_memory(CLOCK_ICON.to_vec()); + Svg::new(h) +} + +const CLOCK_RED_ICON: &[u8] = include_bytes!("../static/icons/clock-red-icon.svg"); +pub fn clock_red_icon() -> Svg { + let h = Handle::from_memory(CLOCK_RED_ICON.to_vec()); + Svg::new(h) +} diff --git a/gui/ui/src/theme.rs b/gui/ui/src/theme.rs index 6dce8354..3a853720 100644 --- a/gui/ui/src/theme.rs +++ b/gui/ui/src/theme.rs @@ -288,6 +288,7 @@ pub enum Pill { Simple, Primary, Success, + Warning, } impl Pill { @@ -312,6 +313,13 @@ impl Pill { border_color: color::GREY_3, text_color: color::GREY_3.into(), }, + Self::Warning => container::Appearance { + background: iced::Color::TRANSPARENT.into(), + border_radius: 25.0, + border_width: 1.0, + border_color: color::RED, + text_color: color::RED.into(), + }, } } } diff --git a/gui/ui/static/icons/clock-icon.svg b/gui/ui/static/icons/clock-icon.svg new file mode 100644 index 00000000..a8a6e077 --- /dev/null +++ b/gui/ui/static/icons/clock-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/gui/ui/static/icons/clock-red-icon.svg b/gui/ui/static/icons/clock-red-icon.svg new file mode 100644 index 00000000..ccc387cc --- /dev/null +++ b/gui/ui/static/icons/clock-red-icon.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/gui/ui/static/icons/coins-icon.svg b/gui/ui/static/icons/coins-icon.svg new file mode 100644 index 00000000..5abe3a3b --- /dev/null +++ b/gui/ui/static/icons/coins-icon.svg @@ -0,0 +1,3 @@ + + + From 36d4968ebbdd78fe6c7d9fbd904612d92334f2db Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 21 Apr 2023 11:24:11 +0200 Subject: [PATCH 2/5] Add unconfirmed balance to home --- gui/src/app/state/mod.rs | 55 ++++++++++++++++++++-------------- gui/src/app/view/coins.rs | 54 +++++++++++++++------------------ gui/src/app/view/home.rs | 17 ++++++++++- gui/src/app/view/spend/step.rs | 7 ++--- gui/ui/src/component/amount.rs | 53 ++++++++++++++++++++++++++++---- 5 files changed, 121 insertions(+), 65 deletions(-) diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs index 0bb43a10..49d9e1dc 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -47,6 +47,7 @@ pub trait State { pub struct Home { wallet: Arc, balance: Amount, + unconfirmed_balance: Amount, recovery_warning: Option<(Amount, usize)>, recovery_alert: Option<(Amount, usize)>, pending_events: Vec, @@ -57,21 +58,22 @@ pub struct Home { impl Home { pub fn new(wallet: Arc, coins: &[Coin]) -> Self { + let (balance, unconfirmed_balance) = coins.iter().fold( + (Amount::from_sat(0), Amount::from_sat(0)), + |(balance, unconfirmed_balance), coin| { + if coin.spend_info.is_some() { + (balance, unconfirmed_balance) + } else if coin.block_height.is_some() { + (balance + coin.amount, unconfirmed_balance) + } else { + (balance, unconfirmed_balance + coin.amount) + } + }, + ); Self { wallet, - balance: Amount::from_sat( - coins - .iter() - .map(|coin| { - // If the coin is not spent and is its transaction is confirmed - if coin.spend_info.is_none() && coin.block_height.is_some() { - coin.amount.to_sat() - } else { - 0 - } - }) - .sum(), - ), + balance, + unconfirmed_balance, recovery_alert: None, recovery_warning: None, selected_event: None, @@ -103,6 +105,7 @@ impl State for Home { None, view::home::home_view( &self.balance, + &self.unconfirmed_balance, self.recovery_warning.as_ref(), self.recovery_alert.as_ref(), &self.pending_events, @@ -123,19 +126,25 @@ impl State for Home { Ok(coins) => { self.warning = None; self.balance = Amount::from_sat(0); + self.unconfirmed_balance = Amount::from_sat(0); let mut recovery_warning = (Amount::from_sat(0), 0); let mut recovery_alert = (Amount::from_sat(0), 0); for coin in coins { - if coin.spend_info.is_none() && coin.block_height.is_some() { - self.balance += coin.amount; - let timelock = self.wallet.main_descriptor.first_timelock_value(); - let seq = remaining_sequence(&coin, cache.blockheight as u32, timelock); - if seq == 0 { - recovery_alert.0 += coin.amount; - recovery_alert.1 += 1; - } else if seq < timelock as u32 * 10 / 100 { - recovery_warning.0 += coin.amount; - recovery_warning.1 += 1; + if coin.spend_info.is_none() { + if coin.block_height.is_some() { + self.balance += coin.amount; + let timelock = self.wallet.main_descriptor.first_timelock_value(); + let seq = + remaining_sequence(&coin, cache.blockheight as u32, timelock); + if seq == 0 { + recovery_alert.0 += coin.amount; + recovery_alert.1 += 1; + } else if seq < timelock as u32 * 10 / 100 { + recovery_warning.0 += coin.amount; + recovery_warning.1 += 1; + } + } else { + self.unconfirmed_balance += coin.amount; } } } diff --git a/gui/src/app/view/coins.rs b/gui/src/app/view/coins.rs index 790bea90..8ac14b16 100644 --- a/gui/src/app/view/coins.rs +++ b/gui/src/app/view/coins.rs @@ -62,15 +62,12 @@ fn coin_list_view( .push(badge::coin()) .push(if coin.spend_info.is_some() { badge::spent() + } else if coin.block_height.is_none() { + badge::unconfirmed() } else { let seq = remaining_sequence(coin, blockheight, timelock); coin_sequence_label(seq, timelock as u32) }) - .push_maybe(if coin.block_height.is_none() { - Some(badge::unconfirmed()) - } else { - None - }) .spacing(10) .align_items(Alignment::Center) .width(Length::Fill), @@ -208,31 +205,28 @@ pub fn coin_sequence_label<'a, T: 'a>(seq: u32, timelock: u32) -> Container<'a, /// returns y,m,d,h,m pub fn expire_message(sequence: u32) -> String { let mut n_minutes = sequence * 10; - let n_years = n_minutes / 525960; - n_minutes -= n_years * 525960; - let n_months = n_minutes / 43830; - n_minutes -= n_months * 43830; - let n_days = n_minutes / 1440; - n_minutes -= n_days * 1440; - let n_hours = n_minutes / 60; - n_minutes -= n_hours * 60; + if n_minutes <= 1440 { + "Expires today".to_string() + } else if n_minutes <= 2 * 1440 { + "Expires in ≈ 2 days".to_string() + } else { + let n_years = n_minutes / 525960; + n_minutes -= n_years * 525960; + let n_months = n_minutes / 43830; + n_minutes -= n_months * 43830; + let n_days = n_minutes / 1440; - let units: Vec = [ - (n_years, "year"), - (n_months, "month"), - (n_days, "day"), - (n_hours, "hour"), - (n_minutes, "minute"), - ] - .iter() - .filter_map(|(n, u)| { - if *n != 0 { - Some(format!("{} {}{}", n, u, if *n > 1 { "s" } else { "" })) - } else { - None - } - }) - .collect(); + let units: Vec = [(n_years, "year"), (n_months, "month"), (n_days, "day")] + .iter() + .filter_map(|(n, u)| { + if *n != 0 { + Some(format!("{} {}{}", n, u, if *n > 1 { "s" } else { "" })) + } else { + None + } + }) + .collect(); - format!("Expires in {}", units.join(",")) + format!("Expires in {}", units.join(",")) + } } diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index 90fbf4e7..a85b704e 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -17,6 +17,7 @@ pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20; pub fn home_view<'a>( balance: &'a bitcoin::Amount, + unconfirmed_balance: &'a bitcoin::Amount, recovery_warning: Option<&(bitcoin::Amount, usize)>, recovery_alert: Option<&(bitcoin::Amount, usize)>, pending_events: &[HistoryTransaction], @@ -24,7 +25,21 @@ pub fn home_view<'a>( ) -> Element<'a, Message> { Column::new() .push(h3("Balance")) - .push(amount_with_size(balance, H1_SIZE)) + .push( + Column::new() + .push(amount_with_size(balance, H1_SIZE)) + .push_maybe(if unconfirmed_balance.to_sat() != 0 { + Some( + Row::new() + .spacing(10) + .push(text("+").size(H3_SIZE).style(color::GREY_3)) + .push(unconfirmed_amount_with_size(unconfirmed_balance, H3_SIZE)) + .push(text("unconfirmed").size(H3_SIZE).style(color::GREY_3)), + ) + } else { + None + }), + ) .push_maybe(recovery_warning.map(|(a, c)| { Row::new() .spacing(15) diff --git a/gui/src/app/view/spend/step.rs b/gui/src/app/view/spend/step.rs index df606985..8d87565c 100644 --- a/gui/src/app/view/spend/step.rs +++ b/gui/src/app/view/spend/step.rs @@ -209,15 +209,12 @@ fn coin_list_view<'a>( .push(badge::coin()) .push(if coin.spend_info.is_some() { badge::spent() + } else if coin.block_height.is_none() { + badge::unconfirmed() } else { let seq = remaining_sequence(coin, blockheight, timelock); coins::coin_sequence_label(seq, timelock as u32) }) - .push_maybe(if coin.block_height.is_none() { - Some(badge::unconfirmed()) - } else { - None - }) .spacing(10) .align_items(Alignment::Center) .width(Length::Fill), diff --git a/gui/ui/src/component/amount.rs b/gui/ui/src/component/amount.rs index 03971ebe..1c780e8d 100644 --- a/gui/ui/src/component/amount.rs +++ b/gui/ui/src/component/amount.rs @@ -12,9 +12,9 @@ pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> { assert!(sats.len() >= 9); let row = Row::new() .spacing(spacing) - .push(split_digits(sats[0..sats.len() - 6].to_string(), size).into()) + .push(split_digits(sats[0..sats.len() - 6].to_string(), size, true).into()) .push(if a.to_sat() < 1_000_000 { - split_digits(sats[sats.len() - 6..sats.len() - 3].to_string(), size).into() + split_digits(sats[sats.len() - 6..sats.len() - 3].to_string(), size, true).into() } else { Row::new() .push( @@ -25,7 +25,7 @@ pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> { .into() }) .push(if a.to_sat() < 1000 { - split_digits(sats[sats.len() - 3..sats.len()].to_string(), size).into() + split_digits(sats[sats.len() - 3..sats.len()].to_string(), size, true).into() } else { Row::new() .push( @@ -44,7 +44,42 @@ pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> { .align_items(iced::Alignment::Center) } -fn split_digits<'a, T: 'a>(mut s: String, size: u16) -> impl Into> { +pub fn unconfirmed_amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> { + let spacing = if size > P1_SIZE { 10 } else { 5 }; + let sats = format!("{:.8}", a.to_btc()); + assert!(sats.len() >= 9); + let row = Row::new() + .spacing(spacing) + .push(split_digits(sats[0..sats.len() - 6].to_string(), size, false).into()) + .push(if a.to_sat() < 1_000_000 { + split_digits( + sats[sats.len() - 6..sats.len() - 3].to_string(), + size, + false, + ) + .into() + } else { + Row::new() + .push(text(sats[sats.len() - 6..sats.len() - 3].to_string()).size(size)) + .into() + }) + .push(if a.to_sat() < 1000 { + split_digits(sats[sats.len() - 3..sats.len()].to_string(), size, false).into() + } else { + Row::new() + .push(text(sats[sats.len() - 3..sats.len()].to_string()).size(size)) + .into() + }); + + Row::with_children(vec![ + row.into(), + text("BTC").size(size).style(color::GREY_3).into(), + ]) + .spacing(spacing) + .align_items(iced::Alignment::Center) +} + +fn split_digits<'a, T: 'a>(mut s: String, size: u16, bold: bool) -> impl Into> { let prefixes = vec!["0.00", "0.0", "0.", "000", "00", "0"]; for prefix in prefixes { if s.starts_with(prefix) { @@ -53,10 +88,16 @@ fn split_digits<'a, T: 'a>(mut s: String, size: u16) -> impl Into .push(text(s).size(size).style(color::GREY_3)) .push_maybe(if right.is_empty() { None - } else { + } else if bold { Some(text(right).bold().size(size)) + } else { + Some(text(right).size(size)) }); } } - Row::new().push(text(s).bold().size(size)) + if bold { + Row::new().push(text(s).bold().size(size)) + } else { + Row::new().push(text(s).size(size)) + } } From fd67c5dea85bb2cd5a1f47199cc8fb46ff1c5286 Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 21 Apr 2023 12:28:43 +0200 Subject: [PATCH 3/5] gui: change date time format --- gui/src/app/view/transactions.rs | 8 ++++++-- gui/ui/src/component/event.rs | 22 ++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/gui/src/app/view/transactions.rs b/gui/src/app/view/transactions.rs index 454dfb43..57a1a275 100644 --- a/gui/src/app/view/transactions.rs +++ b/gui/src/app/view/transactions.rs @@ -86,7 +86,9 @@ fn tx_list_view<'a>(i: usize, tx: &HistoryTransaction) -> Element<'a, Message> { Container::new( text(format!( "{}", - NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(), + NaiveDateTime::from_timestamp_opt(t as i64, 0) + .unwrap() + .format("%b. %d, %Y - %T"), )) .small(), ) @@ -147,7 +149,9 @@ pub fn tx_view<'a>(cache: &Cache, tx: &'a HistoryTransaction) -> Element<'a, Mes .push(card::simple( Column::new() .push_maybe(tx.time.map(|t| { - let date = NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(); + let date = NaiveDateTime::from_timestamp_opt(t as i64, 0) + .unwrap() + .format("%b. %d, %Y - %T"); Row::new() .width(Length::Fill) .push(Container::new(text("Date:").bold()).width(Length::Fill)) diff --git a/gui/ui/src/component/event.rs b/gui/ui/src/component/event.rs index b0df85fe..b97e4d78 100644 --- a/gui/ui/src/component/event.rs +++ b/gui/ui/src/component/event.rs @@ -39,10 +39,13 @@ pub fn confirmed_outgoing_event<'a, T: Clone + 'a>( Container::new( button( row!( - row!(badge::spend(), text::p2_regular(date.to_string())) - .spacing(10) - .align_items(Alignment::Center) - .width(Length::Fill), + row!( + badge::spend(), + text::p2_regular(date.format("%b. %d, %Y - %T").to_string()) + ) + .spacing(10) + .align_items(Alignment::Center) + .width(Length::Fill), row!(text::p1_regular("-"), amount::amount(amount)) .spacing(5) .align_items(Alignment::Center), @@ -87,10 +90,13 @@ pub fn confirmed_incoming_event<'a, T: Clone + 'a>( Container::new( button( row!( - row!(badge::receive(), text::p2_regular(date.to_string())) - .spacing(10) - .align_items(Alignment::Center) - .width(Length::Fill), + row!( + badge::receive(), + text::p2_regular(date.format("%b. %d, %Y - %T").to_string()) + ) + .spacing(10) + .align_items(Alignment::Center) + .width(Length::Fill), row!(text::p1_regular("+"), amount::amount(amount)) .spacing(5) .align_items(Alignment::Center), From 56e1aa04aa2889e5941e624df3d989de7debf7a5 Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 21 Apr 2023 17:07:35 +0200 Subject: [PATCH 4/5] gui: change home warning close #439 --- gui/src/app/state/mod.rs | 38 ++++++++++---------- gui/src/app/view/coins.rs | 46 ++++++++++++------------ gui/src/app/view/home.rs | 75 ++++++++++++++++++++++----------------- 3 files changed, 84 insertions(+), 75 deletions(-) diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs index 49d9e1dc..5e1867f1 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -48,8 +48,8 @@ pub struct Home { wallet: Arc, balance: Amount, unconfirmed_balance: Amount, - recovery_warning: Option<(Amount, usize)>, - recovery_alert: Option<(Amount, usize)>, + remaining_sequence: Option, + number_of_expiring_coins: usize, pending_events: Vec, events: Vec, selected_event: Option, @@ -74,8 +74,8 @@ impl Home { wallet, balance, unconfirmed_balance, - recovery_alert: None, - recovery_warning: None, + remaining_sequence: None, + number_of_expiring_coins: 0, selected_event: None, events: Vec::new(), pending_events: Vec::new(), @@ -106,8 +106,8 @@ impl State for Home { view::home::home_view( &self.balance, &self.unconfirmed_balance, - self.recovery_warning.as_ref(), - self.recovery_alert.as_ref(), + &self.remaining_sequence, + self.number_of_expiring_coins, &self.pending_events, &self.events, ), @@ -127,8 +127,8 @@ impl State for Home { self.warning = None; self.balance = Amount::from_sat(0); self.unconfirmed_balance = Amount::from_sat(0); - let mut recovery_warning = (Amount::from_sat(0), 0); - let mut recovery_alert = (Amount::from_sat(0), 0); + self.remaining_sequence = None; + self.number_of_expiring_coins = 0; for coin in coins { if coin.spend_info.is_none() { if coin.block_height.is_some() { @@ -136,24 +136,22 @@ impl State for Home { let timelock = self.wallet.main_descriptor.first_timelock_value(); let seq = remaining_sequence(&coin, cache.blockheight as u32, timelock); - if seq == 0 { - recovery_alert.0 += coin.amount; - recovery_alert.1 += 1; - } else if seq < timelock as u32 * 10 / 100 { - recovery_warning.0 += coin.amount; - recovery_warning.1 += 1; + // number of block in a day + if seq <= 144 { + self.number_of_expiring_coins += 1; + } + if let Some(last) = &mut self.remaining_sequence { + if seq < *last { + *last = seq + } + } else { + self.remaining_sequence = Some(seq); } } else { self.unconfirmed_balance += coin.amount; } } } - if recovery_warning.1 > 0 { - self.recovery_warning = Some(recovery_warning); - } - if recovery_alert.1 > 0 { - self.recovery_alert = Some(recovery_alert); - } } }, Message::HistoryTransactions(res) => match res { diff --git a/gui/src/app/view/coins.rs b/gui/src/app/view/coins.rs index 8ac14b16..6dfe6968 100644 --- a/gui/src/app/view/coins.rs +++ b/gui/src/app/view/coins.rs @@ -202,31 +202,33 @@ pub fn coin_sequence_label<'a, T: 'a>(seq: u32, timelock: u32) -> Container<'a, } } -/// returns y,m,d,h,m pub fn expire_message(sequence: u32) -> String { - let mut n_minutes = sequence * 10; - if n_minutes <= 1440 { + if sequence <= 144 { "Expires today".to_string() - } else if n_minutes <= 2 * 1440 { + } else if sequence <= 2 * 144 { "Expires in ≈ 2 days".to_string() } else { - let n_years = n_minutes / 525960; - n_minutes -= n_years * 525960; - let n_months = n_minutes / 43830; - n_minutes -= n_months * 43830; - let n_days = n_minutes / 1440; - - let units: Vec = [(n_years, "year"), (n_months, "month"), (n_days, "day")] - .iter() - .filter_map(|(n, u)| { - if *n != 0 { - Some(format!("{} {}{}", n, u, if *n > 1 { "s" } else { "" })) - } else { - None - } - }) - .collect(); - - format!("Expires in {}", units.join(",")) + format!("Expires in {}", expire_message_units(sequence).join(",")) } } + +/// returns y,m,d +pub fn expire_message_units(sequence: u32) -> Vec { + let mut n_minutes = sequence * 10; + let n_years = n_minutes / 525960; + n_minutes -= n_years * 525960; + let n_months = n_minutes / 43830; + n_minutes -= n_months * 43830; + let n_days = n_minutes / 1440; + + [(n_years, "year"), (n_months, "month"), (n_days, "day")] + .iter() + .filter_map(|(n, u)| { + if *n != 0 { + Some(format!("{} {}{}", n, u, if *n > 1 { "s" } else { "" })) + } else { + None + } + }) + .collect() +} diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index a85b704e..15e8590a 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -11,15 +11,18 @@ use liana_ui::{ widget::*, }; -use crate::{app::view::message::Message, daemon::model::HistoryTransaction}; +use crate::{ + app::view::{coins, message::Message}, + daemon::model::HistoryTransaction, +}; pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20; pub fn home_view<'a>( balance: &'a bitcoin::Amount, unconfirmed_balance: &'a bitcoin::Amount, - recovery_warning: Option<&(bitcoin::Amount, usize)>, - recovery_alert: Option<&(bitcoin::Amount, usize)>, + remaining_sequence: &Option, + number_of_expiring_coins: usize, pending_events: &[HistoryTransaction], events: &Vec, ) -> Element<'a, Message> { @@ -40,39 +43,45 @@ pub fn home_view<'a>( None }), ) - .push_maybe(recovery_warning.map(|(a, c)| { - Row::new() - .spacing(15) - .align_items(Alignment::Center) - .push(icon::hourglass_icon().size(30).style(color::ORANGE)) - .push( + .push_maybe(if number_of_expiring_coins == 0 { + remaining_sequence.map(|sequence| { + Container::new( Row::new() - .spacing(5) - .push(text(format!( - "Recovery path will be soon available for {} coins", - c - ))) - .push(text("(")) - .push(amount(a)) - .push(text(")")), + .spacing(15) + .align_items(Alignment::Center) + .push( + h4_regular(format!( + "Your next coin to expire will in ≈ {}", + coins::expire_message_units(sequence).join(",") + )) + .width(Length::Fill), + ) + .push( + icon::tooltip_icon() + .size(20) + .style(color::GREY_3) + .width(Length::Units(20)), + ) + .width(Length::Fill), ) - .padding(10) - })) - .push_maybe(recovery_alert.map(|(a, c)| { - Row::new() - .spacing(15) - .align_items(Alignment::Center) - .push(icon::hourglass_done_icon().style(color::RED)) - .push( - Row::new() - .spacing(5) - .push(text(format!("Recovery path is available for {} coins", c))) - .push(text("(")) - .push(amount(a)) - .push(text(")")), + .padding(25) + .style(theme::Card::Border) + }) + } else { + Some( + Container::new( + Row::new().spacing(15).align_items(Alignment::Center).push( + h4_regular(format!( + "You have {} coins that are already or about to be expired", + number_of_expiring_coins + )) + .width(Length::Fill), + ), ) - .padding(10) - })) + .padding(25) + .style(theme::Card::Invalid), + ) + }) .push( Column::new() .spacing(10) From d4c2f702abc0d72045734b824c6f69e3fcb92381 Mon Sep 17 00:00:00 2001 From: edouard Date: Fri, 21 Apr 2023 17:22:40 +0200 Subject: [PATCH 5/5] fix gui sidebar: remove counter --- gui/src/app/view/mod.rs | 101 ++++------------------------------------ 1 file changed, 10 insertions(+), 91 deletions(-) diff --git a/gui/src/app/view/mod.rs b/gui/src/app/view/mod.rs index ad9c11fd..b2e957f9 100644 --- a/gui/src/app/view/mod.rs +++ b/gui/src/app/view/mod.rs @@ -73,30 +73,8 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { Button::new( Container::new( Row::new() - .push( - Row::new() - .push(coins_icon().width(Length::Units(20))) - .push(text("Coins")) - .spacing(10) - .width(iced::Length::Fill) - .align_items(iced::Alignment::Center), - ) - .push( - Container::new( - text(format!( - " {} ", - cache - .coins - .iter() - // TODO: Remove when cache contains only current coins. - .filter(|coin| coin.spend_info.is_none()) - .count() - )) - .small() - .bold(), - ) - .style(theme::Container::Pill(theme::Pill::Primary)), - ) + .push(coins_icon().width(Length::Units(20))) + .push(text("Coins")) .spacing(10) .width(iced::Length::Fill) .align_items(iced::Alignment::Center), @@ -112,30 +90,8 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { Button::new( Container::new( Row::new() - .push( - Row::new() - .push(coins_icon().width(Length::Units(20))) - .push(text("Coins")) - .spacing(10) - .width(iced::Length::Fill) - .align_items(iced::Alignment::Center), - ) - .push( - Container::new( - text(format!( - " {} ", - cache - .coins - .iter() - // TODO: Remove when cache contains only current coins. - .filter(|coin| coin.spend_info.is_none()) - .count() - )) - .small() - .bold(), - ) - .style(theme::Pill::Primary), - ) + .push(coins_icon().width(Length::Units(20))) + .push(text("Coins")) .spacing(10) .width(iced::Length::Fill) .align_items(iced::Alignment::Center), @@ -153,26 +109,8 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { Button::new( Container::new( Row::new() - .push( - Row::new() - .push(history_icon().width(Length::Units(20))) - .push(text("PSBTs")) - .spacing(10) - .width(iced::Length::Fill) - .align_items(iced::Alignment::Center), - ) - .push_maybe(if cache.spend_txs.is_empty() { - None - } else { - Some( - Container::new( - text(format!(" {} ", cache.spend_txs.len())) - .small() - .bold(), - ) - .style(theme::Pill::Primary), - ) - }) + .push(history_icon().width(Length::Units(20))) + .push(text("PSBTs")) .spacing(10) .width(iced::Length::Fill) .align_items(iced::Alignment::Center), @@ -188,26 +126,8 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { Button::new( Container::new( Row::new() - .push( - Row::new() - .push(history_icon().width(Length::Units(20))) - .push(text("PSBTs")) - .spacing(10) - .width(iced::Length::Fill) - .align_items(iced::Alignment::Center), - ) - .push_maybe(if cache.spend_txs.is_empty() { - None - } else { - Some( - Container::new( - text(format!(" {} ", cache.spend_txs.len())) - .small() - .bold(), - ) - .style(theme::Pill::Primary), - ) - }) + .push(history_icon().width(Length::Units(20))) + .push(text("PSBTs")) .spacing(10) .width(iced::Length::Fill) .align_items(iced::Alignment::Center), @@ -284,10 +204,10 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { .push( Container::new( liana_grey_logo() - .height(Length::Units(150)) + .height(Length::Units(120)) .width(Length::Units(60)), ) - .padding(15), + .padding(10), ) .push(home_button) .push(spend_button) @@ -295,7 +215,6 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { .push(coins_button) .push(psbt_button) .push(transactions_button) - .spacing(15) .height(Length::Fill), ) .push(