notedeck

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

mod.rs (23894B)


      1 use crate::{
      2     error::Error,
      3     multi_subscriber::MultiSubscriber,
      4     subscriptions::{self, SubKind, Subscriptions},
      5     timeline::kind::ListKind,
      6     Result,
      7 };
      8 
      9 use notedeck::{
     10     filter, CachedNote, FilterError, FilterState, FilterStates, NoteCache, NoteRef, UnknownIds,
     11 };
     12 
     13 use egui_virtual_list::VirtualList;
     14 use enostr::{PoolRelay, Pubkey, RelayPool};
     15 use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction};
     16 use std::cell::RefCell;
     17 use std::rc::Rc;
     18 
     19 use tracing::{debug, error, info, warn};
     20 
     21 pub mod cache;
     22 pub mod kind;
     23 pub mod route;
     24 
     25 pub use cache::TimelineCache;
     26 pub use kind::{ColumnTitle, PubkeySource, ThreadSelection, TimelineKind};
     27 
     28 //#[derive(Debug, Hash, Clone, Eq, PartialEq)]
     29 //pub type TimelineId = TimelineKind;
     30 
     31 /*
     32 
     33 impl TimelineId {
     34     pub fn kind(&self) -> &TimelineKind {
     35         &self.kind
     36     }
     37 
     38     pub fn new(id: TimelineKind) -> Self {
     39         TimelineId(id)
     40     }
     41 
     42     pub fn profile(pubkey: Pubkey) -> Self {
     43         TimelineId::new(TimelineKind::Profile(PubkeySource::pubkey(pubkey)))
     44     }
     45 }
     46 
     47 impl fmt::Display for TimelineId {
     48     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
     49         write!(f, "TimelineId({})", self.0)
     50     }
     51 }
     52 */
     53 
     54 #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
     55 pub enum ViewFilter {
     56     Notes,
     57 
     58     #[default]
     59     NotesAndReplies,
     60 }
     61 
     62 impl ViewFilter {
     63     pub fn name(&self) -> &'static str {
     64         match self {
     65             ViewFilter::Notes => "Notes",
     66             ViewFilter::NotesAndReplies => "Notes & Replies",
     67         }
     68     }
     69 
     70     pub fn filter_notes(cache: &CachedNote, note: &Note) -> bool {
     71         !cache.reply.borrow(note.tags()).is_reply()
     72     }
     73 
     74     fn identity(_cache: &CachedNote, _note: &Note) -> bool {
     75         true
     76     }
     77 
     78     pub fn filter(&self) -> fn(&CachedNote, &Note) -> bool {
     79         match self {
     80             ViewFilter::Notes => ViewFilter::filter_notes,
     81             ViewFilter::NotesAndReplies => ViewFilter::identity,
     82         }
     83     }
     84 }
     85 
     86 /// A timeline view is a filtered view of notes in a timeline. Two standard views
     87 /// are "Notes" and "Notes & Replies". A timeline is associated with a Filter,
     88 /// but a TimelineTab is a further filtered view of this Filter that can't
     89 /// be captured by a Filter itself.
     90 #[derive(Default, Debug)]
     91 pub struct TimelineTab {
     92     pub notes: Vec<NoteRef>,
     93     pub selection: i32,
     94     pub filter: ViewFilter,
     95     pub list: Rc<RefCell<VirtualList>>,
     96 }
     97 
     98 impl TimelineTab {
     99     pub fn new(filter: ViewFilter) -> Self {
    100         TimelineTab::new_with_capacity(filter, 1000)
    101     }
    102 
    103     pub fn only_notes_and_replies() -> Vec<Self> {
    104         vec![TimelineTab::new(ViewFilter::NotesAndReplies)]
    105     }
    106 
    107     pub fn no_replies() -> Vec<Self> {
    108         vec![TimelineTab::new(ViewFilter::Notes)]
    109     }
    110 
    111     pub fn full_tabs() -> Vec<Self> {
    112         vec![
    113             TimelineTab::new(ViewFilter::Notes),
    114             TimelineTab::new(ViewFilter::NotesAndReplies),
    115         ]
    116     }
    117 
    118     pub fn new_with_capacity(filter: ViewFilter, cap: usize) -> Self {
    119         let selection = 0i32;
    120         let mut list = VirtualList::new();
    121         list.hide_on_resize(None);
    122         list.over_scan(1000.0);
    123         let list = Rc::new(RefCell::new(list));
    124         let notes: Vec<NoteRef> = Vec::with_capacity(cap);
    125 
    126         TimelineTab {
    127             notes,
    128             selection,
    129             filter,
    130             list,
    131         }
    132     }
    133 
    134     fn insert(&mut self, new_refs: &[NoteRef], reversed: bool) {
    135         if new_refs.is_empty() {
    136             return;
    137         }
    138         let num_prev_items = self.notes.len();
    139         let (notes, merge_kind) = crate::timeline::merge_sorted_vecs(&self.notes, new_refs);
    140 
    141         self.notes = notes;
    142         let new_items = self.notes.len() - num_prev_items;
    143 
    144         // TODO: technically items could have been added inbetween
    145         if new_items > 0 {
    146             let mut list = self.list.borrow_mut();
    147 
    148             match merge_kind {
    149                 // TODO: update egui_virtual_list to support spliced inserts
    150                 MergeKind::Spliced => {
    151                     debug!(
    152                         "spliced when inserting {} new notes, resetting virtual list",
    153                         new_refs.len()
    154                     );
    155                     list.reset();
    156                 }
    157                 MergeKind::FrontInsert => {
    158                     // only run this logic if we're reverse-chronological
    159                     // reversed in this case means chronological, since the
    160                     // default is reverse-chronological. yeah it's confusing.
    161                     if !reversed {
    162                         debug!("inserting {} new notes at start", new_refs.len());
    163                         list.items_inserted_at_start(new_items);
    164                     }
    165                 }
    166             }
    167         }
    168     }
    169 
    170     pub fn select_down(&mut self) {
    171         debug!("select_down {}", self.selection + 1);
    172         if self.selection + 1 > self.notes.len() as i32 {
    173             return;
    174         }
    175 
    176         self.selection += 1;
    177     }
    178 
    179     pub fn select_up(&mut self) {
    180         debug!("select_up {}", self.selection - 1);
    181         if self.selection - 1 < 0 {
    182             return;
    183         }
    184 
    185         self.selection -= 1;
    186     }
    187 }
    188 
    189 /// A column in a deck. Holds navigation state, loaded notes, column kind, etc.
    190 #[derive(Debug)]
    191 pub struct Timeline {
    192     pub kind: TimelineKind,
    193     // We may not have the filter loaded yet, so let's make it an option so
    194     // that codepaths have to explicitly handle it
    195     pub filter: FilterStates,
    196     pub views: Vec<TimelineTab>,
    197     pub selected_view: usize,
    198 
    199     pub subscription: Option<MultiSubscriber>,
    200 }
    201 
    202 impl Timeline {
    203     /// Create a timeline from a contact list
    204     pub fn contact_list(contact_list: &Note, pubkey: &[u8; 32]) -> Result<Self> {
    205         let with_hashtags = false;
    206         let filter = filter::filter_from_tags(contact_list, Some(pubkey), with_hashtags)?
    207             .into_follow_filter();
    208 
    209         Ok(Timeline::new(
    210             TimelineKind::contact_list(Pubkey::new(*pubkey)),
    211             FilterState::ready(filter),
    212             TimelineTab::full_tabs(),
    213         ))
    214     }
    215 
    216     pub fn thread(selection: ThreadSelection) -> Self {
    217         let filter = vec![
    218             nostrdb::Filter::new()
    219                 .kinds([1])
    220                 .event(selection.root_id.bytes())
    221                 .build(),
    222             nostrdb::Filter::new()
    223                 .ids([selection.root_id.bytes()])
    224                 .limit(1)
    225                 .build(),
    226         ];
    227         Timeline::new(
    228             TimelineKind::Thread(selection),
    229             FilterState::ready(filter),
    230             TimelineTab::only_notes_and_replies(),
    231         )
    232     }
    233 
    234     pub fn last_per_pubkey(list: &Note, list_kind: &ListKind) -> Result<Self> {
    235         let kind = 1;
    236         let notes_per_pk = 1;
    237         let filter = filter::last_n_per_pubkey_from_tags(list, kind, notes_per_pk)?;
    238 
    239         Ok(Timeline::new(
    240             TimelineKind::last_per_pubkey(*list_kind),
    241             FilterState::ready(filter),
    242             TimelineTab::only_notes_and_replies(),
    243         ))
    244     }
    245 
    246     pub fn hashtag(hashtag: String) -> Self {
    247         let filter = Filter::new()
    248             .kinds([1])
    249             .limit(filter::default_limit())
    250             .tags([hashtag.to_lowercase()], 't')
    251             .build();
    252 
    253         Timeline::new(
    254             TimelineKind::Hashtag(hashtag),
    255             FilterState::ready(vec![filter]),
    256             TimelineTab::only_notes_and_replies(),
    257         )
    258     }
    259 
    260     pub fn make_view_id(id: &TimelineKind, selected_view: usize) -> egui::Id {
    261         egui::Id::new((id, selected_view))
    262     }
    263 
    264     pub fn view_id(&self) -> egui::Id {
    265         Timeline::make_view_id(&self.kind, self.selected_view)
    266     }
    267 
    268     pub fn new(kind: TimelineKind, filter_state: FilterState, views: Vec<TimelineTab>) -> Self {
    269         let filter = FilterStates::new(filter_state);
    270         let subscription: Option<MultiSubscriber> = None;
    271         let selected_view = 0;
    272 
    273         Timeline {
    274             kind,
    275             filter,
    276             views,
    277             subscription,
    278             selected_view,
    279         }
    280     }
    281 
    282     pub fn current_view(&self) -> &TimelineTab {
    283         &self.views[self.selected_view]
    284     }
    285 
    286     pub fn current_view_mut(&mut self) -> &mut TimelineTab {
    287         &mut self.views[self.selected_view]
    288     }
    289 
    290     /// Get the note refs for NotesAndReplies. If we only have Notes, then
    291     /// just return that instead
    292     pub fn all_or_any_notes(&self) -> &[NoteRef] {
    293         self.notes(ViewFilter::NotesAndReplies).unwrap_or_else(|| {
    294             self.notes(ViewFilter::Notes)
    295                 .expect("should have at least notes")
    296         })
    297     }
    298 
    299     pub fn notes(&self, view: ViewFilter) -> Option<&[NoteRef]> {
    300         self.view(view).map(|v| &*v.notes)
    301     }
    302 
    303     pub fn view(&self, view: ViewFilter) -> Option<&TimelineTab> {
    304         self.views.iter().find(|tab| tab.filter == view)
    305     }
    306 
    307     pub fn view_mut(&mut self, view: ViewFilter) -> Option<&mut TimelineTab> {
    308         self.views.iter_mut().find(|tab| tab.filter == view)
    309     }
    310 
    311     /// Initial insert of notes into a timeline. Subsequent inserts should
    312     /// just use the insert function
    313     pub fn insert_new(
    314         &mut self,
    315         txn: &Transaction,
    316         ndb: &Ndb,
    317         note_cache: &mut NoteCache,
    318         notes: &[NoteRef],
    319     ) {
    320         let filters = {
    321             let views = &self.views;
    322             let filters: Vec<fn(&CachedNote, &Note) -> bool> =
    323                 views.iter().map(|v| v.filter.filter()).collect();
    324             filters
    325         };
    326 
    327         for note_ref in notes {
    328             for (view, filter) in filters.iter().enumerate() {
    329                 if let Ok(note) = ndb.get_note_by_key(txn, note_ref.key) {
    330                     if filter(
    331                         note_cache.cached_note_or_insert_mut(note_ref.key, &note),
    332                         &note,
    333                     ) {
    334                         self.views[view].notes.push(*note_ref)
    335                     }
    336                 }
    337             }
    338         }
    339     }
    340 
    341     /// The main function used for inserting notes into timelines. Handles
    342     /// inserting into multiple views if we have them. All timeline note
    343     /// insertions should use this function.
    344     pub fn insert(
    345         &mut self,
    346         new_note_ids: &[NoteKey],
    347         ndb: &Ndb,
    348         txn: &Transaction,
    349         unknown_ids: &mut UnknownIds,
    350         note_cache: &mut NoteCache,
    351         reversed: bool,
    352     ) -> Result<()> {
    353         let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len());
    354 
    355         for key in new_note_ids {
    356             let note = if let Ok(note) = ndb.get_note_by_key(txn, *key) {
    357                 note
    358             } else {
    359                 error!("hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", key);
    360                 continue;
    361             };
    362 
    363             // Ensure that unknown ids are captured when inserting notes
    364             // into the timeline
    365             UnknownIds::update_from_note(txn, ndb, unknown_ids, note_cache, &note);
    366 
    367             let created_at = note.created_at();
    368             new_refs.push((
    369                 note,
    370                 NoteRef {
    371                     key: *key,
    372                     created_at,
    373                 },
    374             ));
    375         }
    376 
    377         for view in &mut self.views {
    378             match view.filter {
    379                 ViewFilter::NotesAndReplies => {
    380                     let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
    381 
    382                     view.insert(&refs, reversed);
    383                 }
    384 
    385                 ViewFilter::Notes => {
    386                     let mut filtered_refs = Vec::with_capacity(new_refs.len());
    387                     for (note, nr) in &new_refs {
    388                         let cached_note = note_cache.cached_note_or_insert(nr.key, note);
    389 
    390                         if ViewFilter::filter_notes(cached_note, note) {
    391                             filtered_refs.push(*nr);
    392                         }
    393                     }
    394 
    395                     view.insert(&filtered_refs, reversed);
    396                 }
    397             }
    398         }
    399 
    400         Ok(())
    401     }
    402 
    403     pub fn poll_notes_into_view(
    404         &mut self,
    405         ndb: &Ndb,
    406         txn: &Transaction,
    407         unknown_ids: &mut UnknownIds,
    408         note_cache: &mut NoteCache,
    409         reversed: bool,
    410     ) -> Result<()> {
    411         if !self.kind.should_subscribe_locally() {
    412             // don't need to poll for timelines that don't have local subscriptions
    413             return Ok(());
    414         }
    415 
    416         let sub = self
    417             .subscription
    418             .as_ref()
    419             .and_then(|s| s.local_subid)
    420             .ok_or(Error::App(notedeck::Error::no_active_sub()))?;
    421 
    422         let new_note_ids = ndb.poll_for_notes(sub, 500);
    423         if new_note_ids.is_empty() {
    424             return Ok(());
    425         } else {
    426             debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids);
    427         }
    428 
    429         self.insert(&new_note_ids, ndb, txn, unknown_ids, note_cache, reversed)
    430     }
    431 }
    432 
    433 pub enum MergeKind {
    434     FrontInsert,
    435     Spliced,
    436 }
    437 
    438 pub fn merge_sorted_vecs<T: Ord + Copy>(vec1: &[T], vec2: &[T]) -> (Vec<T>, MergeKind) {
    439     let mut merged = Vec::with_capacity(vec1.len() + vec2.len());
    440     let mut i = 0;
    441     let mut j = 0;
    442     let mut result: Option<MergeKind> = None;
    443 
    444     while i < vec1.len() && j < vec2.len() {
    445         if vec1[i] <= vec2[j] {
    446             if result.is_none() && j < vec2.len() {
    447                 // if we're pushing from our large list and still have
    448                 // some left in vec2, then this is a splice
    449                 result = Some(MergeKind::Spliced);
    450             }
    451             merged.push(vec1[i]);
    452             i += 1;
    453         } else {
    454             merged.push(vec2[j]);
    455             j += 1;
    456         }
    457     }
    458 
    459     // Append any remaining elements from either vector
    460     if i < vec1.len() {
    461         merged.extend_from_slice(&vec1[i..]);
    462     }
    463     if j < vec2.len() {
    464         merged.extend_from_slice(&vec2[j..]);
    465     }
    466 
    467     (merged, result.unwrap_or(MergeKind::FrontInsert))
    468 }
    469 
    470 /// When adding a new timeline, we may have a situation where the
    471 /// FilterState is NeedsRemote. This can happen if we don't yet have the
    472 /// contact list, etc. For these situations, we query all of the relays
    473 /// with the same sub_id. We keep track of this sub_id and update the
    474 /// filter with the latest version of the returned filter (ie contact
    475 /// list) when they arrive.
    476 ///
    477 /// We do this by maintaining this sub_id in the filter state, even when
    478 /// in the ready state. See: [`FilterReady`]
    479 #[allow(clippy::too_many_arguments)]
    480 pub fn setup_new_timeline(
    481     timeline: &mut Timeline,
    482     ndb: &Ndb,
    483     subs: &mut Subscriptions,
    484     pool: &mut RelayPool,
    485     note_cache: &mut NoteCache,
    486     since_optimize: bool,
    487 ) {
    488     // if we're ready, setup local subs
    489     if is_timeline_ready(ndb, pool, note_cache, timeline) {
    490         if let Err(err) = setup_timeline_nostrdb_sub(ndb, note_cache, timeline) {
    491             error!("setup_new_timeline: {err}");
    492         }
    493     }
    494 
    495     for relay in &mut pool.relays {
    496         send_initial_timeline_filter(ndb, since_optimize, subs, relay, timeline);
    497     }
    498 }
    499 
    500 /// Send initial filters for a specific relay. This typically gets called
    501 /// when we first connect to a new relay for the first time. For
    502 /// situations where you are adding a new timeline, use
    503 /// setup_new_timeline.
    504 pub fn send_initial_timeline_filters(
    505     ndb: &Ndb,
    506     since_optimize: bool,
    507     timeline_cache: &mut TimelineCache,
    508     subs: &mut Subscriptions,
    509     pool: &mut RelayPool,
    510     relay_id: &str,
    511 ) -> Option<()> {
    512     info!("Sending initial filters to {}", relay_id);
    513     let relay = &mut pool.relays.iter_mut().find(|r| r.url() == relay_id)?;
    514 
    515     for (_kind, timeline) in timeline_cache.timelines.iter_mut() {
    516         send_initial_timeline_filter(ndb, since_optimize, subs, relay, timeline);
    517     }
    518 
    519     Some(())
    520 }
    521 
    522 pub fn send_initial_timeline_filter(
    523     ndb: &Ndb,
    524     can_since_optimize: bool,
    525     subs: &mut Subscriptions,
    526     relay: &mut PoolRelay,
    527     timeline: &mut Timeline,
    528 ) {
    529     let filter_state = timeline.filter.get_mut(relay.url());
    530 
    531     match filter_state {
    532         FilterState::Broken(err) => {
    533             error!(
    534                 "FetchingRemote state in broken state when sending initial timeline filter? {err}"
    535             );
    536         }
    537 
    538         FilterState::FetchingRemote(_unisub) => {
    539             error!("FetchingRemote state when sending initial timeline filter?");
    540         }
    541 
    542         FilterState::GotRemote(_sub) => {
    543             error!("GotRemote state when sending initial timeline filter?");
    544         }
    545 
    546         FilterState::Ready(filter) => {
    547             let filter = filter.to_owned();
    548             let new_filters = filter.into_iter().map(|f| {
    549                 // limit the size of remote filters
    550                 let default_limit = filter::default_remote_limit();
    551                 let mut lim = f.limit().unwrap_or(default_limit);
    552                 let mut filter = f;
    553                 if lim > default_limit {
    554                     lim = default_limit;
    555                     filter = filter.limit_mut(lim);
    556                 }
    557 
    558                 let notes = timeline.all_or_any_notes();
    559 
    560                 // Should we since optimize? Not always. For example
    561                 // if we only have a few notes locally. One way to
    562                 // determine this is by looking at the current filter
    563                 // and seeing what its limit is. If we have less
    564                 // notes than the limit, we might want to backfill
    565                 // older notes
    566                 if can_since_optimize && filter::should_since_optimize(lim, notes.len()) {
    567                     filter = filter::since_optimize_filter(filter, notes);
    568                 } else {
    569                     warn!("Skipping since optimization for {:?}: number of local notes is less than limit, attempting to backfill.", &timeline.kind);
    570                 }
    571 
    572                 filter
    573             }).collect();
    574 
    575             //let sub_id = damus.gen_subid(&SubKind::Initial);
    576             let sub_id = subscriptions::new_sub_id();
    577             subs.subs.insert(sub_id.clone(), SubKind::Initial);
    578 
    579             if let Err(err) = relay.subscribe(sub_id, new_filters) {
    580                 error!("error subscribing: {err}");
    581             }
    582         }
    583 
    584         // we need some data first
    585         FilterState::NeedsRemote(filter) => {
    586             fetch_contact_list(filter.to_owned(), ndb, subs, relay, timeline)
    587         }
    588     }
    589 }
    590 
    591 fn fetch_contact_list(
    592     filter: Vec<Filter>,
    593     ndb: &Ndb,
    594     subs: &mut Subscriptions,
    595     relay: &mut PoolRelay,
    596     timeline: &mut Timeline,
    597 ) {
    598     let sub_kind = SubKind::FetchingContactList(timeline.kind.clone());
    599     let sub_id = subscriptions::new_sub_id();
    600     let local_sub = ndb.subscribe(&filter).expect("sub");
    601 
    602     timeline.filter.set_relay_state(
    603         relay.url().to_string(),
    604         FilterState::fetching_remote(sub_id.clone(), local_sub),
    605     );
    606 
    607     subs.subs.insert(sub_id.clone(), sub_kind);
    608 
    609     info!("fetching contact list from {}", relay.url());
    610     if let Err(err) = relay.subscribe(sub_id, filter) {
    611         error!("error subscribing: {err}");
    612     }
    613 }
    614 
    615 fn setup_initial_timeline(
    616     ndb: &Ndb,
    617     timeline: &mut Timeline,
    618     note_cache: &mut NoteCache,
    619     filters: &[Filter],
    620 ) -> Result<()> {
    621     // some timelines are one-shot and a refreshed, like last_per_pubkey algo feed
    622     if timeline.kind.should_subscribe_locally() {
    623         let local_sub = ndb.subscribe(filters)?;
    624         match &mut timeline.subscription {
    625             None => {
    626                 timeline.subscription = Some(MultiSubscriber::with_initial_local_sub(
    627                     local_sub,
    628                     filters.to_vec(),
    629                 ));
    630             }
    631 
    632             Some(msub) => {
    633                 msub.local_subid = Some(local_sub);
    634             }
    635         };
    636     }
    637 
    638     debug!(
    639         "querying nostrdb sub {:?} {:?}",
    640         timeline.subscription, timeline.filter
    641     );
    642 
    643     let mut lim = 0i32;
    644     for filter in filters {
    645         lim += filter.limit().unwrap_or(1) as i32;
    646     }
    647 
    648     let txn = Transaction::new(ndb)?;
    649     let notes: Vec<NoteRef> = ndb
    650         .query(&txn, filters, lim)?
    651         .into_iter()
    652         .map(NoteRef::from_query_result)
    653         .collect();
    654 
    655     timeline.insert_new(&txn, ndb, note_cache, &notes);
    656 
    657     Ok(())
    658 }
    659 
    660 pub fn setup_initial_nostrdb_subs(
    661     ndb: &Ndb,
    662     note_cache: &mut NoteCache,
    663     timeline_cache: &mut TimelineCache,
    664 ) -> Result<()> {
    665     for (_kind, timeline) in timeline_cache.timelines.iter_mut() {
    666         if let Err(err) = setup_timeline_nostrdb_sub(ndb, note_cache, timeline) {
    667             error!("setup_initial_nostrdb_subs: {err}");
    668         }
    669     }
    670 
    671     Ok(())
    672 }
    673 
    674 fn setup_timeline_nostrdb_sub(
    675     ndb: &Ndb,
    676     note_cache: &mut NoteCache,
    677     timeline: &mut Timeline,
    678 ) -> Result<()> {
    679     let filter_state = timeline
    680         .filter
    681         .get_any_ready()
    682         .ok_or(Error::App(notedeck::Error::empty_contact_list()))?
    683         .to_owned();
    684 
    685     setup_initial_timeline(ndb, timeline, note_cache, &filter_state)?;
    686 
    687     Ok(())
    688 }
    689 
    690 /// Check our timeline filter and see if we have any filter data ready.
    691 /// Our timelines may require additional data before it is functional. For
    692 /// example, when we have to fetch a contact list before we do the actual
    693 /// following list query.
    694 pub fn is_timeline_ready(
    695     ndb: &Ndb,
    696     pool: &mut RelayPool,
    697     note_cache: &mut NoteCache,
    698     timeline: &mut Timeline,
    699 ) -> bool {
    700     // TODO: we should debounce the filter states a bit to make sure we have
    701     // seen all of the different contact lists from each relay
    702     if let Some(_f) = timeline.filter.get_any_ready() {
    703         return true;
    704     }
    705 
    706     let (relay_id, sub) = if let Some((relay_id, sub)) = timeline.filter.get_any_gotremote() {
    707         (relay_id.to_string(), sub)
    708     } else {
    709         return false;
    710     };
    711 
    712     // We got at least one eose for our filter request. Let's see
    713     // if nostrdb is done processing it yet.
    714     let res = ndb.poll_for_notes(sub, 1);
    715     if res.is_empty() {
    716         debug!(
    717             "check_timeline_filter_state: no notes found (yet?) for timeline {:?}",
    718             timeline
    719         );
    720         return false;
    721     }
    722 
    723     info!("notes found for contact timeline after GotRemote!");
    724 
    725     let note_key = res[0];
    726     let with_hashtags = false;
    727 
    728     let filter = {
    729         let txn = Transaction::new(ndb).expect("txn");
    730         let note = ndb.get_note_by_key(&txn, note_key).expect("note");
    731         let add_pk = timeline.kind.pubkey().map(|pk| pk.bytes());
    732         filter::filter_from_tags(&note, add_pk, with_hashtags).map(|f| f.into_follow_filter())
    733     };
    734 
    735     // TODO: into_follow_filter is hardcoded to contact lists, let's generalize
    736     match filter {
    737         Err(notedeck::Error::Filter(e)) => {
    738             error!("got broken when building filter {e}");
    739             timeline
    740                 .filter
    741                 .set_relay_state(relay_id, FilterState::broken(e));
    742             false
    743         }
    744         Err(err) => {
    745             error!("got broken when building filter {err}");
    746             timeline
    747                 .filter
    748                 .set_relay_state(relay_id, FilterState::broken(FilterError::EmptyContactList));
    749             false
    750         }
    751         Ok(filter) => {
    752             // we just switched to the ready state, we should send initial
    753             // queries and setup the local subscription
    754             info!("Found contact list! Setting up local and remote contact list query");
    755             setup_initial_timeline(ndb, timeline, note_cache, &filter).expect("setup init");
    756             timeline
    757                 .filter
    758                 .set_relay_state(relay_id, FilterState::ready(filter.clone()));
    759 
    760             //let ck = &timeline.kind;
    761             //let subid = damus.gen_subid(&SubKind::Column(ck.clone()));
    762             let subid = subscriptions::new_sub_id();
    763             pool.subscribe(subid, filter);
    764             true
    765         }
    766     }
    767 }