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/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/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 c546bb99..90fbf4e7 100644 --- a/gui/src/app/view/home.rs +++ b/gui/src/app/view/home.rs @@ -5,19 +5,13 @@ use iced::{alignment, Alignment, Length}; use liana::miniscript::bitcoin; use liana_ui::{ color, - component::{badge, card, text::*}, + component::{amount::*, event, text::*}, icon, theme, util::Collection, widget::*, }; -use crate::{ - app::{ - cache::Cache, - view::{message::Message, util::*}, - }, - daemon::model::HistoryTransaction, -}; +use crate::{app::view::message::Message, daemon::model::HistoryTransaction}; pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20; @@ -29,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) @@ -66,6 +61,7 @@ pub fn home_view<'a>( .push( Column::new() .spacing(10) + .push(h4_bold("Last payments")) .push( pending_events .iter() @@ -77,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)) @@ -104,128 +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() -} - -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..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; @@ -10,6 +9,7 @@ pub mod receive; pub mod recovery; pub mod settings; pub mod spend; +pub mod transactions; pub use message::*; use warning::warn; @@ -41,6 +41,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 +294,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..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}, }; @@ -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/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..70925c60 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, @@ -205,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/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 new file mode 100644 index 00000000..102f64a5 --- /dev/null +++ b/gui/src/app/view/transactions.rs @@ -0,0 +1,190 @@ +use chrono::NaiveDateTime; + +use iced::{alignment, Alignment, Length}; + +use liana_ui::{ + component::{amount::*, badge, card, text::*}, + icon, theme, + util::Collection, + widget::*, +}; + +use crate::{ + app::{cache::Cache, view::message::Message}, + 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 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), + ) + .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() +} 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..7a5e57fc 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,35 @@ impl Section for HardwareWallets { .into() } } + +pub struct Events {} + +impl Section for Events { + fn title(&self) -> &'static str { + "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::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::NaiveDateTime::new(d, t), + &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 77% rename from gui/src/app/view/util.rs rename to gui/ui/src/component/amount.rs index b9b997d6..03971ebe 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); @@ -36,9 +36,12 @@ pub fn amount_with_size<'a, T: 'a>(a: &Amount, size: u16) -> impl Into(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 new file mode 100644 index 00000000..b0df85fe --- /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(5) + .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::NaiveDateTime, + 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(5) + .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(5) + .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::NaiveDateTime, + 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(5) + .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;