diff --git a/damus/Features/Settings/Models/UserSettingsStore.swift b/damus/Features/Settings/Models/UserSettingsStore.swift index 7e8481531..c2b4994b3 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 cf30c2f4a..63b023cdc 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 3c9e15538..c456cfec2 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