notedeck

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

commit 7aca39aae8b894c7218ae758c72bd274591497c2
parent aa467b9be060c83d750f8804bf1b030d8abf6156
Author: kernelkind <kernelkind@gmail.com>
Date:   Thu, 31 Jul 2025 18:52:28 -0400

add `NotesFreshness` to `TimelineTab`

necessary for notifications indicator

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

Diffstat:
Mcrates/notedeck_columns/src/timeline/mod.rs | 108+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 106 insertions(+), 2 deletions(-)

diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs @@ -8,6 +8,7 @@ use crate::{ use notedeck::{ contacts::hybrid_contacts_filter, + debouncer::Debouncer, filter::{self, HybridFilter}, tr, Accounts, CachedNote, ContactState, FilterError, FilterState, FilterStates, Localization, NoteCache, NoteRef, UnknownIds, @@ -16,8 +17,11 @@ use notedeck::{ use egui_virtual_list::VirtualList; use enostr::{PoolRelay, Pubkey, RelayPool}; use nostrdb::{Filter, Ndb, Note, NoteKey, Transaction}; -use std::cell::RefCell; -use std::rc::Rc; +use std::{ + cell::RefCell, + time::{Duration, UNIX_EPOCH}, +}; +use std::{rc::Rc, time::SystemTime}; use tracing::{debug, error, info, warn}; @@ -103,6 +107,7 @@ pub struct TimelineTab { pub selection: i32, pub filter: ViewFilter, pub list: Rc<RefCell<VirtualList>>, + pub freshness: NotesFreshness, } impl TimelineTab { @@ -138,6 +143,7 @@ impl TimelineTab { selection, filter, list, + freshness: NotesFreshness::default(), } } @@ -780,3 +786,101 @@ pub fn is_timeline_ready( } } } + +#[derive(Debug)] +pub struct NotesFreshness { + debouncer: Debouncer, + state: NotesFreshnessState, +} + +#[derive(Debug)] +enum NotesFreshnessState { + Fresh { + timestamp_viewed: u64, + }, + Stale { + have_unseen: bool, + timestamp_last_viewed: u64, + }, +} + +impl Default for NotesFreshness { + fn default() -> Self { + Self { + debouncer: Debouncer::new(Duration::from_secs(2)), + state: NotesFreshnessState::Stale { + have_unseen: true, + timestamp_last_viewed: 0, + }, + } + } +} + +impl NotesFreshness { + pub fn set_fresh(&mut self) { + if !self.debouncer.should_act() { + return; + } + self.state = NotesFreshnessState::Fresh { + timestamp_viewed: timestamp_now(), + }; + self.debouncer.bounce(); + } + + pub fn update(&mut self, check_have_unseen: impl FnOnce(u64) -> bool) { + if !self.debouncer.should_act() { + return; + } + + match &self.state { + NotesFreshnessState::Fresh { timestamp_viewed } => { + let Ok(dur) = SystemTime::now() + .duration_since(UNIX_EPOCH + Duration::from_secs(*timestamp_viewed)) + else { + return; + }; + + if dur > Duration::from_secs(2) { + self.state = NotesFreshnessState::Stale { + have_unseen: check_have_unseen(*timestamp_viewed), + timestamp_last_viewed: *timestamp_viewed, + }; + } + } + NotesFreshnessState::Stale { + have_unseen, + timestamp_last_viewed, + } => { + if *have_unseen { + return; + } + + self.state = NotesFreshnessState::Stale { + have_unseen: check_have_unseen(*timestamp_last_viewed), + timestamp_last_viewed: *timestamp_last_viewed, + }; + } + } + + self.debouncer.bounce(); + } + + pub fn has_unseen(&self) -> bool { + match &self.state { + NotesFreshnessState::Fresh { + timestamp_viewed: _, + } => false, + NotesFreshnessState::Stale { + have_unseen, + timestamp_last_viewed: _, + } => *have_unseen, + } + } +} + +fn timestamp_now() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or(Duration::ZERO) + .as_secs() +}