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:
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));