feat(settings): allow sorting thread replies newest first

This commit is contained in:
Fernando López Guevara
2025-07-29 21:30:35 -03:00
parent 40764d7368
commit f2153f53dc
7 changed files with 154 additions and 97 deletions

View File

@@ -5,6 +5,7 @@ mod token_handler;
mod zoom; mod zoom;
pub use app_size::AppSizeHandler; pub use app_size::AppSizeHandler;
pub use settings_handler::Settings;
pub use settings_handler::SettingsHandler; pub use settings_handler::SettingsHandler;
pub use theme_handler::ThemeHandler; pub use theme_handler::ThemeHandler;
pub use token_handler::TokenHandler; pub use token_handler::TokenHandler;

View File

@@ -29,6 +29,7 @@ pub struct Settings {
pub locale: String, pub locale: String,
pub zoom_factor: f32, pub zoom_factor: f32,
pub show_source_client: String, pub show_source_client: String,
pub show_replies_newest_first: bool,
} }
impl Default for Settings { impl Default for Settings {
@@ -38,10 +39,12 @@ impl Default for Settings {
theme: DEFAULT_THEME, theme: DEFAULT_THEME,
locale: DEFAULT_LOCALE.to_string(), locale: DEFAULT_LOCALE.to_string(),
zoom_factor: DEFAULT_ZOOM_FACTOR, 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 { pub struct SettingsHandler {
directory: Directory, directory: Directory,
current_settings: Option<Settings>, 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() { if self.current_settings.is_none() {
self.current_settings = Some(Settings::default()); self.current_settings = Some(Settings::default());
} }
@@ -162,6 +165,11 @@ impl SettingsHandler {
self.save(); 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) pub fn update_batch<F>(&mut self, update_fn: F)
where where
F: FnOnce(&mut Settings), F: FnOnce(&mut Settings),
@@ -204,6 +212,13 @@ impl SettingsHandler {
.unwrap_or(DEFAULT_SHOW_SOURCE_CLIENT.to_string()) .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 { pub fn is_loaded(&self) -> bool {
self.current_settings.is_some() self.current_settings.is_some()
} }

View File

@@ -14,18 +14,13 @@ use crate::{
view_state::ViewState, view_state::ViewState,
Result, Result,
}; };
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool}; use enostr::{ClientMessage, PoolRelay, Pubkey, RelayEvent, RelayMessage, RelayPool};
use nostrdb::Transaction; use nostrdb::Transaction;
use notedeck::{ use notedeck::{
tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, tr, ui::is_narrow, Accounts, AppAction, AppContext, DataPath, DataPathType, FilterState, Images, JobsCache, Localization, SettingsHandler, UnknownIds
Images, JobsCache, Localization, UnknownIds,
};
use notedeck_ui::{
media::{MediaViewer, MediaViewerFlags, MediaViewerState},
NoteOptions,
}; };
use notedeck_ui::{media::{MediaViewer, MediaViewerFlags, MediaViewerState}, NoteOptions};
use std::collections::{BTreeSet, HashMap}; use std::collections::{BTreeSet, HashMap};
use std::path::Path; use std::path::Path;
use std::time::Duration; use std::time::Duration;
@@ -443,6 +438,11 @@ impl Damus {
let mut options = AppOptions::default(); let mut options = AppOptions::default();
let tmp_columns = !parsed_args.columns.is_empty(); let tmp_columns = !parsed_args.columns.is_empty();
options.set(AppOptions::TmpColumns, tmp_columns); 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 { let decks_cache = if tmp_columns {
info!("DecksCache: loading from command line arguments"); info!("DecksCache: loading from command line arguments");
@@ -487,37 +487,11 @@ impl Damus {
// cache.add_deck_default(*pk); // cache.add_deck_default(*pk);
//} //}
}; };
let settings_handler = &app_context.settings_handler;
let support = Support::new(app_context.path); let support = Support::new(app_context.path);
let mut note_options = NoteOptions::default();
note_options.set( let note_options = get_note_options(parsed_args, settings_handler);
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 jobs = JobsCache::default(); 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) { fn circle_icon(ui: &mut egui::Ui, openness: f32, response: &egui::Response) {
let stroke = ui.style().interact(&response).fg_stroke; let stroke = ui.style().interact(&response).fg_stroke;
@@ -620,6 +627,7 @@ fn render_damus_mobile(
let mut app_action: Option<AppAction> = None; let mut app_action: Option<AppAction> = None;
let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize; let active_col = app.columns_mut(app_ctx.i18n, app_ctx.accounts).selected as usize;
if !app.columns(app_ctx.accounts).columns().is_empty() { if !app.columns(app_ctx.accounts).columns().is_empty() {
let r = nav::render_nav( let r = nav::render_nav(
active_col, active_col,

View File

@@ -21,7 +21,7 @@ use crate::{
note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView}, note::{custom_zap::CustomZapView, NewPostAction, PostAction, PostType, QuoteRepostView},
profile::EditProfileView, profile::EditProfileView,
search::{FocusState, SearchView}, search::{FocusState, SearchView},
settings::{SettingsAction, ShowSourceClientOption}, settings::SettingsAction,
support::SupportView, support::SupportView,
wallet::{get_default_zap_state, WalletAction, WalletState, WalletView}, wallet::{get_default_zap_state, WalletAction, WalletState, WalletView},
AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView, AccountsView, PostReplyView, PostView, ProfileView, RelayView, SettingsView, ThreadView,
@@ -586,24 +586,9 @@ fn render_nav_body(
.map(RenderNavAction::RelayAction), .map(RenderNavAction::RelayAction),
Route::Settings => { 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 { SettingsView::new(ctx.i18n, ctx.img_cache, &mut settings)
"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) .ui(ui)
.map(RenderNavAction::SettingsAction) .map(RenderNavAction::SettingsAction)
} }

View File

@@ -1,5 +1,7 @@
use egui::{vec2, Button, Color32, ComboBox, Frame, Margin, RichText, ThemePreference}; use egui::{vec2, Button, Color32, ComboBox, Frame, Margin, RichText, ScrollArea, ThemePreference};
use notedeck::{tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, SettingsHandler}; use notedeck::{
tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, Settings, SettingsHandler,
};
use notedeck_ui::NoteOptions; use notedeck_ui::NoteOptions;
use strum::Display; use strum::Display;
@@ -97,6 +99,7 @@ pub enum SettingsAction {
SetTheme(ThemePreference), SetTheme(ThemePreference),
SetShowSourceClient(ShowSourceClientOption), SetShowSourceClient(ShowSourceClientOption),
SetLocale(LanguageIdentifier), SetLocale(LanguageIdentifier),
SetRepliestNewestFirst(bool),
OpenRelays, OpenRelays,
OpenCacheFolder, OpenCacheFolder,
ClearCacheFolder, ClearCacheFolder,
@@ -135,6 +138,11 @@ impl SettingsAction {
settings_handler.set_locale(language.to_string()); 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 => { Self::OpenCacheFolder => {
use opener; use opener;
let _ = opener::open(img_cache.base_path.clone()); let _ = opener::open(img_cache.base_path.clone());
@@ -149,9 +157,7 @@ impl SettingsAction {
} }
pub struct SettingsView<'a> { pub struct SettingsView<'a> {
theme: &'a mut String, settings: &'a mut Settings,
selected_language: &'a mut String,
show_note_client: &'a mut ShowSourceClientOption,
i18n: &'a mut Localization, i18n: &'a mut Localization,
img_cache: &'a mut Images, img_cache: &'a mut Images,
} }
@@ -181,30 +187,30 @@ where
impl<'a> SettingsView<'a> { impl<'a> SettingsView<'a> {
pub fn new( 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, 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 {
Self { Self {
show_note_client, settings,
theme,
img_cache, img_cache,
selected_language,
i18n, i18n,
} }
} }
/// Get the localized name for a language identifier /// Get the localized name for a language identifier
fn get_selected_language_name(&mut self) -> String { 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 self.i18n
.get_locale_native_name(&lang_id) .get_locale_native_name(&lang_id)
.map(|s| s.to_owned()) .map(|s| s.to_owned())
.unwrap_or_else(|| lang_id.to_string()) .unwrap_or_else(|| lang_id.to_string())
} else { } else {
self.selected_language.clone() self.settings.locale.clone()
} }
} }
@@ -289,7 +295,7 @@ impl<'a> SettingsView<'a> {
.map(|s| s.to_owned()) .map(|s| s.to_owned())
.unwrap_or_else(|| lang.to_string()); .unwrap_or_else(|| lang.to_string());
if ui if ui
.selectable_value(self.selected_language, lang.to_string(), name) .selectable_value(&mut self.settings.locale, lang.to_string(), name)
.clicked() .clicked()
{ {
action = Some(SettingsAction::SetLocale(lang.to_owned())) action = Some(SettingsAction::SetLocale(lang.to_owned()))
@@ -304,10 +310,11 @@ impl<'a> SettingsView<'a> {
"Theme:", "Theme:",
"Label for theme, Appearance settings section", "Label for theme, Appearance settings section",
)); ));
if ui if ui
.selectable_value( .selectable_value(
self.theme, &mut self.settings.theme,
THEME_LIGHT.into(), ThemePreference::Light,
small_richtext( small_richtext(
self.i18n, self.i18n,
THEME_LIGHT.into(), THEME_LIGHT.into(),
@@ -318,10 +325,11 @@ impl<'a> SettingsView<'a> {
{ {
action = Some(SettingsAction::SetTheme(ThemePreference::Light)); action = Some(SettingsAction::SetTheme(ThemePreference::Light));
} }
if ui if ui
.selectable_value( .selectable_value(
self.theme, &mut self.settings.theme,
THEME_DARK.into(), ThemePreference::Dark,
small_richtext( small_richtext(
self.i18n, self.i18n,
THEME_DARK.into(), THEME_DARK.into(),
@@ -435,11 +443,32 @@ impl<'a> SettingsView<'a> {
let title = tr!(self.i18n, "Others", "Label for others settings section"); let title = tr!(self.i18n, "Others", "Label for others settings section");
settings_group(ui, title, |ui| { 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.horizontal_wrapped(|ui| {
ui.label(small_richtext( ui.label(small_richtext(
self.i18n, self.i18n,
"Show source client", "Source client",
"Label for Show source client, others settings section", "Label for Source client, others settings section",
)); ));
for option in [ for option in [
@@ -447,9 +476,12 @@ impl<'a> SettingsView<'a> {
ShowSourceClientOption::Top, ShowSourceClientOption::Top,
ShowSourceClientOption::Bottom, ShowSourceClientOption::Bottom,
] { ] {
let mut current: ShowSourceClientOption =
self.settings.show_source_client.clone().into();
if ui if ui
.selectable_value( .selectable_value(
self.show_note_client, &mut current,
option, option,
RichText::new(option.label(self.i18n)) RichText::new(option.label(self.i18n))
.text_style(NotedeckTextStyle::Small.text_style()), .text_style(NotedeckTextStyle::Small.text_style()),
@@ -491,6 +523,7 @@ impl<'a> SettingsView<'a> {
Frame::default() Frame::default()
.inner_margin(Margin::symmetric(10, 10)) .inner_margin(Margin::symmetric(10, 10))
.show(ui, |ui| { .show(ui, |ui| {
ScrollArea::vertical().show(ui, |ui| {
if let Some(new_action) = self.appearance_section(ui) { if let Some(new_action) = self.appearance_section(ui) {
action = Some(new_action); action = Some(new_action);
} }
@@ -513,6 +546,7 @@ impl<'a> SettingsView<'a> {
action = Some(new_action); action = Some(new_action);
} }
}); });
});
action action
} }

View File

@@ -115,7 +115,10 @@ impl<'a, 'd> ThreadView<'a, 'd> {
.unwrap() .unwrap()
.list; .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 { if !full_chain {
// TODO(kernelkind): insert UI denoting we don't have the full chain yet // 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); 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 mut notes = Vec::new();
let selected_is_root = self.chain.is_empty(); let selected_is_root = self.chain.is_empty();
@@ -246,6 +253,11 @@ impl<'a> ThreadNoteBuilder<'a> {
unread_and_have_replies: false, 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 { for reply in self.replies {
notes.push(ThreadNote { notes.push(ThreadNote {
unread_and_have_replies: *seen_flags.get(reply.id()).unwrap_or(&false), unread_and_have_replies: *seen_flags.get(reply.id()).unwrap_or(&false),

View File

@@ -25,6 +25,8 @@ bitflags! {
/// Show note's client in the note header /// Show note's client in the note header
const ShowNoteClientTop = 1 << 12; const ShowNoteClientTop = 1 << 12;
const ShowNoteClientBottom = 1 << 13; const ShowNoteClientBottom = 1 << 13;
const RepliesNewestFirst = 1 << 14;
} }
} }