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:
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;