notedeck

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

commit 62ffe9b219cd956c8af846fd0564641c7a10e57f
parent 4c867f9fc245065793ee96b88498de5aa32ff315
Author: kernelkind <kernelkind@gmail.com>
Date:   Wed,  1 Oct 2025 19:52:26 -0400

feat: "All" & "Mentions" notifications tabs like Damus iOS

Signed-off-by: kernelkind <kernelkind@gmail.com>

Diffstat:
Mcrates/notedeck_columns/src/timeline/kind.rs | 2+-
Mcrates/notedeck_columns/src/timeline/mod.rs | 96++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mcrates/notedeck_columns/src/ui/timeline.rs | 13++-----------
3 files changed, 61 insertions(+), 50 deletions(-)

diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs @@ -577,7 +577,7 @@ impl TimelineKind { Some(Timeline::new( TimelineKind::notifications(pk), FilterState::ready(vec![notifications_filter]), - TimelineTab::only_notes_and_replies(), + TimelineTab::notifications(), )) } diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs @@ -40,12 +40,15 @@ pub use note_units::{CompositeType, InsertionResponse, NoteUnits}; pub use timeline_units::{TimelineUnits, UnknownPks}; pub use unit::{CompositeUnit, NoteUnit, ReactionUnit, RepostUnit}; -#[derive(Copy, Clone, Eq, PartialEq, Debug, Default)] +#[derive(Copy, Clone, Eq, PartialEq, Debug, Default, PartialOrd, Ord)] pub enum ViewFilter { + MentionsOnly, Notes, #[default] NotesAndReplies, + + All, } impl ViewFilter { @@ -59,6 +62,10 @@ impl ViewFilter { "Filter label for notes and replies view" ) } + ViewFilter::All => tr!(i18n, "All", "Filter label for all notes view"), + ViewFilter::MentionsOnly => { + tr!(i18n, "Mentions", "Filter label for mentions only view") + } } } @@ -70,10 +77,26 @@ impl ViewFilter { true } + fn notes_and_replies(_cache: &CachedNote, note: &Note) -> bool { + note.kind() == 1 || note.kind() == 6 + } + + fn mentions_only(cache: &CachedNote, note: &Note) -> bool { + if note.kind() != 1 { + return false; + } + + let note_reply = cache.reply.borrow(note.tags()); + + note_reply.is_reply() || note_reply.mention().is_some() + } + pub fn filter(&self) -> fn(&CachedNote, &Note) -> bool { match self { ViewFilter::Notes => ViewFilter::filter_notes, - ViewFilter::NotesAndReplies => ViewFilter::identity, + ViewFilter::NotesAndReplies => ViewFilter::notes_and_replies, + ViewFilter::All => ViewFilter::identity, + ViewFilter::MentionsOnly => ViewFilter::mentions_only, } } } @@ -111,6 +134,13 @@ impl TimelineTab { ] } + pub fn notifications() -> Vec<Self> { + vec![ + TimelineTab::new(ViewFilter::All), + TimelineTab::new(ViewFilter::MentionsOnly), + ] + } + pub fn new_with_capacity(filter: ViewFilter, cap: usize) -> Self { let selection = 0i32; let mut list = VirtualList::new(); @@ -298,14 +328,17 @@ impl Timeline { &mut self.views[self.selected_view] } - /// Get the note refs for NotesAndReplies. If we only have Notes, then - /// just return that instead + /// Get the note refs for the filter with the widest scope pub fn all_or_any_entries(&self) -> &TimelineUnits { - self.entries(ViewFilter::NotesAndReplies) - .unwrap_or_else(|| { - self.entries(ViewFilter::Notes) - .expect("should have at least notes") - }) + let widest_filter = self + .views + .iter() + .map(|view| view.filter) + .max() + .expect("at least one filter exists"); + + self.entries(widest_filter) + .expect("should have at least notes") } pub fn entries(&self, view: ViewFilter) -> Option<&TimelineUnits> { @@ -409,38 +442,25 @@ impl Timeline { } for view in &mut self.views { - match view.filter { - ViewFilter::NotesAndReplies => { - let res: Vec<&NotePayload<'_>> = payloads.iter().collect(); - if let Some(res) = - view.insert(res, ndb, txn, reversed, self.enable_front_insert) - { - res.process(unknown_ids, ndb, txn); - } - } - - ViewFilter::Notes => { - let mut filtered_payloads = Vec::with_capacity(payloads.len()); - for payload in &payloads { - let cached_note = - note_cache.cached_note_or_insert(payload.key, &payload.note); + let should_include = view.filter.filter(); + let mut filtered_payloads = Vec::with_capacity(payloads.len()); + for payload in &payloads { + let cached_note = note_cache.cached_note_or_insert(payload.key, &payload.note); - if ViewFilter::filter_notes(cached_note, &payload.note) { - filtered_payloads.push(payload); - } - } - - if let Some(res) = view.insert( - filtered_payloads, - ndb, - txn, - reversed, - self.enable_front_insert, - ) { - res.process(unknown_ids, ndb, txn); - } + if should_include(cached_note, &payload.note) { + filtered_payloads.push(payload); } } + + if let Some(res) = view.insert( + filtered_payloads, + ndb, + txn, + reversed, + self.enable_front_insert, + ) { + res.process(unknown_ids, ndb, txn); + } } Ok(()) diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs @@ -15,7 +15,7 @@ use tracing::{error, warn}; use crate::nav::BodyResponse; use crate::timeline::{ CompositeType, CompositeUnit, NoteUnit, ReactionUnit, RepostUnit, TimelineCache, TimelineKind, - TimelineTab, ViewFilter, + TimelineTab, }; use notedeck::{ note::root_note_id_from_selected_id, tr, Localization, NoteAction, NoteContext, ScrollInfo, @@ -302,16 +302,7 @@ pub fn tabs_ui( let ind = state.index(); - let txt = match views[ind as usize].filter { - ViewFilter::Notes => tr!(i18n, "Notes", "Label for notes-only filter"), - ViewFilter::NotesAndReplies => { - tr!( - i18n, - "Notes & Replies", - "Label for notes and replies filter" - ) - } - }; + let txt = views[ind as usize].filter.name(i18n); let res = ui.add(egui::Label::new(txt.clone()).selectable(false));