notedeck

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

commit bd352f76d40ed386853c0f7a4b25519d31462a70
parent 32c7f83bd7a807155de6a13b66a7d43f48c25e13
Author: William Casarin <jb55@jb55.com>
Date:   Sat, 22 Feb 2025 14:25:46 -0800

feat: add scramble flag for development text scrambling

This commit introduces a new scramble option to help reduce distractions
during development by scrambling text using rot13. When enabled via the
new `--scramble` flag, text displayed in various views is transformed,
making it easier to focus on layout and behavior without reading the
actual content.

App & Args Updates

  - Added a `scramble: bool` field to the main application state (in `app.rs`).

  - Extended argument parsing (in `args.rs`) to recognize the `--scramble` flag.

NoteOptions Enhancement

  - Introduced a new bit flag `scramble_text` in `NoteOptions` with
    corresponding setter/getter methods.

UI Adjustments

  - Propagated the scramble flag through note rendering functions across
    navigation, timeline, and note view modules.

  - Updated several UI components (e.g., in `nav.rs`, `route.rs`, and
    `contents.rs`) to accept and apply the new note options.

Rot13 Implementation

  - Implemented a helper function (`rot13`) to scramble text
    conditionally when the scramble option is enabled.

This feature is intended for development builds only, offering a way to
obscure text content during UI tweaks and testing.

Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mcrates/notedeck_columns/src/app.rs | 5+++++
Mcrates/notedeck_columns/src/args.rs | 4++++
Mcrates/notedeck_columns/src/nav.rs | 14++++++++++++--
Mcrates/notedeck_columns/src/timeline/route.rs | 16++++++++--------
Mcrates/notedeck_columns/src/ui/note/contents.rs | 30+++++++++++++++++++++++++++---
Mcrates/notedeck_columns/src/ui/note/mod.rs | 21++++++++++++++-------
Mcrates/notedeck_columns/src/ui/note/options.rs | 4++++
Mcrates/notedeck_columns/src/ui/note/post.rs | 8+++++++-
Mcrates/notedeck_columns/src/ui/note/quote_repost.rs | 10+++++++++-
Mcrates/notedeck_columns/src/ui/note/reply.rs | 23+++++++++++++++++------
Mcrates/notedeck_columns/src/ui/note/reply_description.rs | 8++++++--
Mcrates/notedeck_columns/src/ui/thread.rs | 13++++---------
Mcrates/notedeck_columns/src/ui/timeline.rs | 12++++++++----
13 files changed, 125 insertions(+), 43 deletions(-)

diff --git a/crates/notedeck_columns/src/app.rs b/crates/notedeck_columns/src/app.rs @@ -52,6 +52,9 @@ pub struct Damus { pub since_optimize: bool, pub textmode: bool, + /// Scramble text for development + pub scramble: bool, + pub unrecognized_args: BTreeSet<String>, } @@ -430,6 +433,7 @@ impl Damus { drafts: Drafts::default(), state: DamusState::Initializing, textmode: parsed_args.textmode, + scramble: parsed_args.scramble, //frame_history: FrameHistory::default(), view_state: ViewState::default(), tmp_columns, @@ -474,6 +478,7 @@ impl Damus { drafts: Drafts::default(), state: DamusState::Initializing, textmode: false, + scramble: false, tmp_columns: true, //frame_history: FrameHistory::default(), view_state: ViewState::default(), diff --git a/crates/notedeck_columns/src/args.rs b/crates/notedeck_columns/src/args.rs @@ -8,6 +8,7 @@ pub struct ColumnsArgs { pub columns: Vec<ArgColumn>, pub since_optimize: bool, pub textmode: bool, + pub scramble: bool, } impl ColumnsArgs { @@ -17,6 +18,7 @@ impl ColumnsArgs { columns: vec![], since_optimize: true, textmode: false, + scramble: false, }; let mut i = 0; @@ -28,6 +30,8 @@ impl ColumnsArgs { res.textmode = true; } else if arg == "--no-since-optimize" { res.since_optimize = false; + } else if arg == "--scramble" { + res.scramble = true; } else if arg == "--filter" { i += 1; let filter = if let Some(next_arg) = args.get(i) { diff --git a/crates/notedeck_columns/src/nav.rs b/crates/notedeck_columns/src/nav.rs @@ -16,7 +16,7 @@ use crate::{ column::NavTitle, configure_deck::ConfigureDeckView, edit_deck::{EditDeckResponse, EditDeckView}, - note::{PostAction, PostType}, + note::{NoteOptions, PostAction, PostType}, profile::EditProfileView, support::SupportView, RelayView, View, @@ -243,6 +243,13 @@ fn render_nav_body( col: usize, inner_rect: egui::Rect, ) -> Option<RenderNavAction> { + let note_options = { + let mut options = NoteOptions::default(); + options.set_textmode(app.textmode); + options.set_scramble_text(app.scramble); + options + }; + match top { Route::Timeline(kind) => render_timeline_route( ctx.ndb, @@ -253,7 +260,7 @@ fn render_nav_body( ctx.accounts, kind, col, - app.textmode, + note_options, depth, ui, ), @@ -310,6 +317,7 @@ fn render_nav_body( ctx.img_cache, &note, inner_rect, + note_options, ) .id_source(id) .show(ui) @@ -345,6 +353,7 @@ fn render_nav_body( draft, &note, inner_rect, + note_options, ) .id_source(id) .show(ui) @@ -366,6 +375,7 @@ fn render_nav_body( ctx.note_cache, kp, inner_rect, + note_options, ) .ui(&txn, ui); diff --git a/crates/notedeck_columns/src/timeline/route.rs b/crates/notedeck_columns/src/timeline/route.rs @@ -19,15 +19,13 @@ pub fn render_timeline_route( accounts: &mut Accounts, kind: &TimelineKind, col: usize, - textmode: bool, + mut note_options: NoteOptions, depth: usize, ui: &mut egui::Ui, ) -> Option<RenderNavAction> { - let note_options = { - let mut options = NoteOptions::new(kind == &TimelineKind::Universe); - options.set_textmode(textmode); - options - }; + if kind == &TimelineKind::Universe { + note_options.set_hide_media(true); + } match kind { TimelineKind::List(_) @@ -63,6 +61,7 @@ pub fn render_timeline_route( col, ui, &accounts.mutefun(), + note_options, ) } else { // we render profiles like timelines if they are at the root @@ -88,7 +87,7 @@ pub fn render_timeline_route( unknown_ids, img_cache, id.selected_or_root(), - textmode, + note_options, &accounts.mutefun(), ) .id_source(egui::Id::new(("threadscroll", col))) @@ -109,6 +108,7 @@ pub fn render_profile_route( col: usize, ui: &mut egui::Ui, is_muted: &MuteFun, + note_options: NoteOptions, ) -> Option<RenderNavAction> { let action = ProfileView::new( pubkey, @@ -120,7 +120,7 @@ pub fn render_profile_route( img_cache, unknown_ids, is_muted, - NoteOptions::default(), + note_options, ) .ui(ui); diff --git a/crates/notedeck_columns/src/ui/note/contents.rs b/crates/notedeck_columns/src/ui/note/contents.rs @@ -67,6 +67,7 @@ impl egui::Widget for &mut NoteContents<'_> { /// Render an inline note preview with a border. These are used when /// notes are references within a note +#[allow(clippy::too_many_arguments)] pub fn render_note_preview( ui: &mut egui::Ui, ndb: &Ndb, @@ -75,6 +76,7 @@ pub fn render_note_preview( txn: &Transaction, id: &[u8; 32], parent: NoteKey, + note_options: NoteOptions, ) -> NoteResponse { #[cfg(feature = "profiling")] puffin::profile_function!(); @@ -112,7 +114,7 @@ pub fn render_note_preview( ui.visuals().noninteractive().bg_stroke.color, )) .show(ui, |ui| { - ui::NoteView::new(ndb, note_cache, img_cache, &note) + ui::NoteView::new(ndb, note_cache, img_cache, &note, note_options) .actionbar(false) .small_pfp(true) .wide(true) @@ -225,7 +227,11 @@ fn render_note_contents( BlockType::Text => { #[cfg(feature = "profiling")] puffin::profile_scope!("text contents"); - ui.add(egui::Label::new(block.as_str()).selectable(selectable)); + if options.has_scramble_text() { + ui.add(egui::Label::new(rot13(block.as_str())).selectable(selectable)); + } else { + ui.add(egui::Label::new(block.as_str()).selectable(selectable)); + } } _ => { @@ -236,7 +242,7 @@ fn render_note_contents( }); let preview_note_action = if let Some((id, _block_str)) = inline_note { - render_note_preview(ui, ndb, note_cache, img_cache, txn, id, note_key).action + render_note_preview(ui, ndb, note_cache, img_cache, txn, id, note_key, options).action } else { None }; @@ -253,6 +259,24 @@ fn render_note_contents( NoteResponse::new(response.response).with_action(note_action) } +fn rot13(input: &str) -> String { + input + .chars() + .map(|c| { + if c.is_ascii_lowercase() { + // Rotate lowercase letters + (((c as u8 - b'a' + 13) % 26) + b'a') as char + } else if c.is_ascii_uppercase() { + // Rotate uppercase letters + (((c as u8 - b'A' + 13) % 26) + b'A') as char + } else { + // Leave other characters unchanged + c + } + }) + .collect() +} + fn image_carousel( ui: &mut egui::Ui, img_cache: &mut ImageCache, diff --git a/crates/notedeck_columns/src/ui/note/mod.rs b/crates/notedeck_columns/src/ui/note/mod.rs @@ -76,8 +76,11 @@ impl<'a> NoteView<'a> { note_cache: &'a mut NoteCache, img_cache: &'a mut ImageCache, note: &'a nostrdb::Note<'a>, + mut flags: NoteOptions, ) -> Self { - let flags = NoteOptions::actionbar | NoteOptions::note_previews; + flags.set_actionbar(true); + flags.set_note_previews(true); + let parent: Option<NoteKey> = None; Self { ndb, @@ -89,11 +92,6 @@ impl<'a> NoteView<'a> { } } - pub fn note_options(mut self, options: NoteOptions) -> Self { - *self.options_mut() = options; - self - } - pub fn textmode(mut self, enable: bool) -> Self { self.options_mut().set_textmode(enable); self @@ -287,7 +285,14 @@ impl<'a> NoteView<'a> { .text_style(style.text_style()), ); }); - NoteView::new(self.ndb, self.note_cache, self.img_cache, &note_to_repost).show(ui) + NoteView::new( + self.ndb, + self.note_cache, + self.img_cache, + &note_to_repost, + self.flags, + ) + .show(ui) } else { self.show_standard(ui) } @@ -393,6 +398,7 @@ impl<'a> NoteView<'a> { self.ndb, self.img_cache, self.note_cache, + self.flags, ) }) .inner; @@ -464,6 +470,7 @@ impl<'a> NoteView<'a> { self.ndb, self.img_cache, self.note_cache, + self.flags, ); if action.is_some() { diff --git a/crates/notedeck_columns/src/ui/note/options.rs b/crates/notedeck_columns/src/ui/note/options.rs @@ -15,6 +15,9 @@ bitflags! { const textmode = 0b0000000001000000; const options_button = 0b0000000010000000; const hide_media = 0b0000000100000000; + + /// Scramble text so that its not distracting during development + const scramble_text = 0b0000001000000000; } } @@ -52,6 +55,7 @@ impl NoteOptions { create_bit_methods!(set_wide, has_wide, wide); create_bit_methods!(set_options_button, has_options_button, options_button); create_bit_methods!(set_hide_media, has_hide_media, hide_media); + create_bit_methods!(set_scramble_text, has_scramble_text, scramble_text); pub fn new(is_universe_timeline: bool) -> Self { let mut options = NoteOptions::default(); diff --git a/crates/notedeck_columns/src/ui/note/post.rs b/crates/notedeck_columns/src/ui/note/post.rs @@ -4,7 +4,7 @@ use crate::media_upload::{nostrbuild_nip96_upload, MediaPath}; use crate::post::{downcast_post_buffer, MentionType, NewPost}; use crate::profile::get_display_name; use crate::ui::search_results::SearchResultsView; -use crate::ui::{self, Preview, PreviewConfig}; +use crate::ui::{self, note::NoteOptions, Preview, PreviewConfig}; use crate::Result; use egui::text::{CCursorRange, LayoutJob}; use egui::text_edit::TextEditOutput; @@ -27,6 +27,7 @@ pub struct PostView<'a> { poster: FilledKeypair<'a>, id_source: Option<egui::Id>, inner_rect: egui::Rect, + note_options: NoteOptions, } #[derive(Clone)] @@ -82,6 +83,7 @@ pub struct PostResponse { } impl<'a> PostView<'a> { + #[allow(clippy::too_many_arguments)] pub fn new( ndb: &'a Ndb, draft: &'a mut Draft, @@ -90,6 +92,7 @@ impl<'a> PostView<'a> { note_cache: &'a mut NoteCache, poster: FilledKeypair<'a>, inner_rect: egui::Rect, + note_options: NoteOptions, ) -> Self { let id_source: Option<egui::Id> = None; PostView { @@ -101,6 +104,7 @@ impl<'a> PostView<'a> { id_source, post_type, inner_rect, + note_options, } } @@ -302,6 +306,7 @@ impl<'a> PostView<'a> { txn, id.bytes(), nostrdb::NoteKey::new(0), + self.note_options, ); }); }); @@ -686,6 +691,7 @@ mod preview { app.note_cache, self.poster.to_filled(), ui.available_rect_before_wrap(), + NoteOptions::default(), ) .ui(&txn, ui); } diff --git a/crates/notedeck_columns/src/ui/note/quote_repost.rs b/crates/notedeck_columns/src/ui/note/quote_repost.rs @@ -2,7 +2,10 @@ use enostr::{FilledKeypair, NoteId}; use nostrdb::Ndb; use notedeck::{ImageCache, NoteCache}; -use crate::{draft::Draft, ui}; +use crate::{ + draft::Draft, + ui::{self, note::NoteOptions}, +}; use super::{PostResponse, PostType}; @@ -15,9 +18,11 @@ pub struct QuoteRepostView<'a> { quoting_note: &'a nostrdb::Note<'a>, id_source: Option<egui::Id>, inner_rect: egui::Rect, + note_options: NoteOptions, } impl<'a> QuoteRepostView<'a> { + #[allow(clippy::too_many_arguments)] pub fn new( ndb: &'a Ndb, poster: FilledKeypair<'a>, @@ -26,6 +31,7 @@ impl<'a> QuoteRepostView<'a> { draft: &'a mut Draft, quoting_note: &'a nostrdb::Note<'a>, inner_rect: egui::Rect, + note_options: NoteOptions, ) -> Self { let id_source: Option<egui::Id> = None; QuoteRepostView { @@ -37,6 +43,7 @@ impl<'a> QuoteRepostView<'a> { quoting_note, id_source, inner_rect, + note_options, } } @@ -52,6 +59,7 @@ impl<'a> QuoteRepostView<'a> { self.note_cache, self.poster, self.inner_rect, + self.note_options, ) .id_source(id) .ui(self.quoting_note.txn().unwrap(), ui) diff --git a/crates/notedeck_columns/src/ui/note/reply.rs b/crates/notedeck_columns/src/ui/note/reply.rs @@ -1,6 +1,6 @@ use crate::draft::Draft; use crate::ui; -use crate::ui::note::{PostResponse, PostType}; +use crate::ui::note::{NoteOptions, PostResponse, PostType}; use enostr::{FilledKeypair, NoteId}; use nostrdb::Ndb; @@ -15,9 +15,11 @@ pub struct PostReplyView<'a> { note: &'a nostrdb::Note<'a>, id_source: Option<egui::Id>, inner_rect: egui::Rect, + note_options: NoteOptions, } impl<'a> PostReplyView<'a> { + #[allow(clippy::too_many_arguments)] pub fn new( ndb: &'a Ndb, poster: FilledKeypair<'a>, @@ -26,6 +28,7 @@ impl<'a> PostReplyView<'a> { img_cache: &'a mut ImageCache, note: &'a nostrdb::Note<'a>, inner_rect: egui::Rect, + note_options: NoteOptions, ) -> Self { let id_source: Option<egui::Id> = None; PostReplyView { @@ -37,6 +40,7 @@ impl<'a> PostReplyView<'a> { img_cache, id_source, inner_rect, + note_options, } } @@ -67,11 +71,17 @@ impl<'a> PostReplyView<'a> { egui::Frame::none() .outer_margin(egui::Margin::same(note_offset)) .show(ui, |ui| { - ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, self.note) - .actionbar(false) - .medium_pfp(true) - .options_button(true) - .show(ui); + ui::NoteView::new( + self.ndb, + self.note_cache, + self.img_cache, + self.note, + self.note_options, + ) + .actionbar(false) + .medium_pfp(true) + .options_button(true) + .show(ui); }); let id = self.id(); @@ -87,6 +97,7 @@ impl<'a> PostReplyView<'a> { self.note_cache, self.poster, self.inner_rect, + self.note_options, ) .id_source(id) .ui(self.note.txn().unwrap(), ui) diff --git a/crates/notedeck_columns/src/ui/note/reply_description.rs b/crates/notedeck_columns/src/ui/note/reply_description.rs @@ -1,4 +1,7 @@ -use crate::{actionbar::NoteAction, ui}; +use crate::{ + actionbar::NoteAction, + ui::{self, note::NoteOptions}, +}; use egui::{Label, RichText, Sense}; use nostrdb::{Ndb, Note, NoteReply, Transaction}; use notedeck::{ImageCache, NoteCache}; @@ -11,6 +14,7 @@ pub fn reply_desc( ndb: &Ndb, img_cache: &mut ImageCache, note_cache: &mut NoteCache, + note_options: NoteOptions, ) -> Option<NoteAction> { #[cfg(feature = "profiling")] puffin::profile_function!(); @@ -41,7 +45,7 @@ pub fn reply_desc( if r.hovered() { r.on_hover_ui_at_pointer(|ui| { ui.set_max_width(400.0); - ui::NoteView::new(ndb, note_cache, img_cache, note) + ui::NoteView::new(ndb, note_cache, img_cache, note, note_options) .actionbar(false) .wide(true) .show(ui); diff --git a/crates/notedeck_columns/src/ui/thread.rs b/crates/notedeck_columns/src/ui/thread.rs @@ -17,7 +17,7 @@ pub struct ThreadView<'a> { unknown_ids: &'a mut UnknownIds, img_cache: &'a mut ImageCache, selected_note_id: &'a [u8; 32], - textmode: bool, + note_options: NoteOptions, id_source: egui::Id, is_muted: &'a MuteFun, } @@ -31,7 +31,7 @@ impl<'a> ThreadView<'a> { unknown_ids: &'a mut UnknownIds, img_cache: &'a mut ImageCache, selected_note_id: &'a [u8; 32], - textmode: bool, + note_options: NoteOptions, is_muted: &'a MuteFun, ) -> Self { let id_source = egui::Id::new("threadscroll_threadview"); @@ -42,7 +42,7 @@ impl<'a> ThreadView<'a> { unknown_ids, img_cache, selected_note_id, - textmode, + note_options, id_source, is_muted, } @@ -101,15 +101,10 @@ impl<'a> ThreadView<'a> { error!("error polling notes into thread timeline: {err}"); } - // This is threadview. We are not the universe view... - let is_universe = false; - let mut note_options = NoteOptions::new(is_universe); - note_options.set_textmode(self.textmode); - TimelineTabView::new( thread_timeline.current_view(), true, - note_options, + self.note_options, &txn, self.ndb, self.note_cache, diff --git a/crates/notedeck_columns/src/ui/timeline.rs b/crates/notedeck_columns/src/ui/timeline.rs @@ -388,10 +388,14 @@ impl<'a> TimelineTabView<'a> { if !muted { ui::padding(8.0, ui, |ui| { - let resp = - ui::NoteView::new(self.ndb, self.note_cache, self.img_cache, &note) - .note_options(self.note_options) - .show(ui); + let resp = ui::NoteView::new( + self.ndb, + self.note_cache, + self.img_cache, + &note, + self.note_options, + ) + .show(ui); if let Some(note_action) = resp.action { action = Some(note_action)