commit f2153f53dcf9b0fee85ff61c6bea7b9b92478aaa
parent 40764d736809438d571b5cc77442f1f069ce92ca
Author: Fernando LoĢpez Guevara <fernando.lguevara@gmail.com>
Date: Tue, 29 Jul 2025 21:30:35 -0300
feat(settings): allow sorting thread replies newest first
Diffstat:
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;
}
}