mirror of
git://jb55.com/damus
synced 2024-09-30 00:40:45 +00:00
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>
This commit is contained in:
parent
305ee03b0e
commit
49283f2bb2
@ -423,6 +423,7 @@
|
|||||||
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
BAB68BED29543FA3007BA466 /* SelectWalletView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BAB68BEC29543FA3007BA466 /* SelectWalletView.swift */; };
|
||||||
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
|
D2277EEA2A089BD5006C3807 /* Router.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2277EE92A089BD5006C3807 /* Router.swift */; };
|
||||||
D71DC1EC2A9129C3006E207C /* PostViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D71DC1EB2A9129C3006E207C /* PostViewTests.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 */; };
|
D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; };
|
||||||
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
|
D7DEEF2F2A8C021E00E0C99F /* NostrEventTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7DEEF2E2A8C021E00E0C99F /* NostrEventTests.swift */; };
|
||||||
E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4FA1C022A24BB7F00482697 /* SearchSettingsView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
E4FA1C022A24BB7F00482697 /* SearchSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSettingsView.swift; sourceTree = "<group>"; };
|
||||||
@ -1287,6 +1289,7 @@
|
|||||||
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */,
|
3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */,
|
||||||
3A5E47C42A4A6CF400C0D090 /* Trie.swift */,
|
3A5E47C42A4A6CF400C0D090 /* Trie.swift */,
|
||||||
3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */,
|
3A90B1802A4EA3AF00000D94 /* UserSearchCache.swift */,
|
||||||
|
D723C38D2AB8D83400065664 /* ContentFilters.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -2861,6 +2864,7 @@
|
|||||||
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
4C75EFA427FA577B0006080F /* PostView.swift in Sources */,
|
||||||
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
|
4C30AC7229A5677A00E2BD5A /* NotificationsView.swift in Sources */,
|
||||||
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
|
4C1A9A2129DDD3E100516EAC /* KeySettingsView.swift in Sources */,
|
||||||
|
D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */,
|
||||||
4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */,
|
4C32B95A2A9AD44700DC3548 /* Verifiable.swift in Sources */,
|
||||||
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
|
4C73C5142A4437C10062CAC0 /* ZapUserView.swift in Sources */,
|
||||||
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
|
501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */,
|
||||||
|
@ -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 {
|
struct ContentView: View {
|
||||||
let keypair: Keypair
|
let keypair: Keypair
|
||||||
|
|
||||||
@ -96,6 +82,11 @@ struct ContentView: View {
|
|||||||
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
@StateObject var navigationCoordinator: NavigationCoordinator = NavigationCoordinator()
|
||||||
@AppStorage("has_seen_suggested_users") private var hasSeenSuggestedUsers = false
|
@AppStorage("has_seen_suggested_users") private var hasSeenSuggestedUsers = false
|
||||||
let sub_id = UUID().description
|
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
|
@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.
|
// This is needed or else there is a bug when switching from the 3rd or 2nd tab to first. no idea why.
|
||||||
mystery
|
mystery
|
||||||
|
|
||||||
contentTimelineView(filter: FilterState.posts.filter)
|
contentTimelineView(filter: damus_filter.get_filter(.posts))
|
||||||
.tag(FilterState.posts)
|
.tag(FilterState.posts)
|
||||||
.id(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)
|
.tag(FilterState.posts_and_replies)
|
||||||
.id(FilterState.posts_and_replies)
|
.id(FilterState.posts_and_replies)
|
||||||
}
|
}
|
||||||
|
58
damus/Models/ContentFilters.swift
Normal file
58
damus/Models/ContentFilters.swift
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -110,6 +110,9 @@ class UserSettingsStore: ObservableObject {
|
|||||||
@Setting(key: "always_show_images", default_value: false)
|
@Setting(key: "always_show_images", default_value: false)
|
||||||
var always_show_images: Bool
|
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)
|
@Setting(key: "zap_vibration", default_value: true)
|
||||||
var zap_vibration: Bool
|
var zap_vibration: Bool
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ struct ConfigView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
NavigationLink(value: Route.AppearanceSettings(settings: settings)) {
|
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)) {
|
NavigationLink(value: Route.SearchSettings(settings: settings)) {
|
||||||
|
@ -14,6 +14,11 @@ struct SearchHomeView: View {
|
|||||||
@StateObject var model: SearchHomeModel
|
@StateObject var model: SearchHomeModel
|
||||||
@State var search: String = ""
|
@State var search: String = ""
|
||||||
@FocusState private var isFocused: Bool
|
@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) })
|
let preferredLanguages = Set(Locale.preferredLanguages.map { localeToLanguage($0) })
|
||||||
|
|
||||||
@ -50,6 +55,10 @@ struct SearchHomeView: View {
|
|||||||
damus: damus_state,
|
damus: damus_state,
|
||||||
show_friend_icon: true,
|
show_friend_icon: true,
|
||||||
filter: { ev in
|
filter: { ev in
|
||||||
|
if !damus_filter.filter(ev: ev) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
if damus_state.muted_threads.isMutedThread(ev, keypair: self.damus_state.keypair) {
|
if damus_state.muted_threads.isMutedThread(ev, keypair: self.damus_state.keypair) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -86,6 +86,15 @@ struct AppearanceSettingsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
.navigationTitle(NSLocalizedString("Appearance", comment: "Navigation title for text and appearance settings."))
|
.navigationTitle(NSLocalizedString("Appearance", comment: "Navigation title for text and appearance settings."))
|
||||||
|
Loading…
Reference in New Issue
Block a user