notedeck

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

commit 7c7bd5afdaf76f93acd546ed79ed438546e92d03
parent 25994522c8c5245f59fd58dccdb5083694c852b2
Author: kernelkind <kernelkind@gmail.com>
Date:   Tue, 24 Feb 2026 22:41:51 -0500

feat(outbox-int): route remote thread subs through scoped subs

Replace direct RelayPool subscribe/unsubscribe calls in thread subscriptions with ScopedSubApi declarations while preserving the existing local remote bookkeeping and depender semantics for now.

This is an intermediate migration step: thread_sub.rs now declares remote reply subscriptions through scoped subs, and thread open/close call paths (actionbar, thread routing cleanup, nav/toolbar cleanup) plumb a ScopedSubApi handle from RemoteApi.

The commit intentionally keeps the remotes map and depender counting in place so the backend swap can be reviewed separately from the upcoming owner-lifecycle rewrite (per-scope owners) and per-account local bookkeeping changes.

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

Diffstat:
Mcrates/notedeck_columns/src/actionbar.rs | 3++-
Mcrates/notedeck_columns/src/nav.rs | 1+
Mcrates/notedeck_columns/src/route.rs | 5+++--
Mcrates/notedeck_columns/src/timeline/sub/thread_sub.rs | 73++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mcrates/notedeck_columns/src/timeline/thread.rs | 24+++++++++++++++---------
Mcrates/notedeck_columns/src/toolbar.rs | 1+
6 files changed, 72 insertions(+), 35 deletions(-)

diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs @@ -135,12 +135,13 @@ fn execute_note_action( tracing::error!("No thread selection for {}?", hex::encode(note_id.bytes())); break 'ex; }; + let mut scoped_subs = remote.scoped_subs(accounts); timeline_res = threads .open( ndb, txn, - pool, + &mut scoped_subs, &thread_selection, preview, col, diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs @@ -304,6 +304,7 @@ fn process_nav_resp( &mut app.view_state, ctx.ndb, ctx.legacy_pool, + &mut ctx.remote.scoped_subs(ctx.accounts), return_type, col, ); diff --git a/crates/notedeck_columns/src/route.rs b/crates/notedeck_columns/src/route.rs @@ -3,7 +3,7 @@ use enostr::{NoteId, Pubkey, RelayPool}; use nostrdb::Ndb; use notedeck::{ tr, Localization, NoteZapTargetOwned, ReplacementType, ReportTarget, RootNoteIdBuf, Router, - WalletType, + ScopedSubApi, WalletType, }; use std::ops::Range; @@ -800,6 +800,7 @@ pub fn cleanup_popped_route( view_state: &mut ViewState, ndb: &mut Ndb, pool: &mut RelayPool, + scoped_subs: &mut ScopedSubApi, return_type: ReturnType, col_index: usize, ) { @@ -810,7 +811,7 @@ pub fn cleanup_popped_route( } } Route::Thread(selection) => { - threads.close(ndb, pool, selection, return_type, col_index); + threads.close(ndb, scoped_subs, selection, return_type, col_index); } Route::EditProfile(pk) => { view_state.pubkey_to_profile_state.remove(pk); diff --git a/crates/notedeck_columns/src/timeline/sub/thread_sub.rs b/crates/notedeck_columns/src/timeline/sub/thread_sub.rs @@ -1,8 +1,8 @@ use egui_nav::ReturnType; -use enostr::{NoteId, RelayPool}; +use enostr::NoteId; use hashbrown::HashMap; use nostrdb::{Filter, Ndb, Subscription}; -use uuid::Uuid; +use notedeck::{RelaySelection, ScopedSubApi, ScopedSubIdentity, SubConfig, SubKey, SubOwnerKey}; use crate::timeline::{ sub::{ndb_sub, ndb_unsub}, @@ -22,14 +22,14 @@ type MetaId = usize; pub struct Remote { pub filter: Vec<Filter>, - subid: String, + owner: SubOwnerKey, dependers: usize, } impl std::fmt::Debug for Remote { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Remote") - .field("subid", &self.subid) + .field("owner", &self.owner) .field("dependers", &self.dependers) .finish() } @@ -51,12 +51,12 @@ impl ThreadSubs { pub fn subscribe( &mut self, ndb: &mut Ndb, - pool: &mut RelayPool, + scoped_subs: &mut ScopedSubApi<'_, '_>, meta_id: usize, id: &ThreadSelection, local_sub_filter: Vec<Filter>, new_scope: bool, - remote_sub_filter: impl FnOnce() -> Vec<Filter>, + remote_sub_filter: Vec<Filter>, ) { let cur_scopes = self.scopes.entry(meta_id).or_default(); @@ -72,7 +72,12 @@ impl ThreadSubs { hashbrown::hash_map::RawEntryMut::Vacant(entry) => { let (_, res) = entry.insert( NoteId::new(*id.root_id.bytes()), - sub_remote(pool, remote_sub_filter, id), + sub_remote( + scoped_subs, + &NoteId::new(*id.root_id.bytes()), + remote_sub_filter, + id, + ), ); res @@ -92,7 +97,7 @@ impl ThreadSubs { pub fn unsubscribe( &mut self, ndb: &mut Ndb, - pool: &mut RelayPool, + scoped_subs: &mut ScopedSubApi<'_, '_>, meta_id: usize, id: &ThreadSelection, return_type: ReturnType, @@ -126,8 +131,8 @@ impl ThreadSubs { .remotes .remove(&id.root_id.bytes()) .expect("code above should guarentee existence"); - tracing::debug!("Remotely unsubscribed: {}", remote.subid); - pool.unsubscribe(remote.subid); + tracing::debug!("Remotely unsubscribed: {:?}", remote.owner); + let _ = scoped_subs.drop_owner(remote.owner); } tracing::debug!( @@ -218,6 +223,23 @@ fn log_scope_root_mismatch(scope: &Scope, id: &ThreadSelection) { } } +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +enum ThreadScopedSub { + RepliesByRoot, +} + +fn thread_remote_sub_key(root_id: &RootNoteId) -> SubKey { + SubKey::builder(ThreadScopedSub::RepliesByRoot) + .with(*root_id.bytes()) + .finish() +} + +fn thread_remote_owner_key(root_id: &RootNoteId) -> SubOwnerKey { + SubOwnerKey::builder(ThreadScopedSub::RepliesByRoot) + .with(*root_id.bytes()) + .finish() +} + fn sub_current_scope( ndb: &mut Ndb, selection: &ThreadSelection, @@ -245,25 +267,30 @@ fn sub_current_scope( } fn sub_remote( - pool: &mut RelayPool, - remote_sub_filter: impl FnOnce() -> Vec<Filter>, + scoped_subs: &mut ScopedSubApi<'_, '_>, + root_id: &RootNoteId, + remote_sub_filter: Vec<Filter>, id: impl std::fmt::Debug, ) -> Remote { - let subid = Uuid::new_v4().to_string(); - - let filter = remote_sub_filter(); - - let remote = Remote { - filter: filter.clone(), - subid: subid.clone(), - dependers: 0, - }; + let filter = remote_sub_filter; + let owner = thread_remote_owner_key(root_id); + let key = thread_remote_sub_key(root_id); tracing::debug!("Remote subscribe for {:?}", id); - pool.subscribe(subid, filter); + let identity = ScopedSubIdentity::global(owner, key); + let config = SubConfig { + relays: RelaySelection::AccountsRead, + filters: filter.clone(), + use_transparent: false, + }; + let _ = scoped_subs.ensure_sub(identity, config); - remote + Remote { + filter, + owner, + dependers: 0, + } } fn local_sub_new_scope( diff --git a/crates/notedeck_columns/src/timeline/thread.rs b/crates/notedeck_columns/src/timeline/thread.rs @@ -1,9 +1,9 @@ use egui_nav::ReturnType; use egui_virtual_list::VirtualList; -use enostr::{NoteId, RelayPool}; +use enostr::NoteId; use hashbrown::{hash_map::RawEntryMut, HashMap}; use nostrdb::{Filter, Ndb, Note, NoteKey, NoteReplyBuf, Transaction}; -use notedeck::{NoteCache, NoteRef, UnknownIds}; +use notedeck::{NoteCache, NoteRef, ScopedSubApi, UnknownIds}; use crate::{ actionbar::{process_thread_notes, NewThreadNotes}, @@ -66,7 +66,7 @@ impl Threads { &mut self, ndb: &mut Ndb, txn: &Transaction, - pool: &mut RelayPool, + scoped_subs: &mut ScopedSubApi<'_, '_>, thread: &ThreadSelection, new_scope: bool, col: usize, @@ -113,10 +113,15 @@ impl Threads { .collect::<Vec<_>>() }); - self.subs - .subscribe(ndb, pool, col, thread, local_sub_filter, new_scope, || { - replies_filter_remote(thread) - }); + self.subs.subscribe( + ndb, + scoped_subs, + col, + thread, + local_sub_filter, + new_scope, + replies_filter_remote(thread), + ); new_notes.map(|notes| NewThreadNotes { selected_note_id: NoteId::new(*selected_note_id), @@ -127,13 +132,14 @@ impl Threads { pub fn close( &mut self, ndb: &mut Ndb, - pool: &mut RelayPool, + scoped_subs: &mut ScopedSubApi<'_, '_>, thread: &ThreadSelection, return_type: ReturnType, id: usize, ) { tracing::info!("Closing thread: {:?}", thread); - self.subs.unsubscribe(ndb, pool, id, thread, return_type); + self.subs + .unsubscribe(ndb, scoped_subs, id, thread, return_type); } /// Responsible for making sure the chain and the direct replies are up to date diff --git a/crates/notedeck_columns/src/toolbar.rs b/crates/notedeck_columns/src/toolbar.rs @@ -103,6 +103,7 @@ fn pop_to_root(app: &mut Damus, ctx: &mut AppContext, col_index: usize) { &mut app.view_state, ctx.ndb, ctx.legacy_pool, + &mut ctx.remote.scoped_subs(ctx.accounts), ReturnType::Click, col_index, );