From 196b8cc3e91fcbbe9f17a7e0945010dc38dae4fb Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 19 Apr 2023 11:50:24 +0200 Subject: [PATCH 1/3] gui: new transactions panel close #437 --- gui/src/app/menu.rs | 1 + gui/src/app/mod.rs | 6 +- gui/src/app/state/mod.rs | 4 +- gui/src/app/state/transactions.rs | 171 ++++++++++++++++++++++++++ gui/src/app/view/home.rs | 78 +----------- gui/src/app/view/mod.rs | 30 +++++ gui/src/app/view/psbts.rs | 1 + gui/src/app/view/transactions.rs | 191 ++++++++++++++++++++++++++++++ 8 files changed, 404 insertions(+), 78 deletions(-) create mode 100644 gui/src/app/state/transactions.rs create mode 100644 gui/src/app/view/transactions.rs diff --git a/gui/src/app/menu.rs b/gui/src/app/menu.rs index 08995e63..8d0f1857 100644 --- a/gui/src/app/menu.rs +++ b/gui/src/app/menu.rs @@ -3,6 +3,7 @@ pub enum Menu { Home, Receive, PSBTs, + Transactions, Settings, Coins, CreateSpendTx, diff --git a/gui/src/app/mod.rs b/gui/src/app/mod.rs index 0c7085b0..d9553fa8 100644 --- a/gui/src/app/mod.rs +++ b/gui/src/app/mod.rs @@ -24,7 +24,10 @@ use liana_ui::widget::Element; pub use config::Config; pub use message::Message; -use state::{CoinsPanel, CreateSpendPanel, Home, PsbtsPanel, ReceivePanel, RecoveryPanel, State}; +use state::{ + CoinsPanel, CreateSpendPanel, Home, PsbtsPanel, ReceivePanel, RecoveryPanel, State, + TransactionsPanel, +}; use crate::{ app::{cache::Cache, error::Error, menu::Menu, wallet::Wallet}, @@ -81,6 +84,7 @@ impl App { ) .into(), menu::Menu::Receive => ReceivePanel::default().into(), + menu::Menu::Transactions => TransactionsPanel::new().into(), menu::Menu::PSBTs => PsbtsPanel::new(self.wallet.clone(), &self.cache.spend_txs).into(), menu::Menu::CreateSpendTx => CreateSpendPanel::new( self.wallet.clone(), diff --git a/gui/src/app/state/mod.rs b/gui/src/app/state/mod.rs index ebbabfae..0bb43a10 100644 --- a/gui/src/app/state/mod.rs +++ b/gui/src/app/state/mod.rs @@ -3,6 +3,7 @@ mod psbts; mod recovery; mod settings; mod spend; +mod transactions; use std::convert::TryInto; use std::sync::Arc; @@ -23,6 +24,7 @@ pub use psbts::PsbtsPanel; pub use recovery::RecoveryPanel; pub use settings::SettingsState; pub use spend::CreateSpendPanel; +pub use transactions::TransactionsPanel; pub trait State { fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message>; @@ -91,7 +93,7 @@ impl State for Home { return view::modal( false, self.warning.as_ref(), - view::home::event_view(cache, event), + view::transactions::tx_view(cache, event), None::>, ); } diff --git a/gui/src/app/state/transactions.rs b/gui/src/app/state/transactions.rs new file mode 100644 index 00000000..a61740c1 --- /dev/null +++ b/gui/src/app/state/transactions.rs @@ -0,0 +1,171 @@ +use std::convert::TryInto; +use std::sync::Arc; +use std::time::{SystemTime, UNIX_EPOCH}; + +use iced::Command; +use liana_ui::widget::*; + +use crate::app::{cache::Cache, error::Error, menu::Menu, message::Message, view, State}; + +use crate::daemon::{model::HistoryTransaction, Daemon}; + +#[derive(Default)] +pub struct TransactionsPanel { + pending_txs: Vec, + txs: Vec, + selected_tx: Option, + warning: Option, +} + +impl TransactionsPanel { + pub fn new() -> Self { + Self { + selected_tx: None, + txs: Vec::new(), + pending_txs: Vec::new(), + warning: None, + } + } +} + +impl State for TransactionsPanel { + fn view<'a>(&'a self, cache: &'a Cache) -> Element<'a, view::Message> { + if let Some(i) = self.selected_tx { + let tx = if i < self.pending_txs.len() { + &self.pending_txs[i] + } else { + &self.txs[i - self.pending_txs.len()] + }; + return view::modal( + false, + self.warning.as_ref(), + view::transactions::tx_view(cache, tx), + None::>, + ); + } + view::dashboard( + &Menu::Transactions, + cache, + None, + view::transactions::transactions_view(&self.pending_txs, &self.txs), + ) + } + + fn update( + &mut self, + daemon: Arc, + _cache: &Cache, + message: Message, + ) -> Command { + match message { + Message::HistoryTransactions(res) => match res { + Err(e) => self.warning = Some(e), + Ok(txs) => { + self.warning = None; + for tx in txs { + if !self.txs.iter().any(|other| other.tx == tx.tx) { + self.txs.push(tx); + } + } + } + }, + Message::PendingTransactions(res) => match res { + Err(e) => self.warning = Some(e), + Ok(txs) => { + self.warning = None; + for tx in txs { + if !self.pending_txs.iter().any(|other| other.tx == tx.tx) { + self.pending_txs.push(tx); + } + } + } + }, + Message::View(view::Message::Close) => { + self.selected_tx = None; + } + Message::View(view::Message::Select(i)) => { + self.selected_tx = Some(i); + } + Message::View(view::Message::Next) => { + if let Some(last) = self.txs.last() { + let daemon = daemon.clone(); + let last_tx_date = last.time.unwrap(); + return Command::perform( + async move { + let mut limit = view::home::HISTORY_EVENT_PAGE_SIZE; + let mut txs = daemon.list_history_txs(0_u32, last_tx_date, limit)?; + + // because gethistory cursor is inclusive and use blocktime + // multiple txs can occur in the same block. + // If there is more tx in the same block that the + // HISTORY_EVENT_PAGE_SIZE they can not be retrieved by changing + // the cursor value (blocktime) but by increasing the limit. + // + // 1. Check if the txs retrieved have all the same blocktime + let blocktime = if let Some(tx) = txs.first() { + tx.time + } else { + return Ok(txs); + }; + + // 2. Retrieve a larger batch of tx with the same cursor but + // a larger limit. + while !txs.iter().any(|evt| evt.time != blocktime) + && txs.len() as u64 == limit + { + // increments of the equivalent of one page more. + limit += view::home::HISTORY_EVENT_PAGE_SIZE; + txs = daemon.list_history_txs(0, last_tx_date, limit)?; + } + Ok(txs) + }, + Message::HistoryTransactions, + ); + } + } + _ => {} + }; + Command::none() + } + + fn load(&self, daemon: Arc) -> Command { + let daemon1 = daemon.clone(); + let daemon2 = daemon.clone(); + let daemon3 = daemon.clone(); + let now: u32 = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + .try_into() + .unwrap(); + Command::batch(vec![ + Command::perform( + async move { daemon3.list_pending_txs().map_err(|e| e.into()) }, + Message::PendingTransactions, + ), + Command::perform( + async move { + daemon1 + .list_history_txs(0, now, view::home::HISTORY_EVENT_PAGE_SIZE) + .map_err(|e| e.into()) + }, + Message::HistoryTransactions, + ), + Command::perform( + async move { + daemon2 + .list_coins() + .map(|res| res.coins) + .map_err(|e| e.into()) + }, + Message::Coins, + ), + ]) + } +} + +impl From for Box { + fn from(s: TransactionsPanel) -> Box { + Box::new(s) + } +} diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index c546bb99..438e5966 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -5,17 +5,14 @@ use iced::{alignment, Alignment, Length}; use liana::miniscript::bitcoin; use liana_ui::{ color, - component::{badge, card, text::*}, + component::{badge, text::*}, icon, theme, util::Collection, widget::*, }; use crate::{ - app::{ - cache::Cache, - view::{message::Message, util::*}, - }, + app::view::{message::Message, util::*}, daemon::model::HistoryTransaction, }; @@ -158,74 +155,3 @@ fn event_list_view<'a>(i: usize, event: &HistoryTransaction) -> Element<'a, Mess .style(theme::Container::Card(theme::Card::Simple)) .into() } - -pub fn event_view<'a>(cache: &Cache, event: &'a HistoryTransaction) -> Element<'a, Message> { - Column::new() - .push( - Row::new() - .push(if event.is_external() { - badge::receive() - } else { - badge::spend() - }) - .spacing(10) - .align_items(Alignment::Center), - ) - .push(if event.is_external() { - amount_with_size(&event.incoming_amount, 50) - } else { - amount_with_size(&event.outgoing_amount, 50) - }) - .push_maybe( - event - .fee_amount - .map(|fee| Row::new().push(text("Miner Fee: ")).push(amount(&fee))), - ) - .push(card::simple( - Column::new() - .push_maybe(event.time.map(|t| { - let date = NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(); - Row::new() - .width(Length::Fill) - .push(Container::new(text("Date:").bold()).width(Length::Fill)) - .push(Container::new(text(format!("{}", date))).width(Length::Shrink)) - })) - .push( - Row::new() - .width(Length::Fill) - .align_items(Alignment::Center) - .push(Container::new(text("Txid:").bold()).width(Length::Fill)) - .push( - Row::new() - .align_items(Alignment::Center) - .push(Container::new(text(format!("{}", event.tx.txid())).small())) - .push( - Button::new(icon::clipboard_icon()) - .on_press(Message::Clipboard(event.tx.txid().to_string())) - .style(theme::Button::TransparentBorder), - ) - .width(Length::Shrink), - ), - ) - .spacing(5), - )) - .push(super::spend::detail::inputs_and_outputs_view( - &event.coins, - &event.tx, - cache.network, - if event.is_external() { - None - } else { - Some(event.change_indexes.clone()) - }, - if event.is_external() { - Some(event.change_indexes.clone()) - } else { - None - }, - )) - .align_items(Alignment::Center) - .spacing(20) - .max_width(800) - .into() -} diff --git a/gui/src/app/view/mod.rs b/gui/src/app/view/mod.rs index 19d92876..f4c99cd4 100644 --- a/gui/src/app/view/mod.rs +++ b/gui/src/app/view/mod.rs @@ -10,6 +10,7 @@ pub mod receive; pub mod recovery; pub mod settings; pub mod spend; +pub mod transactions; pub use message::*; use warning::warn; @@ -41,6 +42,34 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { .width(iced::Length::Fill) }; + let transactions_button = if *menu == Menu::Transactions { + Button::new( + row!( + history_icon().width(Length::Units(20)), + text("Transactions") + ) + .spacing(10) + .padding(10) + .align_items(iced::Alignment::Center), + ) + .style(theme::Button::Menu(true)) + .on_press(Message::Reload) + .width(iced::Length::Fill) + } else { + Button::new( + row!( + history_icon().width(Length::Units(20)), + text("Transactions") + ) + .spacing(10) + .padding(10) + .align_items(iced::Alignment::Center), + ) + .style(theme::Button::Menu(false)) + .on_press(Message::Menu(Menu::Transactions)) + .width(iced::Length::Fill) + }; + let coins_button = if *menu == Menu::Coins { Button::new( Container::new( @@ -266,6 +295,7 @@ pub fn sidebar<'a>(menu: &Menu, cache: &'a Cache) -> Container<'a, Message> { .push(receive_button) .push(coins_button) .push(psbt_button) + .push(transactions_button) .spacing(15) .height(Length::Fill), ) diff --git a/gui/src/app/view/psbts.rs b/gui/src/app/view/psbts.rs index 53cc6cac..ea97d11b 100644 --- a/gui/src/app/view/psbts.rs +++ b/gui/src/app/view/psbts.rs @@ -63,6 +63,7 @@ pub fn psbts_view<'a>(spend_txs: &[SpendTx]) -> Element<'a, Message> { Column::new() .push( Row::new() + .align_items(Alignment::Center) .spacing(10) .push(Container::new(h3("PSBTs")).width(Length::Fill)) .push( diff --git a/gui/src/app/view/transactions.rs b/gui/src/app/view/transactions.rs new file mode 100644 index 00000000..adac858c --- /dev/null +++ b/gui/src/app/view/transactions.rs @@ -0,0 +1,191 @@ +use chrono::NaiveDateTime; + +use iced::{alignment, Alignment, Length}; + +use liana_ui::{ + component::{badge, card, text::*}, + icon, theme, + util::Collection, + widget::*, +}; + +use crate::{ + app::{ + cache::Cache, + view::{message::Message, util::*}, + }, + daemon::model::HistoryTransaction, +}; + +pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20; + +pub fn transactions_view<'a>( + pending_txs: &[HistoryTransaction], + txs: &Vec, +) -> Element<'a, Message> { + Column::new() + .push(Container::new(h3("Transactions")).width(Length::Fill)) + .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( + txs.iter() + .enumerate() + .fold(Column::new().spacing(10), |col, (i, tx)| { + col.push(tx_list_view(i + pending_txs.len(), tx)) + }), + ) + .push_maybe( + if txs.len() % HISTORY_EVENT_PAGE_SIZE as usize == 0 && !txs.is_empty() { + Some( + Container::new( + Button::new( + text("See more") + .width(Length::Fill) + .horizontal_alignment(alignment::Horizontal::Center), + ) + .width(Length::Fill) + .padding(15) + .style(theme::Button::TransparentBorder) + .on_press(Message::Next), + ) + .width(Length::Fill) + .style(theme::Container::Card(theme::Card::Simple)), + ) + } else { + None + }, + ), + ) + .align_items(Alignment::Center) + .spacing(20) + .into() +} + +fn tx_list_view<'a>(i: usize, tx: &HistoryTransaction) -> Element<'a, Message> { + Container::new( + Button::new( + Row::new() + .push( + Row::new() + .push(if tx.is_external() { + badge::receive() + } else { + badge::spend() + }) + .push(if let Some(t) = tx.time { + Container::new( + text(format!( + "{}", + NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(), + )) + .small(), + ) + } else { + badge::unconfirmed() + }) + .spacing(10) + .align_items(Alignment::Center) + .width(Length::Fill), + ) + .push(if tx.is_external() { + Row::new() + .spacing(5) + .push(text("+")) + .push(amount(&tx.incoming_amount)) + .align_items(Alignment::Center) + } else { + Row::new() + .spacing(5) + .push(text("-")) + .push(amount(&tx.outgoing_amount)) + .align_items(Alignment::Center) + }) + .align_items(Alignment::Center) + .spacing(20), + ) + .padding(10) + .on_press(Message::Select(i)) + .style(theme::Button::TransparentBorder), + ) + .style(theme::Container::Card(theme::Card::Simple)) + .into() +} + +pub fn tx_view<'a>(cache: &Cache, tx: &'a HistoryTransaction) -> Element<'a, Message> { + Column::new() + .push( + Row::new() + .push(if tx.is_external() { + badge::receive() + } else { + badge::spend() + }) + .spacing(10) + .align_items(Alignment::Center), + ) + .push(if tx.is_external() { + amount_with_size(&tx.incoming_amount, 50) + } else { + amount_with_size(&tx.outgoing_amount, 50) + }) + .push_maybe( + tx.fee_amount + .map(|fee| Row::new().push(text("Miner Fee: ")).push(amount(&fee))), + ) + .push(card::simple( + Column::new() + .push_maybe(tx.time.map(|t| { + let date = NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(); + Row::new() + .width(Length::Fill) + .push(Container::new(text("Date:").bold()).width(Length::Fill)) + .push(Container::new(text(format!("{}", date))).width(Length::Shrink)) + })) + .push( + Row::new() + .width(Length::Fill) + .align_items(Alignment::Center) + .push(Container::new(text("Txid:").bold()).width(Length::Fill)) + .push( + Row::new() + .align_items(Alignment::Center) + .push(Container::new(text(format!("{}", tx.tx.txid())).small())) + .push( + Button::new(icon::clipboard_icon()) + .on_press(Message::Clipboard(tx.tx.txid().to_string())) + .style(theme::Button::TransparentBorder), + ) + .width(Length::Shrink), + ), + ) + .spacing(5), + )) + .push(super::spend::detail::inputs_and_outputs_view( + &tx.coins, + &tx.tx, + cache.network, + if tx.is_external() { + None + } else { + Some(tx.change_indexes.clone()) + }, + if tx.is_external() { + Some(tx.change_indexes.clone()) + } else { + None + }, + )) + .align_items(Alignment::Center) + .spacing(20) + .max_width(800) + .into() +} From e7e8028d8a2d49dde176f2ed3a32ec078ea4fabe Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 19 Apr 2023 19:34:47 +0200 Subject: [PATCH 2/3] ui: add history events to design-system --- gui/Cargo.lock | 2 + gui/src/app/view/coins.rs | 7 +- gui/src/app/view/home.rs | 7 +- gui/src/app/view/mod.rs | 1 - gui/src/app/view/psbts.rs | 4 +- gui/src/app/view/recovery.rs | 7 +- gui/src/app/view/spend/detail.rs | 3 +- gui/src/app/view/spend/step.rs | 3 +- gui/src/app/view/transactions.rs | 7 +- gui/src/daemon/model.rs | 4 + gui/ui/Cargo.toml | 2 + gui/ui/examples/design-system/Cargo.toml | 1 + gui/ui/examples/design-system/src/main.rs | 11 +- gui/ui/examples/design-system/src/section.rs | 32 +++++- .../util.rs => ui/src/component/amount.rs} | 8 +- gui/ui/src/component/event.rs | 106 ++++++++++++++++++ gui/ui/src/component/mod.rs | 2 + 17 files changed, 173 insertions(+), 34 deletions(-) rename gui/{src/app/view/util.rs => ui/src/component/amount.rs} (87%) create mode 100644 gui/ui/src/component/event.rs diff --git a/gui/Cargo.lock b/gui/Cargo.lock index cfdc1be0..a59766a9 100644 --- a/gui/Cargo.lock +++ b/gui/Cargo.lock @@ -1887,6 +1887,8 @@ dependencies = [ name = "liana_ui" version = "0.1.0" dependencies = [ + "bitcoin", + "chrono", "iced", "iced_lazy", "iced_native", diff --git a/gui/src/app/view/coins.rs b/gui/src/app/view/coins.rs index 83bfe0c6..4e0ccf55 100644 --- a/gui/src/app/view/coins.rs +++ b/gui/src/app/view/coins.rs @@ -2,17 +2,14 @@ use iced::{Alignment, Length}; use liana_ui::{ color, - component::{badge, separation, text::*}, + component::{amount::*, badge, separation, text::*}, icon, theme, util::Collection, widget::*, }; use crate::{ - app::{ - cache::Cache, - view::{message::Message, util::*}, - }, + app::{cache::Cache, view::message::Message}, daemon::model::{remaining_sequence, Coin}, }; diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index 438e5966..c8bad9f3 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -5,16 +5,13 @@ use iced::{alignment, Alignment, Length}; use liana::miniscript::bitcoin; use liana_ui::{ color, - component::{badge, text::*}, + component::{amount::*, badge, text::*}, icon, theme, util::Collection, widget::*, }; -use crate::{ - app::view::{message::Message, util::*}, - daemon::model::HistoryTransaction, -}; +use crate::{app::view::message::Message, daemon::model::HistoryTransaction}; pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20; diff --git a/gui/src/app/view/mod.rs b/gui/src/app/view/mod.rs index f4c99cd4..4210afb7 100644 --- a/gui/src/app/view/mod.rs +++ b/gui/src/app/view/mod.rs @@ -1,5 +1,4 @@ mod message; -mod util; mod warning; pub mod coins; diff --git a/gui/src/app/view/psbts.rs b/gui/src/app/view/psbts.rs index ea97d11b..b7c6ea6c 100644 --- a/gui/src/app/view/psbts.rs +++ b/gui/src/app/view/psbts.rs @@ -2,14 +2,14 @@ use iced::{widget::Space, Alignment, Length}; use liana_ui::{ color, - component::{badge, button, card, form, text::*}, + component::{amount::*, badge, button, card, form, text::*}, icon, theme, util::Collection, widget::*, }; use crate::{ - app::{error::Error, menu::Menu, view::util::*}, + app::{error::Error, menu::Menu}, daemon::model::{SpendStatus, SpendTx}, }; diff --git a/gui/src/app/view/recovery.rs b/gui/src/app/view/recovery.rs index 3129a70b..07f24879 100644 --- a/gui/src/app/view/recovery.rs +++ b/gui/src/app/view/recovery.rs @@ -11,15 +11,12 @@ use liana::miniscript::bitcoin::{ }; use liana_ui::{ - component::{button, form, text::*}, + component::{amount::*, button, form, text::*}, icon, theme, widget::*, }; -use crate::app::view::{ - message::{CreateSpendMessage, Message}, - util::amount, -}; +use crate::app::view::message::{CreateSpendMessage, Message}; #[allow(clippy::too_many_arguments)] pub fn recovery<'a>( diff --git a/gui/src/app/view/spend/detail.rs b/gui/src/app/view/spend/detail.rs index 72e09685..08f49839 100644 --- a/gui/src/app/view/spend/detail.rs +++ b/gui/src/app/view/spend/detail.rs @@ -16,6 +16,7 @@ use liana::{ use liana_ui::{ color, component::{ + amount::*, badge, button, card, collapse::Collapse, form, hw, separation, @@ -29,7 +30,7 @@ use liana_ui::{ use crate::{ app::{ error::Error, - view::{hw::hw_list_view, message::*, util::*, warning::warn}, + view::{hw::hw_list_view, message::*, warning::warn}, }, daemon::model::{Coin, SpendStatus, SpendTx}, hw::HardwareWallet, diff --git a/gui/src/app/view/spend/step.rs b/gui/src/app/view/spend/step.rs index 50905f98..cd4288ac 100644 --- a/gui/src/app/view/spend/step.rs +++ b/gui/src/app/view/spend/step.rs @@ -5,6 +5,7 @@ use liana::miniscript::bitcoin::Amount; use liana_ui::{ color, component::{ + amount::*, badge, button, form, text::{text, Text}, }, @@ -17,7 +18,7 @@ use crate::{ app::{ cache::Cache, error::Error, - view::{message::*, modal, util::amount}, + view::{message::*, modal}, }, daemon::model::{remaining_sequence, Coin}, }; diff --git a/gui/src/app/view/transactions.rs b/gui/src/app/view/transactions.rs index adac858c..b5a110dd 100644 --- a/gui/src/app/view/transactions.rs +++ b/gui/src/app/view/transactions.rs @@ -3,17 +3,14 @@ use chrono::NaiveDateTime; use iced::{alignment, Alignment, Length}; use liana_ui::{ - component::{badge, card, text::*}, + component::{amount::*, badge, card, text::*}, icon, theme, util::Collection, widget::*, }; use crate::{ - app::{ - cache::Cache, - view::{message::Message, util::*}, - }, + app::{cache::Cache, view::message::Message}, daemon::model::HistoryTransaction, }; diff --git a/gui/src/daemon/model.rs b/gui/src/daemon/model.rs index a0b5e174..2737a4b0 100644 --- a/gui/src/daemon/model.rs +++ b/gui/src/daemon/model.rs @@ -180,4 +180,8 @@ impl HistoryTransaction { pub fn is_external(&self) -> bool { self.coins.is_empty() } + + pub fn is_self_send(&self) -> bool { + !self.coins.is_empty() && self.outgoing_amount == Amount::from_sat(0) + } } diff --git a/gui/ui/Cargo.toml b/gui/ui/Cargo.toml index 227e7220..cf064395 100644 --- a/gui/ui/Cargo.toml +++ b/gui/ui/Cargo.toml @@ -9,3 +9,5 @@ edition = "2021" iced = { version = "0.7", features = ["svg", "image"] } iced_native = "0.8" iced_lazy = { version = "0.4"} +bitcoin = "0.29" +chrono = "0.4" diff --git a/gui/ui/examples/design-system/Cargo.toml b/gui/ui/examples/design-system/Cargo.toml index 7ea4e589..fc397025 100644 --- a/gui/ui/examples/design-system/Cargo.toml +++ b/gui/ui/examples/design-system/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" iced = "0.7" iced_native = "0.8" web-sys = "0.3.61" +chrono = "0.4" liana_ui = { path = "../.." } [workspace] diff --git a/gui/ui/examples/design-system/src/main.rs b/gui/ui/examples/design-system/src/main.rs index f30d19ec..27b9d10a 100644 --- a/gui/ui/examples/design-system/src/main.rs +++ b/gui/ui/examples/design-system/src/main.rs @@ -45,6 +45,7 @@ impl Application for DesignSystem { Box::new(section::Typography {}), Box::new(section::Buttons {}), Box::new(section::HardwareWallets {}), + Box::new(section::Events {}), ], current: 0, }; @@ -123,12 +124,14 @@ impl Application for DesignSystem { .height(Length::Fill); container(row![ - sidebar.width(Length::Units(200)), - Space::with_width(Length::Units(150)), - scrollable(column![ + sidebar.width(Length::FillPortion(2)), + Space::with_width(Length::FillPortion(1)), + container(scrollable(column![ Space::with_height(Length::Units(150)), container(self.sections[self.current].view()).width(Length::Fill) - ]), + ])) + .width(Length::FillPortion(8)), + Space::with_width(Length::FillPortion(1)), ]) .width(Length::Fill) .height(Length::Fill) diff --git a/gui/ui/examples/design-system/src/section.rs b/gui/ui/examples/design-system/src/section.rs index 00c1a247..fee21969 100644 --- a/gui/ui/examples/design-system/src/section.rs +++ b/gui/ui/examples/design-system/src/section.rs @@ -5,7 +5,7 @@ use iced::{ }; use liana_ui::{ color, - component::{hw, separation, text::*}, + component::{amount::Amount, event, hw, separation, text::*}, theme, widget::Element, }; @@ -253,3 +253,33 @@ impl Section for HardwareWallets { .into() } } + +pub struct Events {} + +impl Section for Events { + fn title(&self) -> &'static str { + "Events " + } + fn view(&self) -> Element { + column![ + h1(self.title()), + column![ + event::unconfirmed_outgoing_event(&Amount::from_sat(32934234), Message::Ignore), + event::confirmed_outgoing_event( + chrono::NaiveDate::from_ymd_opt(2023, 04, 19).unwrap(), + &Amount::from_sat(32934234), + Message::Ignore + ), + event::unconfirmed_incoming_event(&Amount::from_sat(32934234), Message::Ignore), + event::confirmed_incoming_event( + chrono::NaiveDate::from_ymd_opt(2023, 04, 19).unwrap(), + &Amount::from_sat(32934234), + Message::Ignore + ) + ] + .spacing(20) + ] + .spacing(100) + .into() + } +} diff --git a/gui/src/app/view/util.rs b/gui/ui/src/component/amount.rs similarity index 87% rename from gui/src/app/view/util.rs rename to gui/ui/src/component/amount.rs index b9b997d6..824abd93 100644 --- a/gui/src/app/view/util.rs +++ b/gui/ui/src/component/amount.rs @@ -1,12 +1,12 @@ -use liana::miniscript::bitcoin::Amount; +pub use bitcoin::Amount; -use liana_ui::{color, component::text::*, util::Collection, widget::*}; +use crate::{color, component::text::*, util::Collection, widget::*}; -pub fn amount<'a, T: 'a>(a: &Amount) -> impl Into> { +pub fn amount<'a, T: 'a>(a: &Amount) -> Row<'a, T> { amount_with_size(a, P1_SIZE) } -pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> impl Into> { +pub fn 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); diff --git a/gui/ui/src/component/event.rs b/gui/ui/src/component/event.rs new file mode 100644 index 00000000..80644877 --- /dev/null +++ b/gui/ui/src/component/event.rs @@ -0,0 +1,106 @@ +use crate::{ + component::{amount, badge, text}, + theme, + widget::*, +}; +use bitcoin::Amount; +use iced::{ + widget::{button, row}, + Alignment, Length, +}; + +pub fn unconfirmed_outgoing_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) -> Container<'a, T> { + Container::new( + button( + row!( + row!(badge::spend(), badge::unconfirmed()) + .spacing(10) + .align_items(Alignment::Center) + .width(Length::Fill), + row!(text::p1_regular("-"), amount::amount(amount)) + .spacing(5) + .align_items(Alignment::Center), + ) + .align_items(Alignment::Center) + .padding(10) + .spacing(20), + ) + .on_press(msg) + .style(theme::Button::TransparentBorder), + ) + .style(theme::Container::Card(theme::Card::Simple)) +} + +pub fn confirmed_outgoing_event<'a, T: Clone + 'a>( + date: chrono::NaiveDate, + amount: &Amount, + msg: T, +) -> Container<'a, T> { + Container::new( + button( + row!( + row!(badge::spend(), text::p2_regular(date.to_string())) + .spacing(10) + .align_items(Alignment::Center) + .width(Length::Fill), + row!(text::p1_regular("-"), amount::amount(amount)) + .spacing(5) + .align_items(Alignment::Center), + ) + .align_items(Alignment::Center) + .padding(10) + .spacing(20), + ) + .on_press(msg) + .style(theme::Button::TransparentBorder), + ) + .style(theme::Container::Card(theme::Card::Simple)) +} + +pub fn unconfirmed_incoming_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) -> Container<'a, T> { + Container::new( + button( + row!( + row!(badge::receive(), badge::unconfirmed()) + .spacing(10) + .align_items(Alignment::Center) + .width(Length::Fill), + row!(text::p1_regular("+"), amount::amount(amount)) + .spacing(5) + .align_items(Alignment::Center), + ) + .align_items(Alignment::Center) + .padding(10) + .spacing(20), + ) + .on_press(msg) + .style(theme::Button::TransparentBorder), + ) + .style(theme::Container::Card(theme::Card::Simple)) +} + +pub fn confirmed_incoming_event<'a, T: Clone + 'a>( + date: chrono::NaiveDate, + amount: &Amount, + msg: T, +) -> Container<'a, T> { + Container::new( + button( + row!( + row!(badge::receive(), text::p2_regular(date.to_string())) + .spacing(10) + .align_items(Alignment::Center) + .width(Length::Fill), + row!(text::p1_regular("+"), amount::amount(amount)) + .spacing(5) + .align_items(Alignment::Center), + ) + .align_items(Alignment::Center) + .padding(10) + .spacing(20), + ) + .on_press(msg) + .style(theme::Button::TransparentBorder), + ) + .style(theme::Container::Card(theme::Card::Simple)) +} diff --git a/gui/ui/src/component/mod.rs b/gui/ui/src/component/mod.rs index 7ac5ff42..c3252391 100644 --- a/gui/ui/src/component/mod.rs +++ b/gui/ui/src/component/mod.rs @@ -1,7 +1,9 @@ +pub mod amount; pub mod badge; pub mod button; pub mod card; pub mod collapse; +pub mod event; pub mod form; pub mod hw; pub mod modal; From 49be063ccd7629da19910db57a19061e8b19fb7e Mon Sep 17 00:00:00 2001 From: edouard Date: Wed, 19 Apr 2023 22:51:28 +0200 Subject: [PATCH 3/3] gui: separate accounting events in the home panel --- gui/src/app/view/home.rs | 87 +++++++++----------- gui/src/app/view/spend/detail.rs | 2 + gui/src/app/view/transactions.rs | 4 +- gui/ui/examples/design-system/src/section.rs | 6 +- gui/ui/src/component/amount.rs | 11 ++- gui/ui/src/component/event.rs | 12 +-- 6 files changed, 60 insertions(+), 62 deletions(-) diff --git a/gui/src/app/view/home.rs b/gui/src/app/view/home.rs index c8bad9f3..90fbf4e7 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -5,7 +5,7 @@ use iced::{alignment, Alignment, Length}; use liana::miniscript::bitcoin; use liana_ui::{ color, - component::{amount::*, badge, text::*}, + component::{amount::*, event, text::*}, icon, theme, util::Collection, widget::*, @@ -23,7 +23,8 @@ pub fn home_view<'a>( events: &Vec, ) -> Element<'a, Message> { Column::new() - .push(amount_with_size(balance, 50)) + .push(h3("Balance")) + .push(amount_with_size(balance, H1_SIZE)) .push_maybe(recovery_warning.map(|(a, c)| { Row::new() .spacing(15) @@ -60,6 +61,7 @@ pub fn home_view<'a>( .push( Column::new() .spacing(10) + .push(h4_bold("Last payments")) .push( pending_events .iter() @@ -71,6 +73,7 @@ pub fn home_view<'a>( .push( events .iter() + .filter(|event| !event.is_self_send()) .enumerate() .fold(Column::new().spacing(10), |col, (i, event)| { col.push(event_list_view(i + pending_events.len(), event)) @@ -98,57 +101,43 @@ pub fn home_view<'a>( }, ), ) - .align_items(Alignment::Center) .spacing(20) .into() } -fn event_list_view<'a>(i: usize, event: &HistoryTransaction) -> Element<'a, Message> { - Container::new( - Button::new( - Row::new() - .push( - Row::new() - .push(if event.is_external() { - badge::receive() - } else { - badge::spend() - }) - .push(if let Some(t) = event.time { - Container::new( - text(format!( - "{}", - NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(), - )) - .small(), - ) - } else { - badge::unconfirmed() - }) - .spacing(10) - .align_items(Alignment::Center) - .width(Length::Fill), - ) - .push(if event.is_external() { - Row::new() - .spacing(5) - .push(text("+")) - .push(amount(&event.incoming_amount)) - .align_items(Alignment::Center) +fn event_list_view<'a>(i: usize, event: &HistoryTransaction) -> Column<'a, Message> { + event.tx.output.iter().enumerate().fold( + Column::new().spacing(10), + |col, (output_index, output)| { + if event.is_external() { + if !event.change_indexes.contains(&output_index) { + col + } else if let Some(t) = event.time { + col.push(event::confirmed_incoming_event( + NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(), + &Amount::from_sat(output.value), + Message::Select(i), + )) } else { - Row::new() - .spacing(5) - .push(text("-")) - .push(amount(&event.outgoing_amount)) - .align_items(Alignment::Center) - }) - .align_items(Alignment::Center) - .spacing(20), - ) - .padding(10) - .on_press(Message::Select(i)) - .style(theme::Button::TransparentBorder), + col.push(event::unconfirmed_incoming_event( + &Amount::from_sat(output.value), + Message::Select(i), + )) + } + } else if event.change_indexes.contains(&output_index) { + col + } else if let Some(t) = event.time { + col.push(event::confirmed_outgoing_event( + NaiveDateTime::from_timestamp_opt(t as i64, 0).unwrap(), + &Amount::from_sat(output.value), + Message::Select(i), + )) + } else { + col.push(event::unconfirmed_outgoing_event( + &Amount::from_sat(output.value), + Message::Select(i), + )) + } + }, ) - .style(theme::Container::Card(theme::Card::Simple)) - .into() } diff --git a/gui/src/app/view/spend/detail.rs b/gui/src/app/view/spend/detail.rs index 08f49839..70925c60 100644 --- a/gui/src/app/view/spend/detail.rs +++ b/gui/src/app/view/spend/detail.rs @@ -206,6 +206,8 @@ fn spend_header<'a>(tx: &SpendTx) -> Element<'a, Message> { .push(badge::Badge::new(icon::send_icon()).style(theme::Badge::Standard)) .push(if !tx.sigs.recovery_paths().is_empty() { text("Recovery").bold() + } else if tx.spend_amount == Amount::from_sat(0) { + text("Self send").bold() } else { text("Spend").bold() }) diff --git a/gui/src/app/view/transactions.rs b/gui/src/app/view/transactions.rs index b5a110dd..102f64a5 100644 --- a/gui/src/app/view/transactions.rs +++ b/gui/src/app/view/transactions.rs @@ -99,12 +99,14 @@ fn tx_list_view<'a>(i: usize, tx: &HistoryTransaction) -> Element<'a, Message> { .push(text("+")) .push(amount(&tx.incoming_amount)) .align_items(Alignment::Center) - } else { + } else if tx.outgoing_amount != Amount::from_sat(0) { Row::new() .spacing(5) .push(text("-")) .push(amount(&tx.outgoing_amount)) .align_items(Alignment::Center) + } else { + Row::new().push(text("Self send")) }) .align_items(Alignment::Center) .spacing(20), diff --git a/gui/ui/examples/design-system/src/section.rs b/gui/ui/examples/design-system/src/section.rs index fee21969..7a5e57fc 100644 --- a/gui/ui/examples/design-system/src/section.rs +++ b/gui/ui/examples/design-system/src/section.rs @@ -261,18 +261,20 @@ impl Section for Events { "Events " } fn view(&self) -> Element { + let d = chrono::NaiveDate::from_ymd_opt(2015, 6, 3).unwrap(); + let t = chrono::NaiveTime::from_hms_milli_opt(12, 34, 56, 789).unwrap(); column![ h1(self.title()), column![ event::unconfirmed_outgoing_event(&Amount::from_sat(32934234), Message::Ignore), event::confirmed_outgoing_event( - chrono::NaiveDate::from_ymd_opt(2023, 04, 19).unwrap(), + chrono::NaiveDateTime::new(d, t), &Amount::from_sat(32934234), Message::Ignore ), event::unconfirmed_incoming_event(&Amount::from_sat(32934234), Message::Ignore), event::confirmed_incoming_event( - chrono::NaiveDate::from_ymd_opt(2023, 04, 19).unwrap(), + chrono::NaiveDateTime::new(d, t), &Amount::from_sat(32934234), Message::Ignore ) diff --git a/gui/ui/src/component/amount.rs b/gui/ui/src/component/amount.rs index 824abd93..03971ebe 100644 --- a/gui/ui/src/component/amount.rs +++ b/gui/ui/src/component/amount.rs @@ -36,9 +36,12 @@ pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> Row<'a, T> { .into() }); - Row::with_children(vec![row.into(), text("BTC").size(size).into()]) - .spacing(spacing) - .align_items(iced::Alignment::Center) + 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) -> impl Into> { @@ -47,7 +50,7 @@ fn split_digits<'a, T: 'a>(mut s: String, size: u16) -> impl Into if s.starts_with(prefix) { let right = s.split_off(prefix.len()); return Row::new() - .push(text(s).size(size).style(color::GREY_2)) + .push(text(s).size(size).style(color::GREY_3)) .push_maybe(if right.is_empty() { None } else { diff --git a/gui/ui/src/component/event.rs b/gui/ui/src/component/event.rs index 80644877..b0df85fe 100644 --- a/gui/ui/src/component/event.rs +++ b/gui/ui/src/component/event.rs @@ -22,7 +22,7 @@ pub fn unconfirmed_outgoing_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) -> .align_items(Alignment::Center), ) .align_items(Alignment::Center) - .padding(10) + .padding(5) .spacing(20), ) .on_press(msg) @@ -32,7 +32,7 @@ pub fn unconfirmed_outgoing_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) -> } pub fn confirmed_outgoing_event<'a, T: Clone + 'a>( - date: chrono::NaiveDate, + date: chrono::NaiveDateTime, amount: &Amount, msg: T, ) -> Container<'a, T> { @@ -48,7 +48,7 @@ pub fn confirmed_outgoing_event<'a, T: Clone + 'a>( .align_items(Alignment::Center), ) .align_items(Alignment::Center) - .padding(10) + .padding(5) .spacing(20), ) .on_press(msg) @@ -70,7 +70,7 @@ pub fn unconfirmed_incoming_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) -> .align_items(Alignment::Center), ) .align_items(Alignment::Center) - .padding(10) + .padding(5) .spacing(20), ) .on_press(msg) @@ -80,7 +80,7 @@ pub fn unconfirmed_incoming_event<'a, T: Clone + 'a>(amount: &Amount, msg: T) -> } pub fn confirmed_incoming_event<'a, T: Clone + 'a>( - date: chrono::NaiveDate, + date: chrono::NaiveDateTime, amount: &Amount, msg: T, ) -> Container<'a, T> { @@ -96,7 +96,7 @@ pub fn confirmed_incoming_event<'a, T: Clone + 'a>( .align_items(Alignment::Center), ) .align_items(Alignment::Center) - .padding(10) + .padding(5) .spacing(20), ) .on_press(msg)