From d22dd9ed31567a10b44b72cb2755099f57f5e148 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 17 Jul 2025 18:15:28 -0400 Subject: [PATCH 1/9] appease clippy ``` error: large size difference between variants --> crates/notedeck_columns/src/column.rs:249:1 | 249 | / pub enum IntermediaryRoute { 250 | | Timeline(Timeline), | | ------------------ the largest variant contains at least 280 bytes 251 | | Route(Route), | | ------------ the second-largest variant contains at least 72 bytes 252 | | } | |_^ the entire enum is at least 280 bytes | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#large_enum_variant = note: `-D clippy::large-enum-variant` implied by `-D warnings` = help: to override `-D warnings` add `#[allow(clippy::large_enum_variant)]` help: consider boxing the large fields to reduce the total size of the enum | 250 - Timeline(Timeline), 250 + Timeline(Box), | error: could not compile `notedeck_columns` (lib) due to 1 previous error ``` Signed-off-by: kernelkind --- crates/notedeck_columns/src/column.rs | 4 ++-- crates/notedeck_columns/src/storage/decks.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/notedeck_columns/src/column.rs b/crates/notedeck_columns/src/column.rs index dbc5c091..902cc029 100644 --- a/crates/notedeck_columns/src/column.rs +++ b/crates/notedeck_columns/src/column.rs @@ -124,7 +124,7 @@ impl Columns { IntermediaryRoute::Timeline(mut timeline) => { let route = Route::timeline(timeline.kind.clone()); timeline.subscription.increment(); - timeline_cache.insert(timeline.kind.clone(), timeline); + timeline_cache.insert(timeline.kind.clone(), *timeline); route } IntermediaryRoute::Route(route) => route, @@ -247,7 +247,7 @@ impl Columns { } pub enum IntermediaryRoute { - Timeline(Timeline), + Timeline(Box), Route(Route), } diff --git a/crates/notedeck_columns/src/storage/decks.rs b/crates/notedeck_columns/src/storage/decks.rs index c25b92d8..00f3c6e6 100644 --- a/crates/notedeck_columns/src/storage/decks.rs +++ b/crates/notedeck_columns/src/storage/decks.rs @@ -351,9 +351,9 @@ impl CleanIntermediaryRoute { match self { CleanIntermediaryRoute::ToTimeline(timeline_kind) => { let txn = Transaction::new(ndb).unwrap(); - Some(IntermediaryRoute::Timeline( + Some(IntermediaryRoute::Timeline(Box::new( timeline_kind.into_timeline(&txn, ndb)?, - )) + ))) } CleanIntermediaryRoute::ToRoute(route) => Some(IntermediaryRoute::Route(route)), } From 049bb3e8bbe5666bc0159d9bc017016149233949 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 17 Jul 2025 19:01:51 -0400 Subject: [PATCH 2/9] use `NwcError` instead of nwc::Error need to clone Signed-off-by: kernelkind --- crates/notedeck/src/wallet.rs | 50 +++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/crates/notedeck/src/wallet.rs b/crates/notedeck/src/wallet.rs index ff372894..4fae0627 100644 --- a/crates/notedeck/src/wallet.rs +++ b/crates/notedeck/src/wallet.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{fmt::Display, sync::Arc}; use nwc::{ nostr::nips::nip47::{NostrWalletConnectURI, PayInvoiceRequest, PayInvoiceResponse}, @@ -57,7 +57,7 @@ pub enum WalletError { pub struct Wallet { pub uri: String, wallet: Arc>, - balance: Option>>, + balance: Option>>, } #[derive(Clone)] @@ -95,7 +95,7 @@ impl Wallet { }) } - pub fn get_balance(&mut self) -> Option<&Result> { + pub fn get_balance(&mut self) -> Option<&Result> { if self.balance.is_none() { self.balance = Some(get_balance(self.wallet.clone())); return None; @@ -117,11 +117,51 @@ impl Wallet { } } -fn get_balance(nwc: Arc>) -> Promise> { +#[derive(Clone)] +pub enum NwcError { + /// NIP47 error + NIP47(String), + /// Relay + Relay(String), + /// Premature exit + PrematureExit, + /// Request timeout + Timeout, +} + +impl From for NwcError { + fn from(value: nwc::Error) -> Self { + match value { + nwc::error::Error::NIP47(error) => NwcError::NIP47(error.to_string()), + nwc::error::Error::Relay(error) => NwcError::Relay(error.to_string()), + nwc::error::Error::PrematureExit => NwcError::PrematureExit, + nwc::error::Error::Timeout => NwcError::Timeout, + } + } +} + +impl Display for NwcError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + NwcError::NIP47(err) => write!(f, "NIP47 error: {}", err), + NwcError::Relay(err) => write!(f, "Relay error: {}", err), + NwcError::PrematureExit => write!(f, "Premature exit"), + NwcError::Timeout => write!(f, "Request timed out"), + } + } +} + +fn get_balance(nwc: Arc>) -> Promise> { let (sender, promise) = Promise::new(); tokio::spawn(async move { - sender.send(nwc.read().await.get_balance().await); + sender.send( + nwc.read() + .await + .get_balance() + .await + .map_err(nwc::Error::into), + ); }); promise From b9cae65b72de14ee9cf16035a06983bf45da72f7 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 17 Jul 2025 19:24:01 -0400 Subject: [PATCH 3/9] make `UserAccount` cloneable Signed-off-by: kernelkind --- crates/notedeck/src/account/accounts.rs | 1 + crates/notedeck/src/account/contacts.rs | 2 ++ crates/notedeck/src/account/mute.rs | 1 + crates/notedeck/src/account/relay.rs | 1 + crates/notedeck/src/user_account.rs | 1 + crates/notedeck/src/wallet.rs | 12 +++++++++++- crates/notedeck/src/zaps/default_zap.rs | 6 +++--- 7 files changed, 20 insertions(+), 4 deletions(-) diff --git a/crates/notedeck/src/account/accounts.rs b/crates/notedeck/src/account/accounts.rs index 4c3c53c3..46002f53 100644 --- a/crates/notedeck/src/account/accounts.rs +++ b/crates/notedeck/src/account/accounts.rs @@ -375,6 +375,7 @@ fn get_acc_from_storage(user_account_serializable: UserAccountSerializable) -> O }) } +#[derive(Clone)] pub struct AccountData { pub(crate) relay: AccountRelayData, pub(crate) muted: AccountMutedData, diff --git a/crates/notedeck/src/account/contacts.rs b/crates/notedeck/src/account/contacts.rs index 0a7136a2..a2b57d6c 100644 --- a/crates/notedeck/src/account/contacts.rs +++ b/crates/notedeck/src/account/contacts.rs @@ -3,11 +3,13 @@ use std::collections::HashSet; use enostr::Pubkey; use nostrdb::{Filter, Ndb, Note, NoteKey, Subscription, Transaction}; +#[derive(Clone)] pub struct Contacts { pub filter: Filter, pub(super) state: ContactState, } +#[derive(Clone)] pub enum ContactState { Unreceived, Received { diff --git a/crates/notedeck/src/account/mute.rs b/crates/notedeck/src/account/mute.rs index 0e72672e..ee62bc5a 100644 --- a/crates/notedeck/src/account/mute.rs +++ b/crates/notedeck/src/account/mute.rs @@ -5,6 +5,7 @@ use tracing::{debug, error}; use crate::Muted; +#[derive(Clone)] pub(crate) struct AccountMutedData { pub filter: Filter, pub muted: Arc, diff --git a/crates/notedeck/src/account/relay.rs b/crates/notedeck/src/account/relay.rs index 036bb06c..03cb208a 100644 --- a/crates/notedeck/src/account/relay.rs +++ b/crates/notedeck/src/account/relay.rs @@ -7,6 +7,7 @@ use url::Url; use crate::{AccountData, RelaySpec}; +#[derive(Clone)] pub(crate) struct AccountRelayData { pub filter: Filter, pub local: BTreeSet, // used locally but not advertised diff --git a/crates/notedeck/src/user_account.rs b/crates/notedeck/src/user_account.rs index 9192e55f..031b1daf 100644 --- a/crates/notedeck/src/user_account.rs +++ b/crates/notedeck/src/user_account.rs @@ -6,6 +6,7 @@ use crate::{ AccountData, IsFollowing, }; +#[derive(Clone)] pub struct UserAccount { pub key: Keypair, pub wallet: Option, diff --git a/crates/notedeck/src/wallet.rs b/crates/notedeck/src/wallet.rs index 4fae0627..f16423b5 100644 --- a/crates/notedeck/src/wallet.rs +++ b/crates/notedeck/src/wallet.rs @@ -60,6 +60,16 @@ pub struct Wallet { balance: Option>>, } +impl Clone for Wallet { + fn clone(&self) -> Self { + Self { + uri: self.uri.clone(), + wallet: self.wallet.clone(), + balance: None, + } + } +} + #[derive(Clone)] pub struct WalletSerializable { pub uri: String, @@ -236,7 +246,7 @@ fn construct_global_wallet(wallet_handler: &TokenHandler) -> Option { Some(wallet) } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct ZapWallet { pub wallet: Wallet, pub default_zap: DefaultZapMsats, diff --git a/crates/notedeck/src/zaps/default_zap.rs b/crates/notedeck/src/zaps/default_zap.rs index 77d6385d..17409710 100644 --- a/crates/notedeck/src/zaps/default_zap.rs +++ b/crates/notedeck/src/zaps/default_zap.rs @@ -4,7 +4,7 @@ use crate::get_current_wallet; const DEFAULT_ZAP_MSATS: u64 = 10_000; -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct DefaultZapMsats { pub msats: Option, pub pending: PendingDefaultZapState, @@ -83,7 +83,7 @@ impl TokenSerializable for UserZapMsats { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct PendingDefaultZapState { pub amount_sats: String, pub error_message: Option, @@ -110,7 +110,7 @@ fn msats_to_sats_string(msats: u64) -> String { (msats / 1000).to_string() } -#[derive(Debug)] +#[derive(Debug, Clone)] pub enum DefaultZapError { InvalidUserInput, } From 8daa1d2adfabb8c6f18a1dfc453329ce75f1f98a Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 17 Jul 2025 19:24:33 -0400 Subject: [PATCH 4/9] allow removal of Damoose account Signed-off-by: kernelkind --- crates/notedeck/src/account/cache.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/notedeck/src/account/cache.rs b/crates/notedeck/src/account/cache.rs index 843a9193..43a0e319 100644 --- a/crates/notedeck/src/account/cache.rs +++ b/crates/notedeck/src/account/cache.rs @@ -6,6 +6,7 @@ use crate::{SingleUnkIdAction, UserAccount}; pub struct AccountCache { selected: Pubkey, fallback: Pubkey, + fallback_account: UserAccount, // never empty at rest accounts: HashMap, @@ -16,12 +17,13 @@ impl AccountCache { let mut accounts = HashMap::with_capacity(1); let pk = fallback.key.pubkey; - accounts.insert(pk, fallback); + accounts.insert(pk, fallback.clone()); ( Self { selected: pk, fallback: pk, + fallback_account: fallback, accounts, }, SingleUnkIdAction::pubkey(pk), @@ -49,13 +51,18 @@ impl AccountCache { } pub(super) fn remove(&mut self, pk: &Pubkey) -> Option { - // fallback account should never be removed - if *pk == self.fallback { + if *pk == self.fallback && self.accounts.len() == 1 { + // no point in removing it since it'll just get re-added anyway return None; } let removed = self.accounts.remove(pk); + if self.accounts.is_empty() { + self.accounts + .insert(self.fallback, self.fallback_account.clone()); + } + if removed.is_some() && self.selected == *pk { // TODO(kernelkind): choose next better let (next, _) = self From 8263e56f41caa7249691c90981310cdc7d41f856 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 17 Jul 2025 19:33:56 -0400 Subject: [PATCH 5/9] expose `AccountCache::falback` Signed-off-by: kernelkind --- crates/notedeck/src/account/cache.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/notedeck/src/account/cache.rs b/crates/notedeck/src/account/cache.rs index 43a0e319..6806baba 100644 --- a/crates/notedeck/src/account/cache.rs +++ b/crates/notedeck/src/account/cache.rs @@ -97,6 +97,10 @@ impl AccountCache { .get_mut(&self.selected) .expect("guarenteed that selected exists in accounts") } + + pub fn fallback(&self) -> &Pubkey { + &self.fallback + } } impl<'a> IntoIterator for &'a AccountCache { From 0b8a4fdf559ced0e21e9856df2f067d82c29c9f9 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 17 Jul 2025 20:22:48 -0400 Subject: [PATCH 6/9] move select account logic to own method Signed-off-by: kernelkind --- crates/notedeck/src/account/accounts.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/notedeck/src/account/accounts.rs b/crates/notedeck/src/account/accounts.rs index 46002f53..6874fd75 100644 --- a/crates/notedeck/src/account/accounts.rs +++ b/crates/notedeck/src/account/accounts.rs @@ -212,6 +212,18 @@ impl Accounts { return; } + self.select_account_internal(pk_to_select, ndb, txn, pool, ctx); + } + + /// Have already selected in `AccountCache`, updating other things + fn select_account_internal( + &mut self, + pk_to_select: &Pubkey, + ndb: &mut Ndb, + txn: &Transaction, + pool: &mut RelayPool, + ctx: &egui::Context, + ) { if let Some(key_store) = &self.storage_writer { if let Err(e) = key_store.select_key(Some(*pk_to_select)) { tracing::error!("Could not select key {:?}: {e}", pk_to_select); From d4082eb818613d348da4771ea18c96fc218bb1d4 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 17 Jul 2025 20:31:36 -0400 Subject: [PATCH 7/9] bugfix: properly sub to new selected acc after removal of selected Signed-off-by: kernelkind --- crates/notedeck/src/account/accounts.rs | 27 +++++++++++++++++++------ crates/notedeck/src/account/cache.rs | 21 +++++++++++++++---- crates/notedeck_columns/src/nav.rs | 5 ++++- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/crates/notedeck/src/account/accounts.rs b/crates/notedeck/src/account/accounts.rs index 6874fd75..faca5ef6 100644 --- a/crates/notedeck/src/account/accounts.rs +++ b/crates/notedeck/src/account/accounts.rs @@ -97,16 +97,31 @@ impl Accounts { } } - pub fn remove_account(&mut self, pk: &Pubkey) { - let Some(removed) = self.cache.remove(pk) else { - return; + pub fn remove_account( + &mut self, + pk: &Pubkey, + ndb: &mut Ndb, + pool: &mut RelayPool, + ctx: &egui::Context, + ) -> bool { + let Some(resp) = self.cache.remove(pk) else { + return false; }; - if let Some(key_store) = &self.storage_writer { - if let Err(e) = key_store.remove_key(&removed.key) { - tracing::error!("Could not remove account {pk}: {e}"); + if pk != self.cache.fallback() { + if let Some(key_store) = &self.storage_writer { + if let Err(e) = key_store.remove_key(&resp.deleted) { + tracing::error!("Could not remove account {pk}: {e}"); + } } } + + if let Some(swap_to) = resp.swap_to { + let txn = Transaction::new(ndb).expect("txn"); + self.select_account_internal(&swap_to, ndb, &txn, pool, ctx); + } + + true } pub fn contains_full_kp(&self, pubkey: &enostr::Pubkey) -> bool { diff --git a/crates/notedeck/src/account/cache.rs b/crates/notedeck/src/account/cache.rs index 6806baba..541b3f9f 100644 --- a/crates/notedeck/src/account/cache.rs +++ b/crates/notedeck/src/account/cache.rs @@ -50,20 +50,20 @@ impl AccountCache { self.accounts.entry(pk).insert(account) } - pub(super) fn remove(&mut self, pk: &Pubkey) -> Option { + pub(super) fn remove(&mut self, pk: &Pubkey) -> Option { if *pk == self.fallback && self.accounts.len() == 1 { // no point in removing it since it'll just get re-added anyway return None; } - let removed = self.accounts.remove(pk); + let removed = self.accounts.remove(pk)?; if self.accounts.is_empty() { self.accounts .insert(self.fallback, self.fallback_account.clone()); } - if removed.is_some() && self.selected == *pk { + if self.selected == *pk { // TODO(kernelkind): choose next better let (next, _) = self .accounts @@ -71,9 +71,17 @@ impl AccountCache { .next() .expect("accounts can never be empty"); self.selected = *next; + + return Some(AccountDeletionResponse { + deleted: removed.key, + swap_to: Some(*next), + }); } - removed + Some(AccountDeletionResponse { + deleted: removed.key, + swap_to: None, + }) } /// guarenteed that all selected exist in accounts @@ -111,3 +119,8 @@ impl<'a> IntoIterator for &'a AccountCache { self.accounts.iter() } } + +pub struct AccountDeletionResponse { + pub deleted: enostr::Keypair, + pub swap_to: Option, +} diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index 6879eb85..8e72dda9 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -94,7 +94,10 @@ impl SwitchingAction { .router_mut() .go_back(); } - AccountsAction::Remove(to_remove) => ctx.accounts.remove_account(to_remove), + AccountsAction::Remove(to_remove) => { + ctx.accounts + .remove_account(to_remove, ctx.ndb, ctx.pool, ui_ctx); + } }, SwitchingAction::Columns(columns_action) => match *columns_action { ColumnsAction::Remove(index) => { From 1c547bbcaa158eaa52d2c592289b2578caaabafa Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 17 Jul 2025 21:25:09 -0400 Subject: [PATCH 8/9] bugfix: unsubscribe from timelines on deck deletion Signed-off-by: kernelkind --- crates/notedeck_columns/src/decks.rs | 39 ++++++++++++++++++++++++++-- crates/notedeck_columns/src/nav.rs | 7 ++++- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/crates/notedeck_columns/src/decks.rs b/crates/notedeck_columns/src/decks.rs index 30def58b..a0d65a61 100644 --- a/crates/notedeck_columns/src/decks.rs +++ b/crates/notedeck_columns/src/decks.rs @@ -265,10 +265,25 @@ impl Decks { } } - pub fn remove_deck(&mut self, index: usize) { + pub fn remove_deck( + &mut self, + index: usize, + timeline_cache: &mut TimelineCache, + ndb: &mut nostrdb::Ndb, + pool: &mut enostr::RelayPool, + ) { + let Some(deck) = self.remove_deck_internal(index) else { + return; + }; + + delete_deck(deck, timeline_cache, ndb, pool); + } + + fn remove_deck_internal(&mut self, index: usize) -> Option { + let mut res = None; if index < self.decks.len() { if self.decks.len() > 1 { - self.decks.remove(index); + res = Some(self.decks.remove(index)); let info_prefix = format!("Removed deck at index {index}"); match index.cmp(&self.active_deck) { @@ -311,6 +326,26 @@ impl Decks { } else { error!("index was out of bounds"); } + res + } +} + +fn delete_deck( + mut deck: Deck, + timeline_cache: &mut TimelineCache, + ndb: &mut nostrdb::Ndb, + pool: &mut enostr::RelayPool, +) { + let cols = deck.columns_mut(); + let num_cols = cols.num_columns(); + for i in (0..num_cols).rev() { + let kinds_to_pop = cols.delete_column(i); + + for kind in &kinds_to_pop { + if let Err(err) = timeline_cache.pop(kind, ndb, pool) { + error!("error popping timeline: {err}"); + } + } } } diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index 8e72dda9..c2500e21 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -119,7 +119,12 @@ impl SwitchingAction { get_decks_mut(ctx.accounts, decks_cache).set_active(index) } DecksAction::Removing(index) => { - get_decks_mut(ctx.accounts, decks_cache).remove_deck(index) + get_decks_mut(ctx.accounts, decks_cache).remove_deck( + index, + timeline_cache, + ctx.ndb, + ctx.pool, + ); } }, } From 0b27282985008fb4addf31bc724d5cb092c987df Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 17 Jul 2025 21:31:10 -0400 Subject: [PATCH 9/9] bugfix: unsubscribe all decks when log out account Signed-off-by: kernelkind --- crates/notedeck_columns/src/decks.rs | 32 +++++++++++++++++++++++++--- crates/notedeck_columns/src/nav.rs | 12 ++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/crates/notedeck_columns/src/decks.rs b/crates/notedeck_columns/src/decks.rs index a0d65a61..6d42e066 100644 --- a/crates/notedeck_columns/src/decks.rs +++ b/crates/notedeck_columns/src/decks.rs @@ -1,6 +1,6 @@ use std::collections::{hash_map::ValuesMut, HashMap}; -use enostr::Pubkey; +use enostr::{Pubkey, RelayPool}; use nostrdb::Transaction; use notedeck::{AppContext, FALLBACK_PUBKEY}; use tracing::{error, info}; @@ -155,9 +155,24 @@ impl DecksCache { } } - pub fn remove_for(&mut self, key: &Pubkey) { + pub fn remove( + &mut self, + key: &Pubkey, + timeline_cache: &mut TimelineCache, + ndb: &mut nostrdb::Ndb, + pool: &mut RelayPool, + ) { + let Some(decks) = self.account_to_decks.remove(key) else { + return; + }; info!("Removing decks for {:?}", key); - self.account_to_decks.remove(key); + + decks.unsubscribe_all(timeline_cache, ndb, pool); + + if !self.account_to_decks.contains_key(&self.fallback_pubkey) { + self.account_to_decks + .insert(self.fallback_pubkey, Decks::default()); + } } pub fn get_fallback_pubkey(&self) -> &Pubkey { @@ -328,6 +343,17 @@ impl Decks { } res } + + pub fn unsubscribe_all( + self, + timeline_cache: &mut TimelineCache, + ndb: &mut nostrdb::Ndb, + pool: &mut enostr::RelayPool, + ) { + for deck in self.decks { + delete_deck(deck, timeline_cache, ndb, pool); + } + } } fn delete_deck( diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs index c2500e21..f35defb8 100644 --- a/crates/notedeck_columns/src/nav.rs +++ b/crates/notedeck_columns/src/nav.rs @@ -94,9 +94,15 @@ impl SwitchingAction { .router_mut() .go_back(); } - AccountsAction::Remove(to_remove) => { - ctx.accounts - .remove_account(to_remove, ctx.ndb, ctx.pool, ui_ctx); + AccountsAction::Remove(to_remove) => 's: { + if !ctx + .accounts + .remove_account(to_remove, ctx.ndb, ctx.pool, ui_ctx) + { + break 's; + } + + decks_cache.remove(to_remove, timeline_cache, ctx.ndb, ctx.pool); } }, SwitchingAction::Columns(columns_action) => match *columns_action {