notedeck

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

commit 86398fcd1263fce10f0c70de2f902ad9a5a0ee3d
parent f3065e6da9d3c6921618e5e0842d250985f0bc84
Author: kernelkind <kernelkind@gmail.com>
Date:   Wed, 25 Feb 2026 18:52:21 -0500

feat(outbox-int): only use one FilterState for timeline

eose tracking happens in outbox

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

Diffstat:
Mcrates/notedeck/src/filter.rs | 134++-----------------------------------------------------------------------------
Mcrates/notedeck/src/lib.rs | 2+-
Mcrates/notedeck_columns/src/app.rs | 191++++++++++++++++++-------------------------------------------------------------
Mcrates/notedeck_columns/src/nav.rs | 4+++-
Mcrates/notedeck_columns/src/timeline/cache.rs | 6+++---
Mcrates/notedeck_columns/src/timeline/mod.rs | 261+++++++++++++++++++++++++++----------------------------------------------------
Mcrates/notedeck_columns/src/ui/add_column.rs | 7++-----
7 files changed, 141 insertions(+), 464 deletions(-)

diff --git a/crates/notedeck/src/filter.rs b/crates/notedeck/src/filter.rs @@ -1,7 +1,6 @@ use crate::error::{Error, FilterError}; use crate::note::NoteRef; use nostrdb::{Filter, FilterBuilder, Note, Subscription}; -use std::collections::HashMap; use tracing::{debug, warn}; /// A unified subscription has a local and remote component. The remote subid @@ -12,130 +11,17 @@ pub struct UnifiedSubscription { pub remote: String, } -/// Each relay can have a different filter state. For example, some -/// relays may have the contact list, some may not. Let's capture all of -/// these states so that some relays don't stop the states of other -/// relays. -#[derive(Debug)] -pub struct FilterStates { - pub initial_state: FilterState, - pub states: HashMap<String, FilterState>, -} - -impl FilterStates { - pub fn get_mut(&mut self, relay: &str) -> &FilterState { - // if our initial state is ready, then just use that - if let FilterState::Ready(_) = self.initial_state { - &self.initial_state - } else { - // otherwise we look at relay states - if !self.states.contains_key(relay) { - self.states - .insert(relay.to_string(), self.initial_state.clone()); - } - self.states.get(relay).unwrap() - } - } - - pub fn get_any_gotremote(&self) -> Option<GotRemoteResult> { - for (k, v) in self.states.iter() { - if let FilterState::GotRemote(item_type) = v { - return match item_type { - GotRemoteType::Normal(subscription) => Some(GotRemoteResult::Normal { - relay_id: k.to_owned(), - sub_id: *subscription, - }), - GotRemoteType::Contact => Some(GotRemoteResult::Contact { - relay_id: k.to_owned(), - }), - GotRemoteType::PeopleList => Some(GotRemoteResult::PeopleList { - relay_id: k.to_owned(), - }), - }; - } - } - - None - } - - pub fn get_any_ready(&self) -> Option<&HybridFilter> { - if let FilterState::Ready(fs) = &self.initial_state { - Some(fs) - } else { - for (_k, v) in self.states.iter() { - if let FilterState::Ready(ref fs) = v { - return Some(fs); - } - } - - None - } - } - - pub fn new(initial_state: FilterState) -> Self { - Self { - initial_state, - states: HashMap::new(), - } - } - - pub fn set_relay_state(&mut self, relay: String, state: FilterState) { - if self.states.contains_key(&relay) { - let current_state = self.states.get(&relay).unwrap(); - debug!( - "set_relay_state: {:?} -> {:?} on {}", - current_state, state, &relay, - ); - } - self.states.insert(relay, state); - } - - /// For contacts, since that sub is managed elsewhere - pub fn set_all_states(&mut self, state: FilterState) { - for cur_state in self.states.values_mut() { - *cur_state = state.clone(); - } - } -} - /// We may need to fetch some data from relays before our filter is ready. /// [`FilterState`] tracks this. #[derive(Debug, Clone)] pub enum FilterState { NeedsRemote, - FetchingRemote(FetchingRemoteType), - GotRemote(GotRemoteType), + FetchingRemote, + GotRemote, Ready(HybridFilter), Broken(FilterError), } -pub enum GotRemoteResult { - Normal { - relay_id: String, - sub_id: Subscription, - }, - Contact { - relay_id: String, - }, - PeopleList { - relay_id: String, - }, -} - -#[derive(Debug, Clone)] -pub enum FetchingRemoteType { - Normal(UnifiedSubscription), - Contact, - PeopleList, -} - -#[derive(Debug, Clone)] -pub enum GotRemoteType { - Normal(Subscription), - Contact, - PeopleList, -} - impl FilterState { /// We tried to fetch a filter but we wither got no data or the data /// was corrupted, preventing us from getting to the Ready state. @@ -162,22 +48,6 @@ impl FilterState { pub fn needs_remote() -> Self { Self::NeedsRemote } - - /// We got the remote data. Local data should be available to build - /// the filter for the [`FilterState::Ready`] state - pub fn got_remote(local_sub: Subscription) -> Self { - Self::GotRemote(GotRemoteType::Normal(local_sub)) - } - - /// We have sent off a remote subscription to get data needed for the - /// filter. The string is the subscription id - pub fn fetching_remote(sub_id: String, local_sub: Subscription) -> Self { - let unified_sub = UnifiedSubscription { - local: local_sub, - remote: sub_id, - }; - Self::FetchingRemote(FetchingRemoteType::Normal(unified_sub)) - } } pub fn should_since_optimize(limit: u64, num_notes: usize) -> bool { diff --git a/crates/notedeck/src/lib.rs b/crates/notedeck/src/lib.rs @@ -63,7 +63,7 @@ pub use async_loader::{worker_count, AsyncLoader}; pub use context::{AppContext, SoftKeyboardContext}; use enostr::{OutboxSessionHandler, Wakeup}; pub use error::{show_one_error_message, Error, FilterError, ZapError}; -pub use filter::{FilterState, FilterStates, UnifiedSubscription}; +pub use filter::{FilterState, UnifiedSubscription}; pub use fonts::NamedFontFamily; pub use i18n::{CacheStats, FluentArgs, FluentValue, LanguageIdentifier, Localization}; pub use imgcache::{ diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -18,12 +18,12 @@ use crate::{ Result, }; use egui_extras::{Size, StripBuilder}; -use enostr::{ClientMessage, Pubkey, RelayEvent, RelayMessage}; +use enostr::Pubkey; use nostrdb::Transaction; use notedeck::{ - tr, try_process_events_core, ui::is_compiled_as_mobile, ui::is_narrow, Accounts, AppAction, - AppContext, AppResponse, DataPath, DataPathType, FilterState, Images, Localization, - MediaJobSender, NotedeckOptions, SettingsHandler, + tr, ui::is_compiled_as_mobile, ui::is_narrow, Accounts, AppAction, AppContext, AppResponse, + DataPath, DataPathType, FilterState, Images, Localization, MediaJobSender, NotedeckOptions, + SettingsHandler, }; use notedeck_ui::{ media::{MediaViewer, MediaViewerFlags, MediaViewerState}, @@ -185,27 +185,25 @@ fn try_process_event( ) }); - try_process_events_core(app_ctx, ctx, |app_ctx, ev| match (&ev.event).into() { - RelayEvent::Opened => { - let mut scoped_subs = app_ctx.remote.scoped_subs(app_ctx.accounts); - timeline::send_initial_timeline_filters( - damus.options.contains(AppOptions::SinceOptimize), - &mut damus.timeline_cache, - &mut damus.subscriptions, - app_ctx.legacy_pool, - &ev.relay, - app_ctx.accounts, - &mut scoped_subs, - ); + let selected_account_pk = *app_ctx.accounts.selected_account_pubkey(); + for (kind, timeline) in &mut damus.timeline_cache { + if timeline.subscription.dependers(&selected_account_pk) == 0 { + continue; } - RelayEvent::Message(msg) => { - process_message(damus, app_ctx, &ev.relay, &msg); + + if let FilterState::Ready(filter) = &timeline.filter { + if timeline.kind.should_subscribe_locally() + && timeline + .subscription + .get_local(&selected_account_pk) + .is_none() + { + timeline + .subscription + .try_add_local(selected_account_pk, app_ctx.ndb, filter); + } } - _ => {} - }); - for (kind, timeline) in &mut damus.timeline_cache { - let selected_account_pk = *app_ctx.accounts.selected_account_pubkey(); let is_ready = { let mut scoped_subs = app_ctx.remote.scoped_subs(app_ctx.accounts); timeline::is_timeline_ready(app_ctx.ndb, &mut scoped_subs, timeline, app_ctx.accounts) @@ -242,25 +240,14 @@ fn try_process_event( | TimelineKind::Algo(timeline::kind::AlgoTimeline::LastPerPubkey( ListKind::Contact(_), )) => { - timeline::fetch_contact_list( - &mut damus.subscriptions, - timeline, - app_ctx.accounts, - ); + timeline::fetch_contact_list(timeline, app_ctx.accounts); } - TimelineKind::List(ListKind::PeopleList(plr)) + TimelineKind::List(ListKind::PeopleList(_)) | TimelineKind::Algo(timeline::kind::AlgoTimeline::LastPerPubkey( - ListKind::PeopleList(plr), + ListKind::PeopleList(_), )) => { - let plr = plr.clone(); - for relay in &mut app_ctx.legacy_pool.relays { - timeline::fetch_people_list( - &mut damus.subscriptions, - relay, - timeline, - &plr, - ); - } + let txn = Transaction::new(app_ctx.ndb).expect("txn"); + timeline::fetch_people_list(app_ctx.ndb, &txn, timeline); } _ => {} } @@ -288,7 +275,7 @@ fn schedule_timeline_load( return; } - let Some(filter) = timeline.filter.get_any_ready().cloned() else { + let FilterState::Ready(filter) = timeline.filter.clone() else { return; }; @@ -359,15 +346,7 @@ fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Con .subscriptions() .insert("unknownids".to_string(), SubKind::OneShot); - if let Err(err) = timeline::setup_initial_nostrdb_subs( - app_ctx.ndb, - app_ctx.note_cache, - &mut damus.timeline_cache, - app_ctx.unknown_ids, - *app_ctx.accounts.selected_account_pubkey(), - ) { - warn!("update_damus init: {err}"); - } + setup_selected_account_timeline_subs(&mut damus.timeline_cache, app_ctx); if !app_ctx.settings.welcome_completed() { let split = @@ -396,109 +375,18 @@ fn update_damus(damus: &mut Damus, app_ctx: &mut AppContext<'_>, ctx: &egui::Con } } -fn handle_eose( - subscriptions: &Subscriptions, +pub(crate) fn setup_selected_account_timeline_subs( timeline_cache: &mut TimelineCache, - ctx: &mut AppContext<'_>, - subid: &str, - relay_url: &str, -) -> Result<()> { - let sub_kind = if let Some(sub_kind) = subscriptions.subs.get(subid) { - sub_kind - } else { - let n_subids = subscriptions.subs.len(); - warn!( - "got unknown eose subid {}, {} tracked subscriptions", - subid, n_subids - ); - return Ok(()); - }; - - match sub_kind { - SubKind::Timeline(_) => { - // eose on timeline? whatevs - } - SubKind::Initial => { - //let txn = Transaction::new(ctx.ndb)?; - //unknowns::update_from_columns( - // &txn, - // ctx.unknown_ids, - // timeline_cache, - // ctx.ndb, - // ctx.note_cache, - //); - //// this is possible if this is the first time - //if ctx.unknown_ids.ready_to_send() { - // unknown_id_send(ctx.unknown_ids, ctx.pool); - //} - } - - // oneshot subs just close when they're done - SubKind::OneShot => { - let msg = ClientMessage::close(subid.to_string()); - ctx.legacy_pool.send_to(&msg, relay_url); - } - - SubKind::FetchingContactList(timeline_uid) => { - let timeline = if let Some(tl) = timeline_cache.get_mut(timeline_uid) { - tl - } else { - error!( - "timeline uid:{:?} not found for FetchingContactList", - timeline_uid - ); - return Ok(()); - }; - - let filter_state = timeline.filter.get_mut(relay_url); - - let FilterState::FetchingRemote(fetching_remote_type) = filter_state else { - // TODO: we could have multiple contact list results, we need - // to check to see if this one is newer and use that instead - warn!( - "Expected timeline to have FetchingRemote state but was {:?}", - timeline.filter - ); - return Ok(()); - }; - - let new_filter_state = match fetching_remote_type { - notedeck::filter::FetchingRemoteType::Normal(unified_subscription) => { - FilterState::got_remote(unified_subscription.local) - } - notedeck::filter::FetchingRemoteType::Contact => { - FilterState::GotRemote(notedeck::filter::GotRemoteType::Contact) - } - notedeck::filter::FetchingRemoteType::PeopleList => { - FilterState::GotRemote(notedeck::filter::GotRemoteType::PeopleList) - } - }; - - // We take the subscription id and pass it to the new state of - // "GotRemote". This will let future frames know that it can try - // to look for the contact list in nostrdb. - timeline - .filter - .set_relay_state(relay_url.to_string(), new_filter_state); - } - } - - Ok(()) -} - -fn process_message(damus: &mut Damus, ctx: &mut AppContext<'_>, relay: &str, msg: &RelayMessage) { - let RelayMessage::Eose(sid) = msg else { - return; - }; - - if let Err(err) = handle_eose( - &damus.subscriptions, - &mut damus.timeline_cache, - ctx, - sid, - relay, + app_ctx: &mut AppContext<'_>, +) { + if let Err(err) = timeline::setup_initial_nostrdb_subs( + app_ctx.ndb, + app_ctx.note_cache, + timeline_cache, + app_ctx.unknown_ids, + *app_ctx.accounts.selected_account_pubkey(), ) { - error!("error handling eose: {}", err); + warn!("update_damus init: {err}"); } } @@ -871,6 +759,10 @@ fn render_damus_mobile( app_ctx.legacy_pool, ui.ctx(), ); + setup_selected_account_timeline_subs( + &mut app.timeline_cache, + app_ctx, + ); } ProcessNavResult::ExternalNoteAction(note_action) => { @@ -1150,6 +1042,7 @@ fn timelines_view( let txn = nostrdb::Transaction::new(ctx.ndb).expect("txn"); ctx.accounts .select_account(&pubkey, ctx.ndb, &txn, ctx.legacy_pool, ui.ctx()); + setup_selected_account_timeline_subs(&mut app.timeline_cache, ctx); } ProcessNavResult::ExternalNoteAction(note_action) => { diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs @@ -1,6 +1,6 @@ use crate::{ accounts::{render_accounts_route, AccountsAction, AccountsResponse, AccountsRoute}, - app::{get_active_columns_mut, get_decks_mut}, + app::{get_active_columns_mut, get_decks_mut, setup_selected_account_timeline_subs}, column::ColumnsAction, deck_state::DeckState, decks::{Deck, DecksAction, DecksCache}, @@ -120,6 +120,8 @@ impl SwitchingAction { decks_cache.add_deck_default(ctx, timeline_cache, switch_action.switch_to); } + setup_selected_account_timeline_subs(timeline_cache, ctx); + // pop nav after switch get_active_columns_mut(ctx.i18n, ctx.accounts, decks_cache) .column_mut(switch_action.source_column) diff --git a/crates/notedeck_columns/src/timeline/cache.rs b/crates/notedeck_columns/src/timeline/cache.rs @@ -213,7 +213,7 @@ impl TimelineCache { self.timelines.get_mut(id).expect("timeline inserted") }; - if let Some(filter) = timeline.filter.get_any_ready() { + if let FilterState::Ready(filter) = &timeline.filter { debug!("got open with subscription for {:?}", &timeline.kind); timeline.subscription.try_add_local(account_pk, ndb, filter); ensure_remote_timeline_subscription( @@ -258,7 +258,7 @@ impl TimelineCache { Vitality::Fresh(timeline) => (None, timeline), }; - if let Some(filter) = timeline.filter.get_any_ready() { + if let FilterState::Ready(filter) = &timeline.filter { debug!("got open with *new* subscription for {:?}", &timeline.kind); timeline.subscription.try_add_local(account_pk, ndb, filter); ensure_remote_timeline_subscription( @@ -309,7 +309,7 @@ impl TimelineCache { } fn collect_stale_notes(timeline: &Timeline, txn: &Transaction, ndb: &Ndb) -> Vec<NoteRef> { - let Some(filter) = timeline.filter.get_any_ready() else { + let FilterState::Ready(filter) = &timeline.filter else { return Vec::new(); }; diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs @@ -1,7 +1,6 @@ use crate::{ error::Error, scoped_sub_owner_keys::timeline_remote_owner_key, - subscriptions::{self, SubKind, Subscriptions}, timeline::{ kind::{people_list_note_filter, AlgoTimeline, ListKind, PeopleListRef}, note_units::InsertManyResponse, @@ -13,14 +12,14 @@ use crate::{ use notedeck::{ contacts::hybrid_contacts_filter, - filter::{self, HybridFilter}, + filter::{self}, is_future_timestamp, tr, unix_time_secs, Accounts, CachedNote, ContactState, FilterError, - FilterState, FilterStates, Localization, NoteCache, NoteRef, RelaySelection, ScopedSubApi, - ScopedSubIdentity, SubConfig, SubKey, UnknownIds, + FilterState, Localization, NoteCache, NoteRef, RelaySelection, ScopedSubApi, ScopedSubIdentity, + SubConfig, SubKey, UnknownIds, }; use egui_virtual_list::VirtualList; -use enostr::{PoolRelay, Pubkey, RelayPool}; +use enostr::Pubkey; use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction}; use std::rc::Rc; use std::{cell::RefCell, collections::HashSet}; @@ -76,15 +75,16 @@ pub(crate) fn ensure_remote_timeline_subscription( pub(crate) fn update_remote_timeline_subscription( timeline: &mut Timeline, - account_pk: Pubkey, remote_filters: Vec<Filter>, scoped_subs: &mut ScopedSubApi<'_, '_>, ) { - let owner = timeline_remote_owner_key(account_pk, &timeline.kind); + let owner = timeline_remote_owner_key(scoped_subs.selected_account_pubkey(), &timeline.kind); let identity = ScopedSubIdentity::account(owner, timeline_remote_sub_key(&timeline.kind)); let config = timeline_remote_sub_config(remote_filters); let _ = scoped_subs.set_sub(identity, config); - timeline.subscription.mark_remote_seeded(account_pk); + timeline + .subscription + .mark_remote_seeded(scoped_subs.selected_account_pubkey()); } pub fn drop_timeline_remote_owner( @@ -307,7 +307,7 @@ pub struct Timeline { pub kind: TimelineKind, // We may not have the filter loaded yet, so let's make it an option so // that codepaths have to explicitly handle it - pub filter: FilterStates, + pub filter: FilterState, pub views: Vec<TimelineTab>, pub selected_view: usize, pub seen_latest_notes: bool, @@ -381,7 +381,6 @@ impl Timeline { } pub fn new(kind: TimelineKind, filter_state: FilterState, views: Vec<TimelineTab>) -> Self { - let filter = FilterStates::new(filter_state); let subscription = TimelineSub::default(); let selected_view = 0; @@ -390,7 +389,7 @@ impl Timeline { Timeline { kind, - filter, + filter: filter_state, views, subscription, selected_view, @@ -611,10 +610,7 @@ impl Timeline { /// Note: We reset states rather than clearing them so that /// [`Self::set_all_states`] can update them during the rebuild. pub fn invalidate(&mut self) { - self.filter.initial_state = FilterState::NeedsRemote; - for state in self.filter.states.values_mut() { - *state = FilterState::NeedsRemote; - } + self.filter = FilterState::NeedsRemote; self.contact_list_timestamp = None; } } @@ -682,8 +678,6 @@ pub fn setup_new_timeline( timeline: &mut Timeline, ndb: &Ndb, txn: &Transaction, - subs: &mut Subscriptions, - pool: &mut RelayPool, scoped_subs: &mut ScopedSubApi<'_, '_>, note_cache: &mut NoteCache, since_optimize: bool, @@ -691,6 +685,7 @@ pub fn setup_new_timeline( unknown_ids: &mut UnknownIds, ) { let account_pk = *accounts.selected_account_pubkey(); + // if we're ready, setup local subs if is_timeline_ready(ndb, scoped_subs, timeline, accounts) { if let Err(err) = @@ -700,59 +695,30 @@ pub fn setup_new_timeline( } } - for relay in &mut pool.relays { - send_initial_timeline_filter(since_optimize, subs, relay, timeline, accounts, scoped_subs); - } + send_initial_timeline_filter(since_optimize, ndb, txn, timeline, accounts, scoped_subs); timeline.subscription.increment(account_pk); } -/// Send initial filters for a specific relay. This typically gets called -/// when we first connect to a new relay for the first time. For -/// situations where you are adding a new timeline, use -/// setup_new_timeline. -#[profiling::function] -pub fn send_initial_timeline_filters( - since_optimize: bool, - timeline_cache: &mut TimelineCache, - subs: &mut Subscriptions, - pool: &mut RelayPool, - relay_id: &str, - accounts: &Accounts, - scoped_subs: &mut ScopedSubApi<'_, '_>, -) -> Option<()> { - info!("Sending initial filters to {}", relay_id); - let relay = &mut pool.relays.iter_mut().find(|r| r.url() == relay_id)?; - - for (_kind, timeline) in timeline_cache { - send_initial_timeline_filter(since_optimize, subs, relay, timeline, accounts, scoped_subs); - } - - Some(()) -} - pub fn send_initial_timeline_filter( can_since_optimize: bool, - subs: &mut Subscriptions, - relay: &mut PoolRelay, + ndb: &Ndb, + txn: &Transaction, timeline: &mut Timeline, accounts: &Accounts, scoped_subs: &mut ScopedSubApi<'_, '_>, ) { - let account_pk = *accounts.selected_account_pubkey(); - let filter_state = timeline.filter.get_mut(relay.url()); - - match filter_state { + match &timeline.filter { FilterState::Broken(err) => { error!( "FetchingRemote state in broken state when sending initial timeline filter? {err}" ); } - FilterState::FetchingRemote(_unisub) => { + FilterState::FetchingRemote => { error!("FetchingRemote state when sending initial timeline filter?"); } - FilterState::GotRemote(_sub) => { + FilterState::GotRemote => { error!("GotRemote state when sending initial timeline filter?"); } @@ -785,79 +751,65 @@ pub fn send_initial_timeline_filter( filter }).collect(); - update_remote_timeline_subscription(timeline, account_pk, new_filters, scoped_subs); + update_remote_timeline_subscription(timeline, new_filters, scoped_subs); } // we need some data first - FilterState::NeedsRemote => { - let people_list_ref = match &timeline.kind { - TimelineKind::List(ListKind::PeopleList(plr)) - | TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::PeopleList(plr))) => { - Some(plr.clone()) - } - _ => None, - }; - if let Some(plr) = people_list_ref { - fetch_people_list(subs, relay, timeline, &plr); - } else { - fetch_contact_list(subs, timeline, accounts); + FilterState::NeedsRemote => match &timeline.kind { + TimelineKind::List(ListKind::PeopleList(_)) + | TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::PeopleList(_))) => { + fetch_people_list(ndb, txn, timeline); } - } + _ => fetch_contact_list(timeline, accounts), + }, } } -pub fn fetch_contact_list(subs: &mut Subscriptions, timeline: &mut Timeline, accounts: &Accounts) { - if timeline.filter.get_any_ready().is_some() { +pub fn fetch_contact_list(timeline: &mut Timeline, accounts: &Accounts) { + if matches!(&timeline.filter, FilterState::Ready(_)) { return; } let new_filter_state = match accounts.get_selected_account().data.contacts.get_state() { - ContactState::Unreceived => { - FilterState::FetchingRemote(filter::FetchingRemoteType::Contact) - } + ContactState::Unreceived => FilterState::FetchingRemote, ContactState::Received { contacts: _, note_key: _, timestamp: _, - } => FilterState::GotRemote(filter::GotRemoteType::Contact), + } => FilterState::GotRemote, }; - timeline.filter.set_all_states(new_filter_state); + timeline.filter = new_filter_state; +} - let sub = &accounts.get_subs().contacts; - if subs.subs.contains_key(&sub.remote) { +pub fn fetch_people_list(ndb: &Ndb, txn: &Transaction, timeline: &mut Timeline) { + if matches!(&timeline.filter, FilterState::Ready(_)) { return; } - let sub_kind = SubKind::FetchingContactList(timeline.kind.clone()); - subs.subs.insert(sub.remote.clone(), sub_kind); -} - -pub fn fetch_people_list( - subs: &mut Subscriptions, - relay: &mut PoolRelay, - timeline: &mut Timeline, - plr: &PeopleListRef, -) { - if timeline.filter.get_any_ready().is_some() { + let Some(plr) = people_list_ref(&timeline.kind) else { + error!("fetch_people_list called for non-people-list timeline"); + timeline.filter = FilterState::broken(FilterError::EmptyList); return; - } + }; let filter = people_list_note_filter(plr); - let sub_id = subscriptions::new_sub_id(); - if let Err(err) = relay.subscribe(sub_id.clone(), vec![filter]) { - error!("error subscribing for people list: {err}"); + let results = match ndb.query(txn, std::slice::from_ref(&filter), 1) { + Ok(results) => results, + Err(err) => { + error!("people list query failed in fetch_people_list: {err}"); + timeline.filter = FilterState::broken(FilterError::EmptyList); + return; + } + }; + + if results.is_empty() { + timeline.filter = FilterState::FetchingRemote; return; } - timeline.filter.set_relay_state( - relay.url().to_string(), - FilterState::FetchingRemote(filter::FetchingRemoteType::PeopleList), - ); - - let sub_kind = SubKind::FetchingContactList(timeline.kind.clone()); - subs.subs.insert(sub_id, sub_kind); + timeline.filter = FilterState::GotRemote; } #[profiling::function] @@ -867,9 +819,12 @@ fn setup_initial_timeline( timeline: &mut Timeline, note_cache: &mut NoteCache, unknown_ids: &mut UnknownIds, - filters: &HybridFilter, account_pk: Pubkey, ) -> Result<()> { + let FilterState::Ready(filters) = &timeline.filter else { + return Err(Error::App(notedeck::Error::empty_contact_list())); + }; + // some timelines are one-shot and a refreshed, like last_per_pubkey algo feed if timeline.kind.should_subscribe_locally() { timeline @@ -948,21 +903,7 @@ fn setup_timeline_nostrdb_sub( unknown_ids: &mut UnknownIds, account_pk: Pubkey, ) -> Result<()> { - let filter_state = timeline - .filter - .get_any_ready() - .ok_or(Error::App(notedeck::Error::empty_contact_list()))? - .to_owned(); - - setup_initial_timeline( - ndb, - txn, - timeline, - note_cache, - unknown_ids, - &filter_state, - account_pk, - )?; + setup_initial_timeline(ndb, txn, timeline, note_cache, unknown_ids, account_pk)?; Ok(()) } @@ -980,43 +921,24 @@ pub fn is_timeline_ready( ) -> bool { // TODO: we should debounce the filter states a bit to make sure we have // seen all of the different contact lists from each relay - if let Some(filter) = timeline.filter.get_any_ready() { + if let FilterState::Ready(filter) = &timeline.filter { let account_pk = *accounts.selected_account_pubkey(); + let remote_filters = filter.remote().to_vec(); if timeline.subscription.dependers(&account_pk) > 0 && !timeline.subscription.remote_seeded(&account_pk) { - ensure_remote_timeline_subscription( - timeline, - account_pk, - filter.remote().to_vec(), - scoped_subs, - ); + ensure_remote_timeline_subscription(timeline, account_pk, remote_filters, scoped_subs); } return true; } - let Some(res) = timeline.filter.get_any_gotremote() else { + if !matches!(&timeline.filter, FilterState::GotRemote) { return false; - }; - - let (relay_id, note_key) = match res { - filter::GotRemoteResult::Normal { relay_id, sub_id } => { - // We got at least one eose for our filter request. Let's see - // if nostrdb is done processing it yet. - let res = ndb.poll_for_notes(sub_id, 1); - if res.is_empty() { - debug!( - "check_timeline_filter_state: no notes found (yet?) for timeline {:?}", - timeline - ); - return false; - } - - info!("notes found for contact timeline after GotRemote!"); + } - (relay_id, res[0]) - } - filter::GotRemoteResult::Contact { relay_id } => { + let note_key = match &timeline.kind { + TimelineKind::List(ListKind::Contact(_)) + | TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::Contact(_))) => { let ContactState::Received { contacts: _, note_key, @@ -1026,20 +948,10 @@ pub fn is_timeline_ready( return false; }; - (relay_id, *note_key) + *note_key } - filter::GotRemoteResult::PeopleList { relay_id } => { - // Query ndb directly for the kind 30000 note. It should - // have been ingested from the relay by now. - let plr = match &timeline.kind { - TimelineKind::List(ListKind::PeopleList(plr)) - | TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::PeopleList(plr))) => plr, - _ => { - error!("GotRemoteResult::PeopleList but timeline kind is not PeopleList"); - return false; - } - }; - + TimelineKind::List(ListKind::PeopleList(plr)) + | TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::PeopleList(plr))) => { let list_filter = people_list_note_filter(plr); let txn = Transaction::new(ndb).expect("txn"); let results = match ndb.query(&txn, std::slice::from_ref(&list_filter), 1) { @@ -1056,8 +968,9 @@ pub fn is_timeline_ready( } info!("found people list note after GotRemote!"); - (relay_id, results[0].note_key) + results[0].note_key } + _ => return false, }; let with_hashtags = false; @@ -1074,34 +987,36 @@ pub fn is_timeline_ready( match filter { Err(notedeck::Error::Filter(e)) => { error!("got broken when building filter {e}"); - timeline - .filter - .set_relay_state(relay_id, FilterState::broken(e)); + timeline.filter = FilterState::broken(e); false } Err(err) => { error!("got broken when building filter {err}"); - timeline - .filter - .set_relay_state(relay_id, FilterState::broken(FilterError::EmptyList)); + let reason = match &timeline.kind { + TimelineKind::List(ListKind::PeopleList(_)) + | TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::PeopleList(_))) => { + FilterError::EmptyList + } + _ => FilterError::EmptyContactList, + }; + timeline.filter = FilterState::broken(reason); false } Ok(filter) => { // We just switched to the ready state; remote subscriptions can start now. - info!("Found contact list! Setting up remote contact list query"); - timeline - .filter - .set_relay_state(relay_id, FilterState::ready_hybrid(filter.clone())); - - //let ck = &timeline.kind; - //let subid = damus.gen_subid(&SubKind::Column(ck.clone())); - update_remote_timeline_subscription( - timeline, - *accounts.selected_account_pubkey(), - filter.remote().to_vec(), - scoped_subs, - ); + info!("Found list note! Setting up remote timeline query"); + timeline.filter = FilterState::ready_hybrid(filter.clone()); + + update_remote_timeline_subscription(timeline, filter.remote().to_vec(), scoped_subs); true } } } + +fn people_list_ref(kind: &TimelineKind) -> Option<&PeopleListRef> { + match kind { + TimelineKind::List(ListKind::PeopleList(plr)) + | TimelineKind::Algo(AlgoTimeline::LastPerPubkey(ListKind::PeopleList(plr))) => Some(plr), + _ => None, + } +} diff --git a/crates/notedeck_columns/src/ui/add_column.rs b/crates/notedeck_columns/src/ui/add_column.rs @@ -924,8 +924,6 @@ fn attach_timeline_column( &mut timeline, ctx.ndb, &txn, - &mut app.subscriptions, - ctx.legacy_pool, &mut scoped_subs, ctx.note_cache, app.options.contains(AppOptions::SinceOptimize), @@ -1146,13 +1144,12 @@ fn handle_create_people_list(app: &mut Damus, ctx: &mut AppContext<'_>, col: usi return; }; + let mut scoped_subs = ctx.remote.scoped_subs(ctx.accounts); crate::timeline::setup_new_timeline( &mut timeline, ctx.ndb, &txn, - &mut app.subscriptions, - ctx.legacy_pool, - &mut ctx.remote.scoped_subs(ctx.accounts), + &mut scoped_subs, ctx.note_cache, app.options.contains(AppOptions::SinceOptimize), ctx.accounts,