Skip to content

Commit

Permalink
feat: show toasts in UI
Browse files Browse the repository at this point in the history
  • Loading branch information
tvolk131 committed Oct 1, 2024
1 parent 4294ded commit e4ab201
Show file tree
Hide file tree
Showing 12 changed files with 473 additions and 50 deletions.
1 change: 1 addition & 0 deletions assets/icons/close.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
41 changes: 34 additions & 7 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::{collections::BTreeMap, sync::Arc};
use fedimint_core::config::FederationId;
use iced::{
futures::StreamExt,
widget::{column, container, row, scrollable},
widget::{column, container, row, scrollable, stack},
Element, Length, Task,
};
use nip_55::nip_46::{Nip46OverNip55ServerStream, Nip46RequestApproval};
Expand All @@ -14,7 +14,7 @@ use crate::{
fedimint::{FederationView, Wallet},
nostr::{NostrModuleMessage, NostrState},
routes::{self, bitcoin_wallet, unlock, Loadable, Route, RouteName},
ui_components::sidebar,
ui_components::{sidebar, Toast, ToastManager, ToastStatus},
};

#[derive(Debug, Clone)]
Expand All @@ -39,16 +39,21 @@ pub enum Message {
),
ApproveFirstIncomingNip46Request,
RejectFirstIncomingNip46Request,

AddToast(Toast),
CloseToast(usize),
}

pub struct App {
pub page: Route,
toasts: Vec<Toast>,
}

impl Default for App {
fn default() -> Self {
Self {
page: Route::new_locked(),
toasts: Vec::new(),
}
}
}
Expand Down Expand Up @@ -97,10 +102,18 @@ impl App {
Task::none()
}
Message::CopyStringToClipboard(text) => {
// TODO: Display a toast stating whether the copy succeeded or failed.
let _ = arboard::Clipboard::new().map(|mut clipboard| clipboard.set_text(text));

Task::none()
match arboard::Clipboard::new().map(|mut clipboard| clipboard.set_text(text)) {
Ok(_) => Task::done(Message::AddToast(Toast {
title: "Copied to clipboard".to_string(),
body: "The text has been copied to your clipboard.".to_string(),
status: ToastStatus::Good,
})),
Err(e) => Task::done(Message::AddToast(Toast {
title: "Failed to copy to clipboard".to_string(),
body: e.to_string(),
status: ToastStatus::Bad,
})),
}
}
Message::IncomingNip46Request(data) => {
if let Some(connected_state) = self.page.get_connected_state_mut() {
Expand All @@ -127,6 +140,16 @@ impl App {
}
}

Task::none()
}
Message::AddToast(toast) => {
self.toasts.push(toast);

Task::none()
}
Message::CloseToast(index) => {
self.toasts.remove(index);

Task::none()
}
}
Expand All @@ -143,7 +166,11 @@ impl App {
content = Element::new(row![sidebar(self), content]);
};

container(content).center_y(Length::Fill).into()
let content: Element<_, _, _> = container(content).center_y(Length::Fill).into();
let toast_manager: Element<_, _, _> =
ToastManager::new(&self.toasts, Message::CloseToast).into();

stack![content, toast_manager].into()
}

pub fn subscription(&self) -> iced::Subscription<Message> {
Expand Down
8 changes: 1 addition & 7 deletions src/nostr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,12 @@ pub enum NostrModuleMessage {
DisconnectFromRelay(String),
}

#[derive(Clone)]
#[derive(Clone, Default)]
pub struct NostrModule {
client: nostr_sdk::Client,
}

impl NostrModule {
pub fn new() -> Self {
Self {
client: nostr_sdk::Client::default(),
}
}

pub fn update(&self, message: NostrModuleMessage) {
match message {
NostrModuleMessage::ConnectToRelay(url) => {
Expand Down
26 changes: 18 additions & 8 deletions src/routes/bitcoin_wallet/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use crate::{
app,
fedimint::{FederationView, Wallet},
routes::{self, container, Loadable, RouteName},
ui_components::{icon_button, PaletteColor, SvgIcon},
ui_components::{icon_button, PaletteColor, SvgIcon, Toast, ToastStatus},
};

use super::{ConnectedState, SubrouteName};
Expand All @@ -25,7 +25,7 @@ pub enum Message {
// Payment actions.
PayInvoice(Bolt11Invoice, FederationId),
PayInvoiceSucceeded(Bolt11Invoice),
PayInvoiceFailed(Bolt11Invoice),
PayInvoiceFailed((Bolt11Invoice, Arc<anyhow::Error>)),

UpdateFederationViews(BTreeMap<FederationId, FederationView>),
}
Expand Down Expand Up @@ -79,9 +79,11 @@ impl Page {
Ok(()) => app::Message::Routes(routes::Message::BitcoinWalletPage(
super::Message::Send(Message::PayInvoiceSucceeded(invoice)),
)),
// TODO: Display error to user. Probably a toast.
Err(_err) => app::Message::Routes(routes::Message::BitcoinWalletPage(
super::Message::Send(Message::PayInvoiceFailed(invoice)),
Err(err) => app::Message::Routes(routes::Message::BitcoinWalletPage(
super::Message::Send(Message::PayInvoiceFailed((
invoice,
Arc::from(err),
))),
)),
}
})
Expand All @@ -93,16 +95,24 @@ impl Page {
self.loadable_invoice_payment_or = Some(Loadable::Loaded(()));
}

Task::none()
Task::done(app::Message::AddToast(Toast {
title: "Payment succeeded".to_string(),
body: "Invoice was successfully paid".to_string(),
status: ToastStatus::Good,
}))
}
Message::PayInvoiceFailed(invoice) => {
Message::PayInvoiceFailed((invoice, err)) => {
let invoice_or = Bolt11Invoice::from_str(&self.lightning_invoice_input).ok();

if Some(invoice) == invoice_or {
self.loadable_invoice_payment_or = Some(Loadable::Failed);
}

Task::none()
Task::done(app::Message::AddToast(Toast {
title: "Payment failed".to_string(),
body: format!("Failed to pay invoice: {err}"),
status: ToastStatus::Bad,
}))
}
Message::UpdateFederationViews(federation_views) => {
self.federation_combo_box_selected_federation = self
Expand Down
36 changes: 25 additions & 11 deletions src/routes/nostr_keypairs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use secp256k1::Secp256k1;

use crate::{
app,
ui_components::{icon_button, PaletteColor, SvgIcon},
ui_components::{icon_button, PaletteColor, SvgIcon, Toast, ToastStatus},
util::truncate_text,
};

Expand All @@ -33,12 +33,18 @@ pub struct Page {
impl Page {
pub fn update(&mut self, msg: Message) -> Task<app::Message> {
match msg {
Message::SaveKeypair(keypair) => {
// TODO: Surface this error to the UI.
let _ = self.connected_state.db.save_keypair(&keypair);

Task::none()
}
Message::SaveKeypair(keypair) => match self.connected_state.db.save_keypair(&keypair) {
Ok(()) => Task::done(app::Message::AddToast(Toast {
title: "Saved keypair".to_string(),
body: "The keypair was successfully saved.".to_string(),
status: ToastStatus::Good,
})),
Err(_err) => Task::done(app::Message::AddToast(Toast {
title: "Failed to save keypair".to_string(),
body: "The keypair was not saved.".to_string(),
status: ToastStatus::Bad,
})),
},
Message::SaveKeypairNsecInputChanged(new_nsec) => {
if let Subroute::Add(Add {
nsec, keypair_or, ..
Expand All @@ -55,10 +61,18 @@ impl Page {
Task::none()
}
Message::DeleteKeypair { public_key } => {
// TODO: Surface this error to the UI.
_ = self.connected_state.db.remove_keypair(&public_key);

Task::none()
match self.connected_state.db.remove_keypair(&public_key) {
Ok(()) => Task::done(app::Message::AddToast(Toast {
title: "Deleted keypair".to_string(),
body: "The keypair was successfully deleted.".to_string(),
status: ToastStatus::Good,
})),
Err(_err) => Task::done(app::Message::AddToast(Toast {
title: "Failed to delete keypair".to_string(),
body: "The keypair was not deleted.".to_string(),
status: ToastStatus::Bad,
})),
}
}
}
}
Expand Down
36 changes: 29 additions & 7 deletions src/routes/nostr_relays.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use nostr_sdk::Url;
use crate::{
app,
nostr::NostrModuleMessage,
ui_components::{icon_button, PaletteColor, SvgIcon},
ui_components::{icon_button, PaletteColor, SvgIcon, Toast, ToastStatus},
util::truncate_text,
};

Expand All @@ -32,13 +32,24 @@ impl Page {
pub fn update(&mut self, msg: Message) -> Task<app::Message> {
match msg {
Message::SaveRelay { websocket_url } => {
// TODO: Surface this error to the UI.
let _ = self.connected_state.db.save_relay(websocket_url.clone());
let task = match self.connected_state.db.save_relay(websocket_url.clone()) {
Ok(()) => Task::done(app::Message::AddToast(Toast {
title: "Saved relay".to_string(),
body: "The relay was successfully saved.".to_string(),
status: ToastStatus::Good,
})),
Err(_err) => Task::done(app::Message::AddToast(Toast {
title: "Failed to save relay".to_string(),
body: "The relay was not saved.".to_string(),
status: ToastStatus::Bad,
})),
};

self.connected_state
.nostr_module
.update(NostrModuleMessage::ConnectToRelay(websocket_url));

Task::none()
task
}
Message::SaveRelayWebsocketUrlInputChanged(new_websocket_url) => {
if let Subroute::Add(Add { websocket_url }) = &mut self.subroute {
Expand All @@ -48,13 +59,24 @@ impl Page {
Task::none()
}
Message::DeleteRelay { websocket_url } => {
// TODO: Surface this error to the UI.
_ = self.connected_state.db.remove_relay(&websocket_url);
let task = match self.connected_state.db.remove_relay(&websocket_url) {
Ok(()) => Task::done(app::Message::AddToast(Toast {
title: "Deleted relay".to_string(),
body: "The relay was successfully deleted.".to_string(),
status: ToastStatus::Good,
})),
Err(_err) => Task::done(app::Message::AddToast(Toast {
title: "Failed to delete relay".to_string(),
body: "The relay was not deleted.".to_string(),
status: ToastStatus::Bad,
})),
};

self.connected_state
.nostr_module
.update(NostrModuleMessage::DisconnectFromRelay(websocket_url));

Task::none()
task
}
}
}
Expand Down
22 changes: 13 additions & 9 deletions src/routes/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use iced::{

use crate::{
app,
ui_components::{icon_button, PaletteColor, SvgIcon},
ui_components::{icon_button, PaletteColor, SvgIcon, Toast, ToastStatus},
};

use super::{container, ConnectedState, RouteName};
Expand Down Expand Up @@ -56,20 +56,24 @@ impl Page {
current_password,
new_password,
} => {
if self
match self
.connected_state
.db
.change_password(&current_password, &new_password)
.is_ok()
{
// TODO: Show success in UI.

Task::done(app::Message::Routes(super::Message::Navigate(
Ok(()) => Task::done(app::Message::Routes(super::Message::Navigate(
RouteName::Settings(SubrouteName::Main),
)))
} else {
// TODO: Show error in UI.
Task::none()
.chain(Task::done(app::Message::AddToast(Toast {
title: "Password changed".to_string(),
body: "Your password has been changed.".to_string(),
status: ToastStatus::Good,
}))),
Err(_err) => Task::done(app::Message::AddToast(Toast {
title: "Failed to change password".to_string(),
body: "Check that you entered your current password correctly.".to_string(),
status: ToastStatus::Bad,
})),
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/routes/unlock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ impl Page {
wallet_clone.connect_to_joined_federations().await.unwrap();
});

let nostr_module = NostrModule::new();
let nostr_module = NostrModule::default();

// TODO: Add pagination.
let relays = db.list_relays(999, 0).unwrap();
Expand Down
43 changes: 43 additions & 0 deletions src/ui_components/button.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,49 @@ use crate::{

use super::{PaletteColor, SvgIcon};

pub fn mini_icon_button_no_text<'a>(
icon: SvgIcon,
palette_color: PaletteColor,
) -> Button<'a, app::Message, Theme> {
// TODO: Find a way to darken the icon color when the button is disabled.
let svg = icon.view(16.0, 16.0, Color::WHITE);

Button::new(svg)
.style(move |theme, status| {
let border = Border {
color: iced::Color::WHITE,
width: 0.0,
radius: (8.0).into(),
};

let mut bg_color = palette_color.to_color(theme);

if palette_color == PaletteColor::Background {
bg_color = darken(bg_color, 0.05);
}

bg_color = match status {
Status::Active => bg_color,
Status::Hovered => lighten(bg_color, 0.05),
Status::Pressed => lighten(bg_color, 0.1),
Status::Disabled => darken(bg_color, 0.5),
};

let mut text_color = Color::WHITE;
if status == Status::Disabled {
text_color = darken(text_color, 0.5);
}

button::Style {
background: Some(bg_color.into()),
text_color,
border,
shadow: Shadow::default(),
}
})
.padding(6)
}

pub fn icon_button(
text_str: &str,
icon: SvgIcon,
Expand Down
Loading

0 comments on commit e4ab201

Please sign in to comment.