notedeck

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

commit 22483f23fe277577b089399b1fcac17c3ed3b0d3
parent 7c970b5fbfcdbf7468ef2cc15a01d07ba5b719a6
Author: e <e@*.lan>
Date:   Sun, 28 Dec 2025 09:33:25 -0600

timeline: add reset methods for subscription and view cleanup

Add methods to properly reset timeline state when filters need to be
rebuilt (e.g., after follow/unfollow actions).

Changes:
- TimelineSub::reset(): Unsubscribes from ndb and relay pool before
  resetting state. Preserves depender count for reference counting.
- TimelineTab::reset(): Clears cached notes and resets virtual list.
- Timeline::reset_views(): Resets all view tabs.

These methods ensure no subscription leaks when rebuilding timelines,
and that stale notes from unfollowed accounts are cleared.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: e <e@*.lan>
Signed-off-by: alltheseas

Diffstat:
Mcrates/notedeck_columns/src/multi_subscriber.rs | 39+++++++++++++++++++++++++++++++++++++++
Mcrates/notedeck_columns/src/timeline/mod.rs | 20++++++++++++++++++++
2 files changed, 59 insertions(+), 0 deletions(-)

diff --git a/crates/notedeck_columns/src/multi_subscriber.rs b/crates/notedeck_columns/src/multi_subscriber.rs @@ -307,6 +307,45 @@ impl Default for TimelineSub { } impl TimelineSub { + /// Reset the subscription state, properly unsubscribing from ndb and + /// relay pool before clearing. + /// + /// Used when the contact list changes and we need to rebuild the + /// timeline with a new filter. Preserves the depender count so that + /// shared subscription reference counting remains correct. + pub fn reset(&mut self, ndb: &mut Ndb, pool: &mut RelayPool) { + let before = self.state.clone(); + + let dependers = match &mut self.state { + SubState::NoSub { dependers } => *dependers, + + SubState::LocalOnly { local, dependers } => { + if let Err(e) = ndb.unsubscribe(*local) { + tracing::error!("TimelineSub::reset: failed to unsubscribe from ndb: {e}"); + } + *dependers + } + + SubState::RemoteOnly { remote, dependers } => { + pool.unsubscribe(remote.to_owned()); + *dependers + } + + SubState::Unified { unified, dependers } => { + pool.unsubscribe(unified.remote.to_owned()); + if let Err(e) = ndb.unsubscribe(unified.local) { + tracing::error!("TimelineSub::reset: failed to unsubscribe from ndb: {e}"); + } + *dependers + } + }; + + self.state = SubState::NoSub { dependers }; + self.filter = None; + + tracing::debug!("TimelineSub::reset: {:?} => {:?}", before, self.state); + } + pub fn try_add_local(&mut self, ndb: &Ndb, filter: &HybridFilter) { let before = self.state.clone(); match &mut self.state { diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs @@ -150,6 +150,16 @@ impl TimelineTab { } } + /// Reset the tab to an empty state, clearing all cached notes. + /// + /// Used when the contact list changes and we need to rebuild + /// the timeline with a new filter. + pub fn reset(&mut self) { + self.units = TimelineUnits::with_capacity(1000); + self.selection = 0; + self.list.borrow_mut().reset(); + } + fn insert<'a>( &mut self, payloads: Vec<&'a NotePayload>, @@ -352,6 +362,16 @@ impl Timeline { self.views.iter_mut().find(|tab| tab.filter == view) } + /// Reset all views to an empty state, clearing all cached notes. + /// + /// Used when the contact list changes and we need to rebuild + /// the timeline with a new filter. + pub fn reset_views(&mut self) { + for view in &mut self.views { + view.reset(); + } + } + /// Initial insert of notes into a timeline. Subsequent inserts should /// just use the insert function pub fn insert_new(