notedeck

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

mod.rs (10549B)


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