diff --git a/src/app.rs b/src/app.rs index 1ed7924..8458a49 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,7 @@ use crate::app_style::user_requested_visuals_change; use crate::error::Error; use crate::frame_history::FrameHistory; use crate::imgcache::ImageCache; -use crate::notecache::NoteCache; +use crate::notecache::{CachedNote, NoteCache}; use crate::timeline; use crate::timeline::{NoteRef, Timeline}; use crate::ui::is_mobile; @@ -15,7 +15,7 @@ use egui_extras::{Size, StripBuilder}; use enostr::{ClientMessage, Filter, Pubkey, RelayEvent, RelayMessage}; use nostrdb::{BlockType, Config, Mention, Ndb, Note, NoteKey, Transaction}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashSet; use std::hash::Hash; use std::path::Path; use std::time::Duration; @@ -33,11 +33,13 @@ pub enum DamusState { pub struct Damus { state: DamusState, //compose: String, - note_cache: HashMap, + note_cache: NoteCache, pool: RelayPool, + pub textmode: bool, pub timelines: Vec, + pub selected_timeline: i32, pub img_cache: ImageCache, pub ndb: Ndb, @@ -94,7 +96,7 @@ fn send_initial_filters(damus: &mut Damus, relay_url: &str) { for timeline in &damus.timelines { let mut filter = timeline.filter.clone(); for f in &mut filter { - since_optimize_filter(f, &timeline.notes); + since_optimize_filter(f, timeline.notes()); } relay.subscribe(format!("initial{}", c), filter); c += 1; @@ -298,13 +300,14 @@ fn poll_notes_for_timeline<'a>( .collect(); let timeline = &mut damus.timelines[timeline]; - let prev_items = timeline.notes.len(); - timeline.notes = timeline::merge_sorted_vecs(&timeline.notes, &new_refs); - let new_items = timeline.notes.len() - prev_items; + let prev_items = timeline.notes().len(); + timeline.current_view_mut().notes = timeline::merge_sorted_vecs(&timeline.notes(), &new_refs); + let new_items = timeline.notes().len() - prev_items; // TODO: technically items could have been added inbetween if new_items > 0 { timeline + .current_view() .list .clone() .lock() @@ -339,7 +342,7 @@ fn setup_initial_nostrdb_subs(damus: &mut Damus) -> Result<()> { filters, timeline.filter[0].limit.unwrap_or(200) as i32, )?; - timeline.notes = res + timeline.notes_view_mut().notes = res .iter() .map(|qr| NoteRef { key: qr.note_key, @@ -384,7 +387,7 @@ fn get_unknown_ids<'a>(txn: &'a Transaction, damus: &Damus) -> Result = HashSet::new(); for timeline in &damus.timelines { - for noteref in &timeline.notes { + for noteref in timeline.notes() { let note = damus.ndb.get_note_by_key(txn, noteref.key)?; let _ = get_unknown_note_ids(&damus.ndb, txn, ¬e, note.key().unwrap(), &mut ids); } @@ -506,7 +509,8 @@ impl Damus { state: DamusState::Initializing, pool: RelayPool::new(), img_cache: ImageCache::new(imgcache_dir), - note_cache: HashMap::new(), + note_cache: NoteCache::default(), + selected_timeline: 0, timelines, textmode: false, ndb: Ndb::new(data_path.as_ref().to_str().expect("db path ok"), &config).expect("ndb"), @@ -515,10 +519,37 @@ impl Damus { } } - pub fn get_note_cache_mut(&mut self, note_key: NoteKey, note: &Note<'_>) -> &mut NoteCache { + pub fn get_note_cache_mut(&mut self, note_key: NoteKey, note: &Note<'_>) -> &mut CachedNote { self.note_cache + .cache .entry(note_key) - .or_insert_with(|| NoteCache::new(note)) + .or_insert_with(|| CachedNote::new(note)) + } + + pub fn selected_timeline(&mut self) -> &mut Timeline { + &mut self.timelines[self.selected_timeline as usize] + } + + pub fn select_down(&mut self) { + self.selected_timeline().current_view_mut().select_down(); + } + + pub fn select_up(&mut self) { + self.selected_timeline().current_view_mut().select_up(); + } + + pub fn select_left(&mut self) { + if self.selected_timeline - 1 < 0 { + return; + } + self.selected_timeline -= 1; + } + + pub fn select_right(&mut self) { + if self.selected_timeline + 1 >= self.timelines.len() as i32 { + return; + } + self.selected_timeline += 1; } } @@ -598,7 +629,7 @@ fn render_panel(ctx: &egui::Context, app: &mut Damus, timeline_ind: usize) { ui.weak(format!( "{} notes", - &app.timelines[timeline_ind].notes.len() + &app.timelines[timeline_ind].notes().len() )); } }); diff --git a/src/notecache.rs b/src/notecache.rs index 66fe2ce..9b40713 100644 --- a/src/notecache.rs +++ b/src/notecache.rs @@ -1,15 +1,21 @@ use crate::time::time_ago_since; use crate::timecache::TimeCached; -use nostrdb::{Note, NoteReply, NoteReplyBuf}; +use nostrdb::{Note, NoteKey, NoteReply, NoteReplyBuf}; +use std::collections::HashMap; use std::time::Duration; +#[derive(Default)] pub struct NoteCache { + pub cache: HashMap, +} + +pub struct CachedNote { reltime: TimeCached, pub reply: NoteReplyBuf, pub bar_open: bool, } -impl NoteCache { +impl CachedNote { pub fn new(note: &Note<'_>) -> Self { let created_at = note.created_at(); let reltime = TimeCached::new( @@ -18,7 +24,7 @@ impl NoteCache { ); let reply = NoteReply::new(note.tags()).to_owned(); let bar_open = false; - NoteCache { + CachedNote { reltime, reply, bar_open, diff --git a/src/timeline.rs b/src/timeline.rs index f0cbc3f..511dfe9 100644 --- a/src/timeline.rs +++ b/src/timeline.rs @@ -1,10 +1,12 @@ +use crate::notecache::CachedNote; use crate::{ui, Damus}; + use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Direction, Layout}; use egui_tabs::TabColor; use egui_virtual_list::VirtualList; use enostr::Filter; -use nostrdb::{NoteKey, Subscription, Transaction}; +use nostrdb::{Note, NoteKey, Subscription, Transaction}; use std::cmp::Ordering; use std::sync::{Arc, Mutex}; @@ -32,30 +34,116 @@ impl PartialOrd for NoteRef { } } +pub enum ViewFilter { + Notes, + NotesAndReplies, +} + +impl ViewFilter { + pub fn name(&self) -> &'static str { + match self { + ViewFilter::Notes => "Notes", + ViewFilter::NotesAndReplies => "Notes & Replies", + } + } + + fn index(&self) -> usize { + match self { + ViewFilter::Notes => 0, + ViewFilter::NotesAndReplies => 1, + } + } + + fn filter(&self, cache: &CachedNote, note: &Note) -> bool { + match self { + ViewFilter::Notes => !cache.reply.borrow(note.tags()).is_reply(), + ViewFilter::NotesAndReplies => true, + } + } +} + +/// A timeline view is a filtered view of notes in a timeline. Two standard views +/// are "Notes" and "Notes & Replies". A timeline is associated with a Filter, +/// but a TimelineView is a further filtered view of this Filter that can't +/// be captured by a Filter itself. +pub struct TimelineView { + pub notes: Vec, + pub selection: i32, + pub filter: ViewFilter, + pub list: Arc>, +} + +impl TimelineView { + pub fn new(filter: ViewFilter) -> Self { + let selection = 0i32; + let list = Arc::new(Mutex::new(VirtualList::new())); + let notes: Vec = Vec::with_capacity(1000); + + TimelineView { + notes, + selection, + filter, + list, + } + } + + pub fn select_down(&mut self) { + if self.selection + 1 > self.notes.len() as i32 { + return; + } + + self.selection += 1; + } + + pub fn select_up(&mut self) { + if self.selection - 1 < 0 { + return; + } + + self.selection -= 1; + } +} + pub struct Timeline { pub filter: Vec, - pub notes: Vec, + pub views: Vec, + pub selected_view: i32, /// Our nostrdb subscription pub subscription: Option, - - /// State for our virtual list egui widget - pub list: Arc>, } impl Timeline { pub fn new(filter: Vec) -> Self { - let notes: Vec = Vec::with_capacity(1000); let subscription: Option = None; - let list = Arc::new(Mutex::new(VirtualList::new())); + let notes = TimelineView::new(ViewFilter::Notes); + let replies = TimelineView::new(ViewFilter::NotesAndReplies); + let views = vec![notes, replies]; + let selected_view = 0; Timeline { filter, - notes, + views, subscription, - list, + selected_view, } } + + pub fn current_view(&self) -> &TimelineView { + &self.views[self.selected_view as usize] + } + + pub fn current_view_mut(&mut self) -> &mut TimelineView { + &mut self.views[self.selected_view as usize] + } + + pub fn notes(&self) -> &[NoteRef] { + &self.views[ViewFilter::NotesAndReplies.index()].notes + } + + pub fn notes_view_mut(&mut self) -> &mut TimelineView { + &mut self.views[ViewFilter::NotesAndReplies.index()] + } } fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 { @@ -156,15 +244,16 @@ pub fn timeline_view(ui: &mut egui::Ui, app: &mut Damus, timeline: usize) { .animated(false) .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) .show(ui, |ui| { - let len = app.timelines[timeline].notes.len(); - let list = app.timelines[timeline].list.clone(); + let view = app.timelines[timeline].current_view(); + let len = view.notes.len(); + let list = view.list.clone(); list.lock() .unwrap() .ui_custom_layout(ui, len, |ui, start_index| { ui.spacing_mut().item_spacing.y = 0.0; ui.spacing_mut().item_spacing.x = 4.0; - let note_key = app.timelines[timeline].notes[start_index].key; + let note_key = app.timelines[timeline].current_view().notes[start_index].key; let txn = if let Ok(txn) = Transaction::new(&app.ndb) { txn diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index ced21ca..96c9b6e 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -4,7 +4,7 @@ pub mod options; pub use contents::NoteContents; pub use options::NoteOptions; -use crate::{colors, ui, ui::is_mobile, Damus}; +use crate::{colors, notecache::CachedNote, ui, ui::is_mobile, Damus}; use egui::{Label, RichText, Sense}; use nostrdb::{NoteKey, Transaction}; use std::hash::{Hash, Hasher}; @@ -308,7 +308,7 @@ fn secondary_label(ui: &mut egui::Ui, s: impl Into) { fn render_reltime( ui: &mut egui::Ui, - note_cache: &mut crate::notecache::NoteCache, + note_cache: &mut CachedNote, before: bool, ) -> egui::InnerResponse<()> { #[cfg(feature = "profiling")]