diff --git a/src/actionbar.rs b/src/actionbar.rs index e0a4353..593045d 100644 --- a/src/actionbar.rs +++ b/src/actionbar.rs @@ -1,11 +1,11 @@ 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, RelayPool}; +use enostr::{NoteId, Pubkey, RelayPool}; use nostrdb::{Ndb, Transaction}; #[derive(Debug, Eq, PartialEq, Copy, Clone)] @@ -15,13 +15,19 @@ pub enum BarAction { OpenThread(NoteId), } -pub struct NewThreadNotes { - pub root_id: NoteId, +#[derive(Default)] +pub struct NoteActionResponse { + pub bar_action: Option, + pub open_profile: Option, +} + +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 @@ -35,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, note_cache, txn, pool, threads, root_id) } impl BarAction { @@ -88,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)); @@ -117,45 +85,51 @@ impl BarAction { self, ndb: &Ndb, router: &mut Router, - threads: &mut Threads, + threads: &mut NotesHolderStorage, note_cache: &mut NoteCache, pool: &mut RelayPool, 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); } } } -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, + note_cache: &mut NoteCache, + 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, note_cache, 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 ce3c234..e04b8ad 100644 --- a/src/app.rs +++ b/src/app.rs @@ -13,8 +13,10 @@ use crate::{ nav, note::NoteRef, notecache::{CachedNote, NoteCache}, + notes_holder::NotesHolderStorage, + profile::Profile, subscriptions::{SubKind, Subscriptions}, - thread::Threads, + thread::Thread, timeline::{Timeline, TimelineId, TimelineKind, ViewFilter}, ui::{self, DesktopSidePanel}, unknowns::UnknownIds, @@ -53,7 +55,8 @@ pub struct Damus { pub view_state: ViewState, pub unknown_ids: UnknownIds, pub drafts: Drafts, - pub threads: Threads, + pub threads: NotesHolderStorage, + pub profiles: NotesHolderStorage, pub img_cache: ImageCache, pub accounts: AccountManager, pub subscriptions: Subscriptions, @@ -361,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> = @@ -370,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( @@ -693,7 +709,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); } } @@ -709,7 +725,8 @@ impl Damus { unknown_ids: UnknownIds::default(), subscriptions: Subscriptions::default(), since_optimize: parsed_args.since_optimize, - threads: Threads::default(), + threads: NotesHolderStorage::default(), + profiles: NotesHolderStorage::default(), drafts: Drafts::default(), state: DamusState::Initializing, img_cache: ImageCache::new(imgcache_dir.into()), @@ -777,7 +794,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()); @@ -790,7 +807,8 @@ impl Damus { unknown_ids: UnknownIds::default(), subscriptions: Subscriptions::default(), since_optimize: true, - threads: Threads::default(), + threads: NotesHolderStorage::default(), + profiles: NotesHolderStorage::default(), drafts: Drafts::default(), state: DamusState::Initializing, pool: RelayPool::new(), @@ -817,11 +835,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/column.rs b/src/column.rs index 62d45cd..0b98632 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); 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 bca1d03..4f6d395 100644 --- a/src/nav.rs +++ b/src/nav.rs @@ -2,10 +2,15 @@ use crate::{ account_manager::render_accounts_route, app_style::{get_font_size, NotedeckTextStyle}, fonts::NamedFontFamily, + notes_holder::NotesHolder, + profile::Profile, relay_pool_manager::RelayPoolManager, route::Route, - thread::thread_unsubscribe, - timeline::route::{render_timeline_route, AfterRouteExecution, TimelineRoute}, + thread::Thread, + timeline::{ + route::{render_profile_route, render_timeline_route, AfterRouteExecution, TimelineRoute}, + Timeline, + }, ui::{ self, add_column::{AddColumnResponse, AddColumnView}, @@ -18,6 +23,7 @@ use crate::{ use egui::{pos2, Color32, InnerResponse, Stroke}; use egui_nav::{Nav, NavAction, TitleBarResponse}; +use nostrdb::{Ndb, Transaction}; use tracing::{error, info}; pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { @@ -109,6 +115,19 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { } None } + + 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, + &mut app.threads, + col, + ui, + ), } }); @@ -124,18 +143,57 @@ pub fn render_nav(col: usize, app: &mut Damus, ui: &mut egui::Ui) { } } } + + AfterRouteExecution::OpenProfile(pubkey) => { + 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, + &mut app.note_cache, + &txn, + &mut app.pool, + &mut app.profiles, + pubkey.bytes(), + ) { + res.process(&app.ndb, &mut app.note_cache, &txn, &mut app.profiles); + } + } } } 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 { - thread_unsubscribe( + 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.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, - id.bytes(), + &mut app.profiles, + &mut app.pool, + pubkey.bytes(), ); } } else if let Some(NavAction::Navigated) = nav_response.action { @@ -152,23 +210,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, diff --git a/src/notes_holder.rs b/src/notes_holder.rs new file mode 100644 index 0000000..ce3cae3 --- /dev/null +++ b/src/notes_holder.rs @@ -0,0 +1,208 @@ +use std::collections::HashMap; + +use enostr::{Filter, RelayPool}; +use nostrdb::{Ndb, Transaction}; +use tracing::{debug, info, warn}; + +use crate::{ + actionbar::NotesHolderResult, multi_subscriber::MultiSubscriber, note::NoteRef, + notecache::NoteCache, timeline::TimelineTab, Error, Result, +}; + +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), +} + +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("notes_holder_expected_mut used but there was no NotesHolder") + } + + pub fn notes_holder_mutated<'a>( + &'a mut self, + ndb: &Ndb, + note_cache: &mut NoteCache, + 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 NotesHolder lookup for {}", hex::encode(id)); + vec![] + }; + + if notes.is_empty() { + warn!("NotesHolder query returned 0 notes? ") + } else { + info!("found NotesHolder with {} notes", notes.len()); + } + + self.id_to_object.insert( + id.to_owned(), + M::new_notes_holder(txn, ndb, note_cache, id, M::filters(id), 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( + 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<()> { + 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( + "NotesHolder 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 NotesHolder update", results.len()); + results + .into_iter() + .map(NoteRef::from_query_result) + .collect() + } else { + debug!("got no results from NotesHolder update",); + vec![] + } + } + + /// Local NotesHolder unsubscribe + 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, note_cache, txn, id) + .get_ptr(); + + if let Some(multi_subscriber) = notes_holder.get_multi_subscriber() { + multi_subscriber.unsubscribe(ndb, pool); + } + } + + 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, note_cache, txn, id); + + let (holder, result) = match vitality { + Vitality::Stale(holder) => { + // 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 + } 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 + // + // holder.get_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/profile.rs b/src/profile.rs index 9da7026..03fd397 100644 --- a/src/profile.rs +++ b/src/profile.rs @@ -1,4 +1,9 @@ -use nostrdb::ProfileRecord; +use enostr::{Filter, Pubkey}; +use nostrdb::{FilterBuilder, Ndb, ProfileRecord, Transaction}; + +use crate::{ + 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> { One(&'a str), @@ -37,3 +42,82 @@ pub fn get_profile_name<'a>(record: &'a ProfileRecord) -> Option }), } } + +pub struct Profile { + pub timeline: Timeline, + pub multi_subscriber: Option, +} + +impl Profile { + 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)); + + copy_notes_into_timeline(&mut timeline, txn, ndb, note_cache, notes); + + Profile { + timeline, + 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 { + self.timeline.current_view_mut() + } + + 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( + 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) { + self.multi_subscriber = Some(subscriber); + } +} diff --git a/src/route.rs b/src/route.rs index 9b622c9..9fb0f7f 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}; @@ -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,6 +17,7 @@ pub enum Route { Relays, ComposeNote, AddColumn, + Profile(Pubkey), } #[derive(Clone)] @@ -96,6 +97,9 @@ impl Route { }, Route::ComposeNote => "Compose Note".to_owned(), Route::AddColumn => "Add Column".to_owned(), + Route::Profile(pubkey) => { + format!("{}'s Profile", get_profile_displayname_string(ndb, pubkey)) + } }; TitledRoute { @@ -203,6 +207,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/thread.rs b/src/thread.rs index b368075..4a883df 100644 --- a/src/thread.rs +++ b/src/thread.rs @@ -2,13 +2,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}; #[derive(Default)] pub struct Thread { @@ -39,47 +36,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 +58,35 @@ 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( + _: &Transaction, + _: &Ndb, + _: &mut NoteCache, + _: &[u8; 32], + _: Vec, + 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 7c1367c..073479c 100644 --- a/src/timeline/route.rs +++ b/src/timeline/route.rs @@ -4,7 +4,9 @@ use crate::{ draft::Drafts, imgcache::ImageCache, notecache::NoteCache, - thread::Threads, + notes_holder::NotesHolderStorage, + profile::Profile, + thread::Thread, timeline::TimelineId, ui::{ self, @@ -12,10 +14,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 +31,7 @@ pub enum TimelineRoute { pub enum AfterRouteExecution { Post(PostResponse), + OpenProfile(Pubkey), } impl AfterRouteExecution { @@ -44,7 +48,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, @@ -53,10 +57,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,22 +68,26 @@ 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) => { - 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) => { @@ -146,3 +154,31 @@ pub fn render_timeline_route( } } } + +#[allow(clippy::too_many_arguments)] +pub fn render_profile_route( + pubkey: &Pubkey, + ndb: &Ndb, + columns: &mut Columns, + profiles: &mut NotesHolderStorage, + pool: &mut RelayPool, + img_cache: &mut ImageCache, + note_cache: &mut NoteCache, + threads: &mut NotesHolderStorage, + col: usize, + ui: &mut egui::Ui, +) -> Option { + let timeline_response = + 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(); + 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/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 0c83718..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,28 +37,27 @@ pub struct NoteView<'a> { pub struct NoteResponse { pub response: egui::Response, - pub action: Option, pub context_selection: Option, + pub action: NoteActionResponse, } impl NoteResponse { pub fn new(response: egui::Response) -> Self { Self { response, - action: None, context_selection: None, + 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 select_option(mut self, context_selection: Option) -> Self { + self.context_selection = context_selection; + self } } @@ -305,7 +304,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 +313,7 @@ impl<'a> NoteView<'a> { let pfp_size = self.options().pfp_size(); + let sense = Sense::click(); match profile .as_ref() .ok() @@ -326,7 +326,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 +342,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), } } @@ -430,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 = { @@ -444,7 +448,9 @@ impl<'a> NoteView<'a> { // wide design let response = if self.options().has_wide() { ui.horizontal(|ui| { - self.pfp(note_key, &profile, ui); + if self.pfp(note_key, &profile, ui).clicked() { + open_profile = Some(Pubkey::new(*self.note.pubkey())); + }; let size = ui.available_size(); ui.vertical(|ui| { @@ -487,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| { - self.pfp(note_key, &profile, ui); + 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( @@ -534,28 +543,32 @@ 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) } } diff --git a/src/ui/profile/mod.rs b/src/ui/profile/mod.rs index dafd7b3..f9de10c 100644 --- a/src/ui/profile/mod.rs +++ b/src/ui/profile/mod.rs @@ -1,5 +1,75 @@ pub mod picture; pub mod preview; +use egui::{ScrollArea, Widget}; +use enostr::Pubkey; +use nostrdb::{Ndb, Transaction}; pub use picture::ProfilePic; pub use preview::ProfilePreview; + +use crate::{ + actionbar::NoteActionResponse, imgcache::ImageCache, notecache::NoteCache, + notes_holder::NotesHolderStorage, profile::Profile, +}; + +use super::timeline::{tabs_ui, TimelineTabView}; + +pub struct ProfileView<'a> { + pubkey: &'a Pubkey, + col_id: usize, + profiles: &'a mut NotesHolderStorage, + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, +} + +impl<'a> ProfileView<'a> { + pub fn new( + pubkey: &'a Pubkey, + col_id: usize, + profiles: &'a mut NotesHolderStorage, + ndb: &'a Ndb, + note_cache: &'a mut NoteCache, + img_cache: &'a mut ImageCache, + ) -> Self { + ProfileView { + pubkey, + col_id, + profiles, + ndb, + note_cache, + img_cache, + } + } + + 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() + .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 profile = self + .profiles + .notes_holder_mutated(self.ndb, self.note_cache, &txn, self.pubkey.bytes()) + .get_ptr(); + + profile.timeline.selected_view = tabs_ui(ui); + + TimelineTabView::new( + profile.timeline.current_view(), + false, + false, + &txn, + self.ndb, + self.note_cache, + self.img_cache, + ) + .show(ui) + }) + .inner + } +} diff --git a/src/ui/thread.rs b/src/ui/thread.rs index 44ff6f8..6cf6b48 100644 --- a/src/ui/thread.rs +++ b/src/ui/thread.rs @@ -1,11 +1,17 @@ use crate::{ - actionbar::BarAction, imgcache::ImageCache, notecache::NoteCache, thread::Threads, ui, + actionbar::NoteActionResponse, + imgcache::ImageCache, + notecache::NoteCache, + notes_holder::{NotesHolder, NotesHolderStorage}, + thread::Thread, }; use nostrdb::{Ndb, NoteKey, Transaction}; -use tracing::{error, warn}; +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, @@ -17,7 +23,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, @@ -41,9 +47,8 @@ impl<'a> ThreadView<'a> { self } - pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { + pub fn ui(&mut self, ui: &mut egui::Ui) -> NoteActionResponse { let txn = Transaction::new(self.ndb).expect("txn"); - let mut action: Option = None; let selected_note_key = if let Ok(key) = self .ndb @@ -53,7 +58,7 @@ impl<'a> ThreadView<'a> { key } else { // TODO: render 404 ? - return None; + return NoteActionResponse::default(); }; ui.label( @@ -70,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; + return NoteActionResponse::default(); }; let root_id = { @@ -85,7 +90,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, self.note_cache, &txn, root_id) + .get_ptr(); // TODO(jb55): skip poll if ThreadResult is fresh? @@ -94,50 +102,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 let Some(selection) = note_response.context_selection { - selection.process(ui, ¬e); - } - }); - - ui::hline(ui); - //ui.add(egui::Separator::default().spacing(0.0)); - - 1 - }, - ); - }); - - action + 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 187c039..1c2d33c 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -1,12 +1,13 @@ +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 nostrdb::{Ndb, Transaction}; -use tracing::{debug, error, warn}; +use tracing::{error, warn}; pub struct TimelineView<'a> { timeline_id: TimelineId, @@ -39,7 +40,7 @@ impl<'a> TimelineView<'a> { } } - pub fn ui(&mut self, ui: &mut egui::Ui) -> Option { + pub fn ui(&mut self, ui: &mut egui::Ui) -> NoteActionResponse { timeline_ui( ui, self.ndb, @@ -68,7 +69,7 @@ fn timeline_ui( img_cache: &mut ImageCache, reversed: bool, textmode: bool, -) -> Option { +) -> NoteActionResponse { //padding(4.0, ui, |ui| ui.heading("Notifications")); /* let font_id = egui::TextStyle::Body.resolve(ui.style()); @@ -83,7 +84,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 NoteActionResponse::default(); }; timeline.selected_view = tabs_ui(ui); @@ -94,7 +95,6 @@ fn timeline_ui( egui::Id::new(("tlscroll", timeline.view_id())) }; - let mut bar_action: Option = None; egui::ScrollArea::vertical() .id_source(scroll_id) .animated(false) @@ -107,71 +107,25 @@ 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 0; + return NoteActionResponse::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 0; - }; - - 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); - } - }); - - ui::hline(ui); - //ui.add(egui::Separator::default().spacing(0.0)); - - 1 - }); - - 1 - }); - - 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) + }) + .inner } -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) @@ -254,3 +208,90 @@ 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) -> NoteActionResponse { + 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); + + 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); + } + }); + + ui::hline(ui); + //ui.add(egui::Separator::default().spacing(0.0)); + + 1 + }); + + NoteActionResponse { + open_profile, + bar_action, + } + } +}