hashtag-column: allow multiple hashtags

Changelog-Changed: Allow multiple hashtags in hashtag columns
This commit is contained in:
Fernando López Guevara
2025-04-24 19:37:16 -03:00
committed by William Casarin
parent 5c31bf16c8
commit f214e97382
5 changed files with 48 additions and 26 deletions

View File

@@ -88,7 +88,7 @@ fn execute_note_action(
}); });
} }
NoteAction::Hashtag(htag) => { NoteAction::Hashtag(htag) => {
let kind = TimelineKind::Hashtag(htag.clone()); let kind = TimelineKind::Hashtag(vec![htag.clone()]);
router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone()))); router_action = Some(RouterAction::route_to(Route::Timeline(kind.clone())));
timeline_res = timeline_cache timeline_res = timeline_cache
.open(ndb, note_cache, txn, pool, &kind) .open(ndb, note_cache, txn, pool, &kind)

View File

@@ -461,7 +461,7 @@ impl fmt::Display for Route {
TimelineKind::Universe => write!(f, "Universe"), TimelineKind::Universe => write!(f, "Universe"),
TimelineKind::Generic(_) => write!(f, "Custom"), TimelineKind::Generic(_) => write!(f, "Custom"),
TimelineKind::Search(_) => write!(f, "Search"), TimelineKind::Search(_) => write!(f, "Search"),
TimelineKind::Hashtag(ht) => write!(f, "Hashtag ({})", ht), TimelineKind::Hashtag(ht) => write!(f, "Hashtags ({})", ht.join(" ")),
TimelineKind::Profile(_id) => write!(f, "Profile"), TimelineKind::Profile(_id) => write!(f, "Profile"),
}, },
Route::Thread(_) => write!(f, "Thread"), Route::Thread(_) => write!(f, "Thread"),

View File

@@ -213,7 +213,7 @@ pub enum TimelineKind {
/// Generic filter, references a hash of a filter /// Generic filter, references a hash of a filter
Generic(u64), Generic(u64),
Hashtag(String), Hashtag(Vec<String>),
} }
const NOTIFS_TOKEN_DEPRECATED: &str = "notifs"; const NOTIFS_TOKEN_DEPRECATED: &str = "notifs";
@@ -263,7 +263,7 @@ impl Display for TimelineKind {
TimelineKind::Notifications(_) => f.write_str("Notifications"), TimelineKind::Notifications(_) => f.write_str("Notifications"),
TimelineKind::Profile(_) => f.write_str("Profile"), TimelineKind::Profile(_) => f.write_str("Profile"),
TimelineKind::Universe => f.write_str("Universe"), TimelineKind::Universe => f.write_str("Universe"),
TimelineKind::Hashtag(_) => f.write_str("Hashtag"), TimelineKind::Hashtag(_) => f.write_str("Hashtags"),
TimelineKind::Search(_) => f.write_str("Search"), TimelineKind::Search(_) => f.write_str("Search"),
} }
} }
@@ -325,7 +325,7 @@ impl TimelineKind {
} }
TimelineKind::Hashtag(ht) => { TimelineKind::Hashtag(ht) => {
writer.write_token("hashtag"); writer.write_token("hashtag");
writer.write_token(ht); writer.write_token(&ht.join(" "));
} }
} }
} }
@@ -379,7 +379,13 @@ impl TimelineKind {
}, },
|p| { |p| {
p.parse_token("hashtag")?; p.parse_token("hashtag")?;
Ok(TimelineKind::Hashtag(p.pull_token()?.to_string())) Ok(TimelineKind::Hashtag(
p.pull_token()?
.split_whitespace()
.filter(|s| !s.is_empty())
.map(|s| s.to_lowercase().to_string())
.collect(),
))
}, },
|p| { |p| {
p.parse_token("search")?; p.parse_token("search")?;
@@ -437,12 +443,19 @@ impl TimelineKind {
.build()]), .build()]),
TimelineKind::Hashtag(hashtag) => { TimelineKind::Hashtag(hashtag) => {
let url: &str = &hashtag.to_lowercase(); let filters = hashtag
FilterState::ready(vec![Filter::new() .iter()
.kinds([1]) .filter(|tag| !tag.is_empty())
.limit(filter::default_limit()) .map(|tag| {
.tags([url], 't') Filter::new()
.build()]) .kinds([1])
.limit(filter::default_limit())
.tags([tag.to_lowercase().as_str()], 't')
.build()
})
.collect::<Vec<_>>();
FilterState::ready(filters)
} }
TimelineKind::Algo(algo_timeline) => match algo_timeline { TimelineKind::Algo(algo_timeline) => match algo_timeline {
@@ -579,7 +592,7 @@ impl TimelineKind {
TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self), TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self),
TimelineKind::Universe => ColumnTitle::simple("Universe"), TimelineKind::Universe => ColumnTitle::simple("Universe"),
TimelineKind::Generic(_) => ColumnTitle::simple("Custom"), TimelineKind::Generic(_) => ColumnTitle::simple("Custom"),
TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.to_string()), TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.join(" ").to_string()),
} }
} }
} }

View File

@@ -226,18 +226,22 @@ impl Timeline {
)) ))
} }
pub fn hashtag(hashtag: String) -> Self { pub fn hashtag(hashtag: Vec<String>) -> Self {
let hashtag = hashtag.to_lowercase(); let filters = hashtag
let htag: &str = &hashtag; .iter()
let filter = Filter::new() .filter(|tag| !tag.is_empty())
.kinds([1]) .map(|tag| {
.limit(filter::default_limit()) Filter::new()
.tags([htag], 't') .kinds([1])
.build(); .limit(filter::default_limit())
.tags([tag.as_str()], 't')
.build()
})
.collect::<Vec<_>>();
Timeline::new( Timeline::new(
TimelineKind::Hashtag(hashtag), TimelineKind::Hashtag(hashtag),
FilterState::ready(vec![filter]), FilterState::ready(filters),
TimelineTab::only_notes_and_replies(), TimelineTab::only_notes_and_replies(),
) )
} }

View File

@@ -474,7 +474,7 @@ impl<'a> AddColumnView<'a> {
option: AddColumnOption::UndecidedNotification, option: AddColumnOption::UndecidedNotification,
}); });
vec.push(ColumnOptionData { vec.push(ColumnOptionData {
title: "Hashtag", title: "Hashtags",
description: "Stay up to date with a certain hashtag", description: "Stay up to date with a certain hashtag",
icon: egui::include_image!("../../../../assets/icons/hashtag_icon_4x.png"), icon: egui::include_image!("../../../../assets/icons/hashtag_icon_4x.png"),
option: AddColumnOption::UndecidedHashtag, option: AddColumnOption::UndecidedHashtag,
@@ -754,7 +754,7 @@ pub fn hashtag_ui(
let text_edit = egui::TextEdit::singleline(text_buffer) let text_edit = egui::TextEdit::singleline(text_buffer)
.hint_text( .hint_text(
RichText::new("Enter the desired hashtag here") RichText::new("Enter the desired hashtags here (for multiple space-separated)")
.text_style(NotedeckTextStyle::Body.text_style()), .text_style(NotedeckTextStyle::Body.text_style()),
) )
.vertical_align(Align::Center) .vertical_align(Align::Center)
@@ -775,8 +775,13 @@ pub fn hashtag_ui(
} }
if handle_user_input && !text_buffer.is_empty() { if handle_user_input && !text_buffer.is_empty() {
let resp = let resp = AddColumnResponse::Timeline(TimelineKind::Hashtag(
AddColumnResponse::Timeline(TimelineKind::Hashtag(sanitize_hashtag(text_buffer))); sanitize_hashtag(text_buffer)
.split_whitespace()
.filter(|s| !s.is_empty())
.map(|s| s.to_lowercase().to_string())
.collect::<Vec<_>>(),
));
id_string_map.remove(&id); id_string_map.remove(&id);
Some(resp) Some(resp)
} else { } else {