commit ca5ecb3777704cdcdbc6f81984123a6ab2f676f6
parent 5c31bf16c81f98f6399620d01b5df87d37780f6a
Author: William Casarin <jb55@jb55.com>
Date: Tue, 24 Jun 2025 08:30:15 -0700
Merge multiple hashtags in a column
Fernando LoĢpez Guevara (1):
hashtag-column: allow multiple hashtags
William Casarin (2):
hashtag: improve sanitization function
Diffstat:
5 files changed, 52 insertions(+), 27 deletions(-)
diff --git a/crates/notedeck_columns/src/actionbar.rs b/crates/notedeck_columns/src/actionbar.rs
@@ -88,7 +88,7 @@ fn execute_note_action(
});
}
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())));
timeline_res = timeline_cache
.open(ndb, note_cache, txn, pool, &kind)
diff --git a/crates/notedeck_columns/src/route.rs b/crates/notedeck_columns/src/route.rs
@@ -461,7 +461,7 @@ impl fmt::Display for Route {
TimelineKind::Universe => write!(f, "Universe"),
TimelineKind::Generic(_) => write!(f, "Custom"),
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"),
},
Route::Thread(_) => write!(f, "Thread"),
diff --git a/crates/notedeck_columns/src/timeline/kind.rs b/crates/notedeck_columns/src/timeline/kind.rs
@@ -213,7 +213,7 @@ pub enum TimelineKind {
/// Generic filter, references a hash of a filter
Generic(u64),
- Hashtag(String),
+ Hashtag(Vec<String>),
}
const NOTIFS_TOKEN_DEPRECATED: &str = "notifs";
@@ -263,7 +263,7 @@ impl Display for TimelineKind {
TimelineKind::Notifications(_) => f.write_str("Notifications"),
TimelineKind::Profile(_) => f.write_str("Profile"),
TimelineKind::Universe => f.write_str("Universe"),
- TimelineKind::Hashtag(_) => f.write_str("Hashtag"),
+ TimelineKind::Hashtag(_) => f.write_str("Hashtags"),
TimelineKind::Search(_) => f.write_str("Search"),
}
}
@@ -325,7 +325,7 @@ impl TimelineKind {
}
TimelineKind::Hashtag(ht) => {
writer.write_token("hashtag");
- writer.write_token(ht);
+ writer.write_token(&ht.join(" "));
}
}
}
@@ -379,7 +379,13 @@ impl TimelineKind {
},
|p| {
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.parse_token("search")?;
@@ -437,12 +443,19 @@ impl TimelineKind {
.build()]),
TimelineKind::Hashtag(hashtag) => {
- let url: &str = &hashtag.to_lowercase();
- FilterState::ready(vec![Filter::new()
- .kinds([1])
- .limit(filter::default_limit())
- .tags([url], 't')
- .build()])
+ let filters = hashtag
+ .iter()
+ .filter(|tag| !tag.is_empty())
+ .map(|tag| {
+ Filter::new()
+ .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 {
@@ -579,7 +592,7 @@ impl TimelineKind {
TimelineKind::Profile(_pubkey_source) => ColumnTitle::needs_db(self),
TimelineKind::Universe => ColumnTitle::simple("Universe"),
TimelineKind::Generic(_) => ColumnTitle::simple("Custom"),
- TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.to_string()),
+ TimelineKind::Hashtag(hashtag) => ColumnTitle::formatted(hashtag.join(" ").to_string()),
}
}
}
diff --git a/crates/notedeck_columns/src/timeline/mod.rs b/crates/notedeck_columns/src/timeline/mod.rs
@@ -226,18 +226,22 @@ impl Timeline {
))
}
- pub fn hashtag(hashtag: String) -> Self {
- let hashtag = hashtag.to_lowercase();
- let htag: &str = &hashtag;
- let filter = Filter::new()
- .kinds([1])
- .limit(filter::default_limit())
- .tags([htag], 't')
- .build();
+ pub fn hashtag(hashtag: Vec<String>) -> Self {
+ let filters = hashtag
+ .iter()
+ .filter(|tag| !tag.is_empty())
+ .map(|tag| {
+ Filter::new()
+ .kinds([1])
+ .limit(filter::default_limit())
+ .tags([tag.as_str()], 't')
+ .build()
+ })
+ .collect::<Vec<_>>();
Timeline::new(
TimelineKind::Hashtag(hashtag),
- FilterState::ready(vec![filter]),
+ FilterState::ready(filters),
TimelineTab::only_notes_and_replies(),
)
}
diff --git a/crates/notedeck_columns/src/ui/add_column.rs b/crates/notedeck_columns/src/ui/add_column.rs
@@ -474,7 +474,7 @@ impl<'a> AddColumnView<'a> {
option: AddColumnOption::UndecidedNotification,
});
vec.push(ColumnOptionData {
- title: "Hashtag",
+ title: "Hashtags",
description: "Stay up to date with a certain hashtag",
icon: egui::include_image!("../../../../assets/icons/hashtag_icon_4x.png"),
option: AddColumnOption::UndecidedHashtag,
@@ -754,7 +754,7 @@ pub fn hashtag_ui(
let text_edit = egui::TextEdit::singleline(text_buffer)
.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()),
)
.vertical_align(Align::Center)
@@ -775,8 +775,13 @@ pub fn hashtag_ui(
}
if handle_user_input && !text_buffer.is_empty() {
- let resp =
- AddColumnResponse::Timeline(TimelineKind::Hashtag(sanitize_hashtag(text_buffer)));
+ let resp = AddColumnResponse::Timeline(TimelineKind::Hashtag(
+ text_buffer
+ .split_whitespace()
+ .filter(|s| !s.is_empty())
+ .map(|s| sanitize_hashtag(s).to_lowercase().to_string())
+ .collect::<Vec<_>>(),
+ ));
id_string_map.remove(&id);
Some(resp)
} else {
@@ -787,7 +792,10 @@ pub fn hashtag_ui(
}
fn sanitize_hashtag(raw_hashtag: &str) -> String {
- raw_hashtag.replace("#", "")
+ raw_hashtag
+ .chars()
+ .filter(|c| c.is_alphanumeric()) // keep letters and numbers only
+ .collect()
}
#[cfg(test)]