From 44948fdff0f34304795b4f30885ff6a4ab13ea11 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Thu, 10 Oct 2024 17:20:18 -0400 Subject: [PATCH 01/14] init profile routing Signed-off-by: kernelkind --- src/actionbar.rs | 8 ++++++- src/app.rs | 4 ++-- src/column.rs | 10 ++++++++- src/nav.rs | 32 +++++++++++++++++++++++++- src/route.rs | 8 +++++++ src/timeline/route.rs | 40 ++++++++++++++++++++++++++++----- src/ui/note/mod.rs | 30 ++++++++++++++++++------- src/ui/profile/mod.rs | 52 +++++++++++++++++++++++++++++++++++++++++++ src/ui/timeline.rs | 21 ++++++++++++----- 9 files changed, 182 insertions(+), 23 deletions(-) diff --git a/src/actionbar.rs b/src/actionbar.rs index e0a4353..a0c91e3 100644 --- a/src/actionbar.rs +++ b/src/actionbar.rs @@ -5,7 +5,7 @@ use crate::{ route::{Route, Router}, thread::{Thread, ThreadResult, Threads}, }; -use enostr::{NoteId, RelayPool}; +use enostr::{NoteId, Pubkey, RelayPool}; use nostrdb::{Ndb, Transaction}; #[derive(Debug, Eq, PartialEq, Copy, Clone)] @@ -15,6 +15,12 @@ pub enum BarAction { OpenThread(NoteId), } +#[derive(Default)] +pub struct TimelineResponse { + pub bar_action: Option, + pub open_profile: Option, +} + pub struct NewThreadNotes { pub root_id: NoteId, pub notes: Vec, diff --git a/src/app.rs b/src/app.rs index ce3c234..233d990 100644 --- a/src/app.rs +++ b/src/app.rs @@ -693,7 +693,7 @@ impl Damus { let mut columns: Columns = Columns::new(); for col in parsed_args.columns { if let Some(timeline) = col.into_timeline(&ndb, account) { - columns.add_timeline(timeline); + columns.add_new_timeline_column(timeline); } } @@ -777,7 +777,7 @@ impl Damus { let timeline = Timeline::new(TimelineKind::Universe, FilterState::ready(vec![filter])); - columns.add_timeline(timeline); + columns.add_new_timeline_column(timeline); let imgcache_dir = data_path.as_ref().join(ImageCache::rel_datadir()); let _ = std::fs::create_dir_all(imgcache_dir.clone()); diff --git a/src/column.rs b/src/column.rs index 62d45cd..3a7fc99 100644 --- a/src/column.rs +++ b/src/column.rs @@ -45,7 +45,7 @@ impl Columns { Columns::default() } - pub fn add_timeline(&mut self, timeline: Timeline) { + pub fn add_new_timeline_column(&mut self, timeline: Timeline) { let id = Self::get_new_id(); let routes = vec![Route::timeline(timeline.id)]; self.timelines.insert(id, timeline); @@ -60,6 +60,14 @@ impl Columns { self.timelines.insert(col_id, timeline); } + pub fn route_profile_timeline(&mut self, col: usize, timeline: Timeline) { + self.column_mut(col) + .router_mut() + .route_to(Route::Profile(timeline.id)); + + self.timelines.insert(Self::get_new_id(), timeline); + } + pub fn new_column_picker(&mut self) { self.add_column(Column::new(vec![Route::AddColumn])); } diff --git a/src/nav.rs b/src/nav.rs index bca1d03..f8c0fa4 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -5,7 +5,10 @@ use crate::{ relay_pool_manager::RelayPoolManager, route::Route, thread::thread_unsubscribe, - timeline::route::{render_timeline_route, AfterRouteExecution, TimelineRoute}, + timeline::{ + route::{render_profile_route, render_timeline_route, AfterRouteExecution, TimelineRoute}, + PubkeySource, TimelineKind, + }, ui::{ self, add_column::{AddColumnResponse, AddColumnView}, @@ -109,6 +112,18 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { } None } + + Route::Profile(id) => render_profile_route( + *id, + &app.ndb, + &mut app.columns, + &mut app.pool, + &mut app.img_cache, + &mut app.note_cache, + &mut app.threads, + col, + ui, + ), } }); @@ -124,6 +139,21 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { } } } + + AfterRouteExecution::OpenProfile(pubkey) => { + let pubkey_source = match app.accounts.get_selected_account() { + Some(account) if account.pubkey == pubkey => PubkeySource::DeckAuthor, + _ => PubkeySource::Explicit(pubkey), + }; + + if let Some(timeline) = + TimelineKind::profile(pubkey_source).into_timeline(&app.ndb, None) + { + let timeline_id = timeline.id; + app.columns_mut().route_profile_timeline(col, timeline); + app.subscribe_new_timeline(timeline_id); + } + } } } diff --git a/src/route.rs b/src/route.rs index 9b622c9..3ee6be4 100644 --- a/src/route.rs +++ b/src/route.rs @@ -17,6 +17,7 @@ pub enum Route { Relays, ComposeNote, AddColumn, + Profile(TimelineId), } #[derive(Clone)] @@ -96,6 +97,12 @@ impl Route { }, Route::ComposeNote => "Compose Note".to_owned(), Route::AddColumn => "Add Column".to_owned(), + Route::Profile(id) => { + let timeline = columns + .find_timeline(*id) + .expect("expected to find timeline"); + timeline.kind.to_title(ndb) + } }; TitledRoute { @@ -203,6 +210,7 @@ impl fmt::Display for Route { Route::ComposeNote => write!(f, "Compose Note"), Route::AddColumn => write!(f, "Add Column"), + Route::Profile(_) => write!(f, "Profile"), } } } diff --git a/src/timeline/route.rs b/src/timeline/route.rs index 7c1367c..fa9b898 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -12,10 +12,11 @@ use crate::{ post::{PostAction, PostResponse}, QuoteRepostView, }, + profile::ProfileView, }, }; -use enostr::{NoteId, RelayPool}; +use enostr::{NoteId, Pubkey, RelayPool}; use nostrdb::{Ndb, Transaction}; #[derive(Debug, Eq, PartialEq, Clone, Copy)] @@ -28,6 +29,7 @@ pub enum TimelineRoute { pub enum AfterRouteExecution { Post(PostResponse), + OpenProfile(Pubkey), } impl AfterRouteExecution { @@ -53,10 +55,10 @@ pub fn render_timeline_route( ) -> Option { match route { TimelineRoute::Timeline(timeline_id) => { - if let Some(bar_action) = + let timeline_response = ui::TimelineView::new(timeline_id, columns, ndb, note_cache, img_cache, textmode) - .ui(ui) - { + .ui(ui); + if let Some(bar_action) = timeline_response.bar_action { let txn = Transaction::new(ndb).expect("txn"); let mut cur_column = columns.columns_mut(); let router = cur_column[col].router_mut(); @@ -64,7 +66,9 @@ pub fn render_timeline_route( bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); } - None + timeline_response + .open_profile + .map(AfterRouteExecution::OpenProfile) } TimelineRoute::Thread(id) => { @@ -146,3 +150,29 @@ pub fn render_timeline_route( } } } + +#[allow(clippy::too_many_arguments)] +pub fn render_profile_route( + id: TimelineId, + ndb: &Ndb, + columns: &mut Columns, + pool: &mut RelayPool, + img_cache: &mut ImageCache, + note_cache: &mut NoteCache, + threads: &mut Threads, + col: usize, + ui: &mut egui::Ui, +) -> Option { + let timeline_response = ProfileView::new(id, columns, ndb, note_cache, img_cache).ui(ui); + if let Some(bar_action) = timeline_response.bar_action { + let txn = nostrdb::Transaction::new(ndb).expect("txn"); + let mut cur_column = columns.columns_mut(); + let router = cur_column[col].router_mut(); + + bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); + } + + timeline_response + .open_profile + .map(AfterRouteExecution::OpenProfile) +} diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index 0c83718..e2e57c5 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -39,6 +39,7 @@ pub struct NoteResponse { pub response: egui::Response, pub action: Option, pub context_selection: Option, + pub clicked_profile: bool, } impl NoteResponse { @@ -47,6 +48,7 @@ impl NoteResponse { response, action: None, context_selection: None, + clicked_profile: false, } } @@ -60,6 +62,13 @@ impl NoteResponse { ..self } } + + pub fn click_profile(self, clicked_profile: bool) -> Self { + Self { + clicked_profile, + ..self + } + } } impl<'a> View for NoteView<'a> { @@ -305,7 +314,7 @@ impl<'a> NoteView<'a> { note_key: NoteKey, profile: &Result, nostrdb::Error>, ui: &mut egui::Ui, - ) { + ) -> egui::Response { if !self.options().has_wide() { ui.spacing_mut().item_spacing.x = 16.0; } else { @@ -314,6 +323,7 @@ impl<'a> NoteView<'a> { let pfp_size = self.options().pfp_size(); + let sense = Sense::click(); match profile .as_ref() .ok() @@ -326,7 +336,7 @@ impl<'a> NoteView<'a> { let profile_key = profile.as_ref().unwrap().record().note_key(); let note_key = note_key.as_u64(); - let (rect, size, _resp) = ui::anim::hover_expand( + let (rect, size, resp) = ui::anim::hover_expand( ui, egui::Id::new((profile_key, note_key)), pfp_size, @@ -342,13 +352,14 @@ impl<'a> NoteView<'a> { self.img_cache, )); }); + resp } - None => { - ui.add( + None => ui + .add( ui::ProfilePic::new(self.img_cache, ui::ProfilePic::no_pfp_url()) .size(pfp_size), - ); - } + ) + .interact(sense), } } @@ -441,10 +452,12 @@ impl<'a> NoteView<'a> { Pos2::new(x, y) }; + let mut clicked_profile = false; + // wide design let response = if self.options().has_wide() { ui.horizontal(|ui| { - self.pfp(note_key, &profile, ui); + clicked_profile = self.pfp(note_key, &profile, ui).clicked(); let size = ui.available_size(); ui.vertical(|ui| { @@ -498,7 +511,7 @@ impl<'a> NoteView<'a> { } else { // main design ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { - self.pfp(note_key, &profile, ui); + clicked_profile = self.pfp(note_key, &profile, ui).clicked(); ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { selected_option = NoteView::note_header( @@ -557,6 +570,7 @@ impl<'a> NoteView<'a> { NoteResponse::new(response) .with_action(note_action) .select_option(selected_option) + .click_profile(clicked_profile) } } diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs index dafd7b3..51f1284 100644 --- a/src/ui/profile/mod.rs +++ b/src/ui/profile/mod.rs @@ -1,5 +1,57 @@ pub mod picture; pub mod preview; +use egui::{Label, RichText}; +use nostrdb::Ndb; pub use picture::ProfilePic; pub use preview::ProfilePreview; +use tracing::info; + +use crate::{ + actionbar::TimelineResponse, column::Columns, imgcache::ImageCache, notecache::NoteCache, + timeline::TimelineId, +}; + +use super::TimelineView; + +pub struct ProfileView<'a> { + timeline_id: TimelineId, + columns: &'a mut Columns, + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, +} + +impl<'a> ProfileView<'a> { + pub fn new( + timeline_id: TimelineId, + columns: &'a mut Columns, + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, + ) -> Self { + ProfileView { + timeline_id, + columns, + ndb, + note_cache, + img_cache, + } + } + + pub fn ui(&mut self, ui: &mut egui::Ui) -> TimelineResponse { + ui.add(Label::new( + RichText::new("PROFILE VIEW").text_style(egui::TextStyle::Heading), + )); + + TimelineView::new( + self.timeline_id, + self.columns, + self.ndb, + self.note_cache, + self.img_cache, + false, + ) + .ui(ui) + } +} diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index 187c039..2378ef2 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -1,3 +1,4 @@ +use crate::actionbar::TimelineResponse; use crate::{ actionbar::BarAction, column::Columns, imgcache::ImageCache, notecache::NoteCache, timeline::TimelineId, ui, @@ -5,8 +6,9 @@ use crate::{ use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Direction, Layout}; use egui_tabs::TabColor; +use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; -use tracing::{debug, error, warn}; +use tracing::{debug, error, info, warn}; pub struct TimelineView<'a> { timeline_id: TimelineId, @@ -39,7 +41,7 @@ impl<'a> TimelineView<'a> { } } - pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { + pub fn ui(&mut self, ui: &mut egui::Ui) -> TimelineResponse { timeline_ui( ui, self.ndb, @@ -68,7 +70,7 @@ fn timeline_ui( img_cache: &mut ImageCache, reversed: bool, textmode: bool, -) -> Option { +) -> TimelineResponse { //padding(4.0, ui, |ui| ui.heading("Notifications")); /* let font_id = egui::TextStyle::Body.resolve(ui.style()); @@ -83,7 +85,7 @@ fn timeline_ui( error!("tried to render timeline in column, but timeline was missing"); // TODO (jb55): render error when timeline is missing? // this shouldn't happen... - return None; + return TimelineResponse::default(); }; timeline.selected_view = tabs_ui(ui); @@ -94,6 +96,7 @@ fn timeline_ui( egui::Id::new(("tlscroll", timeline.view_id())) }; + let mut open_profile: Option = None; let mut bar_action: Option = None; egui::ScrollArea::vertical() .id_source(scroll_id) @@ -157,6 +160,11 @@ fn timeline_ui( if let Some(context) = resp.context_selection { context.process(ui, ¬e); } + + if resp.clicked_profile { + info!("clicked profile"); + open_profile = Some(Pubkey::new(*note.pubkey())) + } }); ui::hline(ui); @@ -168,7 +176,10 @@ fn timeline_ui( 1 }); - bar_action + TimelineResponse { + open_profile, + bar_action, + } } fn tabs_ui(ui: &mut egui::Ui) -> i32 { From ce3f24abcd07d74f236d9e8d7e1df8d08f62290c Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 11 Oct 2024 16:36:34 -0400 Subject: [PATCH 02/14] add profile preview and implement scrolling Signed-off-by: kernelkind --- src/column.rs | 5 +- src/nav.rs | 6 +- src/route.rs | 8 +-- src/timeline/route.rs | 3 +- src/ui/profile/mod.rs | 43 +++++++---- src/ui/timeline.rs | 164 ++++++++++++++++++++++++++---------------- 6 files changed, 143 insertions(+), 86 deletions(-) diff --git a/src/column.rs b/src/column.rs index 3a7fc99..7415920 100644 --- a/src/column.rs +++ b/src/column.rs @@ -1,5 +1,6 @@ use crate::route::{Route, Router}; use crate::timeline::{Timeline, TimelineId}; +use enostr::Pubkey; use indexmap::IndexMap; use std::iter::Iterator; use std::sync::atomic::{AtomicU32, Ordering}; @@ -60,10 +61,10 @@ impl Columns { self.timelines.insert(col_id, timeline); } - pub fn route_profile_timeline(&mut self, col: usize, timeline: Timeline) { + pub fn route_profile_timeline(&mut self, col: usize, pubkey: Pubkey, timeline: Timeline) { self.column_mut(col) .router_mut() - .route_to(Route::Profile(timeline.id)); + .route_to(Route::Profile(pubkey, timeline.id)); self.timelines.insert(Self::get_new_id(), timeline); } diff --git a/src/nav.rs b/src/nav.rs index f8c0fa4..2b05d51 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -113,8 +113,9 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { None } - Route::Profile(id) => render_profile_route( + Route::Profile(pubkey, id) => render_profile_route( *id, + *pubkey, &app.ndb, &mut app.columns, &mut app.pool, @@ -150,7 +151,8 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { TimelineKind::profile(pubkey_source).into_timeline(&app.ndb, None) { let timeline_id = timeline.id; - app.columns_mut().route_profile_timeline(col, timeline); + app.columns_mut() + .route_profile_timeline(col, pubkey, timeline); app.subscribe_new_timeline(timeline_id); } } diff --git a/src/route.rs b/src/route.rs index 3ee6be4..b8339ac 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,4 +1,4 @@ -use enostr::NoteId; +use enostr::{NoteId, Pubkey}; use nostrdb::Ndb; use std::fmt::{self}; @@ -17,7 +17,7 @@ pub enum Route { Relays, ComposeNote, AddColumn, - Profile(TimelineId), + Profile(Pubkey, TimelineId), } #[derive(Clone)] @@ -97,7 +97,7 @@ impl Route { }, Route::ComposeNote => "Compose Note".to_owned(), Route::AddColumn => "Add Column".to_owned(), - Route::Profile(id) => { + Route::Profile(_, id) => { let timeline = columns .find_timeline(*id) .expect("expected to find timeline"); @@ -210,7 +210,7 @@ impl fmt::Display for Route { Route::ComposeNote => write!(f, "Compose Note"), Route::AddColumn => write!(f, "Add Column"), - Route::Profile(_) => write!(f, "Profile"), + Route::Profile(_, _) => write!(f, "Profile"), } } } diff --git a/src/timeline/route.rs b/src/timeline/route.rs index fa9b898..85fae01 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -154,6 +154,7 @@ pub fn render_timeline_route( #[allow(clippy::too_many_arguments)] pub fn render_profile_route( id: TimelineId, + pubkey: Pubkey, ndb: &Ndb, columns: &mut Columns, pool: &mut RelayPool, @@ -163,7 +164,7 @@ pub fn render_profile_route( col: usize, ui: &mut egui::Ui, ) -> Option { - let timeline_response = ProfileView::new(id, columns, ndb, note_cache, img_cache).ui(ui); + let timeline_response = ProfileView::new(pubkey, id, columns, ndb, note_cache, img_cache).ui(ui); if let Some(bar_action) = timeline_response.bar_action { let txn = nostrdb::Transaction::new(ndb).expect("txn"); let mut cur_column = columns.columns_mut(); diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs index 51f1284..fbba30b 100644 --- a/src/ui/profile/mod.rs +++ b/src/ui/profile/mod.rs @@ -1,11 +1,11 @@ pub mod picture; pub mod preview; -use egui::{Label, RichText}; -use nostrdb::Ndb; +use egui::{ScrollArea, Widget}; +use enostr::Pubkey; +use nostrdb::{Ndb, Transaction}; pub use picture::ProfilePic; pub use preview::ProfilePreview; -use tracing::info; use crate::{ actionbar::TimelineResponse, column::Columns, imgcache::ImageCache, notecache::NoteCache, @@ -15,6 +15,7 @@ use crate::{ use super::TimelineView; pub struct ProfileView<'a> { + pubkey: Pubkey, timeline_id: TimelineId, columns: &'a mut Columns, ndb: &'a Ndb, @@ -24,6 +25,7 @@ pub struct ProfileView<'a> { impl<'a> ProfileView<'a> { pub fn new( + pubkey: Pubkey, timeline_id: TimelineId, columns: &'a mut Columns, ndb: &'a Ndb, @@ -31,6 +33,7 @@ impl<'a> ProfileView<'a> { img_cache: &'a mut ImageCache, ) -> Self { ProfileView { + pubkey, timeline_id, columns, ndb, @@ -40,18 +43,28 @@ impl<'a> ProfileView<'a> { } pub fn ui(&mut self, ui: &mut egui::Ui) -> TimelineResponse { - ui.add(Label::new( - RichText::new("PROFILE VIEW").text_style(egui::TextStyle::Heading), - )); + let scroll_id = egui::Id::new(("profile_scroll", self.timeline_id, self.pubkey)); - TimelineView::new( - self.timeline_id, - self.columns, - self.ndb, - self.note_cache, - self.img_cache, - false, - ) - .ui(ui) + ScrollArea::vertical() + .id_source(scroll_id) + .show(ui, |ui| { + { + let txn = Transaction::new(self.ndb).expect("txn"); + if let Ok(profile) = self.ndb.get_profile_by_pubkey(&txn, self.pubkey.bytes()) { + ProfilePreview::new(&profile, self.img_cache).ui(ui); + } + } + + TimelineView::new( + self.timeline_id, + self.columns, + self.ndb, + self.note_cache, + self.img_cache, + false, + ) + .ui_no_scroll(ui) + }) + .inner } } diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index 2378ef2..cc8c972 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -54,6 +54,23 @@ impl<'a> TimelineView<'a> { ) } + pub fn ui_no_scroll(&mut self, ui: &mut egui::Ui) -> TimelineResponse { + if let Some(timeline) = self.columns.find_timeline_mut(self.timeline_id) { + timeline.selected_view = tabs_ui(ui); + }; + + timeline_ui_no_scroll( + ui, + self.ndb, + self.timeline_id, + self.columns, + self.note_cache, + self.img_cache, + self.reverse, + self.textmode, + ) + } + pub fn reversed(mut self) -> Self { self.reverse = true; self @@ -96,82 +113,105 @@ fn timeline_ui( egui::Id::new(("tlscroll", timeline.view_id())) }; - let mut open_profile: Option = None; - let mut bar_action: Option = None; egui::ScrollArea::vertical() .id_source(scroll_id) .animated(false) .auto_shrink([false, false]) .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) .show(ui, |ui| { - let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) { - timeline + timeline_ui_no_scroll( + ui, + ndb, + timeline_id, + columns, + note_cache, + img_cache, + reversed, + textmode, + ) + }) + .inner +} + +#[allow(clippy::too_many_arguments)] +fn timeline_ui_no_scroll( + ui: &mut egui::Ui, + ndb: &Ndb, + timeline_id: TimelineId, + columns: &mut Columns, + note_cache: &mut NoteCache, + img_cache: &mut ImageCache, + reversed: bool, + textmode: bool, +) -> TimelineResponse { + let mut open_profile: Option = None; + let mut bar_action: Option = None; + + let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) { + timeline + } else { + error!("tried to render timeline in column, but timeline was missing"); + // TODO (jb55): render error when timeline is missing? + // this shouldn't happen... + return TimelineResponse::default(); + }; + + let view = timeline.current_view(); + let len = view.notes.len(); + let txn = if let Ok(txn) = Transaction::new(ndb) { + txn + } else { + warn!("failed to create transaction"); + return TimelineResponse::default(); + }; + + view.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 ind = if reversed { + len - start_index - 1 } else { - error!("tried to render timeline in column, but timeline was missing"); - // TODO (jb55): render error when timeline is missing? - // this shouldn't happen... + start_index + }; + + let note_key = timeline.current_view().notes[ind].key; + + let note = if let Ok(note) = ndb.get_note_by_key(&txn, note_key) { + note + } else { + warn!("failed to query note {:?}", note_key); return 0; }; - let view = timeline.current_view(); - let len = view.notes.len(); - let txn = if let Ok(txn) = Transaction::new(ndb) { - txn - } else { - warn!("failed to create transaction"); - return 0; - }; + ui::padding(8.0, ui, |ui| { + let resp = ui::NoteView::new(ndb, note_cache, img_cache, ¬e) + .note_previews(!textmode) + .selectable_text(false) + .options_button(true) + .show(ui); - view.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; + if let Some(ba) = resp.action { + bar_action = Some(ba); + } else if resp.response.clicked() { + debug!("clicked note"); + } - let ind = if reversed { - len - start_index - 1 - } else { - start_index - }; + if let Some(context) = resp.context_selection { + context.process(ui, ¬e); + } - let note_key = timeline.current_view().notes[ind].key; + if resp.clicked_profile { + info!("clicked profile"); + open_profile = Some(Pubkey::new(*note.pubkey())) + } + }); - let note = if let Ok(note) = 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 resp = ui::NoteView::new(ndb, note_cache, img_cache, ¬e) - .note_previews(!textmode) - .selectable_text(false) - .options_button(true) - .show(ui); - - if let Some(ba) = resp.action { - bar_action = Some(ba); - } else if resp.response.clicked() { - debug!("clicked note"); - } - - if let Some(context) = resp.context_selection { - context.process(ui, ¬e); - } - - if resp.clicked_profile { - info!("clicked profile"); - open_profile = Some(Pubkey::new(*note.pubkey())) - } - }); - - ui::hline(ui); - //ui.add(egui::Separator::default().spacing(0.0)); - - 1 - }); + ui::hline(ui); + //ui.add(egui::Separator::default().spacing(0.0)); 1 }); From 8e6a982843c0712c678764f7da0c610ef0402a27 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 11 Oct 2024 17:47:03 -0400 Subject: [PATCH 03/14] profile unsub Signed-off-by: kernelkind --- src/nav.rs | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/nav.rs b/src/nav.rs index 2b05d51..2071172 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -7,7 +7,7 @@ use crate::{ thread::thread_unsubscribe, timeline::{ route::{render_profile_route, render_timeline_route, AfterRouteExecution, TimelineRoute}, - PubkeySource, TimelineKind, + PubkeySource, Timeline, TimelineKind, }, ui::{ self, @@ -21,6 +21,7 @@ use crate::{ use egui::{pos2, Color32, InnerResponse, Stroke}; use egui_nav::{Nav, NavAction, TitleBarResponse}; +use nostrdb::Ndb; use tracing::{error, info}; pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { @@ -170,6 +171,12 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { id.bytes(), ); } + + if let Some(Route::Profile(_, id)) = r { + if let Some(timeline) = app.columns.find_timeline(id) { + unsubscribe_timeline(&app.ndb, timeline); + } + } } else if let Some(NavAction::Navigated) = nav_response.action { let cur_router = app.columns_mut().column_mut(col).router_mut(); cur_router.navigating = false; @@ -184,23 +191,27 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { app.columns_mut().request_deletion_at_index(col); let tl = app.columns().find_timeline_for_column_index(col); if let Some(timeline) = tl { - if let Some(sub_id) = timeline.subscription { - if let Err(e) = app.ndb.unsubscribe(sub_id) { - error!("unsubscribe error: {}", e); - } else { - info!( - "successfully unsubscribed from timeline {} with sub id {}", - timeline.id, - sub_id.id() - ); - } - } + unsubscribe_timeline(app.ndb(), timeline); } } } } } +fn unsubscribe_timeline(ndb: &Ndb, timeline: &Timeline) { + if let Some(sub_id) = timeline.subscription { + if let Err(e) = ndb.unsubscribe(sub_id) { + error!("unsubscribe error: {}", e); + } else { + info!( + "successfully unsubscribed from timeline {} with sub id {}", + timeline.id, + sub_id.id() + ); + } + } +} + fn title_bar( ui: &mut egui::Ui, allocated_response: egui::Response, From 34aef30fed0882ce4dae22318bdc0128527c77e5 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Fri, 11 Oct 2024 17:58:29 -0400 Subject: [PATCH 04/14] click on thread pfp Signed-off-by: kernelkind --- src/timeline/route.rs | 13 ++++++++----- src/ui/thread.rs | 16 ++++++++++++---- 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/timeline/route.rs b/src/timeline/route.rs index 85fae01..f83aa08 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -72,18 +72,20 @@ pub fn render_timeline_route( } TimelineRoute::Thread(id) => { - if let Some(bar_action) = + let timeline_response = ui::ThreadView::new(threads, ndb, note_cache, img_cache, id.bytes(), textmode) .id_source(egui::Id::new(("threadscroll", col))) - .ui(ui) - { + .ui(ui); + if let Some(bar_action) = timeline_response.bar_action { let txn = Transaction::new(ndb).expect("txn"); let mut cur_column = columns.columns_mut(); let router = cur_column[col].router_mut(); bar_action.execute_and_process_result(ndb, router, threads, note_cache, pool, &txn); } - None + timeline_response + .open_profile + .map(AfterRouteExecution::OpenProfile) } TimelineRoute::Reply(id) => { @@ -164,7 +166,8 @@ pub fn render_profile_route( col: usize, ui: &mut egui::Ui, ) -> Option { - let timeline_response = ProfileView::new(pubkey, id, columns, ndb, note_cache, img_cache).ui(ui); + let timeline_response = + ProfileView::new(pubkey, id, columns, ndb, note_cache, img_cache).ui(ui); if let Some(bar_action) = timeline_response.bar_action { let txn = nostrdb::Transaction::new(ndb).expect("txn"); let mut cur_column = columns.columns_mut(); diff --git a/src/ui/thread.rs b/src/ui/thread.rs index 44ff6f8..a65881e 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -1,6 +1,7 @@ use crate::{ - actionbar::BarAction, imgcache::ImageCache, notecache::NoteCache, thread::Threads, ui, + actionbar::{BarAction, TimelineResponse}, imgcache::ImageCache, notecache::NoteCache, thread::Threads, ui, }; +use enostr::Pubkey; use nostrdb::{Ndb, NoteKey, Transaction}; use tracing::{error, warn}; @@ -41,9 +42,10 @@ impl<'a> ThreadView<'a> { self } - pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { + pub fn ui(&mut self, ui: &mut egui::Ui) -> TimelineResponse { let txn = Transaction::new(self.ndb).expect("txn"); let mut action: Option = None; + let mut open_profile = None; let selected_note_key = if let Ok(key) = self .ndb @@ -53,7 +55,7 @@ impl<'a> ThreadView<'a> { key } else { // TODO: render 404 ? - return None; + return TimelineResponse::default(); }; ui.label( @@ -124,6 +126,9 @@ impl<'a> ThreadView<'a> { if let Some(bar_action) = note_response.action { action = Some(bar_action); } + if note_response.clicked_profile { + open_profile = Some(Pubkey::new(*note.pubkey())) + } if let Some(selection) = note_response.context_selection { selection.process(ui, ¬e); @@ -138,6 +143,9 @@ impl<'a> ThreadView<'a> { ); }); - action + TimelineResponse { + bar_action: action, + open_profile, + } } } From d11d8aa9d045e3f8f529f0ecddb33df493345e31 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 14 Oct 2024 10:58:43 -0400 Subject: [PATCH 05/14] consolidate timelineTab ui to TimelineTabView Signed-off-by: kernelkind --- src/ui/thread.rs | 74 +++++-------------- src/ui/timeline.rs | 174 ++++++++++++++++++++++++++++----------------- 2 files changed, 124 insertions(+), 124 deletions(-) diff --git a/src/ui/thread.rs b/src/ui/thread.rs index a65881e..ae7c24e 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -1,9 +1,10 @@ use crate::{ - actionbar::{BarAction, TimelineResponse}, imgcache::ImageCache, notecache::NoteCache, thread::Threads, ui, + actionbar::TimelineResponse, imgcache::ImageCache, notecache::NoteCache, thread::Threads, }; -use enostr::Pubkey; use nostrdb::{Ndb, NoteKey, Transaction}; -use tracing::{error, warn}; +use tracing::error; + +use super::timeline::TimelineTabView; pub struct ThreadView<'a> { threads: &'a mut Threads, @@ -44,8 +45,6 @@ impl<'a> ThreadView<'a> { pub fn ui(&mut self, ui: &mut egui::Ui) -> TimelineResponse { let txn = Transaction::new(self.ndb).expect("txn"); - let mut action: Option = None; - let mut open_profile = None; let selected_note_key = if let Ok(key) = self .ndb @@ -72,7 +71,7 @@ impl<'a> ThreadView<'a> { let note = if let Ok(note) = self.ndb.get_note_by_key(&txn, selected_note_key) { note } else { - return; + return TimelineResponse::default(); }; let root_id = { @@ -96,56 +95,17 @@ impl<'a> ThreadView<'a> { error!("Thread::poll_notes_into_view: {e}"); } - let len = thread.view().notes.len(); - - thread.view().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 ind = len - 1 - start_index; - - let note_key = thread.view().notes[ind].key; - - let note = if let Ok(note) = self.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 note_response = - ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, ¬e) - .note_previews(!self.textmode) - .textmode(self.textmode) - .options_button(!self.textmode) - .show(ui); - if let Some(bar_action) = note_response.action { - action = Some(bar_action); - } - if note_response.clicked_profile { - open_profile = Some(Pubkey::new(*note.pubkey())) - } - - if let Some(selection) = note_response.context_selection { - selection.process(ui, ¬e); - } - }); - - ui::hline(ui); - //ui.add(egui::Separator::default().spacing(0.0)); - - 1 - }, - ); - }); - - TimelineResponse { - bar_action: action, - open_profile, - } + TimelineTabView::new( + thread.view(), + true, + self.textmode, + &txn, + self.ndb, + self.note_cache, + self.img_cache, + ) + .show(ui) + }) + .inner } } diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index cc8c972..d8951af 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -1,4 +1,5 @@ use crate::actionbar::TimelineResponse; +use crate::timeline::TimelineTab; use crate::{ actionbar::BarAction, column::Columns, imgcache::ImageCache, notecache::NoteCache, timeline::TimelineId, ui, @@ -144,9 +145,6 @@ fn timeline_ui_no_scroll( reversed: bool, textmode: bool, ) -> TimelineResponse { - let mut open_profile: Option = None; - let mut bar_action: Option = None; - let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) { timeline } else { @@ -156,70 +154,17 @@ fn timeline_ui_no_scroll( return TimelineResponse::default(); }; - let view = timeline.current_view(); - let len = view.notes.len(); - let txn = if let Ok(txn) = Transaction::new(ndb) { - txn - } else { - warn!("failed to create transaction"); - return TimelineResponse::default(); - }; - - view.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 ind = if reversed { - len - start_index - 1 - } else { - start_index - }; - - let note_key = timeline.current_view().notes[ind].key; - - let note = if let Ok(note) = 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 resp = ui::NoteView::new(ndb, note_cache, img_cache, ¬e) - .note_previews(!textmode) - .selectable_text(false) - .options_button(true) - .show(ui); - - if let Some(ba) = resp.action { - bar_action = Some(ba); - } else if resp.response.clicked() { - debug!("clicked note"); - } - - if let Some(context) = resp.context_selection { - context.process(ui, ¬e); - } - - if resp.clicked_profile { - info!("clicked profile"); - open_profile = Some(Pubkey::new(*note.pubkey())) - } - }); - - ui::hline(ui); - //ui.add(egui::Separator::default().spacing(0.0)); - - 1 - }); - - TimelineResponse { - open_profile, - bar_action, - } + let txn = Transaction::new(ndb).expect("failed to create txn"); + TimelineTabView::new( + timeline.current_view(), + reversed, + textmode, + &txn, + ndb, + note_cache, + img_cache, + ) + .show(ui) } fn tabs_ui(ui: &mut egui::Ui) -> i32 { @@ -305,3 +250,98 @@ fn shrink_range_to_width(range: egui::Rangef, width: f32) -> egui::Rangef { egui::Rangef::new(min, max) } + +pub struct TimelineTabView<'a> { + tab: &'a TimelineTab, + reversed: bool, + textmode: bool, + txn: &'a Transaction, + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, +} + +impl<'a> TimelineTabView<'a> { + pub fn new( + tab: &'a TimelineTab, + reversed: bool, + textmode: bool, + txn: &'a Transaction, + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, + ) -> Self { + Self { + tab, + reversed, + txn, + textmode, + ndb, + note_cache, + img_cache, + } + } + + pub fn show(&mut self, ui: &mut egui::Ui) -> TimelineResponse { + let mut open_profile = None; + let mut bar_action: Option = None; + let len = self.tab.notes.len(); + + self.tab + .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 ind = if self.reversed { + len - start_index - 1 + } else { + start_index + }; + + let note_key = self.tab.notes[ind].key; + + let note = if let Ok(note) = self.ndb.get_note_by_key(self.txn, note_key) { + note + } else { + warn!("failed to query note {:?}", note_key); + return 0; + }; + + ui::padding(8.0, ui, |ui| { + let resp = ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, ¬e) + .note_previews(!self.textmode) + .selectable_text(false) + .options_button(true) + .show(ui); + + if let Some(ba) = resp.action { + bar_action = Some(ba); + } else if resp.response.clicked() { + debug!("clicked note"); + } + + if let Some(context) = resp.context_selection { + context.process(ui, ¬e); + } + + if resp.clicked_profile { + info!("clicked profile"); + open_profile = Some(Pubkey::new(*note.pubkey())) + } + }); + + ui::hline(ui); + //ui.add(egui::Separator::default().spacing(0.0)); + + 1 + }); + + TimelineResponse { + open_profile, + bar_action, + } + } +} From 780fba30935408ec3ff8c69fcf54312147137b6d Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 9 Oct 2024 12:31:01 -0400 Subject: [PATCH 06/14] generify Threads Signed-off-by: kernelkind --- src/actionbar.rs | 93 +++++++-------------- src/app.rs | 13 +-- src/lib.rs | 1 + src/nav.rs | 22 +++-- src/notes_holder.rs | 189 ++++++++++++++++++++++++++++++++++++++++++ src/thread.rs | 162 +++++------------------------------- src/timeline/route.rs | 7 +- src/ui/thread.rs | 12 ++- 8 files changed, 275 insertions(+), 224 deletions(-) create mode 100644 src/notes_holder.rs 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? From 705a4bdf051f759c9706aeb126bfe3f6062f2102 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Wed, 9 Oct 2024 15:49:24 -0400 Subject: [PATCH 07/14] profile struct Signed-off-by: kernelkind --- src/profile.rs | 67 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/src/profile.rs b/src/profile.rs index 9da7026..77ac8c0 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -1,4 +1,13 @@ -use nostrdb::ProfileRecord; +use enostr::Filter; +use nostrdb::{FilterBuilder, ProfileRecord}; + +use crate::{ + filter, + multi_subscriber::MultiSubscriber, + note::NoteRef, + notes_holder::NotesHolder, + timeline::{Timeline, TimelineTab, ViewFilter}, +}; pub enum DisplayName<'a> { One(&'a str), @@ -37,3 +46,59 @@ pub fn get_profile_name<'a>(record: &'a ProfileRecord) -> Option }), } } + +pub struct Profile { + view: TimelineTab, + pub multi_subscriber: Option, +} + +impl Profile { + 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 = TimelineTab::new_with_capacity(ViewFilter::NotesAndReplies, cap); + view.notes = notes; + + Profile { + view, + multi_subscriber: None, + } + } + + fn filters_raw(pk: &[u8; 32]) -> Vec { + vec![Filter::new() + .authors([pk]) + .kinds([1]) + .limit(filter::default_limit())] + } +} + +impl NotesHolder for Profile { + fn get_multi_subscriber(&mut self) -> Option<&mut MultiSubscriber> { + self.multi_subscriber.as_mut() + } + + fn get_view(&mut self) -> &mut crate::timeline::TimelineTab { + &mut self.view + } + + fn filters(for_id: &[u8; 32]) -> Vec { + Profile::filters_raw(for_id) + .into_iter() + .map(|mut f| f.build()) + .collect() + } + + fn filters_since(for_id: &[u8; 32], since: u64) -> Vec { + Profile::filters_raw(for_id) + .into_iter() + .map(|f| f.since(since).build()) + .collect() + } + + fn new_notes_holder(notes: Vec) -> Self { + Profile::new(notes) + } +} From 6ffe33e924a6557f075702af66b365e39907f6e0 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 14 Oct 2024 12:53:11 -0400 Subject: [PATCH 08/14] integrate profile view caching Signed-off-by: kernelkind --- src/app.rs | 4 ++++ src/column.rs | 9 --------- src/nav.rs | 46 ++++++++++++++++++++++++------------------- src/notes_holder.rs | 17 ++++++++++++---- src/profile.rs | 31 +++++++++++++++-------------- src/route.rs | 13 +++++------- src/thread.rs | 2 +- src/timeline/route.rs | 5 +++-- src/ui/profile/mod.rs | 45 +++++++++++++++++++++++------------------- src/ui/timeline.rs | 2 +- 10 files changed, 94 insertions(+), 80 deletions(-) diff --git a/src/app.rs b/src/app.rs index 534191e..3f07c83 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,6 +14,7 @@ use crate::{ note::NoteRef, notecache::{CachedNote, NoteCache}, notes_holder::NotesHolderStorage, + profile::Profile, subscriptions::{SubKind, Subscriptions}, thread::Thread, timeline::{Timeline, TimelineId, TimelineKind, ViewFilter}, @@ -55,6 +56,7 @@ pub struct Damus { pub unknown_ids: UnknownIds, pub drafts: Drafts, pub threads: NotesHolderStorage, + pub profiles: NotesHolderStorage, pub img_cache: ImageCache, pub accounts: AccountManager, pub subscriptions: Subscriptions, @@ -711,6 +713,7 @@ impl Damus { subscriptions: Subscriptions::default(), since_optimize: parsed_args.since_optimize, threads: NotesHolderStorage::default(), + profiles: NotesHolderStorage::default(), drafts: Drafts::default(), state: DamusState::Initializing, img_cache: ImageCache::new(imgcache_dir.into()), @@ -792,6 +795,7 @@ impl Damus { subscriptions: Subscriptions::default(), since_optimize: true, threads: NotesHolderStorage::default(), + profiles: NotesHolderStorage::default(), drafts: Drafts::default(), state: DamusState::Initializing, pool: RelayPool::new(), diff --git a/src/column.rs b/src/column.rs index 7415920..0b98632 100644 --- a/src/column.rs +++ b/src/column.rs @@ -1,6 +1,5 @@ use crate::route::{Route, Router}; use crate::timeline::{Timeline, TimelineId}; -use enostr::Pubkey; use indexmap::IndexMap; use std::iter::Iterator; use std::sync::atomic::{AtomicU32, Ordering}; @@ -61,14 +60,6 @@ impl Columns { self.timelines.insert(col_id, timeline); } - pub fn route_profile_timeline(&mut self, col: usize, pubkey: Pubkey, timeline: Timeline) { - self.column_mut(col) - .router_mut() - .route_to(Route::Profile(pubkey, timeline.id)); - - self.timelines.insert(Self::get_new_id(), timeline); - } - pub fn new_column_picker(&mut self) { self.add_column(Column::new(vec![Route::AddColumn])); } diff --git a/src/nav.rs b/src/nav.rs index e878d31..d4c1f07 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -3,12 +3,13 @@ use crate::{ app_style::{get_font_size, NotedeckTextStyle}, fonts::NamedFontFamily, notes_holder::NotesHolder, + profile::Profile, relay_pool_manager::RelayPoolManager, route::Route, thread::Thread, timeline::{ route::{render_profile_route, render_timeline_route, AfterRouteExecution, TimelineRoute}, - PubkeySource, Timeline, TimelineKind, + Timeline, }, ui::{ self, @@ -115,11 +116,11 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { None } - Route::Profile(pubkey, id) => render_profile_route( - *id, + Route::Profile(pubkey) => render_profile_route( *pubkey, &app.ndb, &mut app.columns, + &mut app.profiles, &mut app.pool, &mut app.img_cache, &mut app.note_cache, @@ -144,18 +145,19 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { } AfterRouteExecution::OpenProfile(pubkey) => { - let pubkey_source = match app.accounts.get_selected_account() { - Some(account) if account.pubkey == pubkey => PubkeySource::DeckAuthor, - _ => PubkeySource::Explicit(pubkey), - }; - - if let Some(timeline) = - TimelineKind::profile(pubkey_source).into_timeline(&app.ndb, None) - { - let timeline_id = timeline.id; - app.columns_mut() - .route_profile_timeline(col, pubkey, timeline); - app.subscribe_new_timeline(timeline_id); + app.columns + .column_mut(col) + .router_mut() + .route_to(Route::Profile(pubkey)); + let txn = Transaction::new(&app.ndb).expect("txn"); + if let Some(res) = Profile::open( + &app.ndb, + &txn, + &mut app.pool, + &mut app.profiles, + pubkey.bytes(), + ) { + res.process(&app.ndb, &txn, &mut app.profiles); } } } @@ -163,8 +165,8 @@ 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(); + let txn = Transaction::new(&app.ndb).expect("txn"); if let Some(Route::Timeline(TimelineRoute::Thread(id))) = r { - let txn = Transaction::new(&app.ndb).expect("txn"); let root_id = { crate::note::root_note_id_from_selected_id( &app.ndb, @@ -176,10 +178,14 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { Thread::unsubscribe_locally(&txn, &app.ndb, &mut app.threads, &mut app.pool, root_id); } - if let Some(Route::Profile(_, id)) = r { - if let Some(timeline) = app.columns.find_timeline(id) { - unsubscribe_timeline(&app.ndb, timeline); - } + if let Some(Route::Profile(pubkey)) = r { + Profile::unsubscribe_locally( + &txn, + &app.ndb, + &mut app.profiles, + &mut app.pool, + pubkey.bytes(), + ); } } else if let Some(NavAction::Navigated) = nav_response.action { let cur_router = app.columns_mut().column_mut(col).router_mut(); diff --git a/src/notes_holder.rs b/src/notes_holder.rs index a3df309..536a28c 100644 --- a/src/notes_holder.rs +++ b/src/notes_holder.rs @@ -9,11 +9,18 @@ use crate::{ timeline::TimelineTab, Error, Result, }; -#[derive(Default)] pub struct NotesHolderStorage { pub id_to_object: HashMap<[u8; 32], M>, } +impl Default for NotesHolderStorage { + fn default() -> Self { + NotesHolderStorage { + id_to_object: HashMap::new(), + } + } +} + pub enum Vitality<'a, M> { Fresh(&'a mut M), Stale(&'a mut M), @@ -75,8 +82,10 @@ impl NotesHolderStorage { debug!("found thread with {} notes", notes.len()); } - self.id_to_object - .insert(id.to_owned(), M::new_notes_holder(notes)); + self.id_to_object.insert( + id.to_owned(), + M::new_notes_holder(id, M::filters(id), notes), + ); Vitality::Fresh(self.id_to_object.get_mut(id).unwrap()) } } @@ -87,7 +96,7 @@ pub trait NotesHolder { 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; + fn new_notes_holder(id: &[u8; 32], filters: Vec, 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<()> { diff --git a/src/profile.rs b/src/profile.rs index 77ac8c0..28c9775 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -1,12 +1,12 @@ -use enostr::Filter; +use enostr::{Filter, Pubkey}; use nostrdb::{FilterBuilder, ProfileRecord}; use crate::{ - filter, + filter::{self, FilterState}, multi_subscriber::MultiSubscriber, note::NoteRef, notes_holder::NotesHolder, - timeline::{Timeline, TimelineTab, ViewFilter}, + timeline::{PubkeySource, Timeline, TimelineKind}, }; pub enum DisplayName<'a> { @@ -48,21 +48,18 @@ pub fn get_profile_name<'a>(record: &'a ProfileRecord) -> Option } pub struct Profile { - view: TimelineTab, + pub timeline: Timeline, pub multi_subscriber: Option, } impl Profile { - 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 = TimelineTab::new_with_capacity(ViewFilter::NotesAndReplies, cap); - view.notes = notes; + pub fn new(source: PubkeySource, filters: Vec, notes: Vec) -> Self { + let mut timeline = + Timeline::new(TimelineKind::profile(source), FilterState::ready(filters)); + timeline.current_view_mut().notes = notes; Profile { - view, + timeline, multi_subscriber: None, } } @@ -81,7 +78,7 @@ impl NotesHolder for Profile { } fn get_view(&mut self) -> &mut crate::timeline::TimelineTab { - &mut self.view + self.timeline.current_view_mut() } fn filters(for_id: &[u8; 32]) -> Vec { @@ -98,7 +95,11 @@ impl NotesHolder for Profile { .collect() } - fn new_notes_holder(notes: Vec) -> Self { - Profile::new(notes) + fn new_notes_holder(id: &[u8; 32], filters: Vec, notes: Vec) -> Self { + Profile::new(PubkeySource::Explicit(Pubkey::new(*id)), filters, notes) + } + + fn set_multi_subscriber(&mut self, subscriber: MultiSubscriber) { + self.multi_subscriber = Some(subscriber); } } diff --git a/src/route.rs b/src/route.rs index b8339ac..9fb0f7f 100644 --- a/src/route.rs +++ b/src/route.rs @@ -6,7 +6,7 @@ use crate::{ account_manager::AccountsRoute, column::Columns, timeline::{TimelineId, TimelineRoute}, - ui::profile::preview::get_note_users_displayname_string, + ui::profile::preview::{get_note_users_displayname_string, get_profile_displayname_string}, }; /// App routing. These describe different places you can go inside Notedeck. @@ -17,7 +17,7 @@ pub enum Route { Relays, ComposeNote, AddColumn, - Profile(Pubkey, TimelineId), + Profile(Pubkey), } #[derive(Clone)] @@ -97,11 +97,8 @@ impl Route { }, Route::ComposeNote => "Compose Note".to_owned(), Route::AddColumn => "Add Column".to_owned(), - Route::Profile(_, id) => { - let timeline = columns - .find_timeline(*id) - .expect("expected to find timeline"); - timeline.kind.to_title(ndb) + Route::Profile(pubkey) => { + format!("{}'s Profile", get_profile_displayname_string(ndb, pubkey)) } }; @@ -210,7 +207,7 @@ impl fmt::Display for Route { Route::ComposeNote => write!(f, "Compose Note"), Route::AddColumn => write!(f, "Add Column"), - Route::Profile(_, _) => write!(f, "Profile"), + Route::Profile(_) => write!(f, "Profile"), } } } diff --git a/src/thread.rs b/src/thread.rs index 11939db..00159c5 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -66,7 +66,7 @@ impl NotesHolder for Thread { Thread::filters(for_id) } - fn new_notes_holder(notes: Vec) -> Self { + fn new_notes_holder(_: &[u8; 32], _: Vec, notes: Vec) -> Self { Thread::new(notes) } diff --git a/src/timeline/route.rs b/src/timeline/route.rs index cc12a63..3a5597c 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -5,6 +5,7 @@ use crate::{ imgcache::ImageCache, notecache::NoteCache, notes_holder::NotesHolderStorage, + profile::Profile, thread::Thread, timeline::TimelineId, ui::{ @@ -156,10 +157,10 @@ pub fn render_timeline_route( #[allow(clippy::too_many_arguments)] pub fn render_profile_route( - id: TimelineId, pubkey: Pubkey, ndb: &Ndb, columns: &mut Columns, + profiles: &mut NotesHolderStorage, pool: &mut RelayPool, img_cache: &mut ImageCache, note_cache: &mut NoteCache, @@ -168,7 +169,7 @@ pub fn render_profile_route( ui: &mut egui::Ui, ) -> Option { let timeline_response = - ProfileView::new(pubkey, id, columns, ndb, note_cache, img_cache).ui(ui); + ProfileView::new(pubkey, col, profiles, ndb, note_cache, img_cache).ui(ui); if let Some(bar_action) = timeline_response.bar_action { let txn = nostrdb::Transaction::new(ndb).expect("txn"); let mut cur_column = columns.columns_mut(); diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs index fbba30b..03cbaf2 100644 --- a/src/ui/profile/mod.rs +++ b/src/ui/profile/mod.rs @@ -8,16 +8,16 @@ pub use picture::ProfilePic; pub use preview::ProfilePreview; use crate::{ - actionbar::TimelineResponse, column::Columns, imgcache::ImageCache, notecache::NoteCache, - timeline::TimelineId, + actionbar::TimelineResponse, imgcache::ImageCache, notecache::NoteCache, + notes_holder::NotesHolderStorage, profile::Profile, }; -use super::TimelineView; +use super::timeline::{tabs_ui, TimelineTabView}; pub struct ProfileView<'a> { pubkey: Pubkey, - timeline_id: TimelineId, - columns: &'a mut Columns, + col_id: usize, + profiles: &'a mut NotesHolderStorage, ndb: &'a Ndb, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, @@ -26,16 +26,16 @@ pub struct ProfileView<'a> { impl<'a> ProfileView<'a> { pub fn new( pubkey: Pubkey, - timeline_id: TimelineId, - columns: &'a mut Columns, + col_id: usize, + profiles: &'a mut NotesHolderStorage, ndb: &'a Ndb, note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, ) -> Self { ProfileView { pubkey, - timeline_id, - columns, + col_id, + profiles, ndb, note_cache, img_cache, @@ -43,27 +43,32 @@ impl<'a> ProfileView<'a> { } pub fn ui(&mut self, ui: &mut egui::Ui) -> TimelineResponse { - let scroll_id = egui::Id::new(("profile_scroll", self.timeline_id, self.pubkey)); + let scroll_id = egui::Id::new(("profile_scroll", self.col_id, self.pubkey)); ScrollArea::vertical() .id_source(scroll_id) .show(ui, |ui| { - { - let txn = Transaction::new(self.ndb).expect("txn"); - if let Ok(profile) = self.ndb.get_profile_by_pubkey(&txn, self.pubkey.bytes()) { - ProfilePreview::new(&profile, self.img_cache).ui(ui); - } + let txn = Transaction::new(self.ndb).expect("txn"); + if let Ok(profile) = self.ndb.get_profile_by_pubkey(&txn, self.pubkey.bytes()) { + ProfilePreview::new(&profile, self.img_cache).ui(ui); } + let profile = self + .profiles + .notes_holder_mutated(self.ndb, &txn, self.pubkey.bytes()) + .get_ptr(); - TimelineView::new( - self.timeline_id, - self.columns, + profile.timeline.selected_view = tabs_ui(ui); + + TimelineTabView::new( + profile.timeline.current_view(), + false, + false, + &txn, self.ndb, self.note_cache, self.img_cache, - false, ) - .ui_no_scroll(ui) + .show(ui) }) .inner } diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index d8951af..9de4558 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -167,7 +167,7 @@ fn timeline_ui_no_scroll( .show(ui) } -fn tabs_ui(ui: &mut egui::Ui) -> i32 { +pub fn tabs_ui(ui: &mut egui::Ui) -> i32 { ui.spacing_mut().item_spacing.y = 0.0; let tab_res = egui_tabs::Tabs::new(2) From dcb6620dddabd0644dbe37f34569a50ad577125e Mon Sep 17 00:00:00 2001 From: kernelkind Date: Mon, 14 Oct 2024 17:39:10 -0400 Subject: [PATCH 09/14] proper timelineTabs Signed-off-by: kernelkind --- src/actionbar.rs | 7 ++++--- src/app.rs | 37 +++++++++++++++++++++++++------------ src/nav.rs | 13 +++++++++++-- src/notes_holder.rs | 24 +++++++++++++++++------- src/profile.rs | 38 ++++++++++++++++++++++++++++---------- src/thread.rs | 12 ++++++++++-- src/ui/profile/mod.rs | 2 +- src/ui/thread.rs | 2 +- 8 files changed, 97 insertions(+), 38 deletions(-) diff --git a/src/actionbar.rs b/src/actionbar.rs index 71eebf4..1752305 100644 --- a/src/actionbar.rs +++ b/src/actionbar.rs @@ -47,7 +47,7 @@ fn open_thread( 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); - Thread::open(ndb, txn, pool, threads, root_id) + Thread::open(ndb, note_cache, txn, pool, threads, root_id) } impl BarAction { @@ -91,7 +91,7 @@ impl BarAction { txn: &Transaction, ) { if let Some(br) = self.execute(ndb, router, threads, note_cache, pool, txn) { - br.process(ndb, txn, threads); + br.process(ndb, note_cache, txn, threads); } } } @@ -104,6 +104,7 @@ impl NotesHolderResult { pub fn process( &self, ndb: &Ndb, + note_cache: &mut NoteCache, txn: &Transaction, storage: &mut NotesHolderStorage, ) { @@ -111,7 +112,7 @@ impl NotesHolderResult { // update the thread for next render if we have new notes NotesHolderResult::NewNotes(new_notes) => { let holder = storage - .notes_holder_mutated(ndb, txn, &new_notes.id) + .notes_holder_mutated(ndb, note_cache, txn, &new_notes.id) .get_ptr(); new_notes.process(holder); } diff --git a/src/app.rs b/src/app.rs index 3f07c83..e04b8ad 100644 --- a/src/app.rs +++ b/src/app.rs @@ -364,8 +364,24 @@ fn setup_initial_timeline( timeline.subscription, timeline.filter ); let lim = filters[0].limit().unwrap_or(crate::filter::default_limit()) as i32; - let results = ndb.query(&txn, filters, lim)?; + let notes = ndb + .query(&txn, filters, lim)? + .into_iter() + .map(NoteRef::from_query_result) + .collect(); + copy_notes_into_timeline(timeline, &txn, ndb, note_cache, notes); + + Ok(()) +} + +pub fn copy_notes_into_timeline( + timeline: &mut Timeline, + txn: &Transaction, + ndb: &Ndb, + note_cache: &mut NoteCache, + notes: Vec, +) { let filters = { let views = &timeline.views; let filters: Vec bool> = @@ -373,21 +389,18 @@ fn setup_initial_timeline( filters }; - for result in results { + for note_ref in notes { for (view, filter) in filters.iter().enumerate() { - if filter( - note_cache.cached_note_or_insert_mut(result.note_key, &result.note), - &result.note, - ) { - timeline.views[view].notes.push(NoteRef { - key: result.note_key, - created_at: result.note.created_at(), - }) + if let Ok(note) = ndb.get_note_by_key(txn, note_ref.key) { + if filter( + note_cache.cached_note_or_insert_mut(note_ref.key, ¬e), + ¬e, + ) { + timeline.views[view].notes.push(note_ref) + } } } } - - Ok(()) } fn setup_initial_nostrdb_subs( diff --git a/src/nav.rs b/src/nav.rs index d4c1f07..a3e62a3 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -152,12 +152,13 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { let txn = Transaction::new(&app.ndb).expect("txn"); if let Some(res) = Profile::open( &app.ndb, + &mut app.note_cache, &txn, &mut app.pool, &mut app.profiles, pubkey.bytes(), ) { - res.process(&app.ndb, &txn, &mut app.profiles); + res.process(&app.ndb, &mut app.note_cache, &txn, &mut app.profiles); } } } @@ -175,13 +176,21 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { id.bytes(), ) }; - Thread::unsubscribe_locally(&txn, &app.ndb, &mut app.threads, &mut app.pool, root_id); + Thread::unsubscribe_locally( + &txn, + &app.ndb, + &mut app.note_cache, + &mut app.threads, + &mut app.pool, + root_id, + ); } if let Some(Route::Profile(pubkey)) = r { Profile::unsubscribe_locally( &txn, &app.ndb, + &mut app.note_cache, &mut app.profiles, &mut app.pool, pubkey.bytes(), diff --git a/src/notes_holder.rs b/src/notes_holder.rs index 536a28c..a7c7019 100644 --- a/src/notes_holder.rs +++ b/src/notes_holder.rs @@ -2,11 +2,11 @@ use std::collections::HashMap; use enostr::{Filter, RelayPool}; use nostrdb::{Ndb, Transaction}; -use tracing::{debug, warn}; +use tracing::{debug, info, warn}; use crate::{ actionbar::NotesHolderResult, multi_subscriber::MultiSubscriber, note::NoteRef, - timeline::TimelineTab, Error, Result, + notecache::NoteCache, timeline::TimelineTab, Error, Result, }; pub struct NotesHolderStorage { @@ -52,6 +52,7 @@ impl NotesHolderStorage { pub fn notes_holder_mutated<'a>( &'a mut self, ndb: &Ndb, + note_cache: &mut NoteCache, txn: &Transaction, id: &[u8; 32], ) -> Vitality<'a, M> { @@ -79,12 +80,12 @@ impl NotesHolderStorage { if notes.is_empty() { warn!("thread query returned 0 notes? ") } else { - debug!("found thread with {} notes", notes.len()); + info!("found thread with {} notes", notes.len()); } self.id_to_object.insert( id.to_owned(), - M::new_notes_holder(id, M::filters(id), notes), + M::new_notes_holder(txn, ndb, note_cache, id, M::filters(id), notes), ); Vitality::Fresh(self.id_to_object.get_mut(id).unwrap()) } @@ -96,7 +97,14 @@ pub trait NotesHolder { 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(id: &[u8; 32], filters: Vec, notes: Vec) -> Self; + fn new_notes_holder( + txn: &Transaction, + ndb: &Ndb, + note_cache: &mut NoteCache, + id: &[u8; 32], + filters: Vec, + 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<()> { @@ -138,12 +146,13 @@ pub trait NotesHolder { fn unsubscribe_locally( txn: &Transaction, ndb: &Ndb, + note_cache: &mut NoteCache, notes_holder_storage: &mut NotesHolderStorage, pool: &mut RelayPool, id: &[u8; 32], ) { let notes_holder = notes_holder_storage - .notes_holder_mutated(ndb, txn, id) + .notes_holder_mutated(ndb, note_cache, txn, id) .get_ptr(); if let Some(multi_subscriber) = notes_holder.get_multi_subscriber() { @@ -153,12 +162,13 @@ pub trait NotesHolder { fn open( ndb: &Ndb, + note_cache: &mut NoteCache, txn: &Transaction, pool: &mut RelayPool, storage: &mut NotesHolderStorage, id: &[u8; 32], ) -> Option { - let vitality = storage.notes_holder_mutated(ndb, txn, id); + let vitality = storage.notes_holder_mutated(ndb, note_cache, txn, id); let (holder, result) = match vitality { Vitality::Stale(holder) => { diff --git a/src/profile.rs b/src/profile.rs index 28c9775..03fd397 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -1,12 +1,8 @@ use enostr::{Filter, Pubkey}; -use nostrdb::{FilterBuilder, ProfileRecord}; +use nostrdb::{FilterBuilder, Ndb, ProfileRecord, Transaction}; use crate::{ - filter::{self, FilterState}, - multi_subscriber::MultiSubscriber, - note::NoteRef, - notes_holder::NotesHolder, - timeline::{PubkeySource, Timeline, TimelineKind}, + app::copy_notes_into_timeline, filter::{self, FilterState}, multi_subscriber::MultiSubscriber, note::NoteRef, notecache::NoteCache, notes_holder::NotesHolder, timeline::{PubkeySource, Timeline, TimelineKind} }; pub enum DisplayName<'a> { @@ -53,10 +49,18 @@ pub struct Profile { } impl Profile { - pub fn new(source: PubkeySource, filters: Vec, notes: Vec) -> Self { + pub fn new( + txn: &Transaction, + ndb: &Ndb, + note_cache: &mut NoteCache, + source: PubkeySource, + filters: Vec, + notes: Vec, + ) -> Self { let mut timeline = Timeline::new(TimelineKind::profile(source), FilterState::ready(filters)); - timeline.current_view_mut().notes = notes; + + copy_notes_into_timeline(&mut timeline, txn, ndb, note_cache, notes); Profile { timeline, @@ -95,8 +99,22 @@ impl NotesHolder for Profile { .collect() } - fn new_notes_holder(id: &[u8; 32], filters: Vec, notes: Vec) -> Self { - Profile::new(PubkeySource::Explicit(Pubkey::new(*id)), filters, notes) + fn new_notes_holder( + txn: &Transaction, + ndb: &Ndb, + note_cache: &mut NoteCache, + id: &[u8; 32], + filters: Vec, + notes: Vec, + ) -> Self { + Profile::new( + txn, + ndb, + note_cache, + PubkeySource::Explicit(Pubkey::new(*id)), + filters, + notes, + ) } fn set_multi_subscriber(&mut self, subscriber: MultiSubscriber) { diff --git a/src/thread.rs b/src/thread.rs index 00159c5..4a883df 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -1,10 +1,11 @@ use crate::{ multi_subscriber::MultiSubscriber, note::NoteRef, + notecache::NoteCache, notes_holder::NotesHolder, timeline::{TimelineTab, ViewFilter}, }; -use nostrdb::{Filter, FilterBuilder}; +use nostrdb::{Filter, FilterBuilder, Ndb, Transaction}; #[derive(Default)] pub struct Thread { @@ -66,7 +67,14 @@ impl NotesHolder for Thread { Thread::filters(for_id) } - fn new_notes_holder(_: &[u8; 32], _: Vec, notes: Vec) -> Self { + fn new_notes_holder( + _: &Transaction, + _: &Ndb, + _: &mut NoteCache, + _: &[u8; 32], + _: Vec, + notes: Vec, + ) -> Self { Thread::new(notes) } diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs index 03cbaf2..7a87402 100644 --- a/src/ui/profile/mod.rs +++ b/src/ui/profile/mod.rs @@ -54,7 +54,7 @@ impl<'a> ProfileView<'a> { } let profile = self .profiles - .notes_holder_mutated(self.ndb, &txn, self.pubkey.bytes()) + .notes_holder_mutated(self.ndb, self.note_cache, &txn, self.pubkey.bytes()) .get_ptr(); profile.timeline.selected_view = tabs_ui(ui); diff --git a/src/ui/thread.rs b/src/ui/thread.rs index 2651a9a..38a724b 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -89,7 +89,7 @@ impl<'a> ThreadView<'a> { let thread = self .threads - .notes_holder_mutated(self.ndb, &txn, root_id) + .notes_holder_mutated(self.ndb, self.note_cache, &txn, root_id) .get_ptr(); // TODO(jb55): skip poll if ThreadResult is fresh? From e5ba897ce66f692847781f1303368ba0438a3e2b Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 15 Oct 2024 11:22:48 -0400 Subject: [PATCH 10/14] revert timeline no_scroll stuff Signed-off-by: kernelkind --- src/ui/timeline.rs | 74 +++++++++++----------------------------------- 1 file changed, 17 insertions(+), 57 deletions(-) diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index 9de4558..24e5ac3 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -55,23 +55,6 @@ impl<'a> TimelineView<'a> { ) } - pub fn ui_no_scroll(&mut self, ui: &mut egui::Ui) -> TimelineResponse { - if let Some(timeline) = self.columns.find_timeline_mut(self.timeline_id) { - timeline.selected_view = tabs_ui(ui); - }; - - timeline_ui_no_scroll( - ui, - self.ndb, - self.timeline_id, - self.columns, - self.note_cache, - self.img_cache, - self.reverse, - self.textmode, - ) - } - pub fn reversed(mut self) -> Self { self.reverse = true; self @@ -120,53 +103,30 @@ fn timeline_ui( .auto_shrink([false, false]) .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) .show(ui, |ui| { - timeline_ui_no_scroll( - ui, - ndb, - timeline_id, - columns, - note_cache, - img_cache, + let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) { + timeline + } else { + error!("tried to render timeline in column, but timeline was missing"); + // TODO (jb55): render error when timeline is missing? + // this shouldn't happen... + return TimelineResponse::default(); + }; + + let txn = Transaction::new(ndb).expect("failed to create txn"); + TimelineTabView::new( + timeline.current_view(), reversed, textmode, + &txn, + ndb, + note_cache, + img_cache, ) + .show(ui) }) .inner } -#[allow(clippy::too_many_arguments)] -fn timeline_ui_no_scroll( - ui: &mut egui::Ui, - ndb: &Ndb, - timeline_id: TimelineId, - columns: &mut Columns, - note_cache: &mut NoteCache, - img_cache: &mut ImageCache, - reversed: bool, - textmode: bool, -) -> TimelineResponse { - let timeline = if let Some(timeline) = columns.find_timeline_mut(timeline_id) { - timeline - } else { - error!("tried to render timeline in column, but timeline was missing"); - // TODO (jb55): render error when timeline is missing? - // this shouldn't happen... - return TimelineResponse::default(); - }; - - let txn = Transaction::new(ndb).expect("failed to create txn"); - TimelineTabView::new( - timeline.current_view(), - reversed, - textmode, - &txn, - ndb, - note_cache, - img_cache, - ) - .show(ui) -} - pub fn tabs_ui(ui: &mut egui::Ui) -> i32 { ui.spacing_mut().item_spacing.y = 0.0; From 0a077ae797915eb2337c6565bb7338e5efa26637 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 15 Oct 2024 11:45:13 -0400 Subject: [PATCH 11/14] fix unnecessary copy every frame Signed-off-by: kernelkind --- src/nav.rs | 2 +- src/timeline/route.rs | 2 +- src/ui/profile/mod.rs | 4 ++-- src/ui/thread.rs | 7 +++++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/nav.rs b/src/nav.rs index a3e62a3..4f6d395 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -117,7 +117,7 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { } Route::Profile(pubkey) => render_profile_route( - *pubkey, + pubkey, &app.ndb, &mut app.columns, &mut app.profiles, diff --git a/src/timeline/route.rs b/src/timeline/route.rs index 3a5597c..073479c 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -157,7 +157,7 @@ pub fn render_timeline_route( #[allow(clippy::too_many_arguments)] pub fn render_profile_route( - pubkey: Pubkey, + pubkey: &Pubkey, ndb: &Ndb, columns: &mut Columns, profiles: &mut NotesHolderStorage, diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs index 7a87402..8c9b08e 100644 --- a/src/ui/profile/mod.rs +++ b/src/ui/profile/mod.rs @@ -15,7 +15,7 @@ use crate::{ use super::timeline::{tabs_ui, TimelineTabView}; pub struct ProfileView<'a> { - pubkey: Pubkey, + pubkey: &'a Pubkey, col_id: usize, profiles: &'a mut NotesHolderStorage, ndb: &'a Ndb, @@ -25,7 +25,7 @@ pub struct ProfileView<'a> { impl<'a> ProfileView<'a> { pub fn new( - pubkey: Pubkey, + pubkey: &'a Pubkey, col_id: usize, profiles: &'a mut NotesHolderStorage, ndb: &'a Ndb, diff --git a/src/ui/thread.rs b/src/ui/thread.rs index 38a724b..ee568e8 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -1,6 +1,9 @@ use crate::{ - actionbar::TimelineResponse, imgcache::ImageCache, notecache::NoteCache, - notes_holder::{NotesHolder, NotesHolderStorage}, thread::Thread, + actionbar::TimelineResponse, + imgcache::ImageCache, + notecache::NoteCache, + notes_holder::{NotesHolder, NotesHolderStorage}, + thread::Thread, }; use nostrdb::{Ndb, NoteKey, Transaction}; use tracing::error; From 54d49f011070cc6952ebbfeb56c935a7f68732c7 Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 15 Oct 2024 11:54:05 -0400 Subject: [PATCH 12/14] reword comments and logs thread -> NotesHolder Signed-off-by: kernelkind --- src/notes_holder.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/notes_holder.rs b/src/notes_holder.rs index a7c7019..ce3cae3 100644 --- a/src/notes_holder.rs +++ b/src/notes_holder.rs @@ -46,7 +46,7 @@ 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") + .expect("notes_holder_expected_mut used but there was no NotesHolder") } pub fn notes_holder_mutated<'a>( @@ -73,14 +73,14 @@ impl NotesHolderStorage { .map(NoteRef::from_query_result) .collect() } else { - debug!("got no results from thread lookup for {}", hex::encode(id)); + debug!("got no results from NotesHolder lookup for {}", hex::encode(id)); vec![] }; if notes.is_empty() { - warn!("thread query returned 0 notes? ") + warn!("NotesHolder query returned 0 notes? ") } else { - info!("found thread with {} notes", notes.len()); + info!("found NotesHolder with {} notes", notes.len()); } self.id_to_object.insert( @@ -114,7 +114,7 @@ pub trait NotesHolder { self.get_view().insert(¬e_refs, reversed); } else { return Err(Error::Generic( - "Thread unexpectedly has no MultiSubscriber".to_owned(), + "NotesHolder unexpectedly has no MultiSubscriber".to_owned(), )); } @@ -131,18 +131,18 @@ pub trait NotesHolder { 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()); + debug!("got {} results from NotesHolder update", results.len()); results .into_iter() .map(NoteRef::from_query_result) .collect() } else { - debug!("got no results from thread update",); + debug!("got no results from NotesHolder update",); vec![] } } - /// Local thread unsubscribe + /// Local NotesHolder unsubscribe fn unsubscribe_locally( txn: &Transaction, ndb: &Ndb, @@ -172,7 +172,7 @@ pub trait NotesHolder { let (holder, result) = match vitality { Vitality::Stale(holder) => { - // The thread is stale, let's update it + // The NotesHolder 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 @@ -185,7 +185,7 @@ pub trait NotesHolder { // are already borrowing it mutably. Let's pass it as a // result instead // - // thread.view.insert(¬es); <-- no + // holder.get_view().insert(¬es); <-- no // (holder, holder_result) } From 2d7da838725177f6244a53e126b874a7b2e48fbd Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 15 Oct 2024 12:16:26 -0400 Subject: [PATCH 13/14] rename TimelineResponse -> ColumnNoteResponse Signed-off-by: kernelkind --- src/actionbar.rs | 2 +- src/ui/profile/mod.rs | 4 ++-- src/ui/thread.rs | 8 ++++---- src/ui/timeline.rs | 14 +++++++------- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/actionbar.rs b/src/actionbar.rs index 1752305..593045d 100644 --- a/src/actionbar.rs +++ b/src/actionbar.rs @@ -16,7 +16,7 @@ pub enum BarAction { } #[derive(Default)] -pub struct TimelineResponse { +pub struct NoteActionResponse { pub bar_action: Option, pub open_profile: Option, } diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs index 8c9b08e..f9de10c 100644 --- a/src/ui/profile/mod.rs +++ b/src/ui/profile/mod.rs @@ -8,7 +8,7 @@ pub use picture::ProfilePic; pub use preview::ProfilePreview; use crate::{ - actionbar::TimelineResponse, imgcache::ImageCache, notecache::NoteCache, + actionbar::NoteActionResponse, imgcache::ImageCache, notecache::NoteCache, notes_holder::NotesHolderStorage, profile::Profile, }; @@ -42,7 +42,7 @@ impl<'a> ProfileView<'a> { } } - pub fn ui(&mut self, ui: &mut egui::Ui) -> TimelineResponse { + pub fn ui(&mut self, ui: &mut egui::Ui) -> NoteActionResponse { let scroll_id = egui::Id::new(("profile_scroll", self.col_id, self.pubkey)); ScrollArea::vertical() diff --git a/src/ui/thread.rs b/src/ui/thread.rs index ee568e8..6cf6b48 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -1,5 +1,5 @@ use crate::{ - actionbar::TimelineResponse, + actionbar::NoteActionResponse, imgcache::ImageCache, notecache::NoteCache, notes_holder::{NotesHolder, NotesHolderStorage}, @@ -47,7 +47,7 @@ impl<'a> ThreadView<'a> { self } - pub fn ui(&mut self, ui: &mut egui::Ui) -> TimelineResponse { + pub fn ui(&mut self, ui: &mut egui::Ui) -> NoteActionResponse { let txn = Transaction::new(self.ndb).expect("txn"); let selected_note_key = if let Ok(key) = self @@ -58,7 +58,7 @@ impl<'a> ThreadView<'a> { key } else { // TODO: render 404 ? - return TimelineResponse::default(); + return NoteActionResponse::default(); }; ui.label( @@ -75,7 +75,7 @@ impl<'a> ThreadView<'a> { let note = if let Ok(note) = self.ndb.get_note_by_key(&txn, selected_note_key) { note } else { - return TimelineResponse::default(); + return NoteActionResponse::default(); }; let root_id = { diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index 24e5ac3..bdad7f2 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -1,4 +1,4 @@ -use crate::actionbar::TimelineResponse; +use crate::actionbar::NoteActionResponse; use crate::timeline::TimelineTab; use crate::{ actionbar::BarAction, column::Columns, imgcache::ImageCache, notecache::NoteCache, @@ -42,7 +42,7 @@ impl<'a> TimelineView<'a> { } } - pub fn ui(&mut self, ui: &mut egui::Ui) -> TimelineResponse { + pub fn ui(&mut self, ui: &mut egui::Ui) -> NoteActionResponse { timeline_ui( ui, self.ndb, @@ -71,7 +71,7 @@ fn timeline_ui( img_cache: &mut ImageCache, reversed: bool, textmode: bool, -) -> TimelineResponse { +) -> NoteActionResponse { //padding(4.0, ui, |ui| ui.heading("Notifications")); /* let font_id = egui::TextStyle::Body.resolve(ui.style()); @@ -86,7 +86,7 @@ fn timeline_ui( error!("tried to render timeline in column, but timeline was missing"); // TODO (jb55): render error when timeline is missing? // this shouldn't happen... - return TimelineResponse::default(); + return NoteActionResponse::default(); }; timeline.selected_view = tabs_ui(ui); @@ -109,7 +109,7 @@ fn timeline_ui( error!("tried to render timeline in column, but timeline was missing"); // TODO (jb55): render error when timeline is missing? // this shouldn't happen... - return TimelineResponse::default(); + return NoteActionResponse::default(); }; let txn = Transaction::new(ndb).expect("failed to create txn"); @@ -242,7 +242,7 @@ impl<'a> TimelineTabView<'a> { } } - pub fn show(&mut self, ui: &mut egui::Ui) -> TimelineResponse { + pub fn show(&mut self, ui: &mut egui::Ui) -> NoteActionResponse { let mut open_profile = None; let mut bar_action: Option = None; let len = self.tab.notes.len(); @@ -299,7 +299,7 @@ impl<'a> TimelineTabView<'a> { 1 }); - TimelineResponse { + NoteActionResponse { open_profile, bar_action, } From eedb4e129791acf57c1350a81079d19f516739de Mon Sep 17 00:00:00 2001 From: kernelkind Date: Tue, 15 Oct 2024 13:21:39 -0400 Subject: [PATCH 14/14] NoteActionResponse for note preview pfp clicking Signed-off-by: kernelkind --- src/ui/note/contents.rs | 12 ++++---- src/ui/note/mod.rs | 65 ++++++++++++++++++++--------------------- src/ui/timeline.rs | 20 ++++--------- 3 files changed, 43 insertions(+), 54 deletions(-) diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs index 837e425..1a7bf91 100644 --- a/src/ui/note/contents.rs +++ b/src/ui/note/contents.rs @@ -1,4 +1,4 @@ -use crate::actionbar::BarAction; +use crate::actionbar::NoteActionResponse; use crate::images::ImageType; use crate::imgcache::ImageCache; use crate::notecache::NoteCache; @@ -17,7 +17,7 @@ pub struct NoteContents<'a> { note: &'a Note<'a>, note_key: NoteKey, options: NoteOptions, - action: Option, + action: NoteActionResponse, } impl<'a> NoteContents<'a> { @@ -38,12 +38,12 @@ impl<'a> NoteContents<'a> { note, note_key, options, - action: None, + action: NoteActionResponse::default(), } } - pub fn action(&self) -> Option { - self.action + pub fn action(&self) -> &NoteActionResponse { + &self.action } } @@ -211,7 +211,7 @@ fn render_note_contents( let note_action = if let Some((id, block_str)) = inline_note { render_note_preview(ui, ndb, note_cache, img_cache, txn, id, block_str).action } else { - None + NoteActionResponse::default() }; if !images.is_empty() && !options.has_textmode() { diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index e2e57c5..b328f5c 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -13,7 +13,7 @@ pub use quote_repost::QuoteRepostView; pub use reply::PostReplyView; use crate::{ - actionbar::BarAction, + actionbar::{BarAction, NoteActionResponse}, app_style::NotedeckTextStyle, colors, imgcache::ImageCache, @@ -22,7 +22,7 @@ use crate::{ }; use egui::emath::{pos2, Vec2}; use egui::{Id, Label, Pos2, Rect, Response, RichText, Sense}; -use enostr::NoteId; +use enostr::{NoteId, Pubkey}; use nostrdb::{Ndb, Note, NoteKey, NoteReply, Transaction}; use super::profile::preview::{get_display_name, one_line_display_name_widget}; @@ -37,37 +37,27 @@ pub struct NoteView<'a> { pub struct NoteResponse { pub response: egui::Response, - pub action: Option, pub context_selection: Option, - pub clicked_profile: bool, + pub action: NoteActionResponse, } impl NoteResponse { pub fn new(response: egui::Response) -> Self { Self { response, - action: None, context_selection: None, - clicked_profile: false, + action: NoteActionResponse::default(), } } - pub fn with_action(self, action: Option) -> Self { - Self { action, ..self } + pub fn with_action(mut self, action: NoteActionResponse) -> Self { + self.action = action; + self } - pub fn select_option(self, context_selection: Option) -> Self { - Self { - context_selection, - ..self - } - } - - pub fn click_profile(self, clicked_profile: bool) -> Self { - Self { - clicked_profile, - ..self - } + pub fn select_option(mut self, context_selection: Option) -> Self { + self.context_selection = context_selection; + self } } @@ -441,8 +431,11 @@ impl<'a> NoteView<'a> { puffin::profile_function!(); let note_key = self.note.key().expect("todo: support non-db notes"); let txn = self.note.txn().expect("todo: support non-db notes"); - let mut note_action: Option = None; + + let mut open_profile: Option = None; + let mut bar_action: Option = None; let mut selected_option: Option = None; + let profile = self.ndb.get_profile_by_pubkey(txn, self.note.pubkey()); let maybe_hitbox = maybe_note_hitbox(ui, note_key); let container_right = { @@ -452,12 +445,12 @@ impl<'a> NoteView<'a> { Pos2::new(x, y) }; - let mut clicked_profile = false; - // wide design let response = if self.options().has_wide() { ui.horizontal(|ui| { - clicked_profile = self.pfp(note_key, &profile, ui).clicked(); + if self.pfp(note_key, &profile, ui).clicked() { + open_profile = Some(Pubkey::new(*self.note.pubkey())); + }; let size = ui.available_size(); ui.vertical(|ui| { @@ -500,18 +493,21 @@ impl<'a> NoteView<'a> { self.options(), ); let resp = ui.add(&mut contents); - note_action = note_action.or(contents.action()); + bar_action = bar_action.or(contents.action().bar_action); + open_profile = open_profile.or(contents.action().open_profile); if self.options().has_actionbar() { let ab = render_note_actionbar(ui, self.note.id(), note_key); - note_action = note_action.or(ab.inner); + bar_action = bar_action.or(ab.inner); } resp } else { // main design ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { - clicked_profile = self.pfp(note_key, &profile, ui).clicked(); + if self.pfp(note_key, &profile, ui).clicked() { + open_profile = Some(Pubkey::new(*self.note.pubkey())); + }; ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { selected_option = NoteView::note_header( @@ -547,30 +543,33 @@ impl<'a> NoteView<'a> { self.options(), ); ui.add(&mut contents); - note_action = note_action.or(contents.action()); + bar_action = bar_action.or(contents.action().bar_action); + open_profile = open_profile.or(contents.action().open_profile); if self.options().has_actionbar() { let ab = render_note_actionbar(ui, self.note.id(), note_key); - note_action = note_action.or(ab.inner); + bar_action = bar_action.or(ab.inner); } }); }) .response }; - note_action = check_note_hitbox( + bar_action = check_note_hitbox( ui, self.note.id(), note_key, &response, maybe_hitbox, - note_action, + bar_action, ); NoteResponse::new(response) - .with_action(note_action) + .with_action(NoteActionResponse { + bar_action, + open_profile, + }) .select_option(selected_option) - .click_profile(clicked_profile) } } diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index bdad7f2..1c2d33c 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -1,15 +1,13 @@ -use crate::actionbar::NoteActionResponse; +use crate::actionbar::{BarAction, NoteActionResponse}; use crate::timeline::TimelineTab; use crate::{ - actionbar::BarAction, column::Columns, imgcache::ImageCache, notecache::NoteCache, - timeline::TimelineId, ui, + column::Columns, imgcache::ImageCache, notecache::NoteCache, timeline::TimelineId, ui, }; use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Direction, Layout}; use egui_tabs::TabColor; -use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; -use tracing::{debug, error, info, warn}; +use tracing::{error, warn}; pub struct TimelineView<'a> { timeline_id: TimelineId, @@ -277,20 +275,12 @@ impl<'a> TimelineTabView<'a> { .options_button(true) .show(ui); - if let Some(ba) = resp.action { - bar_action = Some(ba); - } else if resp.response.clicked() { - debug!("clicked note"); - } + bar_action = bar_action.or(resp.action.bar_action); + open_profile = open_profile.or(resp.action.open_profile); if let Some(context) = resp.context_selection { context.process(ui, ¬e); } - - if resp.clicked_profile { - info!("clicked profile"); - open_profile = Some(Pubkey::new(*note.pubkey())) - } }); ui::hline(ui);