From 44b1150765ebaf20f93a9ca296c7e7a2f962c8f2 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Tue, 16 Jul 2024 12:48:57 -0700 Subject: [PATCH] threads: add initial thread support This is a really dumb and broken version of threads, but it will be our foundation for future changes. All it currently does is load whatever notes we have locally for a thread in chronological order. It currently does not open any subscriptions. It is not clear what is replying to what, but hey, its a start. Signed-off-by: William Casarin --- src/app.rs | 24 ++++++++++---- src/lib.rs | 1 + src/thread.rs | 75 +++++++++++++++++++++++++++++++++++++++++++ src/ui/mod.rs | 2 ++ src/ui/thread.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 src/thread.rs create mode 100644 src/ui/thread.rs diff --git a/src/app.rs b/src/app.rs index e9fcb2d..7a5ca84 100644 --- a/src/app.rs +++ b/src/app.rs @@ -10,6 +10,7 @@ use crate::note::NoteRef; use crate::notecache::{CachedNote, NoteCache}; use crate::relay_pool_manager::RelayPoolManager; use crate::route::Route; +use crate::thread::Threads; use crate::timeline; use crate::timeline::{MergeKind, Timeline, ViewFilter}; use crate::ui::note::PostAction; @@ -53,10 +54,11 @@ pub struct Damus { pub timelines: Vec, pub selected_timeline: i32, - pub drafts: Drafts, - pub img_cache: ImageCache, pub ndb: Ndb, + pub drafts: Drafts, + pub threads: Threads, + pub img_cache: ImageCache, pub account_manager: AccountManager, frame_history: crate::frame_history::FrameHistory, @@ -820,6 +822,7 @@ impl Damus { Self { pool, is_mobile, + threads: Threads::default(), drafts: Drafts::default(), state: DamusState::Initializing, img_cache: ImageCache::new(imgcache_dir), @@ -849,6 +852,7 @@ impl Damus { config.set_ingester_threads(2); Self { is_mobile, + threads: Threads::default(), drafts: Drafts::default(), state: DamusState::Initializing, pool: RelayPool::new(), @@ -1013,11 +1017,6 @@ fn render_nav(routes: Vec, timeline_ind: usize, app: &mut Damus, ui: &mut None } - Route::Thread(_key) => { - ui.label("thread view"); - None - } - Route::Relays => { let pool = &mut app_ctx.borrow_mut().pool; let manager = RelayPoolManager::new(pool); @@ -1025,6 +1024,17 @@ fn render_nav(routes: Vec, timeline_ind: usize, app: &mut Damus, ui: &mut None } + Route::Thread(id) => { + let app = &mut app_ctx.borrow_mut(); + if let Ok(txn) = Transaction::new(&app.ndb) { + if let Ok(note) = app.ndb.get_note_by_id(&txn, id.bytes()) { + ui::ThreadView::new(app, timeline_ind, ¬e).ui(ui); + } + } + + None + } + Route::Reply(id) => { let mut app = app_ctx.borrow_mut(); diff --git a/src/lib.rs b/src/lib.rs index 1abd35c..2b2a326 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ pub mod relay_pool_manager; mod result; mod route; mod test_data; +mod thread; mod time; mod timecache; mod timeline; diff --git a/src/thread.rs b/src/thread.rs new file mode 100644 index 0000000..3fbd586 --- /dev/null +++ b/src/thread.rs @@ -0,0 +1,75 @@ +use crate::note::NoteRef; +use crate::timeline::{TimelineView, ViewFilter}; +use nostrdb::{Ndb, Transaction}; +use std::collections::HashMap; +use tracing::debug; + +#[derive(Default)] +pub struct Thread { + pub view: TimelineView, +} + +impl Thread { + pub fn new(notes: Vec) -> Self { + let mut cap = ((notes.len() as f32) * 1.5) as usize; + if cap == 0 { + cap = 25; + } + let mut view = TimelineView::new_with_capacity(ViewFilter::NotesAndReplies, cap); + view.notes = notes; + + Thread { view } + } +} + +#[derive(Default)] +pub struct Threads { + threads: HashMap<[u8; 32], Thread>, +} + +impl Threads { + pub fn thread_mut(&mut self, ndb: &Ndb, txn: &Transaction, root_id: &[u8; 32]) -> &mut Thread { + // 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.threads.contains_key(root_id) { + return self.threads.get_mut(root_id).unwrap(); + } + + // looks like we don't have this thread yet, populate it + // TODO: should we do this in the caller? + let root = if let Ok(root) = ndb.get_note_by_id(txn, root_id) { + root + } else { + debug!("couldnt find root note for id {}", hex::encode(root_id)); + self.threads.insert(root_id.to_owned(), Thread::new(vec![])); + return self.threads.get_mut(root_id).unwrap(); + }; + + // we don't have the thread, query for it! + let filter = nostrdb::Filter::new().event(root.id()).build(); + + // TODO: what should be the max results ? + let notes = if let Ok(mut results) = ndb.query(txn, vec![filter], 10000) { + results.reverse(); + results + .into_iter() + .map(NoteRef::from_query_result) + .collect() + } else { + debug!( + "got no results from thread lookup for {}", + hex::encode(root.id()) + ); + vec![] + }; + + debug!("found thread with {} notes", notes.len()); + self.threads.insert(root_id.to_owned(), Thread::new(notes)); + self.threads.get_mut(root_id).unwrap() + } + + //fn thread_by_id(&self, ndb: &Ndb, id: &[u8; 32]) -> &mut Thread { + //} +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 9c185bf..0331a11 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -10,6 +10,7 @@ pub mod preview; pub mod profile; pub mod relay; pub mod side_panel; +pub mod thread; pub mod username; pub use account_management::AccountManagementView; @@ -22,6 +23,7 @@ pub use preview::{Preview, PreviewApp, PreviewConfig}; pub use profile::{profile_preview_controller, ProfilePic, ProfilePreview}; pub use relay::RelayView; pub use side_panel::{DesktopSidePanel, SidePanelAction}; +pub use thread::ThreadView; pub use username::Username; use egui::Margin; diff --git a/src/ui/thread.rs b/src/ui/thread.rs new file mode 100644 index 0000000..d556acd --- /dev/null +++ b/src/ui/thread.rs @@ -0,0 +1,83 @@ +use crate::{ui, Damus}; +use nostrdb::{Note, NoteReply}; +use tracing::warn; + +pub struct ThreadView<'a> { + app: &'a mut Damus, + timeline: usize, + selected_note: &'a Note<'a>, +} + +impl<'a> ThreadView<'a> { + pub fn new(app: &'a mut Damus, timeline: usize, selected_note: &'a Note<'a>) -> Self { + ThreadView { + app, + timeline, + selected_note, + } + } + + pub fn ui(&mut self, ui: &mut egui::Ui) { + let txn = self.selected_note.txn().unwrap(); + let key = self.selected_note.key().unwrap(); + let scroll_id = egui::Id::new(( + "threadscroll", + self.app.timelines[self.timeline].selected_view, + self.timeline, + key, + )); + egui::ScrollArea::vertical() + .id_source(scroll_id) + .animated(false) + .auto_shrink([false, false]) + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) + .show(ui, |ui| { + let root_id = NoteReply::new(self.selected_note.tags()) + .root() + .map_or_else(|| self.selected_note.id(), |nr| nr.id); + + let (len, list) = { + let thread = self.app.threads.thread_mut(&self.app.ndb, txn, root_id); + let len = thread.view.notes.len(); + (len, &mut thread.view.list) + }; + + list.clone() + .borrow_mut() + .ui_custom_layout(ui, len, |ui, start_index| { + ui.spacing_mut().item_spacing.y = 0.0; + ui.spacing_mut().item_spacing.x = 4.0; + + let note_key = { + let thread = self.app.threads.thread_mut(&self.app.ndb, txn, root_id); + thread.view.notes[start_index].key + }; + + let txn = self.selected_note.txn().unwrap(); + + let note = if let Ok(note) = self.app.ndb.get_note_by_key(txn, note_key) { + note + } else { + warn!("failed to query note {:?}", note_key); + return 0; + }; + + ui::padding(8.0, ui, |ui| { + let textmode = self.app.textmode; + let resp = ui::NoteView::new(self.app, ¬e) + .note_previews(!textmode) + .show(ui); + + if let Some(action) = resp.action { + action.execute(self.app, self.timeline, note.id()); + } + }); + + ui::hline(ui); + //ui.add(egui::Separator::default().spacing(0.0)); + + 1 + }); + }); + } +}