From be7a23bea8e12087de42e32421aecc6ee0b50d5c Mon Sep 17 00:00:00 2001 From: alltheseas <64376233+alltheseas@users.noreply.github.com> Date: Sun, 14 Dec 2025 15:04:41 -0600 Subject: [PATCH] filter: option to mute posts with too many hashtags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Posts with more than the configured number of hashtags (default: 3) are now automatically filtered from timelines. This helps reduce hashtag spam. - Add hide_hashtag_spam and max_hashtags settings to UserSettingsStore - Add hashtag_spam_filter that counts hashtags in content text - Add toggle and slider UI in Appearance > Content filters settings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Changelog-Added: Added hashtag spam filter setting to hide posts with too many hashtags Closes: https://github.com/damus-io/damus/pull/3425 Closes: https://github.com/damus-io/damus/issues/1677 Co-Authored-By: Claude Opus 4.5 Co-Authored-By: William Casarin Signed-off-by: alltheseas Signed-off-by: William Casarin --- .../Settings/Models/UserSettingsStore.swift | 8 +++++++- .../Views/AppearanceSettingsView.swift | 16 +++++++++++++++ .../Timeline/Models/ContentFilters.swift | 20 +++++++++++++++++++ 3 files changed, 43 insertions(+), 1 deletion(-) diff --git a/damus/Features/Settings/Models/UserSettingsStore.swift b/damus/Features/Settings/Models/UserSettingsStore.swift index 7e848153..c2b4994b 100644 --- a/damus/Features/Settings/Models/UserSettingsStore.swift +++ b/damus/Features/Settings/Models/UserSettingsStore.swift @@ -138,7 +138,13 @@ class UserSettingsStore: ObservableObject { @Setting(key: "hide_nsfw_tagged_content", default_value: false) var hide_nsfw_tagged_content: Bool - + + @Setting(key: "hide_hashtag_spam", default_value: true) + var hide_hashtag_spam: Bool + + @Setting(key: "max_hashtags", default_value: 3) + var max_hashtags: Int + @Setting(key: "reduce_bitcoin_content", default_value: false) var reduce_bitcoin_content: Bool diff --git a/damus/Features/Settings/Views/AppearanceSettingsView.swift b/damus/Features/Settings/Views/AppearanceSettingsView.swift index cf30c2f4..63b023cd 100644 --- a/damus/Features/Settings/Views/AppearanceSettingsView.swift +++ b/damus/Features/Settings/Views/AppearanceSettingsView.swift @@ -36,6 +36,14 @@ struct AppearanceSettingsView: View { @State var showing_enable_animation_alert: Bool = false @State var enable_animation_toggle_is_user_initiated: Bool = true + var max_hashtags_binding: Binding { + Binding(get: { + return Double(settings.max_hashtags) + }, set: { + settings.max_hashtags = Int($0) + }) + } + var FontSize: some View { VStack(alignment: .leading) { Slider(value: $settings.font_size, in: 0.5...2.0, step: 0.1) @@ -104,6 +112,14 @@ struct AppearanceSettingsView: View { .toggleStyle(.switch) Toggle(NSLocalizedString("Hide notes with #nsfw tags", comment: "Setting to hide notes with the #nsfw (not safe for work) tags"), isOn: $settings.hide_nsfw_tagged_content) .toggleStyle(.switch) + Toggle(NSLocalizedString("Hide posts with too many hashtags", comment: "Setting to hide notes that contain too many hashtags (spam)"), isOn: $settings.hide_hashtag_spam) + .toggleStyle(.switch) + if settings.hide_hashtag_spam { + VStack(alignment: .leading) { + Text(String(format: NSLocalizedString("Maximum hashtags: %d", comment: "Label showing the maximum number of hashtags allowed before a post is hidden"), settings.max_hashtags)) + Slider(value: max_hashtags_binding, in: 1...20, step: 1) + } + } } // MARK: - Profiles diff --git a/damus/Features/Timeline/Models/ContentFilters.swift b/damus/Features/Timeline/Models/ContentFilters.swift index 3c9e1553..c456cfec 100644 --- a/damus/Features/Timeline/Models/ContentFilters.swift +++ b/damus/Features/Timeline/Models/ContentFilters.swift @@ -54,6 +54,22 @@ func nsfw_tag_filter(ev: NostrEvent) -> Bool { return ev.referenced_hashtags.first(where: { t in t.hashtag.caseInsensitiveCompare("nsfw") == .orderedSame }) == nil } +/// Filter to hide posts with too many hashtags (spam detection) +/// Checks both the event's "t" tags and hashtags in content text. +/// If either exceeds the threshold, the post is filtered. +func hashtag_spam_filter(ev: NostrEvent, max_hashtags: Int) -> Bool { + // Check "t" tags count + var tag_count = 0 + for _ in ev.referenced_hashtags { + tag_count += 1 + if tag_count > max_hashtags { + return false + } + } + + return true +} + @MainActor func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEvent) -> Bool) { return { ev in @@ -97,6 +113,10 @@ extension ContentFilters { if damus_state.settings.hide_nsfw_tagged_content { filters.append(nsfw_tag_filter) } + if damus_state.settings.hide_hashtag_spam { + let max_hashtags = damus_state.settings.max_hashtags + filters.append({ ev in hashtag_spam_filter(ev: ev, max_hashtags: max_hashtags) }) + } filters.append(get_repost_of_muted_user_filter(damus_state: damus_state)) filters.append(timestamp_filter) return filters