Skip to content

Commit ed00301

Browse files
alltheseasclaude
andcommitted
Add hashtag spam filter to automatically mute posts with too many hashtags
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 Changelog-Added: Added hashtag spam filter setting to hide posts with too many hashtags Closes: #1677 Signed-off-by: alltheseas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent d3a5445 commit ed00301

File tree

4 files changed

+56
-2
lines changed

4 files changed

+56
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
### Added
66

7-
- Added new onboarding suggestions based on user-selected interests (Daniel D’Aquino)
7+
- Added hashtag spam filter setting to hide posts with too many hashtags (alltheseas)
8+
- Added new onboarding suggestions based on user-selected interests (Daniel D'Aquino)
89
- Added adjustable max budget setting for Coinos one-click wallets (Daniel D’Aquino)
910
- Added send feature to the wallet view (Daniel D’Aquino)
1011
- Added popover tips to DMs and Notifications toolbars on Trusted Network button (Terry Yiu)

damus/Features/Settings/Models/UserSettingsStore.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,13 @@ class UserSettingsStore: ObservableObject {
138138

139139
@Setting(key: "hide_nsfw_tagged_content", default_value: false)
140140
var hide_nsfw_tagged_content: Bool
141-
141+
142+
@Setting(key: "hide_hashtag_spam", default_value: true)
143+
var hide_hashtag_spam: Bool
144+
145+
@Setting(key: "max_hashtags", default_value: 3)
146+
var max_hashtags: Int
147+
142148
@Setting(key: "reduce_bitcoin_content", default_value: false)
143149
var reduce_bitcoin_content: Bool
144150

damus/Features/Settings/Views/AppearanceSettingsView.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ struct AppearanceSettingsView: View {
3636
@State var showing_enable_animation_alert: Bool = false
3737
@State var enable_animation_toggle_is_user_initiated: Bool = true
3838

39+
var max_hashtags_binding: Binding<Double> {
40+
Binding<Double>(get: {
41+
return Double(settings.max_hashtags)
42+
}, set: {
43+
settings.max_hashtags = Int($0)
44+
})
45+
}
46+
3947
var FontSize: some View {
4048
VStack(alignment: .leading) {
4149
Slider(value: $settings.font_size, in: 0.5...2.0, step: 0.1)
@@ -104,6 +112,14 @@ struct AppearanceSettingsView: View {
104112
.toggleStyle(.switch)
105113
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)
106114
.toggleStyle(.switch)
115+
Toggle(NSLocalizedString("Hide posts with too many hashtags", comment: "Setting to hide notes that contain too many hashtags (spam)"), isOn: $settings.hide_hashtag_spam)
116+
.toggleStyle(.switch)
117+
if settings.hide_hashtag_spam {
118+
VStack(alignment: .leading) {
119+
Text(String(format: NSLocalizedString("Maximum hashtags: %d", comment: "Label showing the maximum number of hashtags allowed before a post is hidden"), settings.max_hashtags))
120+
Slider(value: max_hashtags_binding, in: 1...20, step: 1)
121+
}
122+
}
107123
}
108124

109125
// MARK: - Profiles

damus/Features/Timeline/Models/ContentFilters.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,33 @@ func nsfw_tag_filter(ev: NostrEvent) -> Bool {
5454
return ev.referenced_hashtags.first(where: { t in t.hashtag.caseInsensitiveCompare("nsfw") == .orderedSame }) == nil
5555
}
5656

57+
/// Filter to hide posts with too many hashtags (spam detection)
58+
/// Counts hashtags from the note content text, not just the tags array,
59+
/// since many clients don't properly add hashtag tags.
60+
func hashtag_spam_filter(ev: NostrEvent, max_hashtags: Int) -> Bool {
61+
let content = ev.content
62+
var count = 0
63+
var i = content.startIndex
64+
65+
while i < content.endIndex {
66+
if content[i] == "#" {
67+
let nextIndex = content.index(after: i)
68+
if nextIndex < content.endIndex {
69+
let nextChar = content[nextIndex]
70+
// Check if next char is a valid hashtag start (letter, number, emoji, non-ASCII)
71+
if nextChar.isLetter || nextChar.isNumber || !nextChar.isASCII {
72+
count += 1
73+
if count > max_hashtags {
74+
return false
75+
}
76+
}
77+
}
78+
}
79+
i = content.index(after: i)
80+
}
81+
return true
82+
}
83+
5784
@MainActor
5885
func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEvent) -> Bool) {
5986
return { ev in
@@ -97,6 +124,10 @@ extension ContentFilters {
97124
if damus_state.settings.hide_nsfw_tagged_content {
98125
filters.append(nsfw_tag_filter)
99126
}
127+
if damus_state.settings.hide_hashtag_spam {
128+
let max_hashtags = damus_state.settings.max_hashtags
129+
filters.append({ ev in hashtag_spam_filter(ev: ev, max_hashtags: max_hashtags) })
130+
}
100131
filters.append(get_repost_of_muted_user_filter(damus_state: damus_state))
101132
filters.append(timestamp_filter)
102133
return filters

0 commit comments

Comments
 (0)