use crate::app::{get_unknown_note_ids, UnknownId}; use crate::error::Error; use crate::note::NoteRef; use crate::notecache::CachedNote; use crate::{Damus, Result}; use crate::route::Route; use egui_virtual_list::VirtualList; use enostr::Filter; use nostrdb::{Note, Subscription, Transaction}; use std::cell::RefCell; use std::collections::HashSet; use std::rc::Rc; use tracing::debug; #[derive(Debug, Copy, Clone)] pub enum TimelineSource<'a> { Column { ind: usize }, Thread(&'a [u8; 32]), } impl<'a> TimelineSource<'a> { pub fn column(ind: usize) -> Self { TimelineSource::Column { ind } } pub fn view<'b>( self, app: &'b mut Damus, txn: &Transaction, filter: ViewFilter, ) -> &'b mut TimelineTab { match self { TimelineSource::Column { ind, .. } => app.timelines[ind].view_mut(filter), TimelineSource::Thread(root_id) => { // TODO: replace all this with the raw entry api eventually let thread = if app.threads.root_id_to_thread.contains_key(root_id) { app.threads.thread_expected_mut(root_id) } else { app.threads.thread_mut(&app.ndb, txn, root_id).get_ptr() }; &mut thread.view } } } pub fn sub<'b>(self, app: &'b mut Damus, txn: &Transaction) -> Option<&'b Subscription> { match self { TimelineSource::Column { ind, .. } => app.timelines[ind].subscription.as_ref(), TimelineSource::Thread(root_id) => { // TODO: replace all this with the raw entry api eventually let thread = if app.threads.root_id_to_thread.contains_key(root_id) { app.threads.thread_expected_mut(root_id) } else { app.threads.thread_mut(&app.ndb, txn, root_id).get_ptr() }; thread.subscription() } } } pub fn poll_notes_into_view( &self, app: &mut Damus, txn: &'a Transaction, ids: &mut HashSet>, ) -> Result<()> { let sub_id = if let Some(sub_id) = self.sub(app, txn).map(|s| s.id) { sub_id } else { return Err(Error::no_active_sub()); }; // // TODO(BUG!): poll for these before the txn, otherwise we can hit // a race condition where we hit the "no note??" expect below. This may // require some refactoring due to the missing ids logic // let new_note_ids = app.ndb.poll_for_notes(sub_id, 100); if new_note_ids.is_empty() { return Ok(()); } else { debug!("{} new notes! {:?}", new_note_ids.len(), new_note_ids); } let new_refs: Vec<(Note, NoteRef)> = new_note_ids .iter() .map(|key| { let note = app.ndb.get_note_by_key(txn, *key).expect("no note??"); let cached_note = app .note_cache_mut() .cached_note_or_insert(*key, ¬e) .clone(); let _ = get_unknown_note_ids(&app.ndb, &cached_note, txn, ¬e, *key, ids); let created_at = note.created_at(); ( note, NoteRef { key: *key, created_at, }, ) }) .collect(); // ViewFilter::NotesAndReplies { let refs: Vec = new_refs.iter().map(|(_note, nr)| *nr).collect(); self.view(app, txn, ViewFilter::NotesAndReplies) .insert(&refs); } // // handle the filtered case (ViewFilter::Notes, no replies) // // TODO(jb55): this is mostly just copied from above, let's just use a loop // I initially tried this but ran into borrow checker issues { let mut filtered_refs = Vec::with_capacity(new_refs.len()); for (note, nr) in &new_refs { let cached_note = app.note_cache_mut().cached_note_or_insert(nr.key, note); if ViewFilter::filter_notes(cached_note, note) { filtered_refs.push(*nr); } } self.view(app, txn, ViewFilter::Notes) .insert(&filtered_refs); } Ok(()) } } #[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] pub enum ViewFilter { Notes, #[default] NotesAndReplies, } impl ViewFilter { pub fn name(&self) -> &'static str { match self { ViewFilter::Notes => "Notes", ViewFilter::NotesAndReplies => "Notes & Replies", } } pub fn index(&self) -> usize { match self { ViewFilter::Notes => 0, ViewFilter::NotesAndReplies => 1, } } pub fn filter_notes(cache: &CachedNote, note: &Note) -> bool { !cache.reply.borrow(note.tags()).is_reply() } fn identity(_cache: &CachedNote, _note: &Note) -> bool { true } pub fn filter(&self) -> fn(&CachedNote, &Note) -> bool { match self { ViewFilter::Notes => ViewFilter::filter_notes, ViewFilter::NotesAndReplies => ViewFilter::identity, } } } /// A timeline view is a filtered view of notes in a timeline. Two standard views /// are "Notes" and "Notes & Replies". A timeline is associated with a Filter, /// but a TimelineTab is a further filtered view of this Filter that can't /// be captured by a Filter itself. #[derive(Default)] pub struct TimelineTab { pub notes: Vec, pub selection: i32, pub filter: ViewFilter, pub list: Rc>, } impl TimelineTab { pub fn new(filter: ViewFilter) -> Self { TimelineTab::new_with_capacity(filter, 1000) } pub fn new_with_capacity(filter: ViewFilter, cap: usize) -> Self { let selection = 0i32; let mut list = VirtualList::new(); list.hide_on_resize(None); let list = Rc::new(RefCell::new(list)); let notes: Vec = Vec::with_capacity(cap); TimelineTab { notes, selection, filter, list, } } pub fn insert(&mut self, new_refs: &[NoteRef]) { if new_refs.is_empty() { return; } let num_prev_items = self.notes.len(); let (notes, merge_kind) = crate::timeline::merge_sorted_vecs(&self.notes, new_refs); self.notes = notes; let new_items = self.notes.len() - num_prev_items; // TODO: technically items could have been added inbetween if new_items > 0 { let mut list = self.list.borrow_mut(); match merge_kind { // TODO: update egui_virtual_list to support spliced inserts MergeKind::Spliced => list.reset(), MergeKind::FrontInsert => list.items_inserted_at_start(new_items), } } } pub fn select_down(&mut self) { debug!("select_down {}", self.selection + 1); if self.selection + 1 > self.notes.len() as i32 { return; } self.selection += 1; } pub fn select_up(&mut self) { debug!("select_up {}", self.selection - 1); if self.selection - 1 < 0 { return; } self.selection -= 1; } } pub struct Timeline { pub filter: Vec, pub views: Vec, pub selected_view: i32, pub routes: Vec, pub navigating: bool, pub returning: bool, /// Our nostrdb subscription pub subscription: Option, } impl Timeline { pub fn new(filter: Vec) -> Self { let subscription: Option = None; let notes = TimelineTab::new(ViewFilter::Notes); let replies = TimelineTab::new(ViewFilter::NotesAndReplies); let views = vec![notes, replies]; let selected_view = 0; let routes = vec![Route::Timeline("Timeline".to_string())]; let navigating = false; let returning = false; Timeline { navigating, returning, filter, views, subscription, selected_view, routes, } } pub fn current_view(&self) -> &TimelineTab { &self.views[self.selected_view as usize] } pub fn current_view_mut(&mut self) -> &mut TimelineTab { &mut self.views[self.selected_view as usize] } pub fn notes(&self, view: ViewFilter) -> &[NoteRef] { &self.views[view.index()].notes } pub fn view(&self, view: ViewFilter) -> &TimelineTab { &self.views[view.index()] } pub fn view_mut(&mut self, view: ViewFilter) -> &mut TimelineTab { &mut self.views[view.index()] } } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum MergeKind { FrontInsert, Spliced, } pub fn merge_sorted_vecs(vec1: &[T], vec2: &[T]) -> (Vec, MergeKind) { let mut merged = Vec::with_capacity(vec1.len() + vec2.len()); let mut i = 0; let mut j = 0; let mut result: Option = None; while i < vec1.len() && j < vec2.len() { if vec1[i] <= vec2[j] { if result.is_none() && j < vec2.len() { // if we're pushing from our large list and still have // some left in vec2, then this is a splice result = Some(MergeKind::Spliced); } merged.push(vec1[i]); i += 1; } else { merged.push(vec2[j]); j += 1; } } // Append any remaining elements from either vector if i < vec1.len() { merged.extend_from_slice(&vec1[i..]); } if j < vec2.len() { merged.extend_from_slice(&vec2[j..]); } (merged, result.unwrap_or(MergeKind::FrontInsert)) }