From 149a9cd6ec8bd382c5878388e0868f30c07e1ee2 Mon Sep 17 00:00:00 2001 From: Tommy Volk Date: Fri, 11 Oct 2024 23:28:23 +0200 Subject: [PATCH 1/2] refactor: split wallet add page to separate file --- src/routes/bitcoin_wallet.rs | 281 ++----------------------------- src/routes/bitcoin_wallet/add.rs | 252 +++++++++++++++++++++++++++ 2 files changed, 265 insertions(+), 268 deletions(-) create mode 100644 src/routes/bitcoin_wallet/add.rs diff --git a/src/routes/bitcoin_wallet.rs b/src/routes/bitcoin_wallet.rs index 08eff05..b6f7846 100644 --- a/src/routes/bitcoin_wallet.rs +++ b/src/routes/bitcoin_wallet.rs @@ -1,14 +1,6 @@ -use std::str::FromStr; - -use fedimint_core::{ - config::{ClientConfig, FederationId, META_FEDERATION_NAME_KEY}, - invite_code::InviteCode, - Amount, -}; +use fedimint_core::{config::FederationId, Amount}; use iced::{ - widget::{ - column, container::Style, horizontal_space, row, text_input, Column, Container, Space, Text, - }, + widget::{column, container::Style, horizontal_space, row, Column, Container, Space, Text}, Border, Length, Shadow, Task, Theme, }; @@ -21,30 +13,16 @@ use crate::{ use super::{container, ConnectedState, Loadable, RouteName}; +mod add; mod receive; mod send; #[derive(Debug, Clone)] pub enum Message { - JoinFederationInviteCodeInputChanged(String), - - LoadedFederationConfigFromInviteCode { - // The invite code that was used to load the federation config. - config_invite_code: InviteCode, - // The loaded federation config. - config: ClientConfig, - }, - FailedToLoadFederationConfigFromInviteCode { - // The invite code that was used to attempt to load the federation config. - config_invite_code: InviteCode, - }, - - JoinFederation(InviteCode), - JoinedFederation(InviteCode), - LeaveFederation(FederationId), LeftFederation(FederationId), + Add(add::Message), Send(send::Message), Receive(receive::Message), @@ -57,148 +35,8 @@ pub struct Page { } impl Page { - // TODO: Remove this clippy allow. - #[allow(clippy::too_many_lines)] pub fn update(&mut self, msg: Message) -> Task { match msg { - Message::JoinFederationInviteCodeInputChanged(new_federation_invite_code) => { - let Subroute::Add(Add { - federation_invite_code, - parsed_federation_invite_code_state_or, - }) = &mut self.subroute - else { - return Task::none(); - }; - - *federation_invite_code = new_federation_invite_code; - - if let Ok(invite_code) = InviteCode::from_str(federation_invite_code) { - *parsed_federation_invite_code_state_or = - Some(ParsedFederationInviteCodeState { - invite_code: invite_code.clone(), - loadable_federation_config: Loadable::Loading, - }); - - Task::perform( - async move { - match fedimint_api_client::download_from_invite_code(&invite_code).await - { - Ok(config) => { - app::Message::Routes(super::Message::BitcoinWalletPage( - Message::LoadedFederationConfigFromInviteCode { - config_invite_code: invite_code, - config, - }, - )) - } - // TODO: Include error in message and display it in the UI. - Err(_err) => { - app::Message::Routes(super::Message::BitcoinWalletPage( - Message::FailedToLoadFederationConfigFromInviteCode { - config_invite_code: invite_code, - }, - )) - } - } - }, - |msg| msg, - ) - } else { - *parsed_federation_invite_code_state_or = None; - - Task::none() - } - } - Message::LoadedFederationConfigFromInviteCode { - config_invite_code, - config, - } => { - let Subroute::Add(Add { - parsed_federation_invite_code_state_or, - .. - }) = &mut self.subroute - else { - return Task::none(); - }; - - if let Some(ParsedFederationInviteCodeState { - invite_code, - loadable_federation_config: maybe_loading_federation_config, - }) = parsed_federation_invite_code_state_or - { - // If the invite code has changed since the request was made, ignore the response. - if &config_invite_code == invite_code { - *maybe_loading_federation_config = Loadable::Loaded(config); - } - } - - Task::none() - } - Message::FailedToLoadFederationConfigFromInviteCode { config_invite_code } => { - let Subroute::Add(Add { - parsed_federation_invite_code_state_or, - .. - }) = &mut self.subroute - else { - return Task::none(); - }; - - if let Some(ParsedFederationInviteCodeState { - invite_code, - loadable_federation_config: maybe_loading_federation_config, - }) = parsed_federation_invite_code_state_or - { - // If the invite code has changed since the request was made, ignore the response. - // Also only update the state if the config hasn't already been loaded. - if &config_invite_code == invite_code - && matches!(maybe_loading_federation_config, Loadable::Loading) - { - *maybe_loading_federation_config = Loadable::Failed; - } - } - - Task::none() - } - Message::JoinFederation(invite_code) => { - let wallet = self.connected_state.wallet.clone(); - - Task::stream(async_stream::stream! { - match wallet.join_federation(invite_code.clone()).await { - Ok(()) => { - yield app::Message::AddToast(Toast { - title: "Joined federation".to_string(), - body: "You have successfully joined the federation.".to_string(), - status: ToastStatus::Good, - }); - - yield app::Message::Routes(super::Message::BitcoinWalletPage( - Message::JoinedFederation(invite_code) - )); - } - Err(err) => { - yield app::Message::AddToast(Toast { - title: "Failed to join federation".to_string(), - body: format!("Failed to join the federation: {err}"), - status: ToastStatus::Bad, - }); - } - } - }) - } - Message::JoinedFederation(invite_code) => { - // A verbose way of saying "if the user is currently on the Add page and the invite code matches the one that was just joined, navigate back to the List page". - if let Subroute::Add(add) = &self.subroute { - if let Some(invite_code_state) = &add.parsed_federation_invite_code_state_or { - if invite_code_state.invite_code == invite_code { - return Task::done(app::Message::Routes(super::Message::Navigate( - RouteName::BitcoinWallet(SubrouteName::List), - ))); - } - } - } - - Task::none() - } Message::LeaveFederation(federation_id) => { let wallet = self.connected_state.wallet.clone(); @@ -237,6 +75,13 @@ impl Page { Task::none() } + Message::Add(add_message) => { + if let Subroute::Add(add_page) = &mut self.subroute { + add_page.update(add_message) + } else { + Task::none() + } + } Message::Send(send_message) => { if let Subroute::Send(send_page) = &mut self.subroute { send_page.update(send_message) @@ -292,10 +137,7 @@ impl SubrouteName { view: federation_view.clone(), }) } - Self::Add => Subroute::Add(Add { - federation_invite_code: String::new(), - parsed_federation_invite_code_state_or: None, - }), + Self::Add => Subroute::Add(add::Page::new(connected_state)), Self::Send => Subroute::Send(send::Page::new(connected_state)), Self::Receive => Subroute::Receive(receive::Page::new(connected_state)), } @@ -305,7 +147,7 @@ impl SubrouteName { pub enum Subroute { List(List), FederationDetails(FederationDetails), - Add(Add), + Add(add::Page), Send(send::Page), Receive(receive::Page), } @@ -517,100 +359,3 @@ impl FederationDetails { container } } - -pub struct Add { - federation_invite_code: String, - parsed_federation_invite_code_state_or: Option, -} - -pub struct ParsedFederationInviteCodeState { - invite_code: InviteCode, - loadable_federation_config: Loadable, -} - -impl Add { - fn view<'a>(&self) -> Column<'a, app::Message> { - let mut container = container("Join Federation") - .push( - text_input("Federation Invite Code", &self.federation_invite_code) - .on_input(|input| { - app::Message::Routes(super::Message::BitcoinWalletPage( - Message::JoinFederationInviteCodeInputChanged(input), - )) - }) - .padding(10) - .size(30), - ) - .push( - icon_button("Join Federation", SvgIcon::Groups, PaletteColor::Primary) - .on_press_maybe(self.parsed_federation_invite_code_state_or.as_ref().map( - |parsed_federation_invite_code_state| { - app::Message::Routes(super::Message::BitcoinWalletPage( - Message::JoinFederation( - parsed_federation_invite_code_state.invite_code.clone(), - ), - )) - }, - )), - ); - - if let Some(parsed_federation_invite_code_state) = - &self.parsed_federation_invite_code_state_or - { - container = container - .push(Text::new("Federation ID").size(25)) - .push(Text::new(truncate_text( - &parsed_federation_invite_code_state - .invite_code - .federation_id() - .to_string(), - 21, - true, - ))); - - match &parsed_federation_invite_code_state.loadable_federation_config { - Loadable::Loading => { - container = container.push(Text::new("Loading...")); - } - Loadable::Loaded(client_config) => { - container = container - .push(Text::new("Federation Name").size(25)) - .push(Text::new( - client_config - .meta::(META_FEDERATION_NAME_KEY) - .ok() - .flatten() - .unwrap_or_default(), - )) - .push(Text::new("Modules").size(25)) - .push(Text::new( - client_config - .modules - .values() - .map(|module| module.kind().to_string()) - .collect::>() - .join(", "), - )) - .push(Text::new("Guardians").size(25)); - for peer_url in client_config.global.api_endpoints.values() { - container = container - .push(Text::new(format!("{} ({})", peer_url.name, peer_url.url))); - } - } - Loadable::Failed => { - container = container.push(Text::new("Failed to load client config")); - } - } - } - - container = container.push( - icon_button("Back", SvgIcon::ArrowBack, PaletteColor::Background).on_press( - app::Message::Routes(super::Message::Navigate(RouteName::BitcoinWallet( - SubrouteName::List, - ))), - ), - ); - - container - } -} diff --git a/src/routes/bitcoin_wallet/add.rs b/src/routes/bitcoin_wallet/add.rs new file mode 100644 index 0000000..13a3ba8 --- /dev/null +++ b/src/routes/bitcoin_wallet/add.rs @@ -0,0 +1,252 @@ +use std::str::FromStr; +use std::sync::Arc; + +use fedimint_core::{ + config::{ClientConfig, META_FEDERATION_NAME_KEY}, + invite_code::InviteCode, +}; +use iced::{ + widget::{text_input, Column, Text}, + Task, +}; + +use crate::{ + app, + fedimint::Wallet, + routes::{self, container, ConnectedState, Loadable}, + ui_components::{icon_button, PaletteColor, SvgIcon, Toast, ToastStatus}, + util::truncate_text, +}; + +#[derive(Debug, Clone)] +pub enum Message { + InviteCodeInputChanged(String), + + LoadedFederationConfigFromInviteCode { + // The invite code that was used to load the federation config. + invite_code: InviteCode, + // The loaded federation config from the federation that the invite code belongs to. + config: ClientConfig, + }, + FailedToLoadFederationConfigFromInviteCode { + // The invite code that was used to attempt to load the federation config. + invite_code: InviteCode, + }, + + JoinFederation(InviteCode), + JoinedFederation(InviteCode), +} + +pub struct Page { + wallet: Arc, + invite_code_input: String, + parsed_invite_code_state_or: Option, +} + +struct ParsedInviteCodeState { + invite_code: InviteCode, + loadable_federation_config: Loadable, +} + +impl Page { + pub fn new(connected_state: &ConnectedState) -> Self { + Self { + wallet: connected_state.wallet.clone(), + invite_code_input: String::new(), + parsed_invite_code_state_or: None, + } + } + + // TODO: Remove this clippy allow. + #[allow(clippy::too_many_lines)] + pub fn update(&mut self, msg: Message) -> Task { + match msg { + Message::InviteCodeInputChanged(new_invite_code_input) => { + self.invite_code_input = new_invite_code_input; + + let Ok(invite_code) = InviteCode::from_str(&self.invite_code_input) else { + self.parsed_invite_code_state_or = None; + + return Task::none(); + }; + + self.parsed_invite_code_state_or = Some(ParsedInviteCodeState { + invite_code: invite_code.clone(), + loadable_federation_config: Loadable::Loading, + }); + + Task::future(async { + match fedimint_api_client::download_from_invite_code(&invite_code).await { + Ok(config) => app::Message::Routes(routes::Message::BitcoinWalletPage( + super::Message::Add(Message::LoadedFederationConfigFromInviteCode { + invite_code, + config, + }), + )), + Err(_err) => app::Message::Routes(routes::Message::BitcoinWalletPage( + super::Message::Add( + Message::FailedToLoadFederationConfigFromInviteCode { invite_code }, + ), + )), + } + }) + } + Message::LoadedFederationConfigFromInviteCode { + invite_code, + config, + } => { + if let Some(ParsedInviteCodeState { + invite_code: parsed_invite_code, + loadable_federation_config, + }) = &mut self.parsed_invite_code_state_or + { + // If the invite code has changed since the request was made, ignore the response. + if &invite_code == parsed_invite_code { + *loadable_federation_config = Loadable::Loaded(config); + } + } + + Task::none() + } + Message::FailedToLoadFederationConfigFromInviteCode { invite_code } => { + if let Some(ParsedInviteCodeState { + invite_code: parsed_invite_code, + loadable_federation_config, + }) = &mut self.parsed_invite_code_state_or + { + // If the invite code has changed since the request was made, ignore the response. + // Also only update the state if the user attempted to load the config. + if &invite_code == parsed_invite_code + && matches!(loadable_federation_config, Loadable::Loading) + { + *loadable_federation_config = Loadable::Failed; + } + } + + // TODO: Show toast instead of returning an empty task. + Task::none() + } + Message::JoinFederation(invite_code) => { + let wallet = self.wallet.clone(); + + Task::stream(async_stream::stream! { + match wallet.join_federation(invite_code.clone()).await { + Ok(()) => { + yield app::Message::AddToast(Toast { + title: "Joined federation".to_string(), + body: "You have successfully joined the federation.".to_string(), + status: ToastStatus::Good, + }); + + yield app::Message::Routes(routes::Message::BitcoinWalletPage(super::Message::Add( + Message::JoinedFederation(invite_code) + ))); + } + Err(err) => { + yield app::Message::AddToast(Toast { + title: "Failed to join federation".to_string(), + body: format!("Failed to join the federation: {err}"), + status: ToastStatus::Bad, + }); + } + } + }) + } + Message::JoinedFederation(invite_code) => { + // If the invite code matches the one that was just joined, navigate back to the `List` page. + if let Some(invite_code_state) = &self.parsed_invite_code_state_or { + if invite_code_state.invite_code == invite_code { + return Task::done(app::Message::Routes(routes::Message::Navigate( + routes::RouteName::BitcoinWallet(super::SubrouteName::List), + ))); + } + } + + Task::none() + } + } + } + + pub fn view<'a>(&self) -> Column<'a, app::Message> { + let mut container = container("Join Federation") + .push( + text_input("Federation Invite Code", &self.invite_code_input) + .on_input(|input| { + app::Message::Routes(routes::Message::BitcoinWalletPage( + super::Message::Add(Message::InviteCodeInputChanged(input)), + )) + }) + .padding(10) + .size(30), + ) + .push( + icon_button("Join Federation", SvgIcon::Groups, PaletteColor::Primary) + .on_press_maybe(self.parsed_invite_code_state_or.as_ref().map( + |parsed_federation_invite_code_state| { + app::Message::Routes(routes::Message::BitcoinWalletPage( + super::Message::Add(Message::JoinFederation( + parsed_federation_invite_code_state.invite_code.clone(), + )), + )) + }, + )), + ); + + if let Some(parsed_federation_invite_code_state) = &self.parsed_invite_code_state_or { + container = container + .push(Text::new("Federation ID").size(25)) + .push(Text::new(truncate_text( + &parsed_federation_invite_code_state + .invite_code + .federation_id() + .to_string(), + 21, + true, + ))); + + match &parsed_federation_invite_code_state.loadable_federation_config { + Loadable::Loading => { + container = container.push(Text::new("Loading...")); + } + Loadable::Loaded(client_config) => { + container = container + .push(Text::new("Federation Name").size(25)) + .push(Text::new( + client_config + .meta::(META_FEDERATION_NAME_KEY) + .ok() + .flatten() + .unwrap_or_default(), + )) + .push(Text::new("Modules").size(25)) + .push(Text::new( + client_config + .modules + .values() + .map(|module| module.kind().to_string()) + .collect::>() + .join(", "), + )) + .push(Text::new("Guardians").size(25)); + for peer_url in client_config.global.api_endpoints.values() { + container = container + .push(Text::new(format!("{} ({})", peer_url.name, peer_url.url))); + } + } + Loadable::Failed => { + container = container.push(Text::new("Failed to load client config")); + } + } + } + + container = container.push( + icon_button("Back", SvgIcon::ArrowBack, PaletteColor::Background).on_press( + app::Message::Routes(routes::Message::Navigate(super::RouteName::BitcoinWallet( + super::SubrouteName::List, + ))), + ), + ); + + container + } +} From 50e5b94e674c749e72769558929e1da89c53f9b4 Mon Sep 17 00:00:00 2001 From: Tommy Volk Date: Sat, 12 Oct 2024 11:12:09 +0200 Subject: [PATCH 2/2] refactor: split federation details page to separate file --- src/routes/bitcoin_wallet.rs | 164 ++-------------- .../bitcoin_wallet/federation_details.rs | 177 ++++++++++++++++++ 2 files changed, 193 insertions(+), 148 deletions(-) create mode 100644 src/routes/bitcoin_wallet/federation_details.rs diff --git a/src/routes/bitcoin_wallet.rs b/src/routes/bitcoin_wallet.rs index b6f7846..1f75240 100644 --- a/src/routes/bitcoin_wallet.rs +++ b/src/routes/bitcoin_wallet.rs @@ -1,27 +1,26 @@ -use fedimint_core::{config::FederationId, Amount}; +use fedimint_core::Amount; use iced::{ - widget::{column, container::Style, horizontal_space, row, Column, Container, Space, Text}, + widget::{container::Style, horizontal_space, row, Column, Container, Space, Text}, Border, Length, Shadow, Task, Theme, }; use crate::{ app, fedimint::{FederationView, WalletView}, - ui_components::{icon_button, PaletteColor, SvgIcon, Toast, ToastStatus}, - util::{format_amount, lighten, truncate_text}, + ui_components::{icon_button, PaletteColor, SvgIcon}, + util::{format_amount, lighten}, }; use super::{container, ConnectedState, Loadable, RouteName}; mod add; +mod federation_details; mod receive; mod send; #[derive(Debug, Clone)] pub enum Message { - LeaveFederation(FederationId), - LeftFederation(FederationId), - + FederationDetails(federation_details::Message), Add(add::Message), Send(send::Message), Receive(receive::Message), @@ -37,43 +36,12 @@ pub struct Page { impl Page { pub fn update(&mut self, msg: Message) -> Task { match msg { - Message::LeaveFederation(federation_id) => { - let wallet = self.connected_state.wallet.clone(); - - Task::stream(async_stream::stream! { - match wallet.leave_federation(federation_id).await { - Ok(()) => { - yield app::Message::AddToast(Toast { - title: "Left federation".to_string(), - body: "You have successfully left the federation.".to_string(), - status: ToastStatus::Good, - }); - - yield app::Message::Routes(super::Message::BitcoinWalletPage( - Message::LeftFederation(federation_id) - )); - } - Err(err) => { - yield app::Message::AddToast(Toast { - title: "Failed to leave federation".to_string(), - body: format!("Failed to leave the federation: {err}"), - status: ToastStatus::Bad, - }); - } - } - }) - } - Message::LeftFederation(federation_id) => { - // A verbose way of saying "if the user is currently on the FederationDetails page and the federation ID matches the one that was just left, navigate back to the List page". - if let Subroute::FederationDetails(federation_details) = &self.subroute { - if federation_details.view.federation_id == federation_id { - return Task::done(app::Message::Routes(super::Message::Navigate( - RouteName::BitcoinWallet(SubrouteName::List), - ))); - } + Message::FederationDetails(federation_details_message) => { + if let Subroute::FederationDetails(federation_details_page) = &mut self.subroute { + federation_details_page.update(federation_details_message) + } else { + Task::none() } - - Task::none() } Message::Add(add_message) => { if let Subroute::Add(add_page) = &mut self.subroute { @@ -132,11 +100,9 @@ impl SubrouteName { pub fn to_default_subroute(&self, connected_state: &ConnectedState) -> Subroute { match self { Self::List => Subroute::List(List {}), - Self::FederationDetails(federation_view) => { - Subroute::FederationDetails(FederationDetails { - view: federation_view.clone(), - }) - } + Self::FederationDetails(federation_view) => Subroute::FederationDetails( + federation_details::Page::new(federation_view.clone(), connected_state), + ), Self::Add => Subroute::Add(add::Page::new(connected_state)), Self::Send => Subroute::Send(send::Page::new(connected_state)), Self::Receive => Subroute::Receive(receive::Page::new(connected_state)), @@ -146,7 +112,7 @@ impl SubrouteName { pub enum Subroute { List(List), - FederationDetails(FederationDetails), + FederationDetails(federation_details::Page), Add(add::Page), Send(send::Page), Receive(receive::Page), @@ -157,7 +123,7 @@ impl Subroute { match self { Self::List(_) => SubrouteName::List, Self::FederationDetails(federation_details) => { - SubrouteName::FederationDetails(federation_details.view.clone()) + SubrouteName::FederationDetails(federation_details.clone_view()) } Self::Add(_) => SubrouteName::Add, Self::Send(_) => SubrouteName::Send, @@ -261,101 +227,3 @@ impl List { container } } - -pub struct FederationDetails { - view: FederationView, -} - -impl FederationDetails { - fn view<'a>(&self) -> Column<'a, app::Message> { - let mut container = container("Federation Details") - .push( - Text::new( - self.view - .name_or - .clone() - .unwrap_or_else(|| "Unnamed Federation".to_string()), - ) - .size(25), - ) - .push(Text::new(format!( - "Federation ID: {}", - truncate_text(&self.view.federation_id.to_string(), 23, true) - ))) - .push(Text::new(format_amount(self.view.balance))) - .push(Text::new("Gateways").size(20)); - - for gateway in &self.view.gateways { - let vetted_text = if gateway.vetted { - "Vetted" - } else { - "Not Vetted" - }; - - let column: Column<_, Theme, _> = column![ - Text::new(format!( - "Gateway ID: {}", - truncate_text(&gateway.info.gateway_id.to_string(), 43, true) - )), - Text::new(format!( - "Lightning Node Alias: {}", - truncate_text(&gateway.info.lightning_alias.to_string(), 43, true) - )), - Text::new(format!( - "Lightning Node Public Key: {}", - truncate_text(&gateway.info.node_pub_key.to_string(), 43, true) - )), - Text::new(vetted_text) - ]; - - container = container.push( - Container::new(column) - .padding(10) - .width(Length::Fill) - .style(|theme| -> Style { - Style { - text_color: None, - background: Some(lighten(theme.palette().background, 0.05).into()), - border: Border { - color: iced::Color::WHITE, - width: 0.0, - radius: (8.0).into(), - }, - shadow: Shadow::default(), - } - }), - ); - } - - // TODO: Add a function to `Wallet` to check whether we can safely leave a federation. - // Call it here rather and get rid of `has_zero_balance`. - let has_zero_balance = self.view.balance.msats == 0; - - if !has_zero_balance { - container = container.push( - Text::new("Must have a zero balance in this federation in order to leave.") - .size(20), - ); - } - - container = container.push( - icon_button("Leave Federation", SvgIcon::Delete, PaletteColor::Danger).on_press_maybe( - has_zero_balance.then(|| { - app::Message::Routes(super::Message::BitcoinWalletPage( - Message::LeaveFederation(self.view.federation_id), - )) - }), - ), - ); - - container = container.push( - icon_button("Back", SvgIcon::ArrowBack, PaletteColor::Background).on_press( - app::Message::Routes(super::Message::Navigate(RouteName::BitcoinWallet( - SubrouteName::List, - ))), - ), - ); - - container - } -} diff --git a/src/routes/bitcoin_wallet/federation_details.rs b/src/routes/bitcoin_wallet/federation_details.rs new file mode 100644 index 0000000..08f329a --- /dev/null +++ b/src/routes/bitcoin_wallet/federation_details.rs @@ -0,0 +1,177 @@ +use std::sync::Arc; + +use fedimint_core::config::FederationId; +use iced::{ + widget::{column, container::Style, Column, Container, Text}, + Border, Length, Shadow, Task, Theme, +}; + +use crate::{ + app, + fedimint::{FederationView, Wallet}, + routes::{self, container, ConnectedState}, + ui_components::{icon_button, PaletteColor, SvgIcon, Toast, ToastStatus}, + util::{format_amount, lighten, truncate_text}, +}; + +#[derive(Debug, Clone)] +pub enum Message { + LeaveFederation(FederationId), + LeftFederation(FederationId), +} + +pub struct Page { + wallet: Arc, + view: FederationView, +} + +impl Page { + pub fn new(view: FederationView, connected_state: &ConnectedState) -> Self { + Self { + wallet: connected_state.wallet.clone(), + view, + } + } + + pub fn clone_view(&self) -> FederationView { + self.view.clone() + } + + // TODO: Remove these clippy allows. + #[allow(clippy::needless_pass_by_value)] + #[allow(clippy::needless_pass_by_ref_mut)] + pub fn update(&mut self, msg: Message) -> Task { + match msg { + Message::LeaveFederation(federation_id) => { + let wallet = self.wallet.clone(); + + Task::stream(async_stream::stream! { + match wallet.leave_federation(federation_id).await { + Ok(()) => { + yield app::Message::AddToast(Toast { + title: "Left federation".to_string(), + body: "You have successfully left the federation.".to_string(), + status: ToastStatus::Good, + }); + + yield app::Message::Routes(routes::Message::BitcoinWalletPage( + super::Message::FederationDetails(Message::LeftFederation(federation_id)) + )); + } + Err(err) => { + yield app::Message::AddToast(Toast { + title: "Failed to leave federation".to_string(), + body: format!("Failed to leave the federation: {err}"), + status: ToastStatus::Bad, + }); + } + } + }) + } + Message::LeftFederation(federation_id) => { + // A verbose way of saying "if the user is currently on the FederationDetails page and the federation ID matches the one that was just left, navigate back to the List page". + if self.view.federation_id == federation_id { + return Task::done(app::Message::Routes(routes::Message::Navigate( + routes::RouteName::BitcoinWallet(super::SubrouteName::List), + ))); + } + + Task::none() + } + } + } + + pub fn view<'a>(&self) -> Column<'a, app::Message> { + let mut container = container("Federation Details") + .push( + Text::new( + self.view + .name_or + .clone() + .unwrap_or_else(|| "Unnamed Federation".to_string()), + ) + .size(25), + ) + .push(Text::new(format!( + "Federation ID: {}", + truncate_text(&self.view.federation_id.to_string(), 23, true) + ))) + .push(Text::new(format_amount(self.view.balance))) + .push(Text::new("Gateways").size(20)); + + for gateway in &self.view.gateways { + let vetted_text = if gateway.vetted { + "Vetted" + } else { + "Not Vetted" + }; + + let column: Column<_, Theme, _> = column![ + Text::new(format!( + "Gateway ID: {}", + truncate_text(&gateway.info.gateway_id.to_string(), 43, true) + )), + Text::new(format!( + "Lightning Node Alias: {}", + truncate_text(&gateway.info.lightning_alias.to_string(), 43, true) + )), + Text::new(format!( + "Lightning Node Public Key: {}", + truncate_text(&gateway.info.node_pub_key.to_string(), 43, true) + )), + Text::new(vetted_text) + ]; + + container = container.push( + Container::new(column) + .padding(10) + .width(Length::Fill) + .style(|theme| -> Style { + Style { + text_color: None, + background: Some(lighten(theme.palette().background, 0.05).into()), + border: Border { + color: iced::Color::WHITE, + width: 0.0, + radius: (8.0).into(), + }, + shadow: Shadow::default(), + } + }), + ); + } + + // TODO: Add a function to `Wallet` to check whether we can safely leave a federation. + // Call it here rather and get rid of `has_zero_balance`. + let has_zero_balance = self.view.balance.msats == 0; + + if !has_zero_balance { + container = container.push( + Text::new("Must have a zero balance in this federation in order to leave.") + .size(20), + ); + } + + container = container.push( + icon_button("Leave Federation", SvgIcon::Delete, PaletteColor::Danger).on_press_maybe( + has_zero_balance.then(|| { + app::Message::Routes(routes::Message::BitcoinWalletPage( + super::Message::FederationDetails(Message::LeaveFederation( + self.view.federation_id, + )), + )) + }), + ), + ); + + container = container.push( + icon_button("Back", SvgIcon::ArrowBack, PaletteColor::Background).on_press( + app::Message::Routes(routes::Message::Navigate(routes::RouteName::BitcoinWallet( + super::SubrouteName::List, + ))), + ), + ); + + container + } +}