notedeck

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

commit b0f187fee6732628b481cbfa85732e8ea95a3154
parent 4922bcd48b52377ff8b1ef20ab292872f9c5a161
Author: kernelkind <kernelkind@gmail.com>
Date:   Wed, 25 Feb 2026 19:27:27 -0500

feat(outbox-int): migrate accounts to use outbox

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

Diffstat:
Mcrates/notedeck/src/account/accounts.rs | 412+++++++++++++++++++++++++++++++++++++++----------------------------------------
Mcrates/notedeck/src/account/relay.rs | 131+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mcrates/notedeck/src/app.rs | 12+++---------
Mcrates/notedeck/src/context.rs | 19+++++++++++++++++--
Mcrates/notedeck/src/lib.rs | 2+-
Mcrates/notedeck/src/relayspec.rs | 16+++++++---------
Mcrates/notedeck_columns/src/app.rs | 22+++-------------------
Mcrates/notedeck_columns/src/nav.rs | 42+++++-------------------------------------
8 files changed, 308 insertions(+), 348 deletions(-)

diff --git a/crates/notedeck/src/account/accounts.rs b/crates/notedeck/src/account/accounts.rs @@ -1,22 +1,22 @@ -use hashbrown::HashSet; -use uuid::Uuid; - use crate::account::cache::AccountCache; use crate::account::contacts::Contacts; use crate::account::mute::AccountMutedData; use crate::account::relay::{ - modify_advertised_relays, update_relay_configuration, AccountRelayData, RelayAction, + calculate_relays, modify_advertised_relays, write_relays, AccountRelayData, RelayAction, RelayDefaults, }; +use crate::scoped_subs::{RelaySelection, ScopedSubIdentity, SubConfig, SubKey}; use crate::storage::AccountStorageWriter; use crate::user_account::UserAccountSerializable; use crate::{ - AccountStorage, MuteFun, SingleUnkIdAction, UnifiedSubscription, UnknownIds, UserAccount, - ZapWallet, + AccountStorage, MuteFun, RemoteApi, ScopedSubApi, SingleUnkIdAction, SubOwnerKey, UnknownIds, + UserAccount, ZapWallet, }; -use enostr::{ClientMessage, FilledKeypair, Keypair, NormRelayUrl, Pubkey, RelayId, RelayPool}; -use nostrdb::{Ndb, Note, Transaction}; +use enostr::{FilledKeypair, Keypair, NormRelayUrl, Pubkey, RelayId}; +use hashbrown::HashSet; +use nostrdb::{Filter, Ndb, Note, Subscription, Transaction}; +use std::slice::from_ref; // TODO: remove this use std::sync::Arc; @@ -26,7 +26,8 @@ pub struct Accounts { pub cache: AccountCache, storage_writer: Option<AccountStorageWriter>, relay_defaults: RelayDefaults, - subs: AccountSubs, + ndb_subs: AccountNdbSubs, + scoped_remote_initialized: bool, } impl Accounts { @@ -37,8 +38,6 @@ impl Accounts { fallback: Pubkey, 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( @@ -79,31 +78,31 @@ impl Accounts { selected_data.query(ndb, txn); - let subs = { - AccountSubs::new( - ndb, - pool, - &relay_defaults, - &selected.key.pubkey, - selected_data, - create_wakeup(ctx), - ) - }; + let ndb_subs = AccountNdbSubs::new(ndb, selected_data); Accounts { cache, storage_writer, relay_defaults, - subs, + ndb_subs, + scoped_remote_initialized: false, } } - pub fn remove_account( + pub(crate) fn remove_account( + &mut self, + pk: &Pubkey, + ndb: &mut Ndb, + remote: &mut RemoteApi<'_>, + ) -> bool { + self.remove_account_internal(pk, ndb, remote) + } + + fn remove_account_internal( &mut self, pk: &Pubkey, ndb: &mut Ndb, - pool: &mut RelayPool, - ctx: &egui::Context, + remote: &mut RemoteApi<'_>, ) -> bool { let Some(resp) = self.cache.remove(pk) else { return false; @@ -118,8 +117,14 @@ impl Accounts { } if let Some(swap_to) = resp.swap_to { + let old_pk = resp.deleted.pubkey; let txn = Transaction::new(ndb).expect("txn"); - self.select_account_internal(&swap_to, ndb, &txn, pool, ctx); + self.select_account_internal(&swap_to, old_pk, ndb, &txn, remote); + } + + { + let mut scoped_subs = remote.scoped_subs(&*self); + clear_account_remote_subs_for_account(&mut scoped_subs, resp.deleted.pubkey); } true @@ -220,29 +225,40 @@ impl Accounts { &self.cache.selected().data } - pub fn select_account( + pub(crate) fn select_account( &mut self, pk_to_select: &Pubkey, ndb: &mut Ndb, txn: &Transaction, - pool: &mut RelayPool, - ctx: &egui::Context, + remote: &mut RemoteApi<'_>, ) { + self.select_account_internal_entry(pk_to_select, ndb, txn, remote); + } + + fn select_account_internal_entry( + &mut self, + pk_to_select: &Pubkey, + ndb: &mut Ndb, + txn: &Transaction, + remote: &mut RemoteApi<'_>, + ) { + let old_pk = *self.selected_account_pubkey(); + if !self.cache.select(*pk_to_select) { return; } - self.select_account_internal(pk_to_select, ndb, txn, pool, ctx); + self.select_account_internal(pk_to_select, old_pk, ndb, txn, remote); } /// Have already selected in `AccountCache`, updating other things fn select_account_internal( &mut self, pk_to_select: &Pubkey, + old_pk: Pubkey, ndb: &mut Ndb, txn: &Transaction, - pool: &mut RelayPool, - ctx: &egui::Context, + remote: &mut RemoteApi<'_>, ) { if let Some(key_store) = &self.storage_writer { if let Err(e) = key_store.select_key(Some(*pk_to_select)) { @@ -251,14 +267,11 @@ impl Accounts { } self.get_selected_account_mut().data.query(ndb, txn); - self.subs.swap_to( - ndb, - pool, - &self.relay_defaults, - pk_to_select, - &self.cache.selected().data, - create_wakeup(ctx), - ); + self.ndb_subs.swap_to(ndb, &self.cache.selected().data); + + remote.on_account_switched(old_pk, *pk_to_select, self); + + self.ensure_selected_account_remote_subs(remote); } pub fn mutefun(&self) -> Box<MuteFun> { @@ -279,106 +292,52 @@ impl Accounts { } } - pub fn send_initial_filters(&mut self, pool: &mut RelayPool, relay_url: &str) { - 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, - ); - pool.send_to( - &ClientMessage::req( - self.subs.contacts.remote.clone(), - vec![data.contacts.filter.clone()], - ), - relay_url, - ); - if let Some(cur_pk) = self.selected_filled().map(|s| s.pubkey) { - let giftwraps_filter = nostrdb::Filter::new() - .kinds([1059]) - .pubkeys([cur_pk.bytes()]) - .build(); - pool.send_to( - &ClientMessage::req(self.subs.giftwraps.remote.clone(), vec![giftwraps_filter]), - relay_url, - ); - } - } - - pub fn update(&mut self, ndb: &mut Ndb, pool: &mut RelayPool, ctx: &egui::Context) { + #[profiling::function] + pub fn update(&mut self, ndb: &mut Ndb, remote: &mut RemoteApi<'_>) { // IMPORTANT - This function is called in the UI update loop, // make sure it is fast when idle - let Some(update) = self + let relay_updated = self .cache .selected_mut() .data - .poll_for_updates(ndb, &self.subs) - else { + .poll_for_updates(ndb, &self.ndb_subs); + + if !self.scoped_remote_initialized { + self.ensure_selected_account_remote_subs(remote); return; - }; + } - match update { - // If needed, update the relay configuration - AccountDataUpdate::Relay => { - let acc = self.cache.selected(); - update_relay_configuration( - pool, - &self.relay_defaults, - &acc.key.pubkey, - &acc.data.relay, - create_wakeup(ctx), - ); - } + if !relay_updated { + return; } + + self.retarget_selected_account_read_relays(remote); } pub fn get_full<'a>(&'a self, pubkey: &Pubkey) -> Option<FilledKeypair<'a>> { self.cache.get(pubkey).and_then(|r| r.key.to_full()) } - pub fn process_relay_action( - &mut self, - ctx: &egui::Context, - pool: &mut RelayPool, - action: RelayAction, - ) { + pub(crate) fn process_relay_action(&mut self, remote: &mut RemoteApi<'_>, action: RelayAction) { let acc = self.cache.selected_mut(); - modify_advertised_relays(&acc.key, action, pool, &self.relay_defaults, &mut acc.data); - - update_relay_configuration( - pool, + modify_advertised_relays( + &acc.key, + action, + remote, &self.relay_defaults, - &acc.key.pubkey, - &acc.data.relay, - create_wakeup(ctx), + &mut acc.data, ); - } - pub fn get_subs(&self) -> &AccountSubs { - &self.subs + self.retarget_selected_account_read_relays(remote); } pub fn selected_account_read_relays(&self) -> HashSet<NormRelayUrl> { - self.get_selected_account() - .data - .relay - .advertised - .iter() - .filter(|r| r.is_readable()) - .filter_map(|r| NormRelayUrl::new(&r.url).ok()) - .collect() + calculate_relays( + &self.relay_defaults, + &self.get_selected_account_data().relay, + true, + ) } /// Return the selected account's advertised NIP-65 relays with marker metadata. @@ -389,14 +348,23 @@ impl Accounts { } pub fn selected_account_write_relays(&self) -> Vec<RelayId> { - self.get_selected_account() - .data - .relay - .advertised - .iter() - .filter(|r| r.is_writable()) - .filter_map(|r| Some(RelayId::Websocket(NormRelayUrl::new(&r.url).ok()?))) - .collect() + write_relays( + &self.relay_defaults, + &self.get_selected_account_data().relay, + ) + } + + fn ensure_selected_account_remote_subs(&mut self, remote: &mut RemoteApi<'_>) { + { + let mut scoped_subs = remote.scoped_subs(&*self); + ensure_selected_account_remote_subs_api(&mut scoped_subs, self); + } + self.scoped_remote_initialized = true; + } + + fn retarget_selected_account_read_relays(&mut self, remote: &mut RemoteApi<'_>) { + remote.retarget_selected_account_read_relays(self); + self.scoped_remote_initialized = true; } } @@ -414,13 +382,6 @@ impl<'a> AccType<'a> { } } -fn create_wakeup(ctx: &egui::Context) -> impl Fn() + Send + Sync + Clone + 'static { - let ctx = ctx.clone(); - move || { - ctx.request_repaint(); - } -} - fn add_account_from_storage( cache: &mut AccountCache, user_account_serializable: UserAccountSerializable, @@ -473,22 +434,16 @@ impl AccountData { } } - pub(super) fn poll_for_updates( - &mut self, - ndb: &Ndb, - subs: &AccountSubs, - ) -> Option<AccountDataUpdate> { + #[profiling::function] + pub(super) fn poll_for_updates(&mut self, ndb: &Ndb, ndb_subs: &AccountNdbSubs) -> bool { let txn = Transaction::new(ndb).expect("txn"); - let mut resp = None; - if self.relay.poll_for_updates(ndb, &txn, subs.relay.local) { - resp = Some(AccountDataUpdate::Relay); - } + let relay_updated = self.relay.poll_for_updates(ndb, &txn, ndb_subs.relay_ndb); - self.muted.poll_for_updates(ndb, &txn, subs.mute.local); + self.muted.poll_for_updates(ndb, &txn, ndb_subs.mute_ndb); self.contacts - .poll_for_updates(ndb, &txn, subs.contacts.local); + .poll_for_updates(ndb, &txn, ndb_subs.contacts_ndb); - resp + relay_updated } /// Note: query should be called as close to the subscription as possible @@ -499,90 +454,125 @@ impl AccountData { } } -pub(super) enum AccountDataUpdate { - Relay, -} - pub struct AddAccountResponse { pub switch_to: Pubkey, pub unk_id_action: SingleUnkIdAction, } -pub struct AccountSubs { - relay: UnifiedSubscription, - giftwraps: UnifiedSubscription, - mute: UnifiedSubscription, - pub contacts: UnifiedSubscription, +fn giftwrap_filter(pk: &Pubkey) -> Filter { + // TODO: since optimize + nostrdb::Filter::new() + .kinds([1059]) + .pubkeys([pk.bytes()]) + .build() } -impl AccountSubs { - pub(super) fn new( - ndb: &mut Ndb, - pool: &mut RelayPool, - relay_defaults: &RelayDefaults, - pk: &Pubkey, - data: &AccountData, - wakeup: impl Fn() + Send + Sync + Clone + 'static, - ) -> Self { - // TODO: since optimize - let giftwraps_filter = nostrdb::Filter::new() - .kinds([1059]) - .pubkeys([pk.bytes()]) - .build(); - - update_relay_configuration(pool, relay_defaults, pk, &data.relay, wakeup); +fn account_remote_owner_key() -> SubOwnerKey { + SubOwnerKey::new("core/accounts/remote-subs") +} - let relay = subscribe(ndb, pool, &data.relay.filter); - let giftwraps = subscribe(ndb, pool, &giftwraps_filter); - let mute = subscribe(ndb, pool, &data.muted.filter); - let contacts = subscribe(ndb, pool, &data.contacts.filter); +fn ensure_selected_account_remote_subs_api( + scoped_subs: &mut ScopedSubApi<'_, '_>, + accounts: &Accounts, +) { + let owner = account_remote_owner_key(); + for kind in account_remote_sub_kinds() { + let key = account_remote_sub_key(kind); + let identity = ScopedSubIdentity::account(owner, key); + let config = selected_account_remote_config(accounts, kind); + let _ = scoped_subs.ensure_sub(identity, config); + } +} - Self { - relay, - mute, - contacts, - giftwraps, - } +fn clear_account_remote_subs_for_account( + scoped_subs: &mut ScopedSubApi<'_, '_>, + account_pk: Pubkey, +) { + let owner = account_remote_owner_key(); + for kind in account_remote_sub_kinds() { + let key = account_remote_sub_key(kind); + let identity = ScopedSubIdentity::account(owner, key); + let _ = scoped_subs.clear_sub_for_account(account_pk, identity); } +} - pub(super) 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); - unsubscribe(ndb, pool, &self.contacts); - unsubscribe(ndb, pool, &self.giftwraps); +#[derive(Clone, Copy, Eq, Hash, PartialEq)] +enum AccountRemoteSubKind { + RelayList, + MuteList, + ContactsList, + Giftwrap, +} - *self = AccountSubs::new(ndb, pool, relay_defaults, pk, new_selection_data, wakeup); - } +fn account_remote_sub_kinds() -> [AccountRemoteSubKind; 4] { + [ + AccountRemoteSubKind::RelayList, + AccountRemoteSubKind::MuteList, + AccountRemoteSubKind::ContactsList, + AccountRemoteSubKind::Giftwrap, + ] } -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"); +fn account_remote_sub_key(kind: AccountRemoteSubKind) -> SubKey { + SubKey::new(kind) +} - // remote subscription - let subid = Uuid::new_v4().to_string(); - pool.subscribe(subid.clone(), filters); +fn make_account_remote_config(filters: Vec<Filter>, use_transparent: bool) -> SubConfig { + SubConfig { + relays: RelaySelection::AccountsRead, + filters, + use_transparent, + } +} - UnifiedSubscription { - local: sub, - remote: subid, +fn selected_account_remote_config(accounts: &Accounts, kind: AccountRemoteSubKind) -> SubConfig { + let selected = accounts.get_selected_account_data(); + match kind { + AccountRemoteSubKind::RelayList => { + make_account_remote_config(vec![selected.relay.filter.clone()], false) + } + AccountRemoteSubKind::MuteList => { + make_account_remote_config(vec![selected.muted.filter.clone()], false) + } + AccountRemoteSubKind::ContactsList => { + make_account_remote_config(vec![selected.contacts.filter.clone()], true) + } + AccountRemoteSubKind::Giftwrap => make_account_remote_config( + vec![giftwrap_filter(accounts.selected_account_pubkey())], + false, + ), } } -fn unsubscribe(ndb: &mut Ndb, pool: &mut RelayPool, sub: &UnifiedSubscription) { - pool.unsubscribe(sub.remote.clone()); +struct AccountNdbSubs { + relay_ndb: Subscription, + mute_ndb: Subscription, + contacts_ndb: Subscription, +} + +impl AccountNdbSubs { + pub fn new(ndb: &mut Ndb, data: &AccountData) -> Self { + let relay_ndb = ndb + .subscribe(from_ref(&data.relay.filter)) + .expect("ndb relay list subscription"); + let mute_ndb = ndb + .subscribe(from_ref(&data.muted.filter)) + .expect("ndb sub"); + let contacts_ndb = ndb + .subscribe(from_ref(&data.contacts.filter)) + .expect("ndb sub"); + Self { + relay_ndb, + mute_ndb, + contacts_ndb, + } + } - // local subscription - ndb.unsubscribe(sub.local) - .expect("ndb relay list unsubscribe"); + pub fn swap_to(&mut self, ndb: &mut Ndb, new_selection_data: &AccountData) { + let _ = ndb.unsubscribe(self.relay_ndb); + let _ = ndb.unsubscribe(self.mute_ndb); + let _ = ndb.unsubscribe(self.contacts_ndb); + + *self = AccountNdbSubs::new(ndb, new_selection_data); + } } diff --git a/crates/notedeck/src/account/relay.rs b/crates/notedeck/src/account/relay.rs @@ -1,10 +1,10 @@ use std::collections::BTreeSet; -use crate::{AccountData, RelaySpec}; -use enostr::{Keypair, Pubkey, RelayPool}; -use nostrdb::{Filter, Ndb, NoteBuilder, NoteKey, Subscription, Transaction}; +use crate::{AccountData, RelaySpec, RemoteApi}; +use enostr::{Keypair, NormRelayUrl, RelayId}; +use hashbrown::HashSet; +use nostrdb::{Filter, Ndb, Note, NoteBuilder, NoteKey, Subscription, Transaction}; use tracing::{debug, error, info}; -use url::Url; #[derive(Clone)] pub(crate) struct AccountRelayData { @@ -47,14 +47,6 @@ impl AccountRelayData { self.advertised = relays.into_iter().collect() } - // standardize the format (ie, trailing slashes) to avoid dups - pub fn canonicalize_url(url: &str) -> String { - match Url::parse(url) { - Ok(parsed_url) => parsed_url.to_string(), - Err(_) => url.to_owned(), // If parsing fails, return the original URL. - } - } - pub(crate) fn harvest_nip65_relays( ndb: &Ndb, txn: &Transaction, @@ -73,8 +65,12 @@ impl AccountRelayData { let has_write_marker = tag .get(2) .is_some_and(|m| m.variant().str() == Some("write")); + + let Ok(norm_url) = NormRelayUrl::new(url) else { + continue; + }; relays.push(RelaySpec::new( - Self::canonicalize_url(url), + norm_url, has_read_marker, has_write_marker, )); @@ -96,20 +92,23 @@ impl AccountRelayData { relays } - pub fn publish_nip65_relays(&self, seckey: &[u8; 32], pool: &mut RelayPool) { + pub fn new_nip65_relays_note(&'_ self, seckey: &[u8; 32]) -> Note<'_> { let mut builder = NoteBuilder::new().kind(10002).content(""); for rs in &self.advertised { - builder = builder.start_tag().tag_str("r").tag_str(&rs.url); + builder = builder + .start_tag() + .tag_str("r") + .tag_str(&rs.url.to_string()); if rs.has_read_marker { builder = builder.tag_str("read"); } else if rs.has_write_marker { builder = builder.tag_str("write"); } } - let note = builder.sign(seckey).build().expect("note build"); - pool.send(&enostr::ClientMessage::event(&note).expect("note client message")); + builder.sign(seckey).build().expect("note build") } + #[profiling::function] pub fn poll_for_updates(&mut self, ndb: &Ndb, txn: &Transaction, sub: Subscription) -> bool { let nks = ndb.poll_for_notes(sub, 1); @@ -134,7 +133,7 @@ impl RelayDefaults { pub(crate) fn new(forced_relays: Vec<String>) -> Self { let forced_relays: BTreeSet<RelaySpec> = forced_relays .into_iter() - .map(|u| RelaySpec::new(AccountRelayData::canonicalize_url(&u), false, false)) + .filter_map(|u| Some(RelaySpec::new(NormRelayUrl::new(&u).ok()?, false, false))) .collect(); let bootstrap_relays = [ "wss://relay.damus.io", @@ -145,7 +144,7 @@ impl RelayDefaults { ] .iter() .map(|&url| url.to_string()) - .map(|u| RelaySpec::new(AccountRelayData::canonicalize_url(&u), false, false)) + .filter_map(|u| Some(RelaySpec::new(NormRelayUrl::new(&u).ok()?, false, false))) .collect(); Self { @@ -155,25 +154,40 @@ impl RelayDefaults { } } -pub(super) fn update_relay_configuration( - pool: &mut RelayPool, +pub fn calculate_relays( relay_defaults: &RelayDefaults, - pk: &Pubkey, data: &AccountRelayData, - wakeup: impl Fn() + Send + Sync + Clone + 'static, -) { - debug!( - "updating relay configuration for currently selected {:?}", - pk.hex() - ); - + readable: bool, // are we calculating the readable relays? or the writable? +) -> HashSet<NormRelayUrl> { // If forced relays are set use them only let mut desired_relays = relay_defaults.forced_relays.clone(); // Compose the desired relay lists from the selected account if desired_relays.is_empty() { - desired_relays.extend(data.local.iter().cloned()); - desired_relays.extend(data.advertised.iter().cloned()); + desired_relays.extend( + data.local + .iter() + .filter(|l| { + if readable { + l.is_readable() + } else { + l.is_writable() + } + }) + .cloned(), + ); + desired_relays.extend( + data.advertised + .iter() + .filter(|l| { + if readable { + l.is_readable() + } else { + l.is_writable() + } + }) + .cloned(), + ); } // If no relays are specified at this point use the bootstrap list @@ -181,33 +195,12 @@ pub(super) fn update_relay_configuration( desired_relays = relay_defaults.bootstrap_relays.clone(); } - debug!("current relays: {:?}", pool.urls()); debug!("desired relays: {:?}", desired_relays); - let pool_specs = pool - .urls() - .iter() - .map(|url| RelaySpec::new(url.clone(), false, false)) - .collect(); - let add: BTreeSet<RelaySpec> = desired_relays.difference(&pool_specs).cloned().collect(); - let mut sub: BTreeSet<RelaySpec> = pool_specs.difference(&desired_relays).cloned().collect(); - if !add.is_empty() { - debug!("configuring added relays: {:?}", add); - let _ = pool.add_urls(add.iter().map(|r| r.url.clone()).collect(), wakeup); - } - if !sub.is_empty() { - // certain relays are persistent like the multicast relay, - // although we should probably have a way to explicitly - // disable it - sub.remove(&RelaySpec::new("multicast", false, false)); - - debug!("removing unwanted relays: {:?}", sub); - pool.remove_urls(&sub.iter().map(|r| r.url.clone()).collect()); - } - - debug!("current relays: {:?}", pool.urls()); + desired_relays.into_iter().map(|r| r.url).collect() } +// TODO(kernelkind): these should have `NormRelayUrl` instead of `String`... pub enum RelayAction { Add(String), Remove(String), @@ -225,14 +218,18 @@ impl RelayAction { pub(super) fn modify_advertised_relays( kp: &Keypair, action: RelayAction, - pool: &mut RelayPool, + remote: &mut RemoteApi<'_>, relay_defaults: &RelayDefaults, account_data: &mut AccountData, ) { - let relay_url = AccountRelayData::canonicalize_url(action.get_url()); + let Ok(relay_url) = NormRelayUrl::new(action.get_url()) else { + return; + }; + + let relay_url_str = relay_url.to_string(); match action { - RelayAction::Add(_) => info!("add advertised relay \"{}\"", relay_url), - RelayAction::Remove(_) => info!("remove advertised relay \"{}\"", relay_url), + RelayAction::Add(_) => info!("add advertised relay \"{relay_url_str}\""), + RelayAction::Remove(_) => info!("remove advertised relay \"{relay_url_str}\""), } // let selected = self.cache.selected_mut(); @@ -254,8 +251,22 @@ pub(super) fn modify_advertised_relays( // If we have the secret key publish the NIP-65 relay list if let Some(secretkey) = &kp.secret_key { - account_data + let note = account_data .relay - .publish_nip65_relays(&secretkey.to_secret_bytes(), pool); + .new_nip65_relays_note(&secretkey.to_secret_bytes()); + + let mut publisher = remote.publisher_explicit(); + publisher.publish_note(&note, write_relays(relay_defaults, &account_data.relay)); } } + +pub fn write_relays(relay_defaults: &RelayDefaults, data: &AccountRelayData) -> Vec<RelayId> { + let mut relays: Vec<RelayId> = calculate_relays(relay_defaults, data, false) + .into_iter() + .map(RelayId::Websocket) + .collect(); + + relays.push(RelayId::Multicast); + + relays +} diff --git a/crates/notedeck/src/app.rs b/crates/notedeck/src/app.rs @@ -146,9 +146,7 @@ impl eframe::App for Notedeck { let mut app_ctx = self.app_context(ctx); // handle account updates - app_ctx - .accounts - .update(app_ctx.ndb, app_ctx.legacy_pool, ctx); + app_ctx.accounts.update(app_ctx.ndb, &mut app_ctx.remote); app_ctx .zaps @@ -287,8 +285,6 @@ impl Notedeck { FALLBACK_PUBKEY(), &mut ndb, &txn, - &mut legacy_pool, - ctx, &mut unknown_ids, ); @@ -307,11 +303,9 @@ impl Notedeck { } } - if let Some(first) = parsed_args.keys.first() { - accounts.select_account(&first.pubkey, &mut ndb, &txn, &mut legacy_pool, ctx); - } let outbox_session = if let Some(first) = parsed_args.keys.first() { - let remote = RemoteApi::new(outbox_session, &mut scoped_sub_state); + let mut remote = RemoteApi::new(outbox_session, &mut scoped_sub_state); + accounts.select_account(&first.pubkey, &mut ndb, &txn, &mut remote); remote.export_session() } else { outbox_session.export() diff --git a/crates/notedeck/src/context.rs b/crates/notedeck/src/context.rs @@ -5,8 +5,8 @@ use crate::{ }; use egui_winit::clipboard::Clipboard; -use enostr::RelayPool; -use nostrdb::Ndb; +use enostr::{Pubkey, RelayPool}; +use nostrdb::{Ndb, Transaction}; #[cfg(target_os = "android")] use android_activity::AndroidApp; @@ -53,6 +53,21 @@ impl SoftKeyboardContext { } impl<'a> AppContext<'a> { + pub fn select_account(&mut self, pubkey: &Pubkey) { + let txn = Transaction::new(self.ndb).expect("txn"); + self.accounts + .select_account(pubkey, self.ndb, &txn, &mut self.remote); + } + + pub fn remove_account(&mut self, pubkey: &Pubkey) -> bool { + self.accounts + .remove_account(pubkey, self.ndb, &mut self.remote) + } + + pub fn process_relay_action(&mut self, action: crate::RelayAction) { + self.accounts.process_relay_action(&mut self.remote, action); + } + pub fn soft_keyboard_rect(&self, screen_rect: Rect, ctx: SoftKeyboardContext) -> Option<Rect> { match ctx { SoftKeyboardContext::Virtual => { diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs @@ -53,7 +53,7 @@ mod user_account; mod wallet; mod zaps; -pub use account::accounts::{AccountData, AccountSubs, Accounts}; +pub use account::accounts::{AccountData, Accounts}; pub use account::contacts::{ContactState, IsFollowing}; pub use account::relay::RelayAction; pub use account::FALLBACK_PUBKEY; diff --git a/crates/notedeck/src/relayspec.rs b/crates/notedeck/src/relayspec.rs @@ -1,30 +1,28 @@ use std::cmp::Ordering; use std::fmt; +use enostr::NormRelayUrl; + // A Relay specification includes NIP-65 defined "markers" which // indicate if the relay should be used for reading or writing (or // both). #[derive(Clone)] pub struct RelaySpec { - pub url: String, + pub url: NormRelayUrl, pub has_read_marker: bool, pub has_write_marker: bool, } impl RelaySpec { - pub fn new( - url: impl Into<String>, - mut has_read_marker: bool, - mut has_write_marker: bool, - ) -> Self { + pub fn new(url: NormRelayUrl, mut has_read_marker: bool, mut has_write_marker: bool) -> Self { // if both markers are set turn both off ... if has_read_marker && has_write_marker { has_read_marker = false; has_write_marker = false; } RelaySpec { - url: url.into(), + url, has_read_marker, has_write_marker, } @@ -80,12 +78,12 @@ impl Eq for RelaySpec {} #[allow(clippy::non_canonical_partial_ord_impl)] impl PartialOrd for RelaySpec { fn partial_cmp(&self, other: &Self) -> Option<Ordering> { - Some(self.url.cmp(&other.url)) + Some(self.url.to_string().cmp(&other.url.to_string())) } } impl Ord for RelaySpec { fn cmp(&self, other: &Self) -> Ordering { - self.url.cmp(&other.url) + self.url.to_string().cmp(&other.url.to_string()) } } diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -751,14 +751,7 @@ fn render_damus_mobile( let kp = enostr::Keypair::only_pubkey(pubkey); let _ = app_ctx.accounts.add_account(kp); - let txn = nostrdb::Transaction::new(app_ctx.ndb).expect("txn"); - app_ctx.accounts.select_account( - &pubkey, - app_ctx.ndb, - &txn, - app_ctx.legacy_pool, - ui.ctx(), - ); + app_ctx.select_account(&pubkey); setup_selected_account_timeline_subs( &mut app.timeline_cache, app_ctx, @@ -1011,14 +1004,7 @@ 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, - &mut app.subscriptions, - ui.ctx(), - ); + save_cols = save_cols || action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx); } let mut app_action: Option<AppAction> = None; @@ -1039,9 +1025,7 @@ fn timelines_view( let kp = enostr::Keypair::only_pubkey(pubkey); let _ = ctx.accounts.add_account(kp); - let txn = nostrdb::Transaction::new(ctx.ndb).expect("txn"); - ctx.accounts - .select_account(&pubkey, ctx.ndb, &txn, ctx.legacy_pool, ui.ctx()); + ctx.select_account(&pubkey); setup_selected_account_timeline_subs(&mut app.timeline_cache, ctx); } diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs @@ -8,11 +8,9 @@ use crate::{ profile::{ProfileAction, SaveProfileChanges}, repost::RepostAction, route::{cleanup_popped_route, ColumnsRouter, Route, SingletonRouter}, - subscriptions::Subscriptions, timeline::{ - kind::ListKind, route::{render_thread_route, render_timeline_route}, - TimelineCache, TimelineKind, + TimelineCache, }, ui::{ self, @@ -90,31 +88,11 @@ impl SwitchingAction { timeline_cache: &mut TimelineCache, decks_cache: &mut DecksCache, ctx: &mut AppContext<'_>, - subs: &mut Subscriptions, - ui_ctx: &egui::Context, ) -> bool { match &self { SwitchingAction::Accounts(account_action) => match account_action { AccountsAction::Switch(switch_action) => { - { - let txn = Transaction::new(ctx.ndb).expect("txn"); - ctx.accounts.select_account( - &switch_action.switch_to, - ctx.ndb, - &txn, - ctx.legacy_pool, - ui_ctx, - ); - - let contacts_sub = ctx.accounts.get_subs().contacts.remote.clone(); - // this is cringe but we're gonna get a new sub manager soon... - subs.subs.insert( - contacts_sub, - crate::subscriptions::SubKind::FetchingContactList(TimelineKind::List( - ListKind::Contact(*ctx.accounts.selected_account_pubkey()), - )), - ); - } + ctx.select_account(&switch_action.switch_to); if switch_action.switching_to_new { decks_cache.add_deck_default(ctx, timeline_cache, switch_action.switch_to); @@ -129,10 +107,7 @@ impl SwitchingAction { .go_back(); } AccountsAction::Remove(to_remove) => 's: { - if !ctx - .accounts - .remove_account(to_remove, ctx.ndb, ctx.legacy_pool, ui_ctx) - { + if !ctx.remove_account(to_remove) { break 's; } @@ -607,13 +582,7 @@ fn process_render_nav_action( ) } RenderNavAction::SwitchingAction(switching_action) => { - if switching_action.process( - &mut app.timeline_cache, - &mut app.decks_cache, - ctx, - &mut app.subscriptions, - ui.ctx(), - ) { + if switching_action.process(&mut app.timeline_cache, &mut app.decks_cache, ctx) { return Some(ProcessNavResult::SwitchOccurred); } else { return None; @@ -632,8 +601,7 @@ fn process_render_nav_action( wallet_action.process(ctx.accounts, ctx.global_wallet) } RenderNavAction::RelayAction(action) => { - ctx.accounts - .process_relay_action(ui.ctx(), ctx.legacy_pool, action); + ctx.process_relay_action(action); None } RenderNavAction::SettingsAction(action) => {