diff --git a/src/actionbar.rs b/src/actionbar.rs index a0c91e3..71eebf4 100644 --- a/src/actionbar.rs +++ b/src/actionbar.rs @@ -1,9 +1,9 @@ use crate::{ - multi_subscriber::MultiSubscriber, note::NoteRef, notecache::NoteCache, + notes_holder::{NotesHolder, NotesHolderStorage}, route::{Route, Router}, - thread::{Thread, ThreadResult, Threads}, + thread::Thread, }; use enostr::{NoteId, Pubkey, RelayPool}; use nostrdb::{Ndb, Transaction}; @@ -21,13 +21,13 @@ pub struct TimelineResponse { pub open_profile: Option, } -pub struct NewThreadNotes { - pub root_id: NoteId, +pub struct NewNotes { + pub id: [u8; 32], pub notes: Vec, } -pub enum BarResult { - NewThreadNotes(NewThreadNotes), +pub enum NotesHolderResult { + NewNotes(NewNotes), } /// open_thread is called when a note is selected and we need to navigate @@ -41,51 +41,13 @@ fn open_thread( router: &mut Router, note_cache: &mut NoteCache, pool: &mut RelayPool, - threads: &mut Threads, + threads: &mut NotesHolderStorage, selected_note: &[u8; 32], -) -> Option { +) -> Option { router.route_to(Route::thread(NoteId::new(selected_note.to_owned()))); let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, txn, selected_note); - let thread_res = threads.thread_mut(ndb, txn, root_id); - - let (thread, result) = match thread_res { - ThreadResult::Stale(thread) => { - // The thread is stale, let's update it - let notes = Thread::new_notes(&thread.view().notes, root_id, txn, ndb); - let bar_result = if notes.is_empty() { - None - } else { - Some(BarResult::new_thread_notes( - notes, - NoteId::new(root_id.to_owned()), - )) - }; - - // - // we can't insert and update the VirtualList now, because we - // are already borrowing it mutably. Let's pass it as a - // result instead - // - // thread.view.insert(¬es); <-- no - // - (thread, bar_result) - } - - ThreadResult::Fresh(thread) => (thread, None), - }; - - let multi_subscriber = if let Some(multi_subscriber) = &mut thread.multi_subscriber { - multi_subscriber - } else { - let filters = Thread::filters(root_id); - thread.multi_subscriber = Some(MultiSubscriber::new(filters)); - thread.multi_subscriber.as_mut().unwrap() - }; - - multi_subscriber.subscribe(ndb, pool); - - result + Thread::open(ndb, txn, pool, threads, root_id) } impl BarAction { @@ -94,11 +56,11 @@ impl BarAction { self, ndb: &Ndb, router: &mut Router, - threads: &mut Threads, + threads: &mut NotesHolderStorage, note_cache: &mut NoteCache, pool: &mut RelayPool, txn: &Transaction, - ) -> Option { + ) -> Option { match self { BarAction::Reply(note_id) => { router.route_to(Route::reply(note_id)); @@ -123,7 +85,7 @@ impl BarAction { self, ndb: &Ndb, router: &mut Router, - threads: &mut Threads, + threads: &mut NotesHolderStorage, note_cache: &mut NoteCache, pool: &mut RelayPool, txn: &Transaction, @@ -134,34 +96,39 @@ impl BarAction { } } -impl BarResult { - pub fn new_thread_notes(notes: Vec, root_id: NoteId) -> Self { - BarResult::NewThreadNotes(NewThreadNotes::new(notes, root_id)) +impl NotesHolderResult { + pub fn new_notes(notes: Vec, id: [u8; 32]) -> Self { + NotesHolderResult::NewNotes(NewNotes::new(notes, id)) } - pub fn process(&self, ndb: &Ndb, txn: &Transaction, threads: &mut Threads) { + pub fn process( + &self, + ndb: &Ndb, + txn: &Transaction, + storage: &mut NotesHolderStorage, + ) { match self { // update the thread for next render if we have new notes - BarResult::NewThreadNotes(new_notes) => { - let thread = threads - .thread_mut(ndb, txn, new_notes.root_id.bytes()) + NotesHolderResult::NewNotes(new_notes) => { + let holder = storage + .notes_holder_mutated(ndb, txn, &new_notes.id) .get_ptr(); - new_notes.process(thread); + new_notes.process(holder); } } } } -impl NewThreadNotes { - pub fn new(notes: Vec, root_id: NoteId) -> Self { - NewThreadNotes { notes, root_id } +impl NewNotes { + pub fn new(notes: Vec, id: [u8; 32]) -> Self { + NewNotes { notes, id } } /// Simple helper for processing a NewThreadNotes result. It simply /// inserts/merges the notes into the thread cache - pub fn process(&self, thread: &mut Thread) { + pub fn process(&self, thread: &mut N) { // threads are chronological, ie reversed from reverse-chronological, the default. let reversed = true; - thread.view_mut().insert(&self.notes, reversed); + thread.get_view().insert(&self.notes, reversed); } } diff --git a/src/app.rs b/src/app.rs index 233d990..534191e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,8 +13,9 @@ use crate::{ nav, note::NoteRef, notecache::{CachedNote, NoteCache}, + notes_holder::NotesHolderStorage, subscriptions::{SubKind, Subscriptions}, - thread::Threads, + thread::Thread, timeline::{Timeline, TimelineId, TimelineKind, ViewFilter}, ui::{self, DesktopSidePanel}, unknowns::UnknownIds, @@ -53,7 +54,7 @@ pub struct Damus { pub view_state: ViewState, pub unknown_ids: UnknownIds, pub drafts: Drafts, - pub threads: Threads, + pub threads: NotesHolderStorage, pub img_cache: ImageCache, pub accounts: AccountManager, pub subscriptions: Subscriptions, @@ -709,7 +710,7 @@ impl Damus { unknown_ids: UnknownIds::default(), subscriptions: Subscriptions::default(), since_optimize: parsed_args.since_optimize, - threads: Threads::default(), + threads: NotesHolderStorage::default(), drafts: Drafts::default(), state: DamusState::Initializing, img_cache: ImageCache::new(imgcache_dir.into()), @@ -790,7 +791,7 @@ impl Damus { unknown_ids: UnknownIds::default(), subscriptions: Subscriptions::default(), since_optimize: true, - threads: Threads::default(), + threads: NotesHolderStorage::default(), drafts: Drafts::default(), state: DamusState::Initializing, pool: RelayPool::new(), @@ -817,11 +818,11 @@ impl Damus { &mut self.unknown_ids } - pub fn threads(&self) -> &Threads { + pub fn threads(&self) -> &NotesHolderStorage { &self.threads } - pub fn threads_mut(&mut self) -> &mut Threads { + pub fn threads_mut(&mut self) -> &mut NotesHolderStorage { &mut self.threads } diff --git a/src/lib.rs b/src/lib.rs index fa9c8a0..096c040 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -40,6 +40,7 @@ pub mod ui; mod unknowns; mod user_account; mod view_state; +mod notes_holder; #[cfg(test)] #[macro_use] diff --git a/src/nav.rs b/src/nav.rs index 2071172..e878d31 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -2,9 +2,10 @@ use crate::{ account_manager::render_accounts_route, app_style::{get_font_size, NotedeckTextStyle}, fonts::NamedFontFamily, + notes_holder::NotesHolder, relay_pool_manager::RelayPoolManager, route::Route, - thread::thread_unsubscribe, + thread::Thread, timeline::{ route::{render_profile_route, render_timeline_route, AfterRouteExecution, TimelineRoute}, PubkeySource, Timeline, TimelineKind, @@ -21,7 +22,7 @@ use crate::{ use egui::{pos2, Color32, InnerResponse, Stroke}; use egui_nav::{Nav, NavAction, TitleBarResponse}; -use nostrdb::Ndb; +use nostrdb::{Ndb, Transaction}; use tracing::{error, info}; pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { @@ -163,13 +164,16 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { if let Some(NavAction::Returned) = nav_response.action { let r = app.columns_mut().column_mut(col).router_mut().pop(); if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r { - thread_unsubscribe( - &app.ndb, - &mut app.threads, - &mut app.pool, - &mut app.note_cache, - id.bytes(), - ); + let txn = Transaction::new(&app.ndb).expect("txn"); + let root_id = { + crate::note::root_note_id_from_selected_id( + &app.ndb, + &mut app.note_cache, + &txn, + id.bytes(), + ) + }; + Thread::unsubscribe_locally(&txn, &app.ndb, &mut app.threads, &mut app.pool, root_id); } if let Some(Route::Profile(_, id)) = r { diff --git a/src/notes_holder.rs b/src/notes_holder.rs new file mode 100644 index 0000000..a3df309 --- /dev/null +++ b/src/notes_holder.rs @@ -0,0 +1,189 @@ +use std::collections::HashMap; + +use enostr::{Filter, RelayPool}; +use nostrdb::{Ndb, Transaction}; +use tracing::{debug, warn}; + +use crate::{ + actionbar::NotesHolderResult, multi_subscriber::MultiSubscriber, note::NoteRef, + timeline::TimelineTab, Error, Result, +}; + +#[derive(Default)] +pub struct NotesHolderStorage { + pub id_to_object: HashMap<[u8; 32], M>, +} + +pub enum Vitality<'a, M> { + Fresh(&'a mut M), + Stale(&'a mut M), +} + +impl<'a, M> Vitality<'a, M> { + pub fn get_ptr(self) -> &'a mut M { + match self { + Self::Fresh(ptr) => ptr, + Self::Stale(ptr) => ptr, + } + } + + pub fn is_stale(&self) -> bool { + match self { + Self::Fresh(_ptr) => false, + Self::Stale(_ptr) => true, + } + } +} + +impl NotesHolderStorage { + pub fn notes_holder_expected_mut(&mut self, id: &[u8; 32]) -> &mut M { + self.id_to_object + .get_mut(id) + .expect("thread_expected_mut used but there was no thread") + } + + pub fn notes_holder_mutated<'a>( + &'a mut self, + ndb: &Ndb, + txn: &Transaction, + id: &[u8; 32], + ) -> Vitality<'a, M> { + // we can't use the naive hashmap entry API here because lookups + // require a copy, wait until we have a raw entry api. We could + // also use hashbrown? + + if self.id_to_object.contains_key(id) { + return Vitality::Stale(self.notes_holder_expected_mut(id)); + } + + // we don't have the note holder, query for it! + let filters = M::filters(id); + + let notes = if let Ok(results) = ndb.query(txn, &filters, 1000) { + results + .into_iter() + .map(NoteRef::from_query_result) + .collect() + } else { + debug!("got no results from thread lookup for {}", hex::encode(id)); + vec![] + }; + + if notes.is_empty() { + warn!("thread query returned 0 notes? ") + } else { + debug!("found thread with {} notes", notes.len()); + } + + self.id_to_object + .insert(id.to_owned(), M::new_notes_holder(notes)); + Vitality::Fresh(self.id_to_object.get_mut(id).unwrap()) + } +} + +pub trait NotesHolder { + fn get_multi_subscriber(&mut self) -> Option<&mut MultiSubscriber>; + fn set_multi_subscriber(&mut self, subscriber: MultiSubscriber); + fn get_view(&mut self) -> &mut TimelineTab; + fn filters(for_id: &[u8; 32]) -> Vec; + fn filters_since(for_id: &[u8; 32], since: u64) -> Vec; + fn new_notes_holder(notes: Vec) -> Self; + + #[must_use = "UnknownIds::update_from_note_refs should be used on this result"] + fn poll_notes_into_view(&mut self, txn: &Transaction, ndb: &Ndb) -> Result<()> { + if let Some(multi_subscriber) = self.get_multi_subscriber() { + let reversed = true; + let note_refs: Vec = multi_subscriber.poll_for_notes(ndb, txn)?; + self.get_view().insert(¬e_refs, reversed); + } else { + return Err(Error::Generic( + "Thread unexpectedly has no MultiSubscriber".to_owned(), + )); + } + + Ok(()) + } + + /// Look for new thread notes since our last fetch + fn new_notes(notes: &[NoteRef], id: &[u8; 32], txn: &Transaction, ndb: &Ndb) -> Vec { + if notes.is_empty() { + return vec![]; + } + + let last_note = notes[0]; + let filters = Self::filters_since(id, last_note.created_at + 1); + + if let Ok(results) = ndb.query(txn, &filters, 1000) { + debug!("got {} results from thread update", results.len()); + results + .into_iter() + .map(NoteRef::from_query_result) + .collect() + } else { + debug!("got no results from thread update",); + vec![] + } + } + + /// Local thread unsubscribe + fn unsubscribe_locally( + txn: &Transaction, + ndb: &Ndb, + notes_holder_storage: &mut NotesHolderStorage, + pool: &mut RelayPool, + id: &[u8; 32], + ) { + let notes_holder = notes_holder_storage + .notes_holder_mutated(ndb, txn, id) + .get_ptr(); + + if let Some(multi_subscriber) = notes_holder.get_multi_subscriber() { + multi_subscriber.unsubscribe(ndb, pool); + } + } + + fn open( + ndb: &Ndb, + txn: &Transaction, + pool: &mut RelayPool, + storage: &mut NotesHolderStorage, + id: &[u8; 32], + ) -> Option { + let vitality = storage.notes_holder_mutated(ndb, txn, id); + + let (holder, result) = match vitality { + Vitality::Stale(holder) => { + // The thread is stale, let's update it + let notes = M::new_notes(&holder.get_view().notes, id, txn, ndb); + let holder_result = if notes.is_empty() { + None + } else { + Some(NotesHolderResult::new_notes(notes, id.to_owned())) + }; + + // + // we can't insert and update the VirtualList now, because we + // are already borrowing it mutably. Let's pass it as a + // result instead + // + // thread.view.insert(¬es); <-- no + // + (holder, holder_result) + } + + Vitality::Fresh(thread) => (thread, None), + }; + + let multi_subscriber = if let Some(multi_subscriber) = holder.get_multi_subscriber() { + multi_subscriber + } else { + let filters = M::filters(id); + holder.set_multi_subscriber(MultiSubscriber::new(filters)); + holder.get_multi_subscriber().unwrap() + }; + + multi_subscriber.subscribe(ndb, pool); + + result + } +} diff --git a/src/thread.rs b/src/thread.rs index b368075..11939db 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -1,14 +1,10 @@ use crate::{ multi_subscriber::MultiSubscriber, note::NoteRef, - notecache::NoteCache, + notes_holder::NotesHolder, timeline::{TimelineTab, ViewFilter}, - Error, Result, }; -use enostr::RelayPool; -use nostrdb::{Filter, FilterBuilder, Ndb, Transaction}; -use std::collections::HashMap; -use tracing::{debug, warn}; +use nostrdb::{Filter, FilterBuilder}; #[derive(Default)] pub struct Thread { @@ -39,47 +35,6 @@ impl Thread { &mut self.view } - #[must_use = "UnknownIds::update_from_note_refs should be used on this result"] - pub fn poll_notes_into_view(&mut self, txn: &Transaction, ndb: &Ndb) -> Result<()> { - if let Some(multi_subscriber) = &mut self.multi_subscriber { - let reversed = true; - let note_refs: Vec = multi_subscriber.poll_for_notes(ndb, txn)?; - self.view.insert(¬e_refs, reversed); - } else { - return Err(Error::Generic( - "Thread unexpectedly has no MultiSubscriber".to_owned(), - )); - } - - Ok(()) - } - - /// Look for new thread notes since our last fetch - pub fn new_notes( - notes: &[NoteRef], - root_id: &[u8; 32], - txn: &Transaction, - ndb: &Ndb, - ) -> Vec { - if notes.is_empty() { - return vec![]; - } - - let last_note = notes[0]; - let filters = Thread::filters_since(root_id, last_note.created_at + 1); - - if let Ok(results) = ndb.query(txn, &filters, 1000) { - debug!("got {} results from thread update", results.len()); - results - .into_iter() - .map(NoteRef::from_query_result) - .collect() - } else { - debug!("got no results from thread update",); - vec![] - } - } - fn filters_raw(root: &[u8; 32]) -> Vec { vec![ nostrdb::Filter::new().kinds([1]).event(root), @@ -102,99 +57,28 @@ impl Thread { } } -#[derive(Default)] -pub struct Threads { - /// root id to thread - pub root_id_to_thread: HashMap<[u8; 32], Thread>, -} - -pub enum ThreadResult<'a> { - Fresh(&'a mut Thread), - Stale(&'a mut Thread), -} - -impl<'a> ThreadResult<'a> { - pub fn get_ptr(self) -> &'a mut Thread { - match self { - Self::Fresh(ptr) => ptr, - Self::Stale(ptr) => ptr, - } +impl NotesHolder for Thread { + fn get_multi_subscriber(&mut self) -> Option<&mut MultiSubscriber> { + self.multi_subscriber.as_mut() } - pub fn is_stale(&self) -> bool { - match self { - Self::Fresh(_ptr) => false, - Self::Stale(_ptr) => true, - } - } -} - -impl Threads { - pub fn thread_expected_mut(&mut self, root_id: &[u8; 32]) -> &mut Thread { - self.root_id_to_thread - .get_mut(root_id) - .expect("thread_expected_mut used but there was no thread") - } - - pub fn thread_mut<'a>( - &'a mut self, - ndb: &Ndb, - txn: &Transaction, - root_id: &[u8; 32], - ) -> ThreadResult<'a> { - // we can't use the naive hashmap entry API here because lookups - // require a copy, wait until we have a raw entry api. We could - // also use hashbrown? - - if self.root_id_to_thread.contains_key(root_id) { - return ThreadResult::Stale(self.thread_expected_mut(root_id)); - } - - // we don't have the thread, query for it! - let filters = Thread::filters(root_id); - - let notes = if let Ok(results) = ndb.query(txn, &filters, 1000) { - results - .into_iter() - .map(NoteRef::from_query_result) - .collect() - } else { - debug!( - "got no results from thread lookup for {}", - hex::encode(root_id) - ); - vec![] - }; - - if notes.is_empty() { - warn!("thread query returned 0 notes? ") - } else { - debug!("found thread with {} notes", notes.len()); - } - - self.root_id_to_thread - .insert(root_id.to_owned(), Thread::new(notes)); - ThreadResult::Fresh(self.root_id_to_thread.get_mut(root_id).unwrap()) - } - - //fn thread_by_id(&self, ndb: &Ndb, id: &[u8; 32]) -> &mut Thread { - //} -} - -/// Local thread unsubscribe -pub fn thread_unsubscribe( - ndb: &Ndb, - threads: &mut Threads, - pool: &mut RelayPool, - note_cache: &mut NoteCache, - id: &[u8; 32], -) { - let txn = Transaction::new(ndb).expect("txn"); - let root_id = crate::note::root_note_id_from_selected_id(ndb, note_cache, &txn, id); - - let thread = threads.thread_mut(ndb, &txn, root_id).get_ptr(); - - if let Some(multi_subscriber) = &mut thread.multi_subscriber { - multi_subscriber.unsubscribe(ndb, pool); + fn filters(for_id: &[u8; 32]) -> Vec { + Thread::filters(for_id) + } + + fn new_notes_holder(notes: Vec) -> Self { + Thread::new(notes) + } + + fn get_view(&mut self) -> &mut TimelineTab { + &mut self.view + } + + fn filters_since(for_id: &[u8; 32], since: u64) -> Vec { + Thread::filters_since(for_id, since) + } + + fn set_multi_subscriber(&mut self, subscriber: MultiSubscriber) { + self.multi_subscriber = Some(subscriber); } } diff --git a/src/timeline/route.rs b/src/timeline/route.rs index f83aa08..cc12a63 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -4,7 +4,8 @@ use crate::{ draft::Drafts, imgcache::ImageCache, notecache::NoteCache, - thread::Threads, + notes_holder::NotesHolderStorage, + thread::Thread, timeline::TimelineId, ui::{ self, @@ -46,7 +47,7 @@ pub fn render_timeline_route( drafts: &mut Drafts, img_cache: &mut ImageCache, note_cache: &mut NoteCache, - threads: &mut Threads, + threads: &mut NotesHolderStorage, accounts: &mut AccountManager, route: TimelineRoute, col: usize, @@ -162,7 +163,7 @@ pub fn render_profile_route( pool: &mut RelayPool, img_cache: &mut ImageCache, note_cache: &mut NoteCache, - threads: &mut Threads, + threads: &mut NotesHolderStorage, col: usize, ui: &mut egui::Ui, ) -> Option { diff --git a/src/ui/thread.rs b/src/ui/thread.rs index ae7c24e..2651a9a 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -1,5 +1,6 @@ use crate::{ - actionbar::TimelineResponse, imgcache::ImageCache, notecache::NoteCache, thread::Threads, + actionbar::TimelineResponse, imgcache::ImageCache, notecache::NoteCache, + notes_holder::{NotesHolder, NotesHolderStorage}, thread::Thread, }; use nostrdb::{Ndb, NoteKey, Transaction}; use tracing::error; @@ -7,7 +8,7 @@ use tracing::error; use super::timeline::TimelineTabView; pub struct ThreadView<'a> { - threads: &'a mut Threads, + threads: &'a mut NotesHolderStorage, ndb: &'a Ndb, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, @@ -19,7 +20,7 @@ pub struct ThreadView<'a> { impl<'a> ThreadView<'a> { #[allow(clippy::too_many_arguments)] pub fn new( - threads: &'a mut Threads, + threads: &'a mut NotesHolderStorage, ndb: &'a Ndb, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, @@ -86,7 +87,10 @@ impl<'a> ThreadView<'a> { .map_or_else(|| self.selected_note_id, |nr| nr.id) }; - let thread = self.threads.thread_mut(self.ndb, &txn, root_id).get_ptr(); + let thread = self + .threads + .notes_holder_mutated(self.ndb, &txn, root_id) + .get_ptr(); // TODO(jb55): skip poll if ThreadResult is fresh?