From ac10c7e5b22599048465c3a19ec9b7c0c56b8431 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Wed, 5 Feb 2025 18:43:09 -0800 Subject: [PATCH] hashtags: click hashtags to open them Fixes: https://github.com/damus-io/notedeck/issues/695 Fixes: https://github.com/damus-io/notedeck/issues/713 Changelog-Added: Add ability to click hashtags Signed-off-by: William Casarin --- crates/notedeck_columns/src/actionbar.rs | 75 +++---------------- crates/notedeck_columns/src/timeline/kind.rs | 2 +- crates/notedeck_columns/src/timeline/mod.rs | 2 +- crates/notedeck_columns/src/ui/mention.rs | 6 +- .../notedeck_columns/src/ui/note/contents.rs | 13 +++- crates/notedeck_columns/src/ui/note/mod.rs | 25 +++++-- 6 files changed, 44 insertions(+), 79 deletions(-) diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs index 04f23336..db676512 100644 --- a/crates/notedeck_columns/src/actionbar.rs +++ b/crates/notedeck_columns/src/actionbar.rs @@ -1,20 +1,19 @@ use crate::{ column::Columns, route::{Route, Router}, - timeline::{ThreadSelection, TimelineCache, TimelineKind}, + timeline::{TimelineCache, TimelineKind}, }; -use enostr::{NoteId, Pubkey, RelayPool}; +use enostr::{NoteId, RelayPool}; use nostrdb::{Ndb, NoteKey, Transaction}; -use notedeck::{note::root_note_id_from_selected_id, NoteCache, RootIdError, UnknownIds}; +use notedeck::{NoteCache, UnknownIds}; use tracing::error; -#[derive(Debug, Eq, PartialEq, Copy, Clone)] +#[derive(Debug, Eq, PartialEq, Clone)] pub enum NoteAction { Reply(NoteId), Quote(NoteId), - OpenThread(NoteId), - OpenProfile(Pubkey), + OpenTimeline(TimelineKind), } pub struct NewNotes { @@ -26,52 +25,6 @@ pub enum TimelineOpenResult { NewNotes(NewNotes), } -/// open_thread is called when a note is selected and we need to navigate -/// to a thread It is responsible for managing the subscription and -/// making sure the thread is up to date. In a sense, it's a model for -/// the thread view. We don't have a concept of model/view/controller etc -/// in egui, but this is the closest thing to that. -#[allow(clippy::too_many_arguments)] -fn open_thread( - ndb: &Ndb, - txn: &Transaction, - router: &mut Router, - note_cache: &mut NoteCache, - pool: &mut RelayPool, - timeline_cache: &mut TimelineCache, - selected_note: &[u8; 32], -) -> Option { - router.route_to(Route::thread( - ThreadSelection::from_note_id(ndb, note_cache, txn, NoteId::new(*selected_note)).ok()?, - )); - - match root_note_id_from_selected_id(ndb, note_cache, txn, selected_note) { - Ok(root_id) => timeline_cache.open( - ndb, - note_cache, - txn, - pool, - &TimelineKind::Thread(ThreadSelection::from_root_id(root_id.to_owned())), - ), - - Err(RootIdError::NoteNotFound) => { - error!( - "open_thread: note not found: {}", - hex::encode(selected_note) - ); - None - } - - Err(RootIdError::NoRootId) => { - error!( - "open_thread: note has no root id: {}", - hex::encode(selected_note) - ); - None - } - } -} - impl NoteAction { #[allow(clippy::too_many_arguments)] pub fn execute( @@ -89,19 +42,9 @@ impl NoteAction { None } - NoteAction::OpenThread(note_id) => open_thread( - ndb, - txn, - router, - note_cache, - pool, - timeline_cache, - note_id.bytes(), - ), - - NoteAction::OpenProfile(pubkey) => { - router.route_to(Route::profile(*pubkey)); - timeline_cache.open(ndb, note_cache, txn, pool, &TimelineKind::Profile(*pubkey)) + NoteAction::OpenTimeline(kind) => { + router.route_to(Route::Timeline(kind.to_owned())); + timeline_cache.open(ndb, note_cache, txn, pool, kind) } NoteAction::Quote(note_id) => { @@ -114,7 +57,7 @@ impl NoteAction { /// Execute the NoteAction and process the TimelineOpenResult #[allow(clippy::too_many_arguments)] pub fn execute_and_process_result( - self, + &self, ndb: &Ndb, columns: &mut Columns, col: usize, diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs index 189d1134..7281461e 100644 --- a/crates/notedeck_columns/src/timeline/kind.rs +++ b/crates/notedeck_columns/src/timeline/kind.rs @@ -434,7 +434,7 @@ impl TimelineKind { TimelineKind::Hashtag(hashtag) => FilterState::ready(vec![Filter::new() .kinds([1]) .limit(filter::default_limit()) - .tags([hashtag.clone()], 't') + .tags([hashtag.to_lowercase()], 't') .build()]), TimelineKind::Algo(algo_timeline) => match algo_timeline { diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs index 886bfea6..94777e67 100644 --- a/crates/notedeck_columns/src/timeline/mod.rs +++ b/crates/notedeck_columns/src/timeline/mod.rs @@ -247,7 +247,7 @@ impl Timeline { let filter = Filter::new() .kinds([1]) .limit(filter::default_limit()) - .tags([hashtag.clone()], 't') + .tags([hashtag.to_lowercase()], 't') .build(); Timeline::new( diff --git a/crates/notedeck_columns/src/ui/mention.rs b/crates/notedeck_columns/src/ui/mention.rs index 407c70c7..83e53ac5 100644 --- a/crates/notedeck_columns/src/ui/mention.rs +++ b/crates/notedeck_columns/src/ui/mention.rs @@ -1,5 +1,5 @@ use crate::ui; -use crate::{actionbar::NoteAction, profile::get_display_name}; +use crate::{actionbar::NoteAction, profile::get_display_name, timeline::TimelineKind}; use egui::Sense; use enostr::Pubkey; use nostrdb::{Ndb, Transaction}; @@ -89,7 +89,9 @@ fn mention_ui( let note_action = if resp.clicked() { ui::show_pointer(ui); - Some(NoteAction::OpenProfile(Pubkey::new(*pk))) + Some(NoteAction::OpenTimeline(TimelineKind::profile( + Pubkey::new(*pk), + ))) } else if resp.hovered() { ui::show_pointer(ui); None diff --git a/crates/notedeck_columns/src/ui/note/contents.rs b/crates/notedeck_columns/src/ui/note/contents.rs index 8d698866..a6c6bd30 100644 --- a/crates/notedeck_columns/src/ui/note/contents.rs +++ b/crates/notedeck_columns/src/ui/note/contents.rs @@ -1,10 +1,9 @@ -use crate::actionbar::NoteAction; -use crate::images::ImageType; use crate::ui::{ self, note::{NoteOptions, NoteResponse}, ProfilePic, }; +use crate::{actionbar::NoteAction, images::ImageType, timeline::TimelineKind}; use egui::{Color32, Hyperlink, Image, RichText}; use nostrdb::{BlockType, Mention, Ndb, Note, NoteKey, Transaction}; use tracing::warn; @@ -198,7 +197,15 @@ fn render_note_contents( BlockType::Hashtag => { #[cfg(feature = "profiling")] puffin::profile_scope!("hashtag contents"); - ui.colored_label(link_color, format!("#{}", block.as_str())); + let resp = ui.colored_label(link_color, format!("#{}", block.as_str())); + + if resp.clicked() { + note_action = Some(NoteAction::OpenTimeline(TimelineKind::Hashtag( + block.as_str().to_string(), + ))); + } else if resp.hovered() { + ui::show_pointer(ui); + } } BlockType::Url => { diff --git a/crates/notedeck_columns/src/ui/note/mod.rs b/crates/notedeck_columns/src/ui/note/mod.rs index dda30143..a37d3443 100644 --- a/crates/notedeck_columns/src/ui/note/mod.rs +++ b/crates/notedeck_columns/src/ui/note/mod.rs @@ -17,6 +17,7 @@ pub use reply_description::reply_desc; use crate::{ actionbar::NoteAction, profile::get_display_name, + timeline::{ThreadSelection, TimelineKind}, ui::{self, View}, }; @@ -354,8 +355,9 @@ impl<'a> NoteView<'a> { ui.vertical(|ui| { ui.horizontal(|ui| { if self.pfp(note_key, &profile, ui).clicked() { - note_action = - Some(NoteAction::OpenProfile(Pubkey::new(*self.note.pubkey()))); + note_action = Some(NoteAction::OpenTimeline(TimelineKind::profile( + Pubkey::new(*self.note.pubkey()), + ))); }; let size = ui.available_size(); @@ -415,7 +417,7 @@ impl<'a> NoteView<'a> { ui.add(&mut contents); if let Some(action) = contents.action() { - note_action = Some(*action); + note_action = Some(action.clone()); } if self.options().has_actionbar() { @@ -430,7 +432,9 @@ impl<'a> NoteView<'a> { // main design ui.with_layout(egui::Layout::left_to_right(egui::Align::TOP), |ui| { if self.pfp(note_key, &profile, ui).clicked() { - note_action = Some(NoteAction::OpenProfile(Pubkey::new(*self.note.pubkey()))); + note_action = Some(NoteAction::OpenTimeline(TimelineKind::Profile( + Pubkey::new(*self.note.pubkey()), + ))); }; ui.with_layout(egui::Layout::top_down(egui::Align::LEFT), |ui| { @@ -480,7 +484,7 @@ impl<'a> NoteView<'a> { ui.add(&mut contents); if let Some(action) = contents.action() { - note_action = Some(*action); + note_action = Some(action.clone()); } if self.options().has_actionbar() { @@ -496,7 +500,16 @@ impl<'a> NoteView<'a> { }; let note_action = if note_hitbox_clicked(ui, hitbox_id, &response.rect, maybe_hitbox) { - Some(NoteAction::OpenThread(NoteId::new(*self.note.id()))) + if let Ok(selection) = ThreadSelection::from_note_id( + self.ndb, + self.note_cache, + self.note.txn().unwrap(), + NoteId::new(*self.note.id()), + ) { + Some(NoteAction::OpenTimeline(TimelineKind::Thread(selection))) + } else { + None + } } else { note_action };