Merge remote-tracking branch 'fernando/feat/persist_settings'

This commit is contained in:
William Casarin
2025-07-31 11:48:57 -07:00
25 changed files with 874 additions and 717 deletions

View File

@@ -26,7 +26,7 @@ pub use preview::{Preview, PreviewApp, PreviewConfig};
pub use profile::ProfileView;
pub use relay::RelayView;
pub use settings::SettingsView;
pub use settings::ShowNoteClientOption;
pub use settings::ShowSourceClientOption;
pub use side_panel::{DesktopSidePanel, SidePanelAction};
pub use thread::ThreadView;
pub use timeline::TimelineView;

View File

@@ -1,40 +1,59 @@
use egui::{vec2, Button, Color32, ComboBox, Frame, Margin, RichText, ThemePreference};
use notedeck::{tr, Images, LanguageIdentifier, Localization, NotedeckTextStyle, SettingsHandler};
use notedeck_ui::NoteOptions;
use egui::{
vec2, Button, Color32, ComboBox, FontId, Frame, Margin, RichText, ScrollArea, ThemePreference,
};
use enostr::NoteId;
use nostrdb::Transaction;
use notedeck::{
tr,
ui::{is_narrow, richtext_small},
Images, JobsCache, LanguageIdentifier, Localization, NoteContext, NotedeckTextStyle, Settings,
SettingsHandler, DEFAULT_NOTE_BODY_FONT_SIZE,
};
use notedeck_ui::{NoteOptions, NoteView};
use strum::Display;
use crate::{nav::RouterAction, Damus, Route};
const PREVIEW_NOTE_ID: &str = "note1edjc8ggj07hwv77g2405uh6j2jkk5aud22gktxrvc2wnre4vdwgqzlv2gw";
const THEME_LIGHT: &str = "Light";
const THEME_DARK: &str = "Dark";
const MIN_ZOOM: f32 = 0.5;
const MAX_ZOOM: f32 = 3.0;
const ZOOM_STEP: f32 = 0.1;
const RESET_ZOOM: f32 = 1.0;
#[derive(Clone, Copy, PartialEq, Eq, Display)]
pub enum ShowNoteClientOption {
pub enum ShowSourceClientOption {
Hide,
Top,
Bottom,
}
impl From<ShowNoteClientOption> for String {
fn from(value: ShowNoteClientOption) -> Self {
match value {
ShowNoteClientOption::Hide => "hide".to_string(),
ShowNoteClientOption::Top => "top".to_string(),
ShowNoteClientOption::Bottom => "bottom".to_string(),
impl Into<String> for ShowSourceClientOption {
fn into(self) -> String {
match self {
Self::Hide => "hide".to_string(),
Self::Top => "top".to_string(),
Self::Bottom => "bottom".to_string(),
}
}
}
impl From<NoteOptions> for ShowNoteClientOption {
impl From<NoteOptions> for ShowSourceClientOption {
fn from(note_options: NoteOptions) -> Self {
if note_options.contains(NoteOptions::ShowNoteClientTop) {
ShowNoteClientOption::Top
ShowSourceClientOption::Top
} else if note_options.contains(NoteOptions::ShowNoteClientBottom) {
ShowNoteClientOption::Bottom
ShowSourceClientOption::Bottom
} else {
ShowNoteClientOption::Hide
ShowSourceClientOption::Hide
}
}
}
impl From<String> for ShowNoteClientOption {
impl From<String> for ShowSourceClientOption {
fn from(s: String) -> Self {
match s.to_lowercase().as_str() {
"hide" => Self::Hide,
@@ -45,7 +64,7 @@ impl From<String> for ShowNoteClientOption {
}
}
impl ShowNoteClientOption {
impl ShowSourceClientOption {
pub fn set_note_options(self, note_options: &mut NoteOptions) {
match self {
Self::Hide => {
@@ -62,13 +81,23 @@ impl ShowNoteClientOption {
}
}
}
fn label<'a>(&self, i18n: &'a mut Localization) -> String {
match self {
Self::Hide => tr!(i18n, "Hide", "Option in settings section to hide the source client label in note display"),
Self::Top => tr!(i18n, "Top", "Option in settings section to show the source client label at the top of the note"),
Self::Bottom => tr!(i18n, "Bottom", "Option in settings section to show the source client label at the bottom of the note"),
}
}
}
pub enum SettingsAction {
SetZoomFactor(f32),
SetTheme(ThemePreference),
SetShowSourceClient(ShowNoteClientOption),
SetShowSourceClient(ShowSourceClientOption),
SetLocale(LanguageIdentifier),
SetRepliestNewestFirst(bool),
SetNoteBodyFontSize(f32),
OpenRelays,
OpenCacheFolder,
ClearCacheFolder,
@@ -78,7 +107,7 @@ impl SettingsAction {
pub fn process_settings_action<'a>(
self,
app: &mut Damus,
settings_handler: &'a mut SettingsHandler,
settings: &'a mut SettingsHandler,
i18n: &'a mut Localization,
img_cache: &mut Images,
ctx: &egui::Context,
@@ -86,425 +115,513 @@ impl SettingsAction {
let mut route_action: Option<RouterAction> = None;
match self {
SettingsAction::OpenRelays => {
Self::OpenRelays => {
route_action = Some(RouterAction::route_to(Route::Relays));
}
SettingsAction::SetZoomFactor(zoom_factor) => {
Self::SetZoomFactor(zoom_factor) => {
ctx.set_zoom_factor(zoom_factor);
settings_handler.set_zoom_factor(zoom_factor);
settings.set_zoom_factor(zoom_factor);
}
SettingsAction::SetShowSourceClient(option) => {
Self::SetShowSourceClient(option) => {
option.set_note_options(&mut app.note_options);
settings_handler.set_show_source_client(option);
settings.set_show_source_client(option);
}
SettingsAction::SetTheme(theme) => {
ctx.options_mut(|o| {
o.theme_preference = theme;
});
settings_handler.set_theme(theme);
Self::SetTheme(theme) => {
ctx.set_theme(theme);
settings.set_theme(theme);
}
SettingsAction::SetLocale(language) => {
Self::SetLocale(language) => {
if i18n.set_locale(language.clone()).is_ok() {
settings_handler.set_locale(language.to_string());
settings.set_locale(language.to_string());
}
}
SettingsAction::OpenCacheFolder => {
Self::SetRepliestNewestFirst(value) => {
app.note_options.set(NoteOptions::RepliesNewestFirst, value);
settings.set_show_replies_newest_first(value);
}
Self::OpenCacheFolder => {
use opener;
let _ = opener::open(img_cache.base_path.clone());
}
SettingsAction::ClearCacheFolder => {
Self::ClearCacheFolder => {
let _ = img_cache.clear_folder_contents();
}
Self::SetNoteBodyFontSize(size) => {
let mut style = (*ctx.style()).clone();
style.text_styles.insert(
NotedeckTextStyle::NoteBody.text_style(),
FontId::proportional(size),
);
ctx.set_style(style);
settings.set_note_body_font_size(size);
}
}
settings_handler.save();
route_action
}
}
pub struct SettingsView<'a> {
theme: &'a mut String,
selected_language: &'a mut String,
show_note_client: &'a mut ShowNoteClientOption,
i18n: &'a mut Localization,
img_cache: &'a mut Images,
settings: &'a mut Settings,
note_context: &'a mut NoteContext<'a>,
note_options: &'a mut NoteOptions,
jobs: &'a mut JobsCache,
}
fn settings_group<S>(ui: &mut egui::Ui, title: S, contents: impl FnOnce(&mut egui::Ui))
where
S: Into<String>,
{
Frame::group(ui.style())
.fill(ui.style().visuals.widgets.open.bg_fill)
.inner_margin(10.0)
.show(ui, |ui| {
ui.label(RichText::new(title).text_style(NotedeckTextStyle::Body.text_style()));
ui.separator();
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
contents(ui)
});
});
}
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 ShowNoteClientOption,
i18n: &'a mut Localization,
settings: &'a mut Settings,
note_context: &'a mut NoteContext<'a>,
note_options: &'a mut NoteOptions,
jobs: &'a mut JobsCache,
) -> Self {
Self {
show_note_client,
theme,
img_cache,
selected_language,
i18n,
settings,
note_context,
note_options,
jobs,
}
}
/// 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>() {
self.i18n
if let Ok(lang_id) = self.settings.locale.parse::<LanguageIdentifier>() {
self.note_context
.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()
}
}
/// Get the localized label for ShowNoteClientOption
fn get_show_note_client_label(&mut self, option: ShowNoteClientOption) -> String {
match option {
ShowNoteClientOption::Hide => tr!(
self.i18n,
"Hide",
"Option in settings section to hide the source client label in note display"
),
ShowNoteClientOption::Top => tr!(
self.i18n,
"Top",
"Option in settings section to show the source client label at the top of the note"
),
ShowNoteClientOption::Bottom => tr!(
self.i18n,
"Bottom",
"Option in settings section to show the source client label at the bottom of the note"
),
}.to_string()
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let id = ui.id();
pub fn appearance_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let mut action = None;
Frame::default()
.inner_margin(Margin::symmetric(10, 10))
.show(ui, |ui| {
Frame::group(ui.style())
.fill(ui.style().visuals.widgets.open.bg_fill)
.inner_margin(10.0)
.show(ui, |ui| {
ui.vertical(|ui| {
ui.label(
RichText::new(tr!(
self.i18n,
"Appearance",
"Label for appearance settings section"
))
.text_style(NotedeckTextStyle::Body.text_style()),
);
ui.separator();
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
let current_zoom = ui.ctx().zoom_factor();
ui.horizontal(|ui| {
ui.label(
RichText::new(tr!(
self.i18n,
"Zoom Level:",
"Label for zoom level, Appearance settings section"
))
.text_style(NotedeckTextStyle::Small.text_style()),
);
if ui
.button(
RichText::new("-")
.text_style(NotedeckTextStyle::Small.text_style()),
)
.clicked()
{
let new_zoom = (current_zoom - 0.1).max(0.1);
action = Some(SettingsAction::SetZoomFactor(new_zoom));
};
ui.label(
RichText::new(format!("{:.0}%", current_zoom * 100.0))
.text_style(NotedeckTextStyle::Small.text_style()),
);
if ui
.button(
RichText::new("+")
.text_style(NotedeckTextStyle::Small.text_style()),
)
.clicked()
{
let new_zoom = (current_zoom + 0.1).min(10.0);
action = Some(SettingsAction::SetZoomFactor(new_zoom));
};
if ui
.button(
RichText::new(tr!(
self.i18n,
"Reset",
"Label for reset zoom level, Appearance settings section"
))
.text_style(NotedeckTextStyle::Small.text_style()),
)
.clicked()
{
action = Some(SettingsAction::SetZoomFactor(1.0));
}
});
ui.horizontal(|ui| {
ui.label(
RichText::new(tr!(
self.i18n,
"Language:",
"Label for language, Appearance settings section"
))
.text_style(NotedeckTextStyle::Small.text_style()),
);
ComboBox::from_label("")
.selected_text(self.get_selected_language_name())
.show_ui(ui, |ui| {
for lang in self.i18n.get_available_locales() {
let name = self.i18n
.get_locale_native_name(lang)
.map(|s| s.to_owned())
.unwrap_or_else(|| lang.to_string());
if ui
.selectable_value(
self.selected_language,
lang.to_string(),
name,
)
.clicked()
{
action = Some(SettingsAction::SetLocale(lang.to_owned()))
}
}
})
});
ui.horizontal(|ui| {
ui.label(
RichText::new(tr!(
self.i18n,
"Theme:",
"Label for theme, Appearance settings section"
))
.text_style(NotedeckTextStyle::Small.text_style()),
);
if ui
.selectable_value(
self.theme,
"Light".into(),
RichText::new(tr!(
self.i18n,
"Light",
"Label for Theme Light, Appearance settings section"
))
.text_style(NotedeckTextStyle::Small.text_style()),
)
.clicked()
{
action = Some(SettingsAction::SetTheme(ThemePreference::Light));
}
if ui
.selectable_value(
self.theme,
"Dark".into(),
RichText::new(tr!(
self.i18n,
"Dark",
"Label for Theme Dark, Appearance settings section"
))
.text_style(NotedeckTextStyle::Small.text_style()),
)
.clicked()
{
action = Some(SettingsAction::SetTheme(ThemePreference::Dark));
}
});
});
});
ui.add_space(5.0);
Frame::group(ui.style())
.fill(ui.style().visuals.widgets.open.bg_fill)
.inner_margin(10.0)
.show(ui, |ui| {
ui.label(
RichText::new(tr!(
self.i18n,
"Storage",
"Label for storage settings section"
))
.text_style(NotedeckTextStyle::Body.text_style()),
);
ui.separator();
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
ui.horizontal_wrapped(|ui| {
let static_imgs_size = self
.img_cache
.static_imgs
.cache_size
.lock()
.unwrap();
let gifs_size = self.img_cache.gifs.cache_size.lock().unwrap();
ui.label(
RichText::new(format!("{} {}",
tr!(
self.i18n,
"Image cache size:",
"Label for Image cache size, Storage settings section"
),
format_size(
[static_imgs_size, gifs_size]
.iter()
.fold(0_u64, |acc, cur| acc
+ cur.unwrap_or_default())
)
))
.text_style(NotedeckTextStyle::Small.text_style()),
);
ui.end_row();
if !notedeck::ui::is_compiled_as_mobile() &&
ui.button(RichText::new(tr!(self.i18n, "View folder", "Label for view folder button, Storage settings section"))
.text_style(NotedeckTextStyle::Small.text_style())).clicked() {
action = Some(SettingsAction::OpenCacheFolder);
}
let clearcache_resp = ui.button(
RichText::new(tr!(
self.i18n,
"Clear cache",
"Label for clear cache button, Storage settings section"
))
.text_style(NotedeckTextStyle::Small.text_style())
.color(Color32::LIGHT_RED),
);
let id_clearcache = id.with("clear_cache");
if clearcache_resp.clicked() {
ui.data_mut(|d| d.insert_temp(id_clearcache, true));
}
if ui.data_mut(|d| *d.get_temp_mut_or_default(id_clearcache)) {
let mut confirm_pressed = false;
clearcache_resp.show_tooltip_ui(|ui| {
let confirm_resp = ui.button(tr!(
self.i18n,
"Confirm",
"Label for confirm clear cache, Storage settings section"
));
if confirm_resp.clicked() {
confirm_pressed = true;
}
if confirm_resp.clicked() || ui.button(tr!(
self.i18n,
"Cancel",
"Label for cancel clear cache, Storage settings section"
)).clicked() {
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
}
});
if confirm_pressed {
action = Some(SettingsAction::ClearCacheFolder);
} else if !confirm_pressed
&& clearcache_resp.clicked_elsewhere()
{
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
}
};
});
});
});
ui.add_space(5.0);
Frame::group(ui.style())
.fill(ui.style().visuals.widgets.open.bg_fill)
.inner_margin(10.0)
.show(ui, |ui| {
ui.label(
RichText::new(tr!(
self.i18n,
"Others",
"Label for others settings section"
))
.text_style(NotedeckTextStyle::Body.text_style()),
);
ui.separator();
ui.vertical(|ui| {
ui.spacing_mut().item_spacing = vec2(10.0, 10.0);
ui.horizontal_wrapped(|ui| {
ui.label(
RichText::new(
tr!(
self.i18n,
"Show source client",
"Label for Show source client, others settings section"
))
.text_style(NotedeckTextStyle::Small.text_style()),
);
for option in [
ShowNoteClientOption::Hide,
ShowNoteClientOption::Top,
ShowNoteClientOption::Bottom,
] {
let label = self.get_show_note_client_label(option);
if ui
.selectable_value(
self.show_note_client,
option,
RichText::new(label)
.text_style(NotedeckTextStyle::Small.text_style()),
)
.changed()
{
action = Some(SettingsAction::SetShowSourceClient(option));
}
}
});
});
});
ui.add_space(10.0);
let title = tr!(
self.note_context.i18n,
"Appearance",
"Label for appearance settings section",
);
settings_group(ui, title, |ui| {
ui.horizontal(|ui| {
ui.label(richtext_small(tr!(
self.note_context.i18n,
"Font size:",
"Label for font size, Appearance settings section",
)));
if ui
.add_sized(
[ui.available_width(), 30.0],
.add(
egui::Slider::new(&mut self.settings.note_body_font_size, 8.0..=32.0)
.text(""),
)
.changed()
{
action = Some(SettingsAction::SetNoteBodyFontSize(
self.settings.note_body_font_size,
));
};
if ui
.button(richtext_small(tr!(
self.note_context.i18n,
"Reset",
"Label for reset note body font size, Appearance settings section",
)))
.clicked()
{
action = Some(SettingsAction::SetNoteBodyFontSize(
DEFAULT_NOTE_BODY_FONT_SIZE,
));
}
});
let txn = Transaction::new(self.note_context.ndb).unwrap();
if let Some(note_id) = NoteId::from_bech(PREVIEW_NOTE_ID) {
if let Ok(preview_note) =
self.note_context.ndb.get_note_by_id(&txn, &note_id.bytes())
{
notedeck_ui::padding(8.0, ui, |ui| {
if is_narrow(ui.ctx()) {
ui.set_max_width(ui.available_width());
}
NoteView::new(
self.note_context,
&preview_note,
self.note_options.clone(),
self.jobs,
)
.actionbar(false)
.options_button(false)
.show(ui);
});
ui.separator();
}
}
let current_zoom = ui.ctx().zoom_factor();
ui.horizontal(|ui| {
ui.label(richtext_small(tr!(
self.note_context.i18n,
"Zoom Level:",
"Label for zoom level, Appearance settings section",
)));
let min_reached = current_zoom <= MIN_ZOOM;
let max_reached = current_zoom >= MAX_ZOOM;
if ui
.add_enabled(
!min_reached,
Button::new(
RichText::new(tr!(
self.i18n,
"Configure relays",
"Label for configure relays, settings section"
))
.text_style(NotedeckTextStyle::Small.text_style()),
RichText::new("-").text_style(NotedeckTextStyle::Small.text_style()),
),
)
.clicked()
{
action = Some(SettingsAction::OpenRelays);
let new_zoom = (current_zoom - ZOOM_STEP).max(MIN_ZOOM);
action = Some(SettingsAction::SetZoomFactor(new_zoom));
};
ui.label(
RichText::new(format!("{:.0}%", current_zoom * 100.0))
.text_style(NotedeckTextStyle::Small.text_style()),
);
if ui
.add_enabled(
!max_reached,
Button::new(
RichText::new("+").text_style(NotedeckTextStyle::Small.text_style()),
),
)
.clicked()
{
let new_zoom = (current_zoom + ZOOM_STEP).min(MAX_ZOOM);
action = Some(SettingsAction::SetZoomFactor(new_zoom));
};
if ui
.button(richtext_small(tr!(
self.note_context.i18n,
"Reset",
"Label for reset zoom level, Appearance settings section",
)))
.clicked()
{
action = Some(SettingsAction::SetZoomFactor(RESET_ZOOM));
}
});
ui.horizontal(|ui| {
ui.label(richtext_small(tr!(
self.note_context.i18n,
"Language:",
"Label for language, Appearance settings section",
)));
//
ComboBox::from_label("")
.selected_text(self.get_selected_language_name())
.show_ui(ui, |ui| {
for lang in self.note_context.i18n.get_available_locales() {
let name = self
.note_context
.i18n
.get_locale_native_name(lang)
.map(|s| s.to_owned())
.unwrap_or_else(|| lang.to_string());
if ui
.selectable_value(&mut self.settings.locale, lang.to_string(), name)
.clicked()
{
action = Some(SettingsAction::SetLocale(lang.to_owned()))
}
}
});
});
ui.horizontal(|ui| {
ui.label(richtext_small(tr!(
self.note_context.i18n,
"Theme:",
"Label for theme, Appearance settings section",
)));
if ui
.selectable_value(
&mut self.settings.theme,
ThemePreference::Light,
richtext_small(tr!(
self.note_context.i18n,
THEME_LIGHT,
"Label for Theme Light, Appearance settings section",
)),
)
.clicked()
{
action = Some(SettingsAction::SetTheme(ThemePreference::Light));
}
if ui
.selectable_value(
&mut self.settings.theme,
ThemePreference::Dark,
richtext_small(tr!(
self.note_context.i18n,
THEME_DARK,
"Label for Theme Dark, Appearance settings section",
)),
)
.clicked()
{
action = Some(SettingsAction::SetTheme(ThemePreference::Dark));
}
});
});
action
}
pub fn storage_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let id = ui.id();
let mut action: Option<SettingsAction> = None;
let title = tr!(
self.note_context.i18n,
"Storage",
"Label for storage settings section"
);
settings_group(ui, title, |ui| {
ui.horizontal_wrapped(|ui| {
let static_imgs_size = self
.note_context
.img_cache
.static_imgs
.cache_size
.lock()
.unwrap();
let gifs_size = self.note_context.img_cache.gifs.cache_size.lock().unwrap();
ui.label(
RichText::new(format!(
"{} {}",
tr!(
self.note_context.i18n,
"Image cache size:",
"Label for Image cache size, Storage settings section"
),
format_size(
[static_imgs_size, gifs_size]
.iter()
.fold(0_u64, |acc, cur| acc + cur.unwrap_or_default())
)
))
.text_style(NotedeckTextStyle::Small.text_style()),
);
ui.end_row();
if !notedeck::ui::is_compiled_as_mobile()
&& ui
.button(richtext_small(tr!(
self.note_context.i18n,
"View folder",
"Label for view folder button, Storage settings section",
)))
.clicked()
{
action = Some(SettingsAction::OpenCacheFolder);
}
let clearcache_resp = ui.button(
richtext_small(tr!(
self.note_context.i18n,
"Clear cache",
"Label for clear cache button, Storage settings section",
))
.color(Color32::LIGHT_RED),
);
let id_clearcache = id.with("clear_cache");
if clearcache_resp.clicked() {
ui.data_mut(|d| d.insert_temp(id_clearcache, true));
}
if ui.data_mut(|d| *d.get_temp_mut_or_default(id_clearcache)) {
let mut confirm_pressed = false;
clearcache_resp.show_tooltip_ui(|ui| {
let confirm_resp = ui.button(tr!(
self.note_context.i18n,
"Confirm",
"Label for confirm clear cache, Storage settings section"
));
if confirm_resp.clicked() {
confirm_pressed = true;
}
if confirm_resp.clicked()
|| ui
.button(tr!(
self.note_context.i18n,
"Cancel",
"Label for cancel clear cache, Storage settings section"
))
.clicked()
{
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
}
});
if confirm_pressed {
action = Some(SettingsAction::ClearCacheFolder);
} else if !confirm_pressed && clearcache_resp.clicked_elsewhere() {
ui.data_mut(|d| d.insert_temp(id_clearcache, false));
}
};
});
});
action
}
fn other_options_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let mut action = None;
let title = tr!(
self.note_context.i18n,
"Others",
"Label for others settings section"
);
settings_group(ui, title, |ui| {
ui.horizontal(|ui| {
ui.label(richtext_small(tr!(
self.note_context.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.note_context.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(richtext_small(tr!(
self.note_context.i18n,
"Source client",
"Label for Source client, others settings section",
)));
for option in [
ShowSourceClientOption::Hide,
ShowSourceClientOption::Top,
ShowSourceClientOption::Bottom,
] {
let mut current: ShowSourceClientOption =
self.settings.show_source_client.clone().into();
if ui
.selectable_value(
&mut current,
option,
RichText::new(option.label(self.note_context.i18n))
.text_style(NotedeckTextStyle::Small.text_style()),
)
.changed()
{
action = Some(SettingsAction::SetShowSourceClient(option));
}
}
});
});
action
}
fn manage_relays_section(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let mut action = None;
if ui
.add_sized(
[ui.available_width(), 30.0],
Button::new(richtext_small(tr!(
self.note_context.i18n,
"Configure relays",
"Label for configure relays, settings section",
))),
)
.clicked()
{
action = Some(SettingsAction::OpenRelays);
}
action
}
pub fn ui(&mut self, ui: &mut egui::Ui) -> Option<SettingsAction> {
let mut action: Option<SettingsAction> = None;
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);
}
ui.add_space(5.0);
if let Some(new_action) = self.storage_section(ui) {
action = Some(new_action);
}
ui.add_space(5.0);
if let Some(new_action) = self.other_options_section(ui) {
action = Some(new_action);
}
ui.add_space(10.0);
if let Some(new_action) = self.manage_relays_section(ui) {
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),