notedeck

One damus client to rule them all
git clone git://jb55.com/notedeck
Log | Files | Refs | README | LICENSE

commit 8025be823a8fc7099bb47096271dd7377f19cf28
parent cb2330abac38a8c589754a01e6c919d1fc185dbe
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 19 Dec 2024 08:48:07 -0800

ui: customizable tabs per column view

This reduces the number of choices the user needs to make. Some of these
filters were redundant anyways. This also saves memory.

Universe: Notes
Notificaitons: Notes & Replies
Everything else: Notes, Notes & Replies

Changelog-Changed: Simplified tab selections on some columns
Fixes: https://github.com/damus-io/notedeck/issues/517

Diffstat:
Mcrates/notedeck_columns/src/args.rs | 3++-
Mcrates/notedeck_columns/src/profile.rs | 9++++++---
Mcrates/notedeck_columns/src/timeline/kind.rs | 7++++++-
Mcrates/notedeck_columns/src/timeline/mod.rs | 73++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mcrates/notedeck_columns/src/ui/profile/mod.rs | 3++-
Mcrates/notedeck_columns/src/ui/timeline.rs | 22+++++++++++++++-------
Mcrates/notedeck_columns/src/unknowns.rs | 4++--
7 files changed, 75 insertions(+), 46 deletions(-)

diff --git a/crates/notedeck_columns/src/args.rs b/crates/notedeck_columns/src/args.rs @@ -1,6 +1,6 @@ use notedeck::FilterState; -use crate::timeline::{PubkeySource, Timeline, TimelineKind}; +use crate::timeline::{PubkeySource, Timeline, TimelineKind, TimelineTab}; use enostr::{Filter, Pubkey}; use nostrdb::Ndb; use tracing::{debug, error, info}; @@ -151,6 +151,7 @@ impl ArgColumn { ArgColumn::Generic(filters) => Some(Timeline::new( TimelineKind::Generic, FilterState::ready(filters), + TimelineTab::full_tabs(), )), ArgColumn::Timeline(tk) => tk.into_timeline(ndb, user), } diff --git a/crates/notedeck_columns/src/profile.rs b/crates/notedeck_columns/src/profile.rs @@ -6,7 +6,7 @@ use notedeck::{filter::default_limit, FilterState, MuteFun, NoteCache, NoteRef}; use crate::{ multi_subscriber::MultiSubscriber, notes_holder::NotesHolder, - timeline::{copy_notes_into_timeline, PubkeySource, Timeline, TimelineKind}, + timeline::{copy_notes_into_timeline, PubkeySource, Timeline, TimelineKind, TimelineTab}, }; pub enum DisplayName<'a> { @@ -62,8 +62,11 @@ impl Profile { notes: Vec<NoteRef>, is_muted: &MuteFun, ) -> Self { - let mut timeline = - Timeline::new(TimelineKind::profile(source), FilterState::ready(filters)); + let mut timeline = Timeline::new( + TimelineKind::profile(source), + FilterState::ready(filters), + TimelineTab::full_tabs(), + ); copy_notes_into_timeline(&mut timeline, txn, ndb, note_cache, notes, is_muted); diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs @@ -1,5 +1,5 @@ use crate::error::Error; -use crate::timeline::Timeline; +use crate::timeline::{Timeline, TimelineTab}; use enostr::{Filter, Pubkey}; use nostrdb::{Ndb, Transaction}; use notedeck::{filter::default_limit, FilterError, FilterState}; @@ -119,6 +119,7 @@ impl TimelineKind { .kinds([1]) .limit(default_limit()) .build()]), + TimelineTab::no_replies(), )), TimelineKind::Generic => { @@ -141,6 +142,7 @@ impl TimelineKind { Some(Timeline::new( TimelineKind::profile(pk_src), FilterState::ready(vec![filter]), + TimelineTab::full_tabs(), )) } @@ -159,6 +161,7 @@ impl TimelineKind { Some(Timeline::new( TimelineKind::notifications(pk_src), FilterState::ready(vec![notifications_filter]), + TimelineTab::only_notes_and_replies(), )) } @@ -181,6 +184,7 @@ impl TimelineKind { return Some(Timeline::new( TimelineKind::contact_list(pk_src), FilterState::needs_remote(vec![contact_filter.clone()]), + TimelineTab::full_tabs(), )); } @@ -189,6 +193,7 @@ impl TimelineKind { Some(Timeline::new( TimelineKind::contact_list(pk_src), FilterState::needs_remote(vec![contact_filter]), + TimelineTab::full_tabs(), )) } Err(e) => { diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs @@ -60,13 +60,6 @@ impl ViewFilter { } } - pub fn index(&self) -> usize { - match self { - ViewFilter::Notes => 0, - ViewFilter::NotesAndReplies => 1, - } - } - pub fn filter_notes(cache: &CachedNote, note: &Note) -> bool { !cache.reply.borrow(note.tags()).is_reply() } @@ -100,6 +93,21 @@ impl TimelineTab { TimelineTab::new_with_capacity(filter, 1000) } + pub fn only_notes_and_replies() -> Vec<Self> { + vec![TimelineTab::new(ViewFilter::NotesAndReplies)] + } + + pub fn no_replies() -> Vec<Self> { + vec![TimelineTab::new(ViewFilter::Notes)] + } + + pub fn full_tabs() -> Vec<Self> { + vec![ + TimelineTab::new(ViewFilter::Notes), + TimelineTab::new(ViewFilter::NotesAndReplies), + ] + } + pub fn new_with_capacity(filter: ViewFilter, cap: usize) -> Self { let selection = 0i32; let mut list = VirtualList::new(); @@ -179,7 +187,7 @@ pub struct Timeline { // that codepaths have to explicitly handle it pub filter: FilterStates, pub views: Vec<TimelineTab>, - pub selected_view: i32, + pub selected_view: usize, /// Our nostrdb subscription pub subscription: Option<Subscription>, @@ -198,6 +206,7 @@ impl Timeline { Ok(Timeline::new( TimelineKind::contact_list(pk_src), FilterState::ready(filter), + TimelineTab::full_tabs(), )) } @@ -211,10 +220,11 @@ impl Timeline { Timeline::new( TimelineKind::Hashtag(hashtag), FilterState::ready(vec![filter]), + TimelineTab::full_tabs(), ) } - pub fn make_view_id(id: TimelineId, selected_view: i32) -> egui::Id { + pub fn make_view_id(id: TimelineId, selected_view: usize) -> egui::Id { egui::Id::new((id, selected_view)) } @@ -222,15 +232,12 @@ impl Timeline { Timeline::make_view_id(self.id, self.selected_view) } - pub fn new(kind: TimelineKind, filter_state: FilterState) -> Self { + pub fn new(kind: TimelineKind, filter_state: FilterState, views: Vec<TimelineTab>) -> Self { // global unique id for all new timelines static UIDS: AtomicU32 = AtomicU32::new(0); let filter = FilterStates::new(filter_state); let subscription: Option<Subscription> = None; - let notes = TimelineTab::new(ViewFilter::Notes); - let replies = TimelineTab::new(ViewFilter::NotesAndReplies); - let views = vec![notes, replies]; let selected_view = 0; let id = TimelineId::new(UIDS.fetch_add(1, Ordering::Relaxed)); @@ -245,23 +252,32 @@ impl Timeline { } pub fn current_view(&self) -> &TimelineTab { - &self.views[self.selected_view as usize] + &self.views[self.selected_view] } pub fn current_view_mut(&mut self) -> &mut TimelineTab { - &mut self.views[self.selected_view as usize] + &mut self.views[self.selected_view] } - pub fn notes(&self, view: ViewFilter) -> &[NoteRef] { - &self.views[view.index()].notes + /// Get the note refs for NotesAndReplies. If we only have Notes, then + /// just return that instead + pub fn all_or_any_notes(&self) -> &[NoteRef] { + self.notes(ViewFilter::NotesAndReplies).unwrap_or_else(|| { + self.notes(ViewFilter::Notes) + .expect("should have at least notes") + }) } - pub fn view(&self, view: ViewFilter) -> &TimelineTab { - &self.views[view.index()] + pub fn notes(&self, view: ViewFilter) -> Option<&[NoteRef]> { + self.view(view).map(|v| &*v.notes) } - pub fn view_mut(&mut self, view: ViewFilter) -> &mut TimelineTab { - &mut self.views[view.index()] + pub fn view(&self, view: ViewFilter) -> Option<&TimelineTab> { + self.views.iter().find(|tab| tab.filter == view) + } + + pub fn view_mut(&mut self, view: ViewFilter) -> Option<&mut TimelineTab> { + self.views.iter_mut().find(|tab| tab.filter == view) } pub fn poll_notes_into_view( @@ -314,13 +330,10 @@ impl Timeline { let reversed = false; // ViewFilter::NotesAndReplies - { + if let Some(view) = timeline.view_mut(ViewFilter::NotesAndReplies) { let refs: Vec<NoteRef> = new_refs.iter().map(|(_note, nr)| *nr).collect(); - let reversed = false; - timeline - .view_mut(ViewFilter::NotesAndReplies) - .insert(&refs, reversed); + view.insert(&refs, reversed); } // @@ -328,7 +341,7 @@ impl Timeline { // // TODO(jb55): this is mostly just copied from above, let's just use a loop // I initially tried this but ran into borrow checker issues - { + if let Some(view) = timeline.view_mut(ViewFilter::Notes) { let mut filtered_refs = Vec::with_capacity(new_refs.len()); for (note, nr) in &new_refs { let cached_note = note_cache.cached_note_or_insert(nr.key, note); @@ -338,9 +351,7 @@ impl Timeline { } } - timeline - .view_mut(ViewFilter::Notes) - .insert(&filtered_refs, reversed); + view.insert(&filtered_refs, reversed); } Ok(()) @@ -478,7 +489,7 @@ pub fn send_initial_timeline_filter( filter = filter.limit_mut(lim); } - let notes = timeline.notes(ViewFilter::NotesAndReplies); + let notes = timeline.all_or_any_notes(); // Should we since optimize? Not always. For example // if we only have a few notes locally. One way to diff --git a/crates/notedeck_columns/src/ui/profile/mod.rs b/crates/notedeck_columns/src/ui/profile/mod.rs @@ -67,7 +67,8 @@ impl<'a> ProfileView<'a> { ) .get_ptr(); - profile.timeline.selected_view = tabs_ui(ui); + profile.timeline.selected_view = + tabs_ui(ui, profile.timeline.selected_view, &profile.timeline.views); // poll for new notes and insert them into our existing notes if let Err(e) = profile.poll_notes_into_view(&txn, self.ndb, is_muted) { diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs @@ -1,6 +1,11 @@ use crate::actionbar::NoteAction; use crate::timeline::TimelineTab; -use crate::{column::Columns, timeline::TimelineId, ui, ui::note::NoteOptions}; +use crate::{ + column::Columns, + timeline::{TimelineId, ViewFilter}, + ui, + ui::note::NoteOptions, +}; use egui::containers::scroll_area::ScrollBarVisibility; use egui::{Direction, Layout}; use egui_tabs::TabColor; @@ -86,7 +91,7 @@ fn timeline_ui( return None; }; - timeline.selected_view = tabs_ui(ui); + timeline.selected_view = tabs_ui(ui, timeline.selected_view, &timeline.views); // need this for some reason?? ui.add_space(3.0); @@ -124,11 +129,11 @@ fn timeline_ui( .inner } -pub fn tabs_ui(ui: &mut egui::Ui) -> i32 { +pub fn tabs_ui(ui: &mut egui::Ui, selected: usize, views: &[TimelineTab]) -> usize { ui.spacing_mut().item_spacing.y = 0.0; - let tab_res = egui_tabs::Tabs::new(2) - .selected(1) + let tab_res = egui_tabs::Tabs::new(views.len() as i32) + .selected(selected as i32) .hover_bg(TabColor::none()) .selected_fg(TabColor::none()) .selected_bg(TabColor::none()) @@ -141,7 +146,10 @@ pub fn tabs_ui(ui: &mut egui::Ui) -> i32 { let ind = state.index(); - let txt = if ind == 0 { "Notes" } else { "Notes & Replies" }; + let txt = match views[ind as usize].filter { + ViewFilter::Notes => "Notes", + ViewFilter::NotesAndReplies => "Notes & Replies", + }; let res = ui.add(egui::Label::new(txt).selectable(false)); @@ -189,7 +197,7 @@ pub fn tabs_ui(ui: &mut egui::Ui) -> i32 { ui.painter().hline(underline, underline_y, stroke); - sel + sel as usize } fn get_label_width(ui: &mut egui::Ui, text: &str) -> f32 { diff --git a/crates/notedeck_columns/src/unknowns.rs b/crates/notedeck_columns/src/unknowns.rs @@ -1,4 +1,4 @@ -use crate::{column::Columns, timeline::ViewFilter, Result}; +use crate::{column::Columns, Result}; use nostrdb::{Ndb, NoteKey, Transaction}; use notedeck::{CachedNote, NoteCache, UnknownIds}; use tracing::error; @@ -37,7 +37,7 @@ pub fn get_unknown_ids( let mut new_cached_notes: Vec<(NoteKey, CachedNote)> = vec![]; for timeline in columns.timelines() { - for noteref in timeline.notes(ViewFilter::NotesAndReplies) { + for noteref in timeline.all_or_any_notes() { let note = ndb.get_note_by_key(txn, noteref.key)?; let note_key = note.key().unwrap(); let cached_note = note_cache.cached_note(noteref.key);