diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 90cc34f9..15da775b 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -423,9 +423,11 @@ B501062D2B363036003874F5 /* AuthIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B501062C2B363036003874F5 /* AuthIntegrationTests.swift */; }; B51C1CEA2B55A60A00E312A9 /* AddMuteItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51C1CE82B55A60A00E312A9 /* AddMuteItemView.swift */; }; B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B51C1CE92B55A60A00E312A9 /* MuteDurationMenu.swift */; }; + B533694E2B66D791008A805E /* MutelistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533694D2B66D791008A805E /* MutelistManager.swift */; }; B57B4C622B312BD700A232C0 /* ReconnectRelaysNotify.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */; }; B57B4C642B312BFA00A232C0 /* RelayAuthenticationDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */; }; B57B4C662B312C3700A232C0 /* NostrAuth.swift in Sources */ = {isa = PBXBuildFile; fileRef = B57B4C652B312C3700A232C0 /* NostrAuth.swift */; }; + B59CAD4D2B688D1000677E8B /* MutelistManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B533694D2B66D791008A805E /* MutelistManager.swift */; }; B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */; }; B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5B4D1422B37D47600844320 /* NdbExtensions.swift */; }; BA0F0A6F2B36207E001641B2 /* CameraMediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */; }; @@ -1322,6 +1324,7 @@ B501062C2B363036003874F5 /* AuthIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthIntegrationTests.swift; sourceTree = ""; usesTabs = 0; }; B51C1CE82B55A60A00E312A9 /* AddMuteItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddMuteItemView.swift; sourceTree = ""; }; B51C1CE92B55A60A00E312A9 /* MuteDurationMenu.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MuteDurationMenu.swift; sourceTree = ""; }; + B533694D2B66D791008A805E /* MutelistManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutelistManager.swift; sourceTree = ""; usesTabs = 0; }; B57B4C612B312BD700A232C0 /* ReconnectRelaysNotify.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReconnectRelaysNotify.swift; sourceTree = ""; }; B57B4C632B312BFA00A232C0 /* RelayAuthenticationDetail.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RelayAuthenticationDetail.swift; sourceTree = ""; }; B57B4C652B312C3700A232C0 /* NostrAuth.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NostrAuth.swift; sourceTree = ""; }; @@ -1602,6 +1605,7 @@ D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */, D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */, B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */, + B533694D2B66D791008A805E /* MutelistManager.swift */, ); path = Models; sourceTree = ""; @@ -3274,6 +3278,7 @@ D7CB5D5C2B1176B200AD4105 /* MediaUploader.swift in Sources */, 4C1253562A76C8C60004F4B8 /* BroadcastNotify.swift in Sources */, 4C3BEFD42819DE8F00B3DE84 /* NostrKind.swift in Sources */, + B533694E2B66D791008A805E /* MutelistManager.swift in Sources */, 4C32B9532A9AD44700DC3548 /* Verifier.swift in Sources */, BA10192F2B449556009C57DA /* CameraPreview.swift in Sources */, 4C3EA66028FF5E7700C48A62 /* node_id.c in Sources */, @@ -3575,6 +3580,7 @@ D7CE1B1E2B0BE190002EDAD4 /* midl.c in Sources */, D7CB5D3C2B1130C600AD4105 /* LocalNotification.swift in Sources */, D7CE1B2D2B0BE250002EDAD4 /* take.c in Sources */, + B59CAD4D2B688D1000677E8B /* MutelistManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/damus/Models/MutelistManager.swift b/damus/Models/MutelistManager.swift new file mode 100644 index 00000000..90a640fe --- /dev/null +++ b/damus/Models/MutelistManager.swift @@ -0,0 +1,162 @@ +// +// MutelistManager.swift +// damus +// +// Created by Charlie Fish on 1/28/24. +// + +import Foundation + +class MutelistManager { + private(set) var event: NostrEvent? = nil + + var users: Set = [] + var hashtags: Set = [] + var threads: Set = [] + var words: Set = [] + + func refresh_sets() { + guard let referenced_mute_items = event?.referenced_mute_items else { return } + + var new_users: Set = [] + var new_hashtags: Set = [] + var new_threads: Set = [] + var new_words: Set = [] + + for mute_item in referenced_mute_items { + switch mute_item { + case .user: + new_users.insert(mute_item) + case .hashtag: + new_hashtags.insert(mute_item) + case .word: + new_words.insert(mute_item) + case .thread: + new_threads.insert(mute_item) + } + } + + users = new_users + hashtags = new_hashtags + threads = new_threads + words = new_words + } + + func is_muted(_ item: MuteItem) -> Bool { + switch item { + case .user(_, _): + return users.contains(item) + case .hashtag(_, _): + return hashtags.contains(item) + case .word(_, _): + return words.contains(item) + case .thread(_, _): + return threads.contains(item) + } + } + + func is_event_muted(_ ev: NostrEvent, keypair: Keypair? = nil) -> Bool { + return event_muted_reason(ev, keypair: keypair) != nil + } + + func set_mutelist(_ ev: NostrEvent) { + let oldlist = self.event + self.event = ev + + let old: Set = oldlist?.mute_list ?? Set() + let new: Set = ev.mute_list ?? Set() + let diff = old.symmetricDifference(new) + + var new_mutes = Set() + var new_unmutes = Set() + + for d in diff { + if new.contains(d) { + add_mute_item(d) + new_mutes.insert(d) + } else { + remove_mute_item(d) + new_unmutes.insert(d) + } + } + + if new_mutes.count > 0 { + notify(.new_mutes(new_mutes)) + } + + if new_unmutes.count > 0 { + notify(.new_unmutes(new_unmutes)) + } + } + + private func add_mute_item(_ item: MuteItem) { + switch item { + case .user(_, _): + users.insert(item) + case .hashtag(_, _): + hashtags.insert(item) + case .word(_, _): + words.insert(item) + case .thread(_, _): + threads.insert(item) + } + } + + private func remove_mute_item(_ item: MuteItem) { + switch item { + case .user(_, _): + users.remove(item) + case .hashtag(_, _): + hashtags.remove(item) + case .word(_, _): + words.remove(item) + case .thread(_, _): + threads.remove(item) + } + } + + + /// Check if an event is muted given a collection of ``MutedItem``. + /// + /// - Parameter ev: The ``NostrEvent`` that you want to check the muted reason for. + /// - Returns: The ``MuteItem`` that matched the event. Or `nil` if the event is not muted. + func event_muted_reason(_ ev: NostrEvent, keypair: Keypair? = nil) -> MuteItem? { + // Events from the current user should not be muted. + guard keypair?.pubkey != ev.pubkey else { return nil } + + // Check if user is muted + let check_user_item = MuteItem.user(ev.pubkey, nil) + if users.contains(check_user_item) { + return check_user_item + } + + // Check if hashtag is muted + for hashtag in ev.referenced_hashtags { + let check_hashtag_item = MuteItem.hashtag(hashtag, nil) + if hashtags.contains(check_hashtag_item) { + return check_hashtag_item + } + } + + // Check if thread is muted + for thread_id in ev.referenced_ids { + let check_thread_item = MuteItem.thread(thread_id, nil) + if threads.contains(check_thread_item) { + return check_thread_item + } + } + + // Check if word is muted + if let keypair, let content: String = ev.maybe_get_content(keypair)?.lowercased() { + for word in words { + if case .word(let string, _) = word { + if content.contains(string.lowercased()) { + return word + } + } + } + } + + return nil + } +}