use chrono::NaiveDateTime; use std::collections::HashMap; use iced::{alignment, Alignment, Length}; use liana_ui::{ color, component::{amount::*, badge, card, form, text::*}, icon, theme, util::Collection, widget::*, }; use crate::{ app::{ cache::Cache, error::Error, menu::Menu, view::{dashboard, label, message::Message}, }, daemon::model::HistoryTransaction, }; pub const HISTORY_EVENT_PAGE_SIZE: u64 = 20; pub fn transactions_view<'a>( cache: &'a Cache, pending_txs: &'a [HistoryTransaction], txs: &'a Vec, warning: Option<&'a Error>, ) -> Element<'a, Message> { dashboard( &Menu::Transactions, cache, warning, Column::new() .push(Container::new(h3("Transactions")).width(Length::Fill)) .push( Column::new() .spacing(10) .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() .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(30), ) } fn tx_list_view(i: usize, tx: &HistoryTransaction) -> Element<'_, Message> { Container::new( Button::new( Row::new() .push( Row::new() .push(if tx.is_external() { badge::receive() } else if tx.is_send_to_self() { badge::cycle() } else { badge::spend() }) .push( Column::new() .push_maybe( tx.labels.get(&tx.tx.txid().to_string()).map(p1_regular), ) .push_maybe(tx.time.map(|t| { Container::new( text(format!( "{}", NaiveDateTime::from_timestamp_opt(t as i64, 0) .unwrap() .format("%b. %d, %Y - %T"), )) .style(color::GREY_3) .small(), ) })), ) .spacing(10) .align_items(Alignment::Center) .width(Length::Fill), ) .push_maybe(if tx.time.is_none() { Some(badge::unconfirmed()) } else { None }) .push_maybe(if tx.is_batch() { Some(badge::batch()) } else { None }) .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-transfer")) }) .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: &'a Cache, tx: &'a HistoryTransaction, labels_editing: &'a HashMap>, warning: Option<&'a Error>, ) -> Element<'a, Message> { let txid = tx.tx.txid().to_string(); dashboard( &Menu::Transactions, cache, warning, Column::new() .push(if tx.is_send_to_self() { Container::new(h3("Transaction")).width(Length::Fill) } else if tx.is_external() { Container::new(h3("Incoming transaction")).width(Length::Fill) } else { Container::new(h3("Outgoing transaction")).width(Length::Fill) }) .push(if let Some(outpoint) = tx.is_single_payment() { // if the payment is a payment of a single payment transaction then // the label of the transaction is attached to the label of the payment outpoint let outpoint = outpoint.to_string(); if let Some(label) = labels_editing.get(&outpoint) { label::label_editing(vec![outpoint.clone(), txid.clone()], label, H3_SIZE) } else { label::label_editable( vec![outpoint.clone(), txid.clone()], tx.labels.get(&outpoint), H3_SIZE, ) } } else if let Some(label) = labels_editing.get(&txid) { label::label_editing(vec![txid.clone()], label, H3_SIZE) } else { label::label_editable(vec![txid.clone()], tx.labels.get(&txid), H1_SIZE) }) .push( Column::new().spacing(20).push( Column::new() .push(if tx.is_send_to_self() { Container::new(h1("Self-transfer")) } else if tx.is_external() { Container::new(amount_with_size(&tx.incoming_amount, H1_SIZE)) } else { Container::new(amount_with_size(&tx.outgoing_amount, H1_SIZE)) }) .push_maybe(tx.fee_amount.map(|fee_amount| { Row::new() .align_items(Alignment::Center) .push(h3("Miner fee: ").style(color::GREY_3)) .push(amount_with_size(&fee_amount, H3_SIZE)) .push(text(" ").size(H3_SIZE)) .push( text(format!( "({} sats/vbyte)", fee_amount.to_sat() / tx.tx.vsize() as u64 )) .size(H4_SIZE) .style(color::GREY_3), ) })), ), ) .push(card::simple( Column::new() .push_maybe(tx.time.map(|t| { 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)) .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(txid.clone()).small())) .push( Button::new(icon::clipboard_icon()) .on_press(Message::Clipboard(txid.clone())) .style(theme::Button::TransparentBorder), ) .width(Length::Shrink), ), ) .spacing(5), )) .push(super::psbt::inputs_and_outputs_view( &tx.coins, &tx.tx, cache.network, if tx.is_external() { None } else { Some(tx.change_indexes.clone()) }, &tx.labels, labels_editing, tx.is_single_payment().is_some(), )) .spacing(20), ) }