notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit a73596df48d7473c126e7ed59adf00282ef2e9fb
parent f0158f71b211fd0a47a7ce6b7b95d1be7be91104
Author: kernelkind <kernelkind@gmail.com>
Date:   Tue,  1 Jul 2025 15:04:53 -0400

Clarify & enforce selected-only behavior in `Accounts` subscription

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck/src/account/accounts.rs | 265++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Mcrates/notedeck/src/account/mute.rs | 44+-------------------------------------------
Mcrates/notedeck/src/account/relay.rs | 43+------------------------------------------
Mcrates/notedeck/src/app.rs | 26++++++++++++++------------
Mcrates/notedeck_columns/src/accounts/mod.rs | 1-
Mcrates/notedeck_columns/src/app.rs | 32+++++---------------------------
Mcrates/notedeck_columns/src/nav.rs | 15+++++++++++++--
7 files changed, 182 insertions(+), 244 deletions(-)

diff --git a/crates/notedeck/src/account/accounts.rs b/crates/notedeck/src/account/accounts.rs @@ -1,4 +1,5 @@ use tracing::debug; +use uuid::Uuid; use crate::account::cache::AccountCache; use crate::account::mute::AccountMutedData; @@ -8,7 +9,10 @@ use crate::account::relay::{ }; use crate::storage::AccountStorageWriter; use crate::user_account::UserAccountSerializable; -use crate::{AccountStorage, MuteFun, SingleUnkIdAction, UnknownIds, UserAccount, ZapWallet}; +use crate::{ + AccountStorage, MuteFun, SingleUnkIdAction, UnifiedSubscription, UnknownIds, UserAccount, + ZapWallet, +}; use enostr::{ClientMessage, FilledKeypair, Keypair, Pubkey, RelayPool}; use nostrdb::{Ndb, Note, Transaction}; @@ -21,16 +25,19 @@ pub struct Accounts { pub cache: AccountCache, storage_writer: Option<AccountStorageWriter>, relay_defaults: RelayDefaults, - needs_relay_config: bool, + subs: AccountSubs, } impl Accounts { + #[allow(clippy::too_many_arguments)] pub fn new( key_store: Option<AccountStorage>, forced_relays: Vec<String>, fallback: Pubkey, - ndb: &Ndb, + ndb: &mut Ndb, txn: &Transaction, + pool: &mut RelayPool, + ctx: &egui::Context, unknown_ids: &mut UnknownIds, ) -> Self { let (mut cache, unknown_id) = AccountCache::new(UserAccount::new( @@ -69,11 +76,25 @@ impl Accounts { let relay_defaults = RelayDefaults::new(forced_relays); + let selected = cache.selected(); + let selected_data = &selected.data; + + let subs = { + AccountSubs::new( + ndb, + pool, + &relay_defaults, + &selected.key.pubkey, + selected_data, + create_wakeup(ctx), + ) + }; + Accounts { cache, storage_writer, relay_defaults, - needs_relay_config: true, + subs, } } @@ -89,10 +110,6 @@ impl Accounts { } } - pub fn needs_relay_config(&mut self) { - self.needs_relay_config = true; - } - pub fn contains_full_kp(&self, pubkey: &enostr::Pubkey) -> bool { self.cache .get(pubkey) @@ -192,20 +209,31 @@ impl Accounts { &self.cache.selected().data } - fn get_selected_account_data_mut(&mut self) -> &mut AccountData { - &mut self.cache.selected_mut().data - } - - pub fn select_account(&mut self, pk: &Pubkey) { - if !self.cache.select(*pk) { + pub fn select_account( + &mut self, + pk_to_select: &Pubkey, + ndb: &mut Ndb, + pool: &mut RelayPool, + ctx: &egui::Context, + ) { + if !self.cache.select(*pk_to_select) { return; } if let Some(key_store) = &self.storage_writer { - if let Err(e) = key_store.select_key(Some(*pk)) { - tracing::error!("Could not select key {:?}: {e}", pk); + if let Err(e) = key_store.select_key(Some(*pk_to_select)) { + tracing::error!("Could not select key {:?}: {e}", pk_to_select); } } + + self.subs.swap_to( + ndb, + pool, + &self.relay_defaults, + pk_to_select, + &self.cache.selected().data, + create_wakeup(ctx), + ); } pub fn mutefun(&self) -> Box<MuteFun> { @@ -216,66 +244,53 @@ impl Accounts { } pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) { - for data in (&self.cache).into_iter().map(|(_, acc)| &acc.data) { - // send the active account's relay list subscription - if let Some(relay_subid) = &data.relay.subid { - pool.send_to( - &ClientMessage::req(relay_subid.clone(), vec![data.relay.filter.clone()]), - relay_url, - ); - } - // send the active account's muted subscription - if let Some(muted_subid) = &data.muted.subid { - pool.send_to( - &ClientMessage::req(muted_subid.clone(), vec![data.muted.filter.clone()]), - relay_url, - ); - } - } - } - - // Return accounts which have no account_data yet (added) and accounts - // which have still data but are no longer in our account list (removed). - fn delta_accounts(&self) -> (Vec<[u8; 32]>, Vec<[u8; 32]>) { - let mut added = Vec::new(); - for pubkey in (&self.cache).into_iter().map(|(pk, _)| pk.bytes()) { - if !self.cache.contains(pubkey) { - added.push(*pubkey); - } - } - let mut removed = Vec::new(); - for (pubkey, _) in &self.cache { - if self.cache.get_bytes(pubkey).is_none() { - removed.push(**pubkey); - } - } - (added, removed) + let data = &self.get_selected_account().data; + // send the active account's relay list subscription + pool.send_to( + &ClientMessage::req( + self.subs.relay.remote.clone(), + vec![data.relay.filter.clone()], + ), + relay_url, + ); + // send the active account's muted subscription + pool.send_to( + &ClientMessage::req( + self.subs.mute.remote.clone(), + vec![data.muted.filter.clone()], + ), + relay_url, + ); } fn poll_for_updates(&mut self, ndb: &Ndb) -> bool { let mut changed = false; - for (pubkey, data) in &mut self.cache.iter_mut().map(|(pk, a)| (pk, &mut a.data)) { - if let Some(sub) = data.relay.sub { - let nks = ndb.poll_for_notes(sub, 1); - if !nks.is_empty() { - let txn = Transaction::new(ndb).expect("txn"); - let relays = AccountRelayData::harvest_nip65_relays(ndb, &txn, &nks); - debug!("pubkey {}: updated relays {:?}", pubkey.hex(), relays); - data.relay.advertised = relays.into_iter().collect(); - changed = true; - } - } - if let Some(sub) = data.muted.sub { - let nks = ndb.poll_for_notes(sub, 1); - if !nks.is_empty() { - let txn = Transaction::new(ndb).expect("txn"); - let muted = AccountMutedData::harvest_nip51_muted(ndb, &txn, &nks); - debug!("pubkey {}: updated muted {:?}", pubkey.hex(), muted); - data.muted.muted = Arc::new(muted); - changed = true; - } - } + let relay_sub = self.subs.relay.local; + let mute_sub = self.subs.mute.local; + let acc = self.get_selected_account_mut(); + + let nks = ndb.poll_for_notes(relay_sub, 1); + if !nks.is_empty() { + let txn = Transaction::new(ndb).expect("txn"); + let relays = AccountRelayData::harvest_nip65_relays(ndb, &txn, &nks); + debug!( + "pubkey {}: updated relays {:?}", + acc.key.pubkey.hex(), + relays + ); + acc.data.relay.advertised = relays.into_iter().collect(); + changed = true; + } + + let nks = ndb.poll_for_notes(mute_sub, 1); + if !nks.is_empty() { + let txn = Transaction::new(ndb).expect("txn"); + let muted = AccountMutedData::harvest_nip51_muted(ndb, &txn, &nks); + debug!("pubkey {}: updated muted {:?}", acc.key.pubkey.hex(), muted); + acc.data.muted.muted = Arc::new(muted); + changed = true; } + changed } @@ -283,41 +298,8 @@ impl Accounts { // IMPORTANT - This function is called in the UI update loop, // make sure it is fast when idle - // On the initial update the relays need config even if nothing changes below - let mut need_reconfig = self.needs_relay_config; - - // Do we need to deactivate any existing account subs? - - let selected = self.cache.selected().key.pubkey; - - for (pk, account) in &mut self.cache.iter_mut() { - if *pk == selected { - continue; - } - - let data = &mut account.data; - // this account is not currently selected - if data.relay.sub.is_some() { - // this account has relay subs, deactivate them - data.relay.deactivate(ndb, pool); - } - if data.muted.sub.is_some() { - // this account has muted subs, deactivate them - data.muted.deactivate(ndb, pool); - } - } - - // Were any accounts added or removed? - let (added, removed) = self.delta_accounts(); - if !added.is_empty() || !removed.is_empty() { - need_reconfig = true; - } - - // Did any accounts receive updates (ie NIP-65 relay lists) - need_reconfig = self.poll_for_updates(ndb) || need_reconfig; - // If needed, update the relay configuration - if need_reconfig { + if self.poll_for_updates(ndb) { let acc = self.cache.selected(); update_relay_configuration( pool, @@ -326,18 +308,6 @@ impl Accounts { &acc.data, create_wakeup(ctx), ); - self.needs_relay_config = false; - } - - // Do we need to activate account subs? - let data = self.get_selected_account_data_mut(); - if data.relay.sub.is_none() { - // the currently selected account doesn't have relay subs, activate them - data.relay.activate(ndb, pool); - } - if data.muted.sub.is_none() { - // the currently selected account doesn't have muted subs, activate them - data.muted.activate(ndb, pool); } } @@ -443,3 +413,64 @@ pub struct AddAccountResponse { pub switch_to: Pubkey, pub unk_id_action: SingleUnkIdAction, } + +struct AccountSubs { + relay: UnifiedSubscription, + mute: UnifiedSubscription, +} + +impl AccountSubs { + pub fn new( + ndb: &mut Ndb, + pool: &mut RelayPool, + relay_defaults: &RelayDefaults, + pk: &Pubkey, + data: &AccountData, + wakeup: impl Fn() + Send + Sync + Clone + 'static, + ) -> Self { + let relay = subscribe(ndb, pool, &data.relay.filter); + let mute = subscribe(ndb, pool, &data.muted.filter); + update_relay_configuration(pool, relay_defaults, pk, data, wakeup); + + Self { relay, mute } + } + + pub fn swap_to( + &mut self, + ndb: &mut Ndb, + pool: &mut RelayPool, + relay_defaults: &RelayDefaults, + pk: &Pubkey, + new_selection_data: &AccountData, + wakeup: impl Fn() + Send + Sync + Clone + 'static, + ) { + unsubscribe(ndb, pool, &self.relay); + unsubscribe(ndb, pool, &self.mute); + + *self = AccountSubs::new(ndb, pool, relay_defaults, pk, new_selection_data, wakeup); + } +} + +fn subscribe(ndb: &Ndb, pool: &mut RelayPool, filter: &nostrdb::Filter) -> UnifiedSubscription { + let filters = vec![filter.clone()]; + let sub = ndb + .subscribe(&filters) + .expect("ndb relay list subscription"); + + // remote subscription + let subid = Uuid::new_v4().to_string(); + pool.subscribe(subid.clone(), filters); + + UnifiedSubscription { + local: sub, + remote: subid, + } +} + +fn unsubscribe(ndb: &mut Ndb, pool: &mut RelayPool, sub: &UnifiedSubscription) { + pool.unsubscribe(sub.remote.clone()); + + // local subscription + ndb.unsubscribe(sub.local) + .expect("ndb relay list unsubscribe"); +} diff --git a/crates/notedeck/src/account/mute.rs b/crates/notedeck/src/account/mute.rs @@ -1,16 +1,12 @@ use std::sync::Arc; -use enostr::RelayPool; -use nostrdb::{Filter, Ndb, NoteKey, Subscription, Transaction}; +use nostrdb::{Filter, Ndb, NoteKey, Transaction}; use tracing::{debug, error}; -use uuid::Uuid; use crate::Muted; pub(crate) struct AccountMutedData { pub filter: Filter, - pub subid: Option<String>, - pub sub: Option<Subscription>, pub muted: Arc<Muted>, } @@ -36,48 +32,10 @@ impl AccountMutedData { AccountMutedData { filter, - subid: None, - sub: None, muted: Arc::new(muted), } } - // make this account the current selected account - pub fn activate(&mut self, ndb: &Ndb, pool: &mut RelayPool) { - debug!("activating muted sub {}", self.filter.json().unwrap()); - assert_eq!(self.subid, None, "subid already exists"); - assert_eq!(self.sub, None, "sub already exists"); - - // local subscription - let sub = ndb - .subscribe(&[self.filter.clone()]) - .expect("ndb muted subscription"); - - // remote subscription - let subid = Uuid::new_v4().to_string(); - pool.subscribe(subid.clone(), vec![self.filter.clone()]); - - self.sub = Some(sub); - self.subid = Some(subid); - } - - // this account is no longer the selected account - pub fn deactivate(&mut self, ndb: &mut Ndb, pool: &mut RelayPool) { - debug!("deactivating muted sub {}", self.filter.json().unwrap()); - assert_ne!(self.subid, None, "subid doesn't exist"); - assert_ne!(self.sub, None, "sub doesn't exist"); - - // remote subscription - pool.unsubscribe(self.subid.as_ref().unwrap().clone()); - - // local subscription - ndb.unsubscribe(self.sub.unwrap()) - .expect("ndb muted unsubscribe"); - - self.sub = None; - self.subid = None; - } - pub(crate) fn harvest_nip51_muted(ndb: &Ndb, txn: &Transaction, nks: &[NoteKey]) -> Muted { let mut muted = Muted::default(); for nk in nks.iter() { diff --git a/crates/notedeck/src/account/relay.rs b/crates/notedeck/src/account/relay.rs @@ -1,17 +1,14 @@ use std::collections::BTreeSet; use enostr::{Keypair, Pubkey, RelayPool}; -use nostrdb::{Filter, Ndb, NoteBuilder, NoteKey, Subscription, Transaction}; +use nostrdb::{Filter, Ndb, NoteBuilder, NoteKey, Transaction}; use tracing::{debug, error, info}; use url::Url; -use uuid::Uuid; use crate::{AccountData, RelaySpec}; pub(crate) struct AccountRelayData { pub filter: Filter, - pub subid: Option<String>, - pub sub: Option<Subscription>, pub local: BTreeSet<RelaySpec>, // used locally but not advertised pub advertised: BTreeSet<RelaySpec>, // advertised via NIP-65 } @@ -42,49 +39,11 @@ impl AccountRelayData { AccountRelayData { filter, - subid: None, - sub: None, local: BTreeSet::new(), advertised: relays.into_iter().collect(), } } - // make this account the current selected account - pub fn activate(&mut self, ndb: &Ndb, pool: &mut RelayPool) { - debug!("activating relay sub {}", self.filter.json().unwrap()); - assert_eq!(self.subid, None, "subid already exists"); - assert_eq!(self.sub, None, "sub already exists"); - - // local subscription - let sub = ndb - .subscribe(&[self.filter.clone()]) - .expect("ndb relay list subscription"); - - // remote subscription - let subid = Uuid::new_v4().to_string(); - pool.subscribe(subid.clone(), vec![self.filter.clone()]); - - self.sub = Some(sub); - self.subid = Some(subid); - } - - // this account is no longer the selected account - pub fn deactivate(&mut self, ndb: &mut Ndb, pool: &mut RelayPool) { - debug!("deactivating relay sub {}", self.filter.json().unwrap()); - assert_ne!(self.subid, None, "subid doesn't exist"); - assert_ne!(self.sub, None, "sub doesn't exist"); - - // remote subscription - pool.unsubscribe(self.subid.as_ref().unwrap().clone()); - - // local subscription - ndb.unsubscribe(self.sub.unwrap()) - .expect("ndb relay list unsubscribe"); - - self.sub = None; - self.subid = None; - } - // standardize the format (ie, trailing slashes) to avoid dups pub fn canonicalize_url(url: &str) -> String { match Url::parse(url) { diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs @@ -176,16 +176,27 @@ impl Notedeck { None }; + // AccountManager will setup the pool on first update + let mut pool = RelayPool::new(); + { + let ctx = ctx.clone(); + if let Err(err) = pool.add_multicast_relay(move || ctx.request_repaint()) { + error!("error setting up multicast relay: {err}"); + } + } + let mut unknown_ids = UnknownIds::default(); - let ndb = Ndb::new(&dbpath_str, &config).expect("ndb"); + let mut ndb = Ndb::new(&dbpath_str, &config).expect("ndb"); let txn = Transaction::new(&ndb).expect("txn"); let mut accounts = Accounts::new( keystore, parsed_args.relays.clone(), FALLBACK_PUBKEY(), - &ndb, + &mut ndb, &txn, + &mut pool, + ctx, &mut unknown_ids, ); @@ -200,16 +211,7 @@ impl Notedeck { } if let Some(first) = parsed_args.keys.first() { - accounts.select_account(&first.pubkey); - } - - // AccountManager will setup the pool on first update - let mut pool = RelayPool::new(); - { - let ctx = ctx.clone(); - if let Err(err) = pool.add_multicast_relay(move || ctx.request_repaint()) { - error!("error setting up multicast relay: {err}"); - } + accounts.select_account(&first.pubkey, &mut ndb, &mut pool, ctx); } let img_cache = Images::new(img_cache_dir); diff --git a/crates/notedeck_columns/src/accounts/mod.rs b/crates/notedeck_columns/src/accounts/mod.rs @@ -136,7 +136,6 @@ pub fn process_accounts_view_response( router.route_to(Route::add_account()); } } - accounts.needs_relay_config(); action } diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -14,18 +14,15 @@ use crate::{ Result, }; -use notedeck::{ - Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, UnknownIds, - FALLBACK_PUBKEY, -}; +use notedeck::{Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, UnknownIds}; use notedeck_ui::{jobs::JobsCache, NoteOptions}; -use enostr::{ClientMessage, Keypair, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; +use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; use uuid::Uuid; use egui_extras::{Size, StripBuilder}; -use nostrdb::{Ndb, Transaction}; +use nostrdb::Transaction; use std::collections::{BTreeSet, HashMap}; use std::path::Path; @@ -431,7 +428,6 @@ impl Damus { for (pk, _) in &ctx.accounts.cache { cache.add_deck_default(*pk); } - set_demo(&mut cache, ctx.ndb, ctx.accounts, ctx.unknown_ids); cache }; @@ -697,7 +693,8 @@ fn timelines_view( // StripBuilder rendering let mut save_cols = false; if let Some(action) = side_panel_action { - save_cols = save_cols || action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx); + save_cols = save_cols + || action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx, ui.ctx()); } let mut app_action: Option<AppAction> = None; @@ -762,25 +759,6 @@ pub fn get_decks_mut<'a>(accounts: &Accounts, decks_cache: &'a mut DecksCache) - decks_cache.decks_mut(accounts.selected_account_pubkey()) } -pub fn set_demo( - decks_cache: &mut DecksCache, - ndb: &Ndb, - accounts: &mut Accounts, - unk_ids: &mut UnknownIds, -) { - let fallback = decks_cache.get_fallback_pubkey(); - let txn = Transaction::new(ndb).expect("txn"); - if let Some(resp) = accounts.add_account( - ndb, - &txn, - Keypair::only_pubkey(*decks_cache.get_fallback_pubkey()), - ) { - let txn = Transaction::new(ndb).expect("txn"); - resp.unk_id_action.process_action(unk_ids, ndb, &txn); - } - accounts.select_account(fallback); -} - fn columns_to_decks_cache(cols: Columns, key: &[u8; 32]) -> DecksCache { let mut account_to_decks: HashMap<Pubkey, Decks> = Default::default(); let decks = Decks::new(crate::decks::Deck::new_with_columns( diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs @@ -74,11 +74,17 @@ impl SwitchingAction { timeline_cache: &mut TimelineCache, decks_cache: &mut DecksCache, ctx: &mut AppContext<'_>, + ui_ctx: &egui::Context, ) -> bool { match &self { SwitchingAction::Accounts(account_action) => match account_action { AccountsAction::Switch(switch_action) => { - ctx.accounts.select_account(&switch_action.switch_to); + ctx.accounts.select_account( + &switch_action.switch_to, + ctx.ndb, + ctx.pool, + ui_ctx, + ); // pop nav after switch get_active_columns_mut(ctx.accounts, decks_cache) .column_mut(switch_action.source_column) @@ -374,7 +380,12 @@ fn process_render_nav_action( } RenderNavAction::SwitchingAction(switching_action) => { - if switching_action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx) { + if switching_action.process( + &mut app.timeline_cache, + &mut app.decks_cache, + ctx, + ui.ctx(), + ) { return Some(ProcessNavResult::SwitchOccurred); } else { return None;