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, ¬e); 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 }