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