notedeck

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

commit f2153f53dcf9b0fee85ff61c6bea7b9b92478aaa
parent 40764d736809438d571b5cc77442f1f069ce92ca
Author: Fernando López Guevara <fernando.lguevara@gmail.com>
Date:   Tue, 29 Jul 2025 21:30:35 -0300

feat(settings): allow sorting thread replies newest first

Diffstat:
Mcrates/notedeck/src/persist/mod.rs | 1+
Mcrates/notedeck/src/persist/settings_handler.rs | 19+++++++++++++++++--
Mcrates/notedeck_columns/src/app.rs | 80+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mcrates/notedeck_columns/src/nav.rs | 25+++++--------------------
Mcrates/notedeck_columns/src/ui/settings.rs | 108++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mcrates/notedeck_columns/src/ui/thread.rs | 16++++++++++++++--
Mcrates/notedeck_ui/src/note/options.rs | 2++
7 files changed, 154 insertions(+), 97 deletions(-)

diff --git a/crates/notedeck/src/persist/mod.rs b/crates/notedeck/src/persist/mod.rs @@ -5,6 +5,7 @@ mod token_handler; mod zoom; pub use app_size::AppSizeHandler; +pub use settings_handler::Settings; pub use settings_handler::SettingsHandler; pub use theme_handler::ThemeHandler; pub use token_handler::TokenHandler; diff --git a/crates/notedeck/src/persist/settings_handler.rs b/crates/notedeck/src/persist/settings_handler.rs @@ -29,6 +29,7 @@ pub struct Settings { pub locale: String, pub zoom_factor: f32, pub show_source_client: String, + pub show_replies_newest_first: bool, } impl Default for Settings { @@ -38,10 +39,12 @@ impl Default for Settings { theme: DEFAULT_THEME, locale: DEFAULT_LOCALE.to_string(), zoom_factor: DEFAULT_ZOOM_FACTOR, - show_source_client: "Hide".to_string(), + show_source_client: DEFAULT_SHOW_SOURCE_CLIENT.to_string(), + show_replies_newest_first: false, } } } + pub struct SettingsHandler { directory: Directory, current_settings: Option<Settings>, @@ -129,7 +132,7 @@ impl SettingsHandler { }; } - fn get_settings_mut(&mut self) -> &mut Settings { + pub fn get_settings_mut(&mut self) -> &mut Settings { if self.current_settings.is_none() { self.current_settings = Some(Settings::default()); } @@ -162,6 +165,11 @@ impl SettingsHandler { self.save(); } + pub fn set_show_replies_newest_first(&mut self, value: bool) { + self.get_settings_mut().show_replies_newest_first = value; + self.save(); + } + pub fn update_batch<F>(&mut self, update_fn: F) where F: FnOnce(&mut Settings), @@ -204,6 +212,13 @@ impl SettingsHandler { .unwrap_or(DEFAULT_SHOW_SOURCE_CLIENT.to_string()) } + pub fn show_replies_newest_first(&self) -> bool { + self.current_settings + .as_ref() + .map(|s| s.show_replies_newest_first) + .unwrap_or(false) + } + pub fn is_loaded(&self) -> bool { self.current_settings.is_some() } diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -14,18 +14,13 @@ use crate::{ view_state::ViewState, Result, }; - use egui_extras::{Size, StripBuilder}; use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; use nostrdb::Transaction; use notedeck::{ - tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, - Images, JobsCache, Localization, UnknownIds, -}; -use notedeck_ui::{ - media::{MediaViewer, MediaViewerFlags, MediaViewerState}, - NoteOptions, + tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, Images, JobsCache, Localization, SettingsHandler, UnknownIds }; +use notedeck_ui::{media::{MediaViewer, MediaViewerFlags, MediaViewerState}, NoteOptions}; use std::collections::{BTreeSet, HashMap}; use std::path::Path; use std::time::Duration; @@ -443,6 +438,11 @@ impl Damus { let mut options = AppOptions::default(); let tmp_columns = !parsed_args.columns.is_empty(); options.set(AppOptions::TmpColumns, tmp_columns); + options.set(AppOptions::Debug, app_context.args.debug); + options.set( + AppOptions::SinceOptimize, + parsed_args.is_flag_set(ColumnsFlag::SinceOptimize), + ); let decks_cache = if tmp_columns { info!("DecksCache: loading from command line arguments"); @@ -487,37 +487,11 @@ impl Damus { // cache.add_deck_default(*pk); //} }; + let settings_handler = &app_context.settings_handler; let support = Support::new(app_context.path); - let mut note_options = NoteOptions::default(); - note_options.set( - NoteOptions::Textmode, - parsed_args.is_flag_set(ColumnsFlag::Textmode), - ); - note_options.set( - NoteOptions::ScrambleText, - parsed_args.is_flag_set(ColumnsFlag::Scramble), - ); - note_options.set( - NoteOptions::HideMedia, - parsed_args.is_flag_set(ColumnsFlag::NoMedia), - ); - note_options.set( - NoteOptions::ShowNoteClientTop, - ShowSourceClientOption::Top == app_context.settings_handler.show_source_client().into() - || parsed_args.is_flag_set(ColumnsFlag::ShowNoteClientTop), - ); - note_options.set( - NoteOptions::ShowNoteClientBottom, - ShowSourceClientOption::Bottom - == app_context.settings_handler.show_source_client().into() - || parsed_args.is_flag_set(ColumnsFlag::ShowNoteClientBottom), - ); - options.set(AppOptions::Debug, app_context.args.debug); - options.set( - AppOptions::SinceOptimize, - parsed_args.is_flag_set(ColumnsFlag::SinceOptimize), - ); + + let note_options = get_note_options(parsed_args, settings_handler); let jobs = JobsCache::default(); @@ -599,6 +573,39 @@ impl Damus { } } +fn get_note_options(args: ColumnsArgs, settings_handler: &&mut SettingsHandler) -> NoteOptions { + let mut note_options = NoteOptions::default(); + + note_options.set( + NoteOptions::Textmode, + args.is_flag_set(ColumnsFlag::Textmode), + ); + note_options.set( + NoteOptions::ScrambleText, + args.is_flag_set(ColumnsFlag::Scramble), + ); + note_options.set( + NoteOptions::HideMedia, + args.is_flag_set(ColumnsFlag::NoMedia), + ); + note_options.set( + NoteOptions::ShowNoteClientTop, + ShowSourceClientOption::Top == settings_handler.show_source_client().into() + || args.is_flag_set(ColumnsFlag::ShowNoteClientTop), + ); + note_options.set( + NoteOptions::ShowNoteClientBottom, + ShowSourceClientOption::Bottom == settings_handler.show_source_client().into() + || args.is_flag_set(ColumnsFlag::ShowNoteClientBottom), + ); + + note_options.set( + NoteOptions::RepliesNewestFirst, + settings_handler.show_replies_newest_first(), + ); + note_options +} + /* fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) { let stroke = ui.style().interact(&response).fg_stroke; @@ -620,6 +627,7 @@ fn render_damus_mobile( let mut app_action: Option<AppAction> = None; let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize; + if !app.columns(app_ctx.accounts).columns().is_empty() { let r = nav::render_nav( active_col, diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs @@ -21,7 +21,7 @@ use crate::{ note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView}, profile::EditProfileView, search::{FocusState, SearchView}, - settings::{SettingsAction, ShowSourceClientOption}, + settings::SettingsAction, support::SupportView, wallet::{get_default_zap_state, WalletAction, WalletState, WalletView}, AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView, @@ -586,26 +586,11 @@ fn render_nav_body( .map(RenderNavAction::RelayAction), Route::Settings => { - let mut show_note_client: ShowSourceClientOption = app.note_options.into(); + let mut settings = ctx.settings_handler.get_settings_mut(); - let mut theme: String = (if ui.visuals().dark_mode { - "Dark" - } else { - "Light" - }) - .into(); - - let mut selected_language: String = ctx.i18n.get_current_locale().to_string(); - - SettingsView::new( - ctx.img_cache, - &mut selected_language, - &mut theme, - &mut show_note_client, - ctx.i18n, - ) - .ui(ui) - .map(RenderNavAction::SettingsAction) + SettingsView::new(ctx.i18n, ctx.img_cache, &mut settings) + .ui(ui) + .map(RenderNavAction::SettingsAction) } Route::Reply(id) => { let txn = if let Ok(txn) = Transaction::new(ctx.ndb) { diff --git a/crates/notedeck_columns/src/ui/settings.rs b/crates/notedeck_columns/src/ui/settings.rs @@ -1,5 +1,7 @@ -use egui::{vec2, Button, Color32, ComboBox, Frame, Margin, RichText, ThemePreference}; -use notedeck::{tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, SettingsHandler}; +use egui::{vec2, Button, Color32, ComboBox, Frame, Margin, RichText, ScrollArea, ThemePreference}; +use notedeck::{ + tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, Settings, SettingsHandler, +}; use notedeck_ui::NoteOptions; use strum::Display; @@ -97,6 +99,7 @@ pub enum SettingsAction { SetTheme(ThemePreference), SetShowSourceClient(ShowSourceClientOption), SetLocale(LanguageIdentifier), + SetRepliestNewestFirst(bool), OpenRelays, OpenCacheFolder, ClearCacheFolder, @@ -135,6 +138,11 @@ impl SettingsAction { settings_handler.set_locale(language.to_string()); } } + Self::SetRepliestNewestFirst(value) => { + app.note_options.set(NoteOptions::RepliesNewestFirst, value); + settings_handler.set_show_replies_newest_first(value); + settings_handler.save(); + } Self::OpenCacheFolder => { use opener; let _ = opener::open(img_cache.base_path.clone()); @@ -149,9 +157,7 @@ impl SettingsAction { } pub struct SettingsView<'a> { - theme: &'a mut String, - selected_language: &'a mut String, - show_note_client: &'a mut ShowSourceClientOption, + settings: &'a mut Settings, i18n: &'a mut Localization, img_cache: &'a mut Images, } @@ -181,30 +187,30 @@ where impl<'a> SettingsView<'a> { pub fn new( - img_cache: &'a mut Images, - selected_language: &'a mut String, - theme: &'a mut String, - show_note_client: &'a mut ShowSourceClientOption, i18n: &'a mut Localization, + img_cache: &'a mut Images, + settings: &'a mut Settings, + // theme: &'a mut String, + // show_note_client: &'a mut ShowSourceClientOption, + // show_wide: &'a mut bool, + // show_replies_newest_first: &'a mut bool, ) -> Self { Self { - show_note_client, - theme, + settings, img_cache, - selected_language, i18n, } } /// Get the localized name for a language identifier fn get_selected_language_name(&mut self) -> String { - if let Ok(lang_id) = self.selected_language.parse::<LanguageIdentifier>() { + if let Ok(lang_id) = self.settings.locale.parse::<LanguageIdentifier>() { self.i18n .get_locale_native_name(&lang_id) .map(|s| s.to_owned()) .unwrap_or_else(|| lang_id.to_string()) } else { - self.selected_language.clone() + self.settings.locale.clone() } } @@ -289,7 +295,7 @@ impl<'a> SettingsView<'a> { .map(|s| s.to_owned()) .unwrap_or_else(|| lang.to_string()); if ui - .selectable_value(self.selected_language, lang.to_string(), name) + .selectable_value(&mut self.settings.locale, lang.to_string(), name) .clicked() { action = Some(SettingsAction::SetLocale(lang.to_owned())) @@ -304,10 +310,11 @@ impl<'a> SettingsView<'a> { "Theme:", "Label for theme, Appearance settings section", )); + if ui .selectable_value( - self.theme, - THEME_LIGHT.into(), + &mut self.settings.theme, + ThemePreference::Light, small_richtext( self.i18n, THEME_LIGHT.into(), @@ -318,10 +325,11 @@ impl<'a> SettingsView<'a> { { action = Some(SettingsAction::SetTheme(ThemePreference::Light)); } + if ui .selectable_value( - self.theme, - THEME_DARK.into(), + &mut self.settings.theme, + ThemePreference::Dark, small_richtext( self.i18n, THEME_DARK.into(), @@ -435,11 +443,32 @@ impl<'a> SettingsView<'a> { let title = tr!(self.i18n, "Others", "Label for others settings section"); settings_group(ui, title, |ui| { + ui.horizontal(|ui| { + ui.label(small_richtext( + self.i18n, + "Sort replies newest first", + "Label for Sort replies newest first, others settings section", + )); + + if ui + .toggle_value( + &mut self.settings.show_replies_newest_first, + RichText::new(tr!(self.i18n, "ON", "ON")) + .text_style(NotedeckTextStyle::Small.text_style()), + ) + .changed() + { + action = Some(SettingsAction::SetRepliestNewestFirst( + self.settings.show_replies_newest_first, + )); + } + }); + ui.horizontal_wrapped(|ui| { ui.label(small_richtext( self.i18n, - "Show source client", - "Label for Show source client, others settings section", + "Source client", + "Label for Source client, others settings section", )); for option in [ @@ -447,9 +476,12 @@ impl<'a> SettingsView<'a> { ShowSourceClientOption::Top, ShowSourceClientOption::Bottom, ] { + let mut current: ShowSourceClientOption = + self.settings.show_source_client.clone().into(); + if ui .selectable_value( - self.show_note_client, + &mut current, option, RichText::new(option.label(self.i18n)) .text_style(NotedeckTextStyle::Small.text_style()), @@ -491,27 +523,29 @@ impl<'a> SettingsView<'a> { Frame::default() .inner_margin(Margin::symmetric(10, 10)) .show(ui, |ui| { - if let Some(new_action) = self.appearance_section(ui) { - action = Some(new_action); - } + ScrollArea::vertical().show(ui, |ui| { + if let Some(new_action) = self.appearance_section(ui) { + action = Some(new_action); + } - ui.add_space(5.0); + ui.add_space(5.0); - if let Some(new_action) = self.storage_section(ui) { - action = Some(new_action); - } + if let Some(new_action) = self.storage_section(ui) { + action = Some(new_action); + } - ui.add_space(5.0); + ui.add_space(5.0); - if let Some(new_action) = self.other_options_section(ui) { - action = Some(new_action); - } + if let Some(new_action) = self.other_options_section(ui) { + action = Some(new_action); + } - ui.add_space(10.0); + ui.add_space(10.0); - if let Some(new_action) = self.manage_relays_section(ui) { - action = Some(new_action); - } + if let Some(new_action) = self.manage_relays_section(ui) { + action = Some(new_action); + } + }); }); action diff --git a/crates/notedeck_columns/src/ui/thread.rs b/crates/notedeck_columns/src/ui/thread.rs @@ -115,7 +115,10 @@ impl<'a, 'd> ThreadView<'a, 'd> { .unwrap() .list; - let notes = note_builder.into_notes(&mut self.threads.seen_flags); + let notes = note_builder.into_notes( + self.note_options.contains(NoteOptions::RepliesNewestFirst), + &mut self.threads.seen_flags, + ); if !full_chain { // TODO(kernelkind): insert UI denoting we don't have the full chain yet @@ -223,7 +226,11 @@ impl<'a> ThreadNoteBuilder<'a> { self.replies.push(note); } - pub fn into_notes(mut self, seen_flags: &mut NoteSeenFlags) -> ThreadNotes<'a> { + pub fn into_notes( + mut self, + replies_newer_first: bool, + seen_flags: &mut NoteSeenFlags, + ) -> ThreadNotes<'a> { let mut notes = Vec::new(); let selected_is_root = self.chain.is_empty(); @@ -246,6 +253,11 @@ impl<'a> ThreadNoteBuilder<'a> { unread_and_have_replies: false, }); + if replies_newer_first { + self.replies + .sort_by(|a, b| b.created_at().cmp(&a.created_at())); + } + for reply in self.replies { notes.push(ThreadNote { unread_and_have_replies: *seen_flags.get(reply.id()).unwrap_or(&false), diff --git a/crates/notedeck_ui/src/note/options.rs b/crates/notedeck_ui/src/note/options.rs @@ -25,6 +25,8 @@ bitflags! { /// Show note's client in the note header const ShowNoteClientTop = 1 << 12; const ShowNoteClientBottom = 1 << 13; + + const RepliesNewestFirst = 1 << 14; } }