Clarify & enforce selected-only behavior in Accounts subscription

Signed-off-by: kernelkind <kernelkind@gmail.com>
This commit is contained in:
kernelkind
2025-07-01 15:04:53 -04:00
parent f0158f71b2
commit a73596df48
7 changed files with 182 additions and 244 deletions

View File

@@ -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");
}

View File

@@ -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() {

View File

@@ -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) {

View File

@@ -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);

View File

@@ -136,7 +136,6 @@ pub fn process_accounts_view_response(
router.route_to(Route::add_account());
}
}
accounts.needs_relay_config();
action
}

View File

@@ -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(

View File

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