notedeck

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

commit ac22fc7072b250ce656266538e39441692b14c01
parent 074472eec910b92932af2a8a9b407707b31c20a0
Author: William Casarin <jb55@jb55.com>
Date:   Tue, 15 Jul 2025 13:29:59 -0700

columns: enable toolbar scroll to top

Fixes: https://github.com/damus-io/notedeck/issues/969
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mcrates/notedeck_chrome/src/chrome.rs | 35++++++++++++++++++++++++++---------
Mcrates/notedeck_columns/src/column.rs | 23++++++++++++++++++++---
Mcrates/notedeck_columns/src/decks.rs | 4++++
Mcrates/notedeck_columns/src/lib.rs | 2+-
Mcrates/notedeck_columns/src/nav.rs | 40+++++++++++++++++++++++++++++-----------
Mcrates/notedeck_columns/src/timeline/route.rs | 3+++
Mcrates/notedeck_columns/src/ui/timeline.rs | 15+++++++++++++++
7 files changed, 98 insertions(+), 24 deletions(-)

diff --git a/crates/notedeck_chrome/src/chrome.rs b/crates/notedeck_chrome/src/chrome.rs @@ -6,7 +6,9 @@ use egui::{vec2, Button, Color32, Label, Layout, Rect, RichText, ThemePreference use egui_extras::{Size, StripBuilder}; use nostrdb::{ProfileRecord, Transaction}; use notedeck::{App, AppAction, AppContext, NotedeckTextStyle, UserAccount, WalletType}; -use notedeck_columns::{timeline::kind::ListKind, timeline::TimelineKind, Damus}; +use notedeck_columns::{ + column::SelectionResult, timeline::kind::ListKind, timeline::TimelineKind, Damus, +}; use notedeck_dave::{Dave, DaveAvatar}; use notedeck_ui::{app_images, AnimationHelper, ProfilePic}; @@ -61,11 +63,26 @@ impl ChromePanelAction { fn columns_switch(ctx: &AppContext, chrome: &mut Chrome, kind: &TimelineKind) { chrome.switch_to_columns(); - if let Some(active_columns) = chrome - .get_columns() - .and_then(|cols| cols.decks_cache.active_columns_mut(ctx.accounts)) - { - active_columns.select_by_kind(kind) + let Some(columns_app) = chrome.get_columns_app() else { + return; + }; + + if let Some(active_columns) = columns_app.decks_cache.active_columns_mut(ctx.accounts) { + match active_columns.select_by_kind(kind) { + SelectionResult::NewSelection(_index) => { + // great! no need to go to top yet + } + + SelectionResult::AlreadySelected(_n) => { + // we already selected this, so scroll to top + columns_app.scroll_to_top(); + } + + SelectionResult::Failed => { + // oh no, something went wrong + // TODO(jb55): handle tab selection failure + } + } } } @@ -73,7 +90,7 @@ impl ChromePanelAction { chrome.switch_to_columns(); if let Some(c) = chrome - .get_columns() + .get_columns_app() .and_then(|columns| columns.decks_cache.selected_column_mut(ctx.accounts)) { if c.router().routes().iter().any(|r| r == &route) { @@ -155,7 +172,7 @@ impl Chrome { self.apps.push(app); } - fn get_columns(&mut self) -> Option<&mut Damus> { + fn get_columns_app(&mut self) -> Option<&mut Damus> { for app in &mut self.apps { if let NotedeckApp::Columns(cols) = app { return Some(cols); @@ -632,7 +649,7 @@ fn chrome_handle_app_action( AppAction::Note(note_action) => { chrome.switch_to_columns(); - let Some(columns) = chrome.get_columns() else { + let Some(columns) = chrome.get_columns_app() else { return; }; diff --git a/crates/notedeck_columns/src/column.rs b/crates/notedeck_columns/src/column.rs @@ -42,6 +42,18 @@ pub struct Columns { pub selected: i32, } +/// When selecting columns, return what happened +pub enum SelectionResult { + /// We're already selecting that + AlreadySelected(usize), + + /// New selection success! + NewSelection(usize), + + /// Failed to make a selection + Failed, +} + impl Columns { pub fn new() -> Self { Columns::default() @@ -60,20 +72,25 @@ impl Columns { /// Select the column based on the timeline kind. /// /// TODO: add timeline if missing? - pub fn select_by_kind(&mut self, kind: &TimelineKind) { + pub fn select_by_kind(&mut self, kind: &TimelineKind) -> SelectionResult { for (i, col) in self.columns.iter().enumerate() { for route in col.router().routes() { if let Some(timeline) = route.timeline_id() { if timeline == kind { tracing::info!("selecting {kind:?} column"); - self.select_column(i as i32); - return; + if self.selected as usize == i { + return SelectionResult::AlreadySelected(i); + } else { + self.select_column(i as i32); + return SelectionResult::NewSelection(i); + } } } } } tracing::error!("failed to select {kind:?} column"); + SelectionResult::Failed } pub fn add_new_timeline_column( diff --git a/crates/notedeck_columns/src/decks.rs b/crates/notedeck_columns/src/decks.rs @@ -40,6 +40,10 @@ impl DecksCache { self.active_columns(accounts).and_then(|ad| ad.selected()) } + pub fn selected_column_index(&self, accounts: &notedeck::Accounts) -> Option<usize> { + self.active_columns(accounts).map(|ad| ad.selected as usize) + } + /// Gets a mutable reference to the active columns pub fn active_columns_mut(&mut self, accounts: &notedeck::Accounts) -> Option<&mut Columns> { let account = accounts.get_selected_account(); diff --git a/crates/notedeck_columns/src/lib.rs b/crates/notedeck_columns/src/lib.rs @@ -8,7 +8,7 @@ pub mod actionbar; pub mod app_creation; mod app_style; mod args; -mod column; +pub mod column; mod deck_state; mod decks; mod draft; diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs @@ -4,6 +4,7 @@ use crate::{ column::ColumnsAction, deck_state::DeckState, decks::{Deck, DecksAction, DecksCache}, + options::AppOptions, profile::{ProfileAction, SaveProfileChanges}, route::{Route, Router, SingletonRouter}, timeline::{ @@ -496,17 +497,34 @@ fn render_nav_body( current_account_has_wallet: get_current_wallet(ctx.accounts, ctx.global_wallet).is_some(), }; match top { - Route::Timeline(kind) => render_timeline_route( - &mut app.timeline_cache, - ctx.accounts, - kind, - col, - app.note_options, - depth, - ui, - &mut note_context, - &mut app.jobs, - ), + Route::Timeline(kind) => { + // did something request scroll to top for the selection column? + let scroll_to_top = app + .decks_cache + .selected_column_index(ctx.accounts) + .is_some_and(|ind| ind == col) + && app.options.contains(AppOptions::ScrollToTop); + + let nav_action = render_timeline_route( + &mut app.timeline_cache, + ctx.accounts, + kind, + col, + app.note_options, + depth, + ui, + &mut note_context, + &mut app.jobs, + scroll_to_top, + ); + + // always clear the scroll_to_top request + if scroll_to_top { + app.options.remove(AppOptions::ScrollToTop); + } + + nav_action + } Route::Thread(selection) => render_thread_route( &mut app.threads, ctx.accounts, diff --git a/crates/notedeck_columns/src/timeline/route.rs b/crates/notedeck_columns/src/timeline/route.rs @@ -20,6 +20,7 @@ pub fn render_timeline_route( ui: &mut egui::Ui, note_context: &mut NoteContext, jobs: &mut JobsCache, + scroll_to_top: bool, ) -> Option<RenderNavAction> { match kind { TimelineKind::List(_) @@ -39,6 +40,7 @@ pub fn render_timeline_route( jobs, col, ) + .scroll_to_top(scroll_to_top) .ui(ui); note_action.map(RenderNavAction::NoteAction) @@ -69,6 +71,7 @@ pub fn render_timeline_route( jobs, col, ) + .scroll_to_top(scroll_to_top) .ui(ui); note_action.map(RenderNavAction::NoteAction) diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs @@ -25,6 +25,7 @@ pub struct TimelineView<'a, 'd> { cur_acc: &'a KeypairUnowned<'a>, jobs: &'a mut JobsCache, col: usize, + scroll_to_top: bool, } impl<'a, 'd> TimelineView<'a, 'd> { @@ -40,6 +41,7 @@ impl<'a, 'd> TimelineView<'a, 'd> { col: usize, ) -> Self { let reverse = false; + let scroll_to_top = false; TimelineView { timeline_id, timeline_cache, @@ -50,6 +52,7 @@ impl<'a, 'd> TimelineView<'a, 'd> { cur_acc, jobs, col, + scroll_to_top, } } @@ -65,9 +68,15 @@ impl<'a, 'd> TimelineView<'a, 'd> { self.cur_acc, self.jobs, self.col, + self.scroll_to_top, ) } + pub fn scroll_to_top(mut self, enable: bool) -> Self { + self.scroll_to_top = enable; + self + } + pub fn reversed(mut self) -> Self { self.reverse = true; self @@ -86,6 +95,7 @@ fn timeline_ui( cur_acc: &KeypairUnowned, jobs: &mut JobsCache, col: usize, + scroll_to_top: bool, ) -> Option<NoteAction> { //padding(4.0, ui, |ui| ui.heading("Notifications")); /* @@ -152,6 +162,11 @@ fn timeline_ui( } } + // chrome can ask to scroll to top as well via an app option + if scroll_to_top { + scroll_area = scroll_area.vertical_scroll_offset(0.0); + } + let scroll_output = scroll_area.show(ui, |ui| { let timeline = if let Some(timeline) = timeline_cache.timelines.get(timeline_id) { timeline