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;
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;

View File

@@ -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()
}

View File

@@ -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,

View File

@@ -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,24 +586,9 @@ 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,
)
SettingsView::new(ctx.i18n, ctx.img_cache, &mut settings)
.ui(ui)
.map(RenderNavAction::SettingsAction)
}

View File

@@ -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,6 +523,7 @@ impl<'a> SettingsView<'a> {
Frame::default()
.inner_margin(Margin::symmetric(10, 10))
.show(ui, |ui| {
ScrollArea::vertical().show(ui, |ui| {
if let Some(new_action) = self.appearance_section(ui) {
action = Some(new_action);
}
@@ -513,6 +546,7 @@ impl<'a> SettingsView<'a> {
action = Some(new_action);
}
});
});
action
}

View File

@@ -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),

View File

@@ -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;
}
}