diff --git a/src/actionbar.rs b/src/actionbar.rs index 254bb51..f0a7e99 100644 --- a/src/actionbar.rs +++ b/src/actionbar.rs @@ -138,6 +138,8 @@ impl NewThreadNotes { /// Simple helper for processing a NewThreadNotes result. It simply /// inserts/merges the notes into the thread cache pub fn process(&self, thread: &mut Thread) { - thread.view.insert(&self.notes); + // threads are chronological, ie reversed from reverse-chronological, the default. + let reversed = true; + thread.view.insert(&self.notes, reversed); } } diff --git a/src/timeline.rs b/src/timeline.rs index 86e009b..417c9c2 100644 --- a/src/timeline.rs +++ b/src/timeline.rs @@ -109,12 +109,20 @@ impl<'a> TimelineSource<'a> { new_refs.push((note, NoteRef { key, created_at })); } + // We're assuming reverse-chronological here (timelines). This + // flag ensures we trigger the items_inserted_at_start + // optimization in VirtualList. We need this flag because we can + // insert notes into chronological order sometimes, and this + // optimization doesn't make sense in those situations. + let reversed = false; + // ViewFilter::NotesAndReplies { let refs: Vec = new_refs.iter().map(|(_note, nr)| *nr).collect(); + let reversed = false; self.view(app, txn, ViewFilter::NotesAndReplies) - .insert(&refs); + .insert(&refs, reversed); } // @@ -133,7 +141,7 @@ impl<'a> TimelineSource<'a> { } self.view(app, txn, ViewFilter::Notes) - .insert(&filtered_refs); + .insert(&filtered_refs, reversed); } Ok(()) @@ -211,7 +219,7 @@ impl TimelineTab { } } - pub fn insert(&mut self, new_refs: &[NoteRef]) { + pub fn insert(&mut self, new_refs: &[NoteRef], reversed: bool) { if new_refs.is_empty() { return; } @@ -228,7 +236,14 @@ impl TimelineTab { match merge_kind { // TODO: update egui_virtual_list to support spliced inserts MergeKind::Spliced => list.reset(), - MergeKind::FrontInsert => list.items_inserted_at_start(new_items), + MergeKind::FrontInsert => { + // only run this logic if we're reverse-chronological + // reversed in this case means chronological, since the + // default is reverse-chronological. yeah it's confusing. + if !reversed { + list.items_inserted_at_start(new_items); + } + } } } } diff --git a/src/ui/mention.rs b/src/ui/mention.rs index 7fb15f3..f363390 100644 --- a/src/ui/mention.rs +++ b/src/ui/mention.rs @@ -5,13 +5,26 @@ pub struct Mention<'a> { app: &'a mut Damus, txn: &'a Transaction, pk: &'a [u8; 32], + selectable: bool, size: f32, } impl<'a> Mention<'a> { pub fn new(app: &'a mut Damus, txn: &'a Transaction, pk: &'a [u8; 32]) -> Self { let size = 16.0; - Mention { app, txn, pk, size } + let selectable = true; + Mention { + app, + txn, + pk, + selectable, + size, + } + } + + pub fn selectable(mut self, selectable: bool) -> Self { + self.selectable = selectable; + self } pub fn size(mut self, size: f32) -> Self { @@ -22,7 +35,7 @@ impl<'a> Mention<'a> { impl<'a> egui::Widget for Mention<'a> { fn ui(self, ui: &mut egui::Ui) -> egui::Response { - mention_ui(self.app, self.txn, self.pk, ui, self.size) + mention_ui(self.app, self.txn, self.pk, ui, self.size, self.selectable) } } @@ -32,6 +45,7 @@ fn mention_ui( pk: &[u8; 32], ui: &mut egui::Ui, size: f32, + selectable: bool ) -> egui::Response { #[cfg(feature = "profiling")] puffin::profile_function!(); @@ -46,9 +60,10 @@ fn mention_ui( "??".to_string() }; - let resp = ui.add(egui::Label::new( - egui::RichText::new(name).color(colors::PURPLE).size(size), - )); + let resp = ui.add( + egui::Label::new(egui::RichText::new(name).color(colors::PURPLE).size(size)) + .selectable(selectable), + ); if let Some(rec) = profile.as_ref() { resp.on_hover_ui_at_pointer(|ui| { diff --git a/src/ui/note/contents.rs b/src/ui/note/contents.rs index a352e04..f4ffee1 100644 --- a/src/ui/note/contents.rs +++ b/src/ui/note/contents.rs @@ -110,6 +110,7 @@ fn render_note_contents( #[cfg(feature = "profiling")] puffin::profile_function!(); + let selectable = options.has_selectable_text(); let images: Vec = vec![]; let mut inline_note: Option<(&[u8; 32], &str)> = None; @@ -173,7 +174,7 @@ fn render_note_contents( BlockType::Text => { #[cfg(feature = "profiling")] puffin::profile_scope!("text contents"); - ui.label(block.as_str()); + ui.add(egui::Label::new(block.as_str()).selectable(selectable)); } _ => { diff --git a/src/ui/note/mod.rs b/src/ui/note/mod.rs index 1117ec9..25b322e 100644 --- a/src/ui/note/mod.rs +++ b/src/ui/note/mod.rs @@ -33,11 +33,17 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: #[cfg(feature = "profiling")] puffin::profile_function!(); - ui.add(Label::new( - RichText::new("replying to") - .size(10.0) - .color(colors::GRAY_SECONDARY), - )); + let size = 10.0; + let selectable = false; + + ui.add( + Label::new( + RichText::new("replying to") + .size(size) + .color(colors::GRAY_SECONDARY), + ) + .selectable(selectable), + ); let reply = if let Some(reply) = note_reply.reply() { reply @@ -48,55 +54,91 @@ fn reply_desc(ui: &mut egui::Ui, txn: &Transaction, note_reply: &NoteReply, app: let reply_note = if let Ok(reply_note) = app.ndb.get_note_by_id(txn, reply.id) { reply_note } else { - ui.add(Label::new( - RichText::new("a note") - .size(10.0) - .color(colors::GRAY_SECONDARY), - )); + ui.add( + Label::new( + RichText::new("a note") + .size(size) + .color(colors::GRAY_SECONDARY), + ) + .selectable(selectable), + ); return; }; if note_reply.is_reply_to_root() { // We're replying to the root, let's show this - ui.add(ui::Mention::new(app, txn, reply_note.pubkey()).size(10.0)); - ui.add(Label::new( - RichText::new("'s note") - .size(10.0) - .color(colors::GRAY_SECONDARY), - )); + ui.add( + ui::Mention::new(app, txn, reply_note.pubkey()) + .size(size) + .selectable(selectable), + ); + ui.add( + Label::new( + RichText::new("'s note") + .size(size) + .color(colors::GRAY_SECONDARY), + ) + .selectable(selectable), + ); } else if let Some(root) = note_reply.root() { // replying to another post in a thread, not the root if let Ok(root_note) = app.ndb.get_note_by_id(txn, root.id) { if root_note.pubkey() == reply_note.pubkey() { // simply "replying to bob's note" when replying to bob in his thread - ui.add(ui::Mention::new(app, txn, reply_note.pubkey()).size(10.0)); - ui.add(Label::new( - RichText::new("'s note") - .size(10.0) - .color(colors::GRAY_SECONDARY), - )); + ui.add( + ui::Mention::new(app, txn, reply_note.pubkey()) + .size(size) + .selectable(selectable), + ); + ui.add( + Label::new( + RichText::new("'s note") + .size(size) + .color(colors::GRAY_SECONDARY), + ) + .selectable(selectable), + ); } else { // replying to bob in alice's thread - ui.add(ui::Mention::new(app, txn, reply_note.pubkey()).size(10.0)); - ui.add(Label::new( - RichText::new("in").size(10.0).color(colors::GRAY_SECONDARY), - )); - ui.add(ui::Mention::new(app, txn, root_note.pubkey()).size(10.0)); - ui.add(Label::new( - RichText::new("'s thread") - .size(10.0) - .color(colors::GRAY_SECONDARY), - )); + ui.add( + ui::Mention::new(app, txn, reply_note.pubkey()) + .size(size) + .selectable(selectable), + ); + ui.add( + Label::new(RichText::new("in").size(size).color(colors::GRAY_SECONDARY)) + .selectable(selectable), + ); + ui.add( + ui::Mention::new(app, txn, root_note.pubkey()) + .size(size) + .selectable(selectable), + ); + ui.add( + Label::new( + RichText::new("'s thread") + .size(size) + .color(colors::GRAY_SECONDARY), + ) + .selectable(selectable), + ); } } else { - ui.add(ui::Mention::new(app, txn, reply_note.pubkey()).size(10.0)); - ui.add(Label::new( - RichText::new("in someone's thread") - .size(10.0) - .color(colors::GRAY_SECONDARY), - )); + ui.add( + ui::Mention::new(app, txn, reply_note.pubkey()) + .size(size) + .selectable(selectable), + ); + ui.add( + Label::new( + RichText::new("in someone's thread") + .size(size) + .color(colors::GRAY_SECONDARY), + ) + .selectable(selectable), + ); } } } @@ -127,6 +169,11 @@ impl<'a> NoteView<'a> { self } + pub fn selectable_text(mut self, enable: bool) -> Self { + self.options_mut().set_selectable_text(enable); + self + } + pub fn wide(mut self, enable: bool) -> Self { self.options_mut().set_wide(enable); self diff --git a/src/ui/note/options.rs b/src/ui/note/options.rs index a25d44a..8bc3ad0 100644 --- a/src/ui/note/options.rs +++ b/src/ui/note/options.rs @@ -6,20 +6,45 @@ bitflags! { #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct NoteOptions: u32 { - const actionbar = 0b00000001; - const note_previews = 0b00000010; - const small_pfp = 0b00000100; - const medium_pfp = 0b00001000; - const wide = 0b00010000; + const actionbar = 0b00000001; + const note_previews = 0b00000010; + const small_pfp = 0b00000100; + const medium_pfp = 0b00001000; + const wide = 0b00010000; + const selectable_text = 0b00100000; } } +macro_rules! create_setter { + ($fn_name:ident, $option:ident) => { + #[inline] + pub fn $fn_name(&mut self, enable: bool) { + if enable { + *self |= NoteOptions::$option; + } else { + *self &= !NoteOptions::$option; + } + } + }; +} + impl NoteOptions { + create_setter!(set_small_pfp, small_pfp); + create_setter!(set_medium_pfp, medium_pfp); + create_setter!(set_note_previews, note_previews); + create_setter!(set_selectable_text, selectable_text); + create_setter!(set_actionbar, actionbar); + #[inline] pub fn has_actionbar(self) -> bool { (self & NoteOptions::actionbar) == NoteOptions::actionbar } + #[inline] + pub fn has_selectable_text(self) -> bool { + (self & NoteOptions::selectable_text) == NoteOptions::selectable_text + } + #[inline] pub fn has_note_previews(self) -> bool { (self & NoteOptions::note_previews) == NoteOptions::note_previews @@ -58,40 +83,4 @@ impl NoteOptions { *self &= !NoteOptions::wide; } } - - #[inline] - pub fn set_small_pfp(&mut self, enable: bool) { - if enable { - *self |= NoteOptions::small_pfp; - } else { - *self &= !NoteOptions::small_pfp; - } - } - - #[inline] - pub fn set_medium_pfp(&mut self, enable: bool) { - if enable { - *self |= NoteOptions::medium_pfp; - } else { - *self &= !NoteOptions::medium_pfp; - } - } - - #[inline] - pub fn set_note_previews(&mut self, enable: bool) { - if enable { - *self |= NoteOptions::note_previews; - } else { - *self &= !NoteOptions::note_previews; - } - } - - #[inline] - pub fn set_actionbar(&mut self, enable: bool) { - if enable { - *self |= NoteOptions::actionbar; - } else { - *self &= !NoteOptions::actionbar; - } - } } diff --git a/src/ui/timeline.rs b/src/ui/timeline.rs index 8ea6af2..a3ec99c 100644 --- a/src/ui/timeline.rs +++ b/src/ui/timeline.rs @@ -90,6 +90,7 @@ fn timeline_ui(ui: &mut egui::Ui, app: &mut Damus, timeline: usize, reversed: bo let textmode = app.textmode; let resp = ui::NoteView::new(app, ¬e) .note_previews(!textmode) + .selectable_text(false) .show(ui); if let Some(action) = resp.action {