damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit 49283f2bb21ebcbfd0093b872c59308ec6d7d4f7
parent 305ee03b0e92b31de533b5b862602f7ab150a364
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Mon, 18 Sep 2023 23:27:15 +0000

filters: add "Do not show #nsfw tagged posts" setting

This commit adds a setting where the user can choose to hide notes with
a #nsfw hashtag. This setting was implemented to allow users to filter
out adult or other unsafe content.

I moved the code logic for content filtering into a new file, and
defined a protocol for content filters. Although the logic is still
simple, this might help in developing a flexible API in case we have
more complex filtering needs in the future.

I also modified the name of the "Appearance" setting to "Appearance and
filters", to make it easier for users to intuitively find this setting.
(Note: Re-translations of this string might be necessary)

**PASS**
**iOS:**
- iOS 17.0 (iPhone 14 Pro)

**Damus:** (This commit)
**Steps:**
1. Follow another account that you control (Account B)
2. On account B, post a note saying "#test this is a test". This note should show up on the home feed.
3. On account B, post a note saying "#nsfw this is a test". This note should NOT show up on the home feed
4. Go to settings and disable the NSFW filter. Go back to the home view. The #nsfw post should now show up.
5. Close app and reopen. NSFW post should still show up (i.e. Setting should be persistent)
6. Unfollow account B
7. Close app and reopen.
8. Follow the "#grownostr" hashtag
9. Turn on the NSFW filter
10. On account B, post a note saying "#grownostr this is a test". This note should show up on the home view.
11. On account B, post a note saying "#grownostr #nsfw this is a test". This note should NOT show up.
12. Double-check the "notes and replies" tab. Note should NOT show up there either.
12. Turn off NSFW filter
13. Note from step 11 should now show up.
14. Go to Universe view and find a post with a hashtag. Remember where the post is.
14. Locally change the tag keyword from "nsfw" to that hashtag (Note: I had to test this way because my posts were not showing up in the Universe view)
15. Turn off the filter. Check post is there, in the Universe view.
16. Turn on the filter. Check post is no longer there in the Universe view. (Check the neighboring posts are the same, to make sure)
17. Bring back the code to its normal state.
18. Search for "#nsfw". Make sure that #nsfw appears (I believe this is ok, because it means the person is purposefully searching for it)

Closes: https://github.com/damus-io/damus/issues/1412
Changelog-Added: Add "Do not show #nsfw tagged posts" setting
Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/ContentView.swift | 23+++++++----------------
Adamus/Models/ContentFilters.swift | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Models/UserSettingsStore.swift | 3+++
Mdamus/Views/ConfigView.swift | 2+-
Mdamus/Views/SearchHomeView.swift | 9+++++++++
Mdamus/Views/Settings/AppearanceSettingsView.swift | 9+++++++++
7 files changed, 91 insertions(+), 17 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -423,6 +423,7 @@ BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; }; D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; }; D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.swift */; }; + D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; }; D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; }; E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */; }; @@ -1104,6 +1105,7 @@ BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectWalletView.swift; sourceTree = "<group>"; }; D2277EE92A089BD5006C3807 /* Router.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Router.swift; sourceTree = "<group>"; }; D71DC1EB2A9129C3006E207C /* PostViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostViewTests.swift; sourceTree = "<group>"; }; + D723C38D2AB8D83400065664 /* ContentFilters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentFilters.swift; sourceTree = "<group>"; }; D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = "<group>"; }; D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrEventTests.swift; sourceTree = "<group>"; }; E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; }; @@ -1287,6 +1289,7 @@ 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */, 3A5E47C42A4A6CF400C0D090 /* Trie.swift */, 3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */, + D723C38D2AB8D83400065664 /* ContentFilters.swift */, ); path = Models; sourceTree = "<group>"; @@ -2861,6 +2864,7 @@ 4C75EFA427FA577B0006080F /* PostView.swift in Sources */, 4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */, 4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */, + D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */, 4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */, 4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */, 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -56,20 +56,6 @@ enum Sheets: Identifiable { } } -enum FilterState : Int { - case posts_and_replies = 1 - case posts = 0 - - func filter(ev: NostrEvent) -> Bool { - switch self { - case .posts: - return ev.known_kind == .boost || !ev.is_reply(.empty) - case .posts_and_replies: - return true - } - } -} - struct ContentView: View { let keypair: Keypair @@ -96,6 +82,11 @@ struct ContentView: View { @StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator() @AppStorage("has_seen_suggested_users") private var hasSeenSuggestedUsers = false let sub_id = UUID().description + var damus_filter: DamusFilter { + get { + return DamusFilter(hide_nsfw_tagged_content: self.damus_state?.settings.hide_nsfw_tagged_content ?? true) + } + } @Environment(\.colorScheme) var colorScheme @@ -114,10 +105,10 @@ struct ContentView: View { // This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why. mystery - contentTimelineView(filter: FilterState.posts.filter) + contentTimelineView(filter: damus_filter.get_filter(.posts)) .tag(FilterState.posts) .id(FilterState.posts) - contentTimelineView(filter: FilterState.posts_and_replies.filter) + contentTimelineView(filter: damus_filter.get_filter(.posts_and_replies)) .tag(FilterState.posts_and_replies) .id(FilterState.posts_and_replies) } diff --git a/damus/Models/ContentFilters.swift b/damus/Models/ContentFilters.swift @@ -0,0 +1,58 @@ +// +// ContentFilters.swift +// damus +// +// Created by Daniel D’Aquino on 2023-09-18. +// + +import Foundation + +protocol ContentFilter { + /// Function that implements the content filtering logic + /// - Parameter ev: The nostr event to be processed + /// - Returns: Must return `true` to show events, and return `false` to hide/filter events + func filter(ev: NostrEvent) -> Bool +} + +/// Simple filter to determine whether to show posts or all posts and replies. +enum FilterState : Int, ContentFilter { + case posts_and_replies = 1 + case posts = 0 + + func filter(ev: NostrEvent) -> Bool { + switch self { + case .posts: + return ev.known_kind == .boost || !ev.is_reply(.empty) + case .posts_and_replies: + return true + } + } +} + +/// Simple filter to determine whether to show posts with #nsfw tags +struct NSFWTagFilter: ContentFilter { + func filter(ev: NostrEvent) -> Bool { + return ev.referenced_hashtags.first(where: { t in t.hashtag == "nsfw" }) == nil + } +} + +/// Generic filter with various tweakable settings +struct DamusFilter: ContentFilter { + let hide_nsfw_tagged_content: Bool + + func filter(ev: NostrEvent) -> Bool { + if self.hide_nsfw_tagged_content { + return NSFWTagFilter().filter(ev: ev) + } + else { + return true + } + } + + func get_filter(_ filter_state: FilterState) -> ((NostrEvent) -> Bool) { + return { ev in + return filter_state.filter(ev: ev) && self.filter(ev: ev) + } + } + +} diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift @@ -109,6 +109,9 @@ class UserSettingsStore: ObservableObject { @Setting(key: "always_show_images", default_value: false) var always_show_images: Bool + + @Setting(key: "hide_nsfw_tagged_content", default_value: false) + var hide_nsfw_tagged_content: Bool @Setting(key: "zap_vibration", default_value: true) var zap_vibration: Bool diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift @@ -41,7 +41,7 @@ struct ConfigView: View { } NavigationLink(value: Route.AppearanceSettings(settings: settings)) { - IconLabel(NSLocalizedString("Appearance", comment: "Section header for text and appearance settings"), img_name: "eye", color: .red) + IconLabel(NSLocalizedString("Appearance and filters", comment: "Section header for text, appearance, and content filter settings"), img_name: "eye", color: .red) } NavigationLink(value: Route.SearchSettings(settings: settings)) { diff --git a/damus/Views/SearchHomeView.swift b/damus/Views/SearchHomeView.swift @@ -14,6 +14,11 @@ struct SearchHomeView: View { @StateObject var model: SearchHomeModel @State var search: String = "" @FocusState private var isFocused: Bool + var damus_filter: DamusFilter { + get { + return DamusFilter(hide_nsfw_tagged_content: self.damus_state.settings.hide_nsfw_tagged_content) + } + } let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) }) @@ -50,6 +55,10 @@ struct SearchHomeView: View { damus: damus_state, show_friend_icon: true, filter: { ev in + if !damus_filter.filter(ev: ev) { + return false + } + if damus_state.muted_threads.isMutedThread(ev, keypair: self.damus_state.keypair) { return false } diff --git a/damus/Views/Settings/AppearanceSettingsView.swift b/damus/Views/Settings/AppearanceSettingsView.swift @@ -85,6 +85,15 @@ struct AppearanceSettingsView: View { clear_kingfisher_cache() } } + + // MARK: - Content filters and moderation + Section( + header: Text(NSLocalizedString("Content filters", comment: "Section title for content filtering/moderation configuration.")), + footer: Text(NSLocalizedString("Notes with the #nsfw tag usually contains adult content or other \"Not safe for work\" content", comment: "Section footer clarifying what #nsfw (not safe for work) tags mean")) + ) { + 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) + } }