notedeck

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

timeline.rs (11700B)


      1 use crate::column::{ColumnKind, PubkeySource};
      2 use crate::error::Error;
      3 use crate::note::NoteRef;
      4 use crate::notecache::CachedNote;
      5 use crate::unknowns::UnknownIds;
      6 use crate::{filter, filter::FilterState};
      7 use crate::{Damus, Result};
      8 use std::sync::atomic::{AtomicU32, Ordering};
      9 
     10 use crate::route::Route;
     11 
     12 use egui_virtual_list::VirtualList;
     13 use enostr::Pubkey;
     14 use nostrdb::{Note, Subscription, Transaction};
     15 use std::cell::RefCell;
     16 use std::rc::Rc;
     17 
     18 use tracing::{debug, error};
     19 
     20 #[derive(Debug, Copy, Clone)]
     21 pub enum TimelineSource<'a> {
     22     Column { ind: usize },
     23     Thread(&'a [u8; 32]),
     24 }
     25 
     26 impl<'a> TimelineSource<'a> {
     27     pub fn column(ind: usize) -> Self {
     28         TimelineSource::Column { ind }
     29     }
     30 
     31     pub fn view<'b>(
     32         self,
     33         app: &'b mut Damus,
     34         txn: &Transaction,
     35         filter: ViewFilter,
     36     ) -> &'b mut TimelineTab {
     37         match self {
     38             TimelineSource::Column { ind, .. } => app.timelines[ind].view_mut(filter),
     39             TimelineSource::Thread(root_id) => {
     40                 // TODO: replace all this with the raw entry api eventually
     41 
     42                 let thread = if app.threads.root_id_to_thread.contains_key(root_id) {
     43                     app.threads.thread_expected_mut(root_id)
     44                 } else {
     45                     app.threads.thread_mut(&app.ndb, txn, root_id).get_ptr()
     46                 };
     47 
     48                 &mut thread.view
     49             }
     50         }
     51     }
     52 
     53     pub fn sub(self, app: &mut Damus, txn: &Transaction) -> Option<Subscription> {
     54         match self {
     55             TimelineSource::Column { ind, .. } => app.timelines[ind].subscription,
     56             TimelineSource::Thread(root_id) => {
     57                 // TODO: replace all this with the raw entry api eventually
     58 
     59                 let thread = if app.threads.root_id_to_thread.contains_key(root_id) {
     60                     app.threads.thread_expected_mut(root_id)
     61                 } else {
     62                     app.threads.thread_mut(&app.ndb, txn, root_id).get_ptr()
     63                 };
     64 
     65                 thread.subscription()
     66             }
     67         }
     68     }
     69 
     70     /// Check local subscriptions for new notes and insert them into
     71     /// timelines (threads, columns)
     72     pub fn poll_notes_into_view(&self, txn: &Transaction, app: &mut Damus) -> Result<()> {
     73         let sub = if let Some(sub) = self.sub(app, txn) {
     74             sub
     75         } else {
     76             return Err(Error::no_active_sub());
     77         };
     78 
     79         let new_note_ids = app.ndb.poll_for_notes(sub, 100);
     80         if new_note_ids.is_empty() {
     81             return Ok(());
     82         } else {
     83             debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids);
     84         }
     85 
     86         let mut new_refs: Vec<(Note, NoteRef)> = Vec::with_capacity(new_note_ids.len());
     87 
     88         for key in new_note_ids {
     89             let note = if let Ok(note) = app.ndb.get_note_by_key(txn, key) {
     90                 note
     91             } else {
     92                 error!("hit race condition in poll_notes_into_view: https://github.com/damus-io/nostrdb/issues/35 note {:?} was not added to timeline", key);
     93                 continue;
     94             };
     95 
     96             UnknownIds::update_from_note(txn, app, &note);
     97 
     98             let created_at = note.created_at();
     99             new_refs.push((note, NoteRef { key, created_at }));
    100         }
    101 
    102         // We're assuming reverse-chronological here (timelines). This
    103         // flag ensures we trigger the items_inserted_at_start
    104         // optimization in VirtualList. We need this flag because we can
    105         // insert notes into chronological order sometimes, and this
    106         // optimization doesn't make sense in those situations.
    107         let reversed = false;
    108 
    109         // ViewFilter::NotesAndReplies
    110         {
    111             let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect();
    112 
    113             let reversed = false;
    114             self.view(app, txn, ViewFilter::NotesAndReplies)
    115                 .insert(&refs, reversed);
    116         }
    117 
    118         //
    119         // handle the filtered case (ViewFilter::Notes, no replies)
    120         //
    121         // TODO(jb55): this is mostly just copied from above, let's just use a loop
    122         //             I initially tried this but ran into borrow checker issues
    123         {
    124             let mut filtered_refs = Vec::with_capacity(new_refs.len());
    125             for (note, nr) in &new_refs {
    126                 let cached_note = app.note_cache_mut().cached_note_or_insert(nr.key, note);
    127 
    128                 if ViewFilter::filter_notes(cached_note, note) {
    129                     filtered_refs.push(*nr);
    130                 }
    131             }
    132 
    133             self.view(app, txn, ViewFilter::Notes)
    134                 .insert(&filtered_refs, reversed);
    135         }
    136 
    137         Ok(())
    138     }
    139 }
    140 
    141 #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)]
    142 pub enum ViewFilter {
    143     Notes,
    144 
    145     #[default]
    146     NotesAndReplies,
    147 }
    148 
    149 impl ViewFilter {
    150     pub fn name(&self) -> &'static str {
    151         match self {
    152             ViewFilter::Notes => "Notes",
    153             ViewFilter::NotesAndReplies => "Notes & Replies",
    154         }
    155     }
    156 
    157     pub fn index(&self) -> usize {
    158         match self {
    159             ViewFilter::Notes => 0,
    160             ViewFilter::NotesAndReplies => 1,
    161         }
    162     }
    163 
    164     pub fn filter_notes(cache: &CachedNote, note: &Note) -> bool {
    165         !cache.reply.borrow(note.tags()).is_reply()
    166     }
    167 
    168     fn identity(_cache: &CachedNote, _note: &Note) -> bool {
    169         true
    170     }
    171 
    172     pub fn filter(&self) -> fn(&CachedNote, &Note) -> bool {
    173         match self {
    174             ViewFilter::Notes => ViewFilter::filter_notes,
    175             ViewFilter::NotesAndReplies => ViewFilter::identity,
    176         }
    177     }
    178 }
    179 
    180 /// A timeline view is a filtered view of notes in a timeline. Two standard views
    181 /// are "Notes" and "Notes & Replies". A timeline is associated with a Filter,
    182 /// but a TimelineTab is a further filtered view of this Filter that can't
    183 /// be captured by a Filter itself.
    184 #[derive(Default)]
    185 pub struct TimelineTab {
    186     pub notes: Vec<NoteRef>,
    187     pub selection: i32,
    188     pub filter: ViewFilter,
    189     pub list: Rc<RefCell<VirtualList>>,
    190 }
    191 
    192 impl TimelineTab {
    193     pub fn new(filter: ViewFilter) -> Self {
    194         TimelineTab::new_with_capacity(filter, 1000)
    195     }
    196 
    197     pub fn new_with_capacity(filter: ViewFilter, cap: usize) -> Self {
    198         let selection = 0i32;
    199         let mut list = VirtualList::new();
    200         list.hide_on_resize(None);
    201         list.over_scan(1000.0);
    202         let list = Rc::new(RefCell::new(list));
    203         let notes: Vec<NoteRef> = Vec::with_capacity(cap);
    204 
    205         TimelineTab {
    206             notes,
    207             selection,
    208             filter,
    209             list,
    210         }
    211     }
    212 
    213     pub fn insert(&mut self, new_refs: &[NoteRef], reversed: bool) {
    214         if new_refs.is_empty() {
    215             return;
    216         }
    217         let num_prev_items = self.notes.len();
    218         let (notes, merge_kind) = crate::timeline::merge_sorted_vecs(&self.notes, new_refs);
    219 
    220         self.notes = notes;
    221         let new_items = self.notes.len() - num_prev_items;
    222 
    223         // TODO: technically items could have been added inbetween
    224         if new_items > 0 {
    225             let mut list = self.list.borrow_mut();
    226 
    227             match merge_kind {
    228                 // TODO: update egui_virtual_list to support spliced inserts
    229                 MergeKind::Spliced => {
    230                     debug!(
    231                         "spliced when inserting {} new notes, resetting virtual list",
    232                         new_refs.len()
    233                     );
    234                     list.reset();
    235                 }
    236                 MergeKind::FrontInsert => {
    237                     // only run this logic if we're reverse-chronological
    238                     // reversed in this case means chronological, since the
    239                     // default is reverse-chronological. yeah it's confusing.
    240                     if !reversed {
    241                         list.items_inserted_at_start(new_items);
    242                     }
    243                 }
    244             }
    245         }
    246     }
    247 
    248     pub fn select_down(&mut self) {
    249         debug!("select_down {}", self.selection + 1);
    250         if self.selection + 1 > self.notes.len() as i32 {
    251             return;
    252         }
    253 
    254         self.selection += 1;
    255     }
    256 
    257     pub fn select_up(&mut self) {
    258         debug!("select_up {}", self.selection - 1);
    259         if self.selection - 1 < 0 {
    260             return;
    261         }
    262 
    263         self.selection -= 1;
    264     }
    265 }
    266 
    267 /// A column in a deck. Holds navigation state, loaded notes, column kind, etc.
    268 pub struct Timeline {
    269     pub uid: u32,
    270     pub kind: ColumnKind,
    271     // We may not have the filter loaded yet, so let's make it an option so
    272     // that codepaths have to explicitly handle it
    273     pub filter: FilterState,
    274     pub views: Vec<TimelineTab>,
    275     pub selected_view: i32,
    276     pub routes: Vec<Route>,
    277     pub navigating: bool,
    278     pub returning: bool,
    279 
    280     /// Our nostrdb subscription
    281     pub subscription: Option<Subscription>,
    282 }
    283 
    284 impl Timeline {
    285     /// Create a timeline from a contact list
    286     pub fn contact_list(contact_list: &Note) -> Result<Self> {
    287         let filter = filter::filter_from_tags(contact_list)?.into_follow_filter();
    288         let pk_src = PubkeySource::Explicit(Pubkey::new(*contact_list.pubkey()));
    289 
    290         Ok(Timeline::new(
    291             ColumnKind::contact_list(pk_src),
    292             FilterState::ready(filter),
    293         ))
    294     }
    295 
    296     pub fn new(kind: ColumnKind, filter: FilterState) -> Self {
    297         // global unique id for all new timelines
    298         static UIDS: AtomicU32 = AtomicU32::new(0);
    299 
    300         let subscription: Option<Subscription> = None;
    301         let notes = TimelineTab::new(ViewFilter::Notes);
    302         let replies = TimelineTab::new(ViewFilter::NotesAndReplies);
    303         let views = vec![notes, replies];
    304         let selected_view = 0;
    305         let routes = vec![Route::Timeline(format!("{}", kind))];
    306         let navigating = false;
    307         let returning = false;
    308         let uid = UIDS.fetch_add(1, Ordering::Relaxed);
    309 
    310         Timeline {
    311             uid,
    312             kind,
    313             navigating,
    314             returning,
    315             filter,
    316             views,
    317             subscription,
    318             selected_view,
    319             routes,
    320         }
    321     }
    322 
    323     pub fn current_view(&self) -> &TimelineTab {
    324         &self.views[self.selected_view as usize]
    325     }
    326 
    327     pub fn current_view_mut(&mut self) -> &mut TimelineTab {
    328         &mut self.views[self.selected_view as usize]
    329     }
    330 
    331     pub fn notes(&self, view: ViewFilter) -> &[NoteRef] {
    332         &self.views[view.index()].notes
    333     }
    334 
    335     pub fn view(&self, view: ViewFilter) -> &TimelineTab {
    336         &self.views[view.index()]
    337     }
    338 
    339     pub fn view_mut(&mut self, view: ViewFilter) -> &mut TimelineTab {
    340         &mut self.views[view.index()]
    341     }
    342 }
    343 
    344 pub enum MergeKind {
    345     FrontInsert,
    346     Spliced,
    347 }
    348 
    349 pub fn merge_sorted_vecs<T: Ord + Copy>(vec1: &[T], vec2: &[T]) -> (Vec<T>, MergeKind) {
    350     let mut merged = Vec::with_capacity(vec1.len() + vec2.len());
    351     let mut i = 0;
    352     let mut j = 0;
    353     let mut result: Option<MergeKind> = None;
    354 
    355     while i < vec1.len() && j < vec2.len() {
    356         if vec1[i] <= vec2[j] {
    357             if result.is_none() && j < vec2.len() {
    358                 // if we're pushing from our large list and still have
    359                 // some left in vec2, then this is a splice
    360                 result = Some(MergeKind::Spliced);
    361             }
    362             merged.push(vec1[i]);
    363             i += 1;
    364         } else {
    365             merged.push(vec2[j]);
    366             j += 1;
    367         }
    368     }
    369 
    370     // Append any remaining elements from either vector
    371     if i < vec1.len() {
    372         merged.extend_from_slice(&vec1[i..]);
    373     }
    374     if j < vec2.len() {
    375         merged.extend_from_slice(&vec2[j..]);
    376     }
    377 
    378     (merged, result.unwrap_or(MergeKind::FrontInsert))
    379 }