From 068b89d0877965afff4f474063ea71c663e9ccef Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Wed, 17 Jan 2024 18:17:36 -0700 Subject: [PATCH 01/18] mute: adding new structs/enums for new mute list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adding MuteItem & DamusDuration - Changing RefId hashtag associated type from TagElem to Hashtag - This is done because in MuteItem, we can not create a RefId.hashtag TagElem instance since we don’t have a note associated with a given hashtag mute item. Related: https://github.com/damus-io/damus/issues/1718 Related: https://github.com/damus-io/damus/issues/856 Lighting Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 14 ++ damus/ContentView.swift | 2 +- damus/Models/Contacts+.swift | 2 +- damus/Models/MuteItem.swift | 208 ++++++++++++++++++++++++++ damus/Nostr/ReferencedId.swift | 6 +- damus/Types/DamusDuration.swift | 38 +++++ damusTests/Models/MuteItemTests.swift | 58 +++++++ 7 files changed, 323 insertions(+), 5 deletions(-) create mode 100644 damus/Models/MuteItem.swift create mode 100644 damus/Types/DamusDuration.swift create mode 100644 damusTests/Models/MuteItemTests.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 7588b62f..6829fbef 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -424,9 +424,13 @@ 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 */; }; + 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 */; }; BA10192F2B449556009C57DA /* CameraPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA10192E2B449556009C57DA /* CameraPreview.swift */; }; + B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; }; + B5C60C212B530D5600C5ECA7 /* MuteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */; }; + B5C60C232B532A8700C5ECA7 /* DamusDuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */; }; BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA3759892ABCCDE30018D73B /* ImageResizer.swift */; }; BA37598D2ABCCE500018D73B /* PhotoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */; }; BA37598E2ABCCE500018D73B /* VideoCaptureProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */; }; @@ -1318,9 +1322,12 @@ 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 = ""; }; + B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItemTests.swift; sourceTree = ""; usesTabs = 0; }; B5B4D1422B37D47600844320 /* NdbExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NdbExtensions.swift; sourceTree = ""; usesTabs = 0; }; BA0F0A6E2B36207E001641B2 /* CameraMediaView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CameraMediaView.swift; sourceTree = ""; }; BA10192E2B449556009C57DA /* CameraPreview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraPreview.swift; sourceTree = ""; }; + B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteItem.swift; sourceTree = ""; usesTabs = 0; }; + B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusDuration.swift; sourceTree = ""; usesTabs = 0; }; BA3759892ABCCDE30018D73B /* ImageResizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageResizer.swift; sourceTree = ""; }; BA37598B2ABCCE500018D73B /* PhotoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoCaptureProcessor.swift; sourceTree = ""; }; BA37598C2ABCCE500018D73B /* VideoCaptureProcessor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VideoCaptureProcessor.swift; sourceTree = ""; }; @@ -1591,6 +1598,7 @@ D7EDED1D2B11797D0018B19C /* LongformEvent.swift */, D7EDED322B12ACAE0018B19C /* DamusUserDefaults.swift */, D74AAFC12B153395006CF0F4 /* HeadlessDamusState.swift */, + B5C60C1F2B530D5100C5ECA7 /* MuteItem.swift */, ); path = Models; sourceTree = ""; @@ -2294,6 +2302,7 @@ 4CC14FED2A73FCBB007AEB17 /* Ids */, 7527271D2A93FF0100214108 /* Block.swift */, D798D21D2B0858BB00234419 /* MigratedTypes.swift */, + B5C60C222B532A8700C5ECA7 /* DamusDuration.swift */, ); path = Types; sourceTree = ""; @@ -2680,6 +2689,7 @@ children = ( F944F56D29EA9CCC0067B3BF /* DamusParseContentTests.swift */, 75AD872A2AA23A460085EF2C /* Block+Tests.swift */, + B5A75C292B546D94007AFBC0 /* MuteItemTests.swift */, ); path = Models; sourceTree = ""; @@ -2943,6 +2953,7 @@ ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */, 4C3AC79D2833036D00E1F516 /* FollowingView.swift in Sources */, 5CF72FC229B9142F00124A13 /* ShareAction.swift in Sources */, + B5C60C232B532A8700C5ECA7 /* DamusDuration.swift in Sources */, 4C32B9522A9AD44700DC3548 /* Message.swift in Sources */, 4C8D1A6C29F1DFC200ACDF75 /* FriendIcon.swift in Sources */, 4C30AC7829A577AB00E2BD5A /* EventCache.swift in Sources */, @@ -3050,6 +3061,7 @@ 4C7D09602A098C5D00943473 /* WalletView.swift in Sources */, 4CB8838F296F781C00DC99E7 /* ReactionsView.swift in Sources */, BA4AB0B02A63B94D0070A32A /* EmojiListItemView.swift in Sources */, + B5C60C202B530D5100C5ECA7 /* MuteItem.swift in Sources */, 4C75EFB328049D640006080F /* NostrEvent.swift in Sources */, 4C32B9582A9AD44700DC3548 /* VeriferOptions.swift in Sources */, D74AAFC22B153395006CF0F4 /* HeadlessDamusState.swift in Sources */, @@ -3387,6 +3399,7 @@ B5B4D1432B37D47600844320 /* NdbExtensions.swift in Sources */, 3ACBCB78295FE5C70037388A /* TimeAgoTests.swift in Sources */, D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */, + B5A75C2A2B546D94007AFBC0 /* MuteItemTests.swift in Sources */, 4C4F14A72A2A61A30045A0B9 /* NostrScriptTests.swift in Sources */, D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */, 4C3EA67B28FF7B3900C48A62 /* InvoiceTests.swift in Sources */, @@ -3548,6 +3561,7 @@ D7CB5D472B11718700AD4105 /* Wallet.swift in Sources */, D7CE1B412B0BE719002EDAD4 /* FlatBuffersUtils.swift in Sources */, D7CB5D482B11719300AD4105 /* Profiles.swift in Sources */, + B5C60C212B530D5600C5ECA7 /* MuteItem.swift in Sources */, D798D2262B085C4200234419 /* Bech32.swift in Sources */, D7CE1B482B0BE719002EDAD4 /* Message.swift in Sources */, D7CB5D462B11703D00AD4105 /* Notify.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 6dde4649..3178ac29 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -1116,7 +1116,7 @@ func on_open_url(state: DamusState, url: URL, result: @escaping (OpenResult?) -> result(.event(ev)) } case .hashtag(let ht): - result(.filter(.filter_hashtag([ht.string()]))) + result(.filter(.filter_hashtag([ht.hashtag]))) case .param, .quote: // doesn't really make sense here break diff --git a/damus/Models/Contacts+.swift b/damus/Models/Contacts+.swift index 30f138a6..03a8339e 100644 --- a/damus/Models/Contacts+.swift +++ b/damus/Models/Contacts+.swift @@ -109,7 +109,7 @@ func is_already_following(contacts: NostrEvent, follow: FollowRef) -> Bool { return contacts.references.contains { ref in switch (ref, follow) { case let (.hashtag(ht), .hashtag(follow_ht)): - return ht.string() == follow_ht + return ht.hashtag == follow_ht case let (.pubkey(pk), .pubkey(follow_pk)): return pk == follow_pk case (.hashtag, .pubkey), (.pubkey, .hashtag), diff --git a/damus/Models/MuteItem.swift b/damus/Models/MuteItem.swift new file mode 100644 index 00000000..dcc83e5a --- /dev/null +++ b/damus/Models/MuteItem.swift @@ -0,0 +1,208 @@ +// +// MuteItem.swift +// damus +// +// Created by Charlie Fish on 1/13/24. +// + +import Foundation + +/// Represents an item that is muted. +enum MuteItem: Hashable, Equatable { + /// A user that is muted. + /// + /// The associated type is the ``Pubkey`` that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires. + case user(Pubkey, Date?) + + /// A hashtag that is muted. + /// + /// The associated type is the hashtag string that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires. + case hashtag(Hashtag, Date?) + + /// A word/phrase that is muted. + /// + /// The associated type is the word/phrase that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires. + case word(String, Date?) + + /// A thread that is muted. + /// + /// The associated type is the `id` of the note that is muted. The second associated type is the date that the item should expire at. If no date is supplied, assume the muted item should remain active until it expires. + case thread(NoteId, Date?) + + func is_expired() -> Bool { + switch self { + case .user(_, let expiration_date): + return expiration_date ?? .distantFuture < Date() + case .hashtag(_, let expiration_date): + return expiration_date ?? .distantFuture < Date() + case .word(_, let expiration_date): + return expiration_date ?? .distantFuture < Date() + case .thread(_, let expiration_date): + return expiration_date ?? .distantFuture < Date() + } + } + + static func == (lhs: MuteItem, rhs: MuteItem) -> Bool { + // lhs is the item we want to check (ie. the item the user is attempting to display) + // rhs is the item we want to check against (ie. the item in the mute list) + + switch (lhs, rhs) { + case (.user(let lhs_pubkey, _), .user(let rhs_pubkey, let rhs_expiration_date)): + return lhs_pubkey == rhs_pubkey && !rhs.is_expired() + case (.hashtag(let lhs_hashtag, _), .hashtag(let rhs_hashtag, let rhs_expiration_date)): + return lhs_hashtag == rhs_hashtag && !rhs.is_expired() + case (.word(let lhs_word, _), .word(let rhs_word, let rhs_expiration_date)): + return lhs_word == rhs_word && !rhs.is_expired() + case (.thread(let lhs_thread, _), .thread(let rhs_thread, let rhs_expiration_date)): + return lhs_thread == rhs_thread && !rhs.is_expired() + default: + return false + } + } + + private var refTags: [String] { + switch self { + case .user(let pubkey, _): + return RefId.pubkey(pubkey).tag + case .hashtag(let hashtag, _): + return RefId.hashtag(hashtag).tag + case .word(let string, _): + return ["word", string] + case .thread(let noteId, _): + return RefId.event(noteId).tag + } + } + + var tag: [String] { + var tag = self.refTags + + switch self { + case .user(_, let date): + if let date { + tag.append("\(Int(date.timeIntervalSince1970))") + } + case .hashtag(_, let date): + if let date { + tag.append("\(Int(date.timeIntervalSince1970))") + } + case .word(_, let date): + if let date { + tag.append("\(Int(date.timeIntervalSince1970))") + } + case .thread(_, let date): + if let date { + tag.append("\(Int(date.timeIntervalSince1970))") + } + } + + return tag + } + + var title: String { + switch self { + case .user: + return "user" + case .hashtag: + return "hashtag" + case .word: + return "word" + case .thread: + return "thread" + } + } + + init?(_ tag: [String]) { + guard let tag_id = tag.first else { return nil } + guard let tag_content = tag[safe: 1] else { return nil } + + let tag_expiration_date: Date? = { + if let tag_expiration_string: String = tag[safe: 2], + let tag_expiration_number: TimeInterval = Double(tag_expiration_string) { + return Date(timeIntervalSince1970: tag_expiration_number) + } else { + return nil + } + }() + + switch tag_id { + case "p": + guard let pubkey = Pubkey(hex: tag_content) else { return nil } + self = MuteItem.user(pubkey, tag_expiration_date) + break + case "t": + self = MuteItem.hashtag(Hashtag(hashtag: tag_content), tag_expiration_date) + break + case "word": + self = MuteItem.word(tag_content, tag_expiration_date) + break + case "thread": + guard let note_id = NoteId(hex: tag_content) else { return nil } + self = MuteItem.thread(note_id, tag_expiration_date) + break + default: + return nil + } + } +} + +extension Collection where Element == MuteItem { + /// 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) -> MuteItem? { + return self.first { muted_item in + switch muted_item { + case .user(let pubkey, let expiration_date): + return pubkey == ev.pubkey && !muted_item.is_expired() + case .hashtag(let hashtag, let expiration_date): + return ev.referenced_hashtags.contains(hashtag) && !muted_item.is_expired() + case .word(let word, let expiration_date): + return ev.content.lowercased().contains(word.lowercased()) && !muted_item.is_expired() + case .thread(let note_id, let expiration_date): + return ev.referenced_ids.contains(note_id) && !muted_item.is_expired() + } + } + } + + var users: [Pubkey] { + return self.compactMap { muted_item in + if case .user(let pubkey, _) = muted_item, + !muted_item.is_expired() { + return pubkey + } else { + return nil + } + } + } + var hashtags: [Hashtag] { + return self.compactMap { muted_item in + if case .hashtag(let hashtag, _) = muted_item, + !muted_item.is_expired() { + return hashtag + } else { + return nil + } + } + } + var words: [String] { + return self.compactMap { muted_item in + if case .word(let str, _) = muted_item, + !muted_item.is_expired() { + return str + } else { + return nil + } + } + } + var threads: [NoteId] { + return self.compactMap { muted_item in + if case .thread(let note_id, _) = muted_item, + !muted_item.is_expired() { + return note_id + } else { + return nil + } + } + } +} diff --git a/damus/Nostr/ReferencedId.swift b/damus/Nostr/ReferencedId.swift index abf0fc4f..3d45322a 100644 --- a/damus/Nostr/ReferencedId.swift +++ b/damus/Nostr/ReferencedId.swift @@ -119,7 +119,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable { case event(NoteId) case pubkey(Pubkey) case quote(QuoteId) - case hashtag(TagElem) + case hashtag(Hashtag) case param(TagElem) case naddr(NAddr) @@ -155,7 +155,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable { case .event(let noteId): return noteId.hex() case .pubkey(let pubkey): return pubkey.hex() case .quote(let quote): return quote.hex() - case .hashtag(let string): return string.string() + case .hashtag(let string): return string.hashtag case .param(let string): return string.string() case .naddr(let naddr): return naddr.kind.description + ":" + naddr.author.hex() + ":" + naddr.identifier @@ -176,7 +176,7 @@ enum RefId: TagConvertible, TagKeys, Equatable, Hashable { case .e: return t1.id().map({ .event(NoteId($0)) }) case .p: return t1.id().map({ .pubkey(Pubkey($0)) }) case .q: return t1.id().map({ .quote(QuoteId($0)) }) - case .t: return .hashtag(t1) + case .t: return .hashtag(Hashtag(hashtag: t1.string())) case .d: return .param(t1) case .a: return .naddr(NAddr(identifier: "", author: Pubkey(Data()), relays: [], kind: 0)) } diff --git a/damus/Types/DamusDuration.swift b/damus/Types/DamusDuration.swift new file mode 100644 index 00000000..fa499b41 --- /dev/null +++ b/damus/Types/DamusDuration.swift @@ -0,0 +1,38 @@ +// +// DamusDuration.swift +// damus +// +// Created by Charlie Fish on 1/13/24. +// + +import Foundation + +enum DamusDuration: CaseIterable { + case day + case week + case month + + var title: String { + switch self { + case .day: + return NSLocalizedString("24 hours", comment: "A duration of 24 hours/1 day to be shown to the user. Most likely in the context of how long they want to mute a piece of content for.") + case .week: + return NSLocalizedString("1 week", comment: "A duration of 1 week to be shown to the user. Most likely in the context of how long they want to mute a piece of content for.") + case .month: + return NSLocalizedString("1 month", comment: "A duration of 1 month to be shown to the user. Most likely in the context of how long they want to mute a piece of content for.") + } + } + + var date_from_now: Date? { + let current_date = Date() + + switch self { + case .day: + return Calendar.current.date(byAdding: .day, value: 1, to: current_date) + case .week: + return Calendar.current.date(byAdding: .day, value: 7, to: current_date) + case .month: + return Calendar.current.date(byAdding: .month, value: 1, to: current_date) + } + } +} diff --git a/damusTests/Models/MuteItemTests.swift b/damusTests/Models/MuteItemTests.swift new file mode 100644 index 00000000..c8f0c3bb --- /dev/null +++ b/damusTests/Models/MuteItemTests.swift @@ -0,0 +1,58 @@ +// +// MuteItemTests.swift +// damusTests +// +// Created by Charlie Fish on 1/14/24. +// + +import XCTest +@testable import damus + +class MuteItemTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + // MARK: - `is_expired` + func test_hashtag_is_expired() throws { + XCTAssertTrue(MuteItem.hashtag(Hashtag(hashtag: "test"), Date(timeIntervalSince1970: 0)).is_expired()) + XCTAssertTrue(MuteItem.hashtag(Hashtag(hashtag: "test"), .distantPast).is_expired()) + XCTAssertFalse(MuteItem.hashtag(Hashtag(hashtag: "test"), .distantFuture).is_expired()) + } + func test_user_is_expired() throws { + XCTAssertTrue(MuteItem.user(test_pubkey, Date(timeIntervalSince1970: 0)).is_expired()) + XCTAssertTrue(MuteItem.user(test_pubkey, .distantPast).is_expired()) + XCTAssertFalse(MuteItem.user(test_pubkey, .distantFuture).is_expired()) + } + func test_word_is_expired() throws { + XCTAssertTrue(MuteItem.word("test", Date(timeIntervalSince1970: 0)).is_expired()) + XCTAssertTrue(MuteItem.word("test", .distantPast).is_expired()) + XCTAssertFalse(MuteItem.word("test", .distantFuture).is_expired()) + } + func test_thread_is_expired() throws { + XCTAssertTrue(MuteItem.thread(test_note.id, Date(timeIntervalSince1970: 0)).is_expired()) + XCTAssertTrue(MuteItem.thread(test_note.id, .distantPast).is_expired()) + XCTAssertFalse(MuteItem.thread(test_note.id, .distantFuture).is_expired()) + } + + + // MARK: - `tag` + func test_hashtag_tag() throws { + XCTAssertEqual(MuteItem.hashtag(Hashtag(hashtag: "test"), nil).tag, ["t", "test"]) + XCTAssertEqual(MuteItem.hashtag(Hashtag(hashtag: "test"), Date(timeIntervalSince1970: 1704067200)).tag, ["t", "test", "1704067200"]) + } + func test_user_tag() throws { + XCTAssertEqual(MuteItem.user(test_pubkey, Date(timeIntervalSince1970: 1704067200)).tag, ["p", test_pubkey.hex(), "1704067200"]) + } + func test_word_tag() throws { + XCTAssertEqual(MuteItem.word("test", Date(timeIntervalSince1970: 1704067200)).tag, ["word", "test", "1704067200"]) + } + func test_thread_tag() throws { + XCTAssertEqual(MuteItem.thread(test_note.id, Date(timeIntervalSince1970: 1704067200)).tag, ["e", test_note.id.hex(), "1704067200"]) + } +} From 6d2c3824699936effe6681bb439d7aee65d960a5 Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Wed, 17 Jan 2024 18:17:37 -0700 Subject: [PATCH 02/18] mute: add new UI views for new mute list - Adding MuteDurationMenu & AddMuteItemView - In a future patch I will update AddMuteItemView to actually update and relay the new mute list Related: https://github.com/damus-io/damus/issues/1718 Related: https://github.com/damus-io/damus/issues/856 Lighting Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 8 ++ damus/Views/Muting/AddMuteItemView.swift | 102 ++++++++++++++++++++++ damus/Views/Muting/MuteDurationMenu.swift | 40 +++++++++ 3 files changed, 150 insertions(+) create mode 100644 damus/Views/Muting/AddMuteItemView.swift create mode 100644 damus/Views/Muting/MuteDurationMenu.swift diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 6829fbef..7ec805f6 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -421,6 +421,8 @@ 9CA876E229A00CEA0003B9A3 /* AttachMediaUtility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */; }; ADFE73552AD4793100EC7326 /* QRScanNSECView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */; }; 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 */; }; 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 */; }; @@ -1319,6 +1321,8 @@ 9CA876E129A00CE90003B9A3 /* AttachMediaUtility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachMediaUtility.swift; sourceTree = ""; }; ADFE73542AD4793100EC7326 /* QRScanNSECView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QRScanNSECView.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -2556,6 +2560,8 @@ 4CF0ABDF2981A83000D66079 /* Muting */ = { isa = PBXGroup; children = ( + B51C1CE82B55A60A00E312A9 /* AddMuteItemView.swift */, + B51C1CE92B55A60A00E312A9 /* MuteDurationMenu.swift */, 4CF0ABE02981A83900D66079 /* MutelistView.swift */, ); path = Muting; @@ -3033,6 +3039,7 @@ F7F0BA272978E54D009531F3 /* ParticipantsView.swift in Sources */, 4CF0ABE32981BC7D00D66079 /* UserView.swift in Sources */, 4CE0E2AF29A2E82100DB4CA2 /* EventHolder.swift in Sources */, + B51C1CEA2B55A60A00E312A9 /* AddMuteItemView.swift in Sources */, 4C5D5C992A6AF8F80024563C /* NdbNote.swift in Sources */, 4CF0ABF029857E9200D66079 /* Bech32Object.swift in Sources */, 4C3D52B8298DB5C6001C5831 /* TextEvent.swift in Sources */, @@ -3295,6 +3302,7 @@ 50A60D142A28BEEE00186190 /* RelayLog.swift in Sources */, D7EDED212B117DCA0018B19C /* SequenceUtils.swift in Sources */, BA37598A2ABCCDE40018D73B /* ImageResizer.swift in Sources */, + B51C1CEB2B55A60A00E312A9 /* MuteDurationMenu.swift in Sources */, 4CB88389296AF99A00DC99E7 /* EventDetailBar.swift in Sources */, 4C32B9512A9AD44700DC3548 /* FlatbuffersErrors.swift in Sources */, 4CE8794E2996B16A00F758CC /* RelayToggle.swift in Sources */, diff --git a/damus/Views/Muting/AddMuteItemView.swift b/damus/Views/Muting/AddMuteItemView.swift new file mode 100644 index 00000000..039770d6 --- /dev/null +++ b/damus/Views/Muting/AddMuteItemView.swift @@ -0,0 +1,102 @@ +// +// AddMuteItemView.swift +// damus +// +// Created by Charlie Fish on 1/10/24. +// +import SwiftUI + +struct AddMuteItemView: View { + let state: DamusState + @State var new_text: String = "" + @State var expiration: DamusDuration? + + @Environment(\.dismiss) var dismiss + + var body: some View { + VStack { + Text("Add mute item", comment: "Title text to indicate user to an add an item to their mutelist.") + .font(.system(size: 20, weight: .bold)) + .padding(.vertical) + + Divider() + .padding(.bottom) + + Picker(selection: $expiration) { + Text("Indefinite", comment: "Mute a given item indefinitly (until user unmutes it). As opposed to muting the item for a given period of time.") + ForEach(DamusDuration.allCases, id: \.self) { duration in + Text(duration.title).tag(duration) + } + } label: { + Text("Duration", comment: "The duration in which to mute the given item.") + } + + + HStack { + Label("", image: "copy2") + .onTapGesture { + if let pasted_text = UIPasteboard.general.string { + self.new_text = pasted_text + } + } + TextField(NSLocalizedString("npub, #hashtag, phrase", comment: "Placeholder example for relay server address."), text: $new_text) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + + Label("", image: "close-circle") + .foregroundColor(.accentColor) + .opacity((new_text == "") ? 0.0 : 1.0) + .onTapGesture { + self.new_text = "" + } + } + .padding(10) + .background(.secondary.opacity(0.2)) + .cornerRadius(10) + + Button(action: { + let expiration_date: Date? = self.expiration?.date_from_now + let mute_item: MuteItem? = { + if new_text.starts(with: "npub") { + if let pubkey: Pubkey = bech32_pubkey_decode(new_text) { + return .user(pubkey, expiration_date) + } else { + return nil + } + } else if new_text.starts(with: "#") { + // Remove the starting `#` character + new_text.removeFirst() + return .hashtag(Hashtag(hashtag: new_text), expiration_date) + } else { + return .word(new_text, expiration_date) + } + }() + + // @TODO: in future patch - actually update & relay the new mute list + + new_text = "" + + UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) + + dismiss() + }) { + HStack { + Text(verbatim: "Add mute item") + .bold() + } + .frame(minWidth: 300, maxWidth: .infinity, alignment: .center) + } + .buttonStyle(GradientButtonStyle(padding: 10)) + .padding(.vertical) + + Spacer() + } + .padding() + } +} + +struct AddMuteItemView_Previews: PreviewProvider { + static var previews: some View { + AddMuteItemView(state: test_damus_state) + } +} diff --git a/damus/Views/Muting/MuteDurationMenu.swift b/damus/Views/Muting/MuteDurationMenu.swift new file mode 100644 index 00000000..11498264 --- /dev/null +++ b/damus/Views/Muting/MuteDurationMenu.swift @@ -0,0 +1,40 @@ +// +// MuteDurationMenu.swift +// damus +// +// Created by Charlie Fish on 1/14/24. +// + +import SwiftUI + +struct MuteDurationMenu: View { + var action: (DamusDuration?) -> Void + @ViewBuilder var label: () -> T + + var body: some View { + Menu { + Button { + action(nil) + } label: { + Text("Indefinite", comment: "Mute a given item indefinitly (until user unmutes it). As opposed to muting the item for a given period of time.") + } + ForEach(DamusDuration.allCases, id: \.self) { duration in + Button { + action(duration) + } label: { + Text("\(duration.title)") + } + } + } label: { + self.label() + } + } +} + +#Preview { + MuteDurationMenu { _ in + + } label: { + Text("Mute hashtag") + } +} From f36646116e806cbc64c44f2cf77d689cd021a0ff Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Wed, 17 Jan 2024 18:17:38 -0700 Subject: [PATCH 03/18] mute: migrate Lists.swift to use new MuteItem This patch depends on: Adding new structs/enums for new mute list - Rewrites Lists.swift to use new mute list option - This leads to a lot of changes for changing the type from RefId to the new MuteItem - Update & relay new mute list in AddMuteItemView.swift (fixing previous patch TODO) - Renames `list` to `list_deprecated` - We need to keep this since existing users might have an old mute list Related: https://github.com/damus-io/damus/issues/1718 Related: https://github.com/damus-io/damus/issues/856 Lighting Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/ContentView.swift | 4 +- damus/Models/HomeModel.swift | 7 ++- damus/Models/MuteItem.swift | 7 +++ damus/Nostr/NostrEvent.swift | 12 ++++ damus/Nostr/NostrKind.swift | 3 +- damus/Util/Lists.swift | 76 +++++++----------------- damus/Views/Muting/AddMuteItemView.swift | 14 ++++- damus/Views/Muting/MutelistView.swift | 2 +- damus/Views/Profile/ProfileView.swift | 2 +- damusTests/ListTests.swift | 42 +++++++------ nostrdb/NdbNote.swift | 4 ++ 11 files changed, 91 insertions(+), 82 deletions(-) diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 3178ac29..3fd0608d 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -545,7 +545,7 @@ struct ContentView: View { guard let ds = damus_state, let keypair = ds.keypair.to_full(), let pubkey = muting, - let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(pubkey)) + let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .user(pubkey, nil)) else { return } @@ -578,7 +578,7 @@ struct ContentView: View { return } - guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: .pubkey(pubkey)) else { + guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: .user(pubkey, nil)) else { return } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index 4210ce3e..cd63f13a 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -157,8 +157,11 @@ class HomeModel { case .metadata: // profile metadata processing is handled by nostrdb break - case .list: + case .list_deprecated: handle_list_event(ev) + case .mute_list: + // @TODO: this will be implemented in a future patch + break case .boost: handle_boost_event(sub_id: sub_id, ev) case .like: @@ -461,7 +464,7 @@ class HomeModel { var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata]) our_contacts_filter.authors = [damus_state.pubkey] - var our_blocklist_filter = NostrFilter(kinds: [.list]) + var our_blocklist_filter = NostrFilter(kinds: [.list_deprecated]) our_blocklist_filter.parameter = ["mute"] our_blocklist_filter.authors = [damus_state.pubkey] diff --git a/damus/Models/MuteItem.swift b/damus/Models/MuteItem.swift index dcc83e5a..136f7ff1 100644 --- a/damus/Models/MuteItem.swift +++ b/damus/Models/MuteItem.swift @@ -145,6 +145,13 @@ enum MuteItem: Hashable, Equatable { } } +// - MARK: TagConvertible +extension MuteItem: TagConvertible { + static func from_tag(tag: TagSequence) -> MuteItem? { + return MuteItem(tag.strings()) + } +} + extension Collection where Element == MuteItem { /// Check if an event is muted given a collection of ``MutedItem``. /// diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift index 1af77394..7b4b253b 100644 --- a/damus/Nostr/NostrEvent.swift +++ b/damus/Nostr/NostrEvent.swift @@ -798,3 +798,15 @@ func to_reaction_emoji(ev: NostrEvent) -> String? { } } +extension NostrEvent { + /// The mutelist for a given event + /// + /// If the event is not a mutelist it will return `nil`. + var mute_list: Set? { + if (self.kind == NostrKind.list_deprecated.rawValue && self.referenced_params.contains(where: { p in p.param.matches_str("mute") })) || self.kind == NostrKind.mute_list.rawValue { + return Set(self.referenced_mute_items) + } else { + return nil + } + } +} diff --git a/damus/Nostr/NostrKind.swift b/damus/Nostr/NostrKind.swift index dd645946..18578d8d 100644 --- a/damus/Nostr/NostrKind.swift +++ b/damus/Nostr/NostrKind.swift @@ -17,7 +17,8 @@ enum NostrKind: UInt32, Codable { case boost = 6 case like = 7 case chat = 42 - case list = 30000 + case mute_list = 10000 + case list_deprecated = 30000 case longform = 30023 case zap = 9735 case zap_request = 9734 diff --git a/damus/Util/Lists.swift b/damus/Util/Lists.swift index 9c67221e..303e6fde 100644 --- a/damus/Util/Lists.swift +++ b/damus/Util/Lists.swift @@ -7,64 +7,30 @@ import Foundation -func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: RefId) -> NostrEvent? { - return create_or_update_list_event(keypair: keypair, mprev: mprev, to_add: to_add, list_name: "mute", list_type: "p") +func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: Set) -> NostrEvent? { + let muted_items: Set = (mprev?.mute_list ?? Set()).union(to_add).filter { !$0.is_expired() } + let tags: [[String]] = muted_items.map { $0.tag } + return NostrEvent(content: mprev?.content ?? "", keypair: keypair.to_keypair(), kind: NostrKind.mute_list.rawValue, tags: tags) } -func remove_from_mutelist(keypair: FullKeypair, prev: NostrEvent, to_remove: RefId) -> NostrEvent? { - return remove_from_list_event(keypair: keypair, prev: prev, to_remove: to_remove) +func create_or_update_mutelist(keypair: FullKeypair, mprev: NostrEvent?, to_add: MuteItem) -> NostrEvent? { + return create_or_update_mutelist(keypair: keypair, mprev: mprev, to_add: [to_add]) } -func create_or_update_list_event(keypair: FullKeypair, mprev: NostrEvent?, to_add: RefId, list_name: String, list_type: String) -> NostrEvent? { - if let prev = mprev, - prev.pubkey == keypair.pubkey, - matches_list_name(tags: prev.tags, name: list_name) - { - return add_to_list_event(keypair: keypair, prev: prev, to_add: to_add) +func remove_from_mutelist(keypair: FullKeypair, prev: NostrEvent?, to_remove: MuteItem) -> NostrEvent? { + let muted_items: Set = (prev?.mute_list ?? Set()).subtracting([to_remove]).filter { !$0.is_expired() } + let tags: [[String]] = muted_items.map { $0.tag } + return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: NostrKind.mute_list.rawValue, tags: tags) +} + +func toggle_from_mutelist(keypair: FullKeypair, prev: NostrEvent?, to_toggle: MuteItem) -> NostrEvent? { + let existing_muted_items: Set = (prev?.mute_list ?? Set()) + + if existing_muted_items.contains(to_toggle) { + // Already exists, remove + return remove_from_mutelist(keypair: keypair, prev: prev, to_remove: to_toggle) + } else { + // Doesn't exist, add + return create_or_update_mutelist(keypair: keypair, mprev: prev, to_add: to_toggle) } - - let tags = [["d", list_name], [list_type, to_add.description]] - return NostrEvent(content: "", keypair: keypair.to_keypair(), kind: 30000, tags: tags) -} - -func remove_from_list_event(keypair: FullKeypair, prev: NostrEvent, to_remove: RefId) -> NostrEvent? { - var removed = false - - let tags = prev.tags.reduce(into: [[String]](), { acc, tag in - if let ref_id = RefId.from_tag(tag: tag), ref_id == to_remove { - removed = true - return - } - acc.append(tag.strings()) - }) - - guard removed else { - return nil - } - - return NostrEvent(content: prev.content, keypair: keypair.to_keypair(), kind: 30000, tags: tags) -} - -func add_to_list_event(keypair: FullKeypair, prev: NostrEvent, to_add: RefId) -> NostrEvent? { - for tag in prev.tags { - // we are already muting this user - if let ref = RefId.from_tag(tag: tag), to_add == ref { - return nil - } - } - - var tags = prev.tags.strings() - tags.append(to_add.tag) - - return NostrEvent(content: prev.content, keypair: keypair.to_keypair(), kind: 30000, tags: tags) -} - -func matches_list_name(tags: Tags, name: String) -> Bool { - for tag in tags { - if tag.count >= 2 && tag[0].matches_char("d") { - return tag[1].matches_str(name) - } - } - - return false } diff --git a/damus/Views/Muting/AddMuteItemView.swift b/damus/Views/Muting/AddMuteItemView.swift index 039770d6..73a058d7 100644 --- a/damus/Views/Muting/AddMuteItemView.swift +++ b/damus/Views/Muting/AddMuteItemView.swift @@ -72,7 +72,19 @@ struct AddMuteItemView: View { } }() - // @TODO: in future patch - actually update & relay the new mute list + // Actually update & relay the new mute list + if let mute_item { + guard + let full_keypair = state.keypair.to_full(), + let existing_mutelist = state.contacts.mutelist, + let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: mute_item) + else { + return + } + + state.contacts.set_mutelist(mutelist) + state.postbox.send(mutelist) + } new_text = "" diff --git a/damus/Views/Muting/MutelistView.swift b/damus/Views/Muting/MutelistView.swift index c914c150..5c58c478 100644 --- a/damus/Views/Muting/MutelistView.swift +++ b/damus/Views/Muting/MutelistView.swift @@ -17,7 +17,7 @@ struct MutelistView: View { let keypair = damus_state.keypair.to_full(), let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, - to_remove: .pubkey(pubkey)) + to_remove: .user(pubkey, nil)) else { return } diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index 47dc0fe8..647a8183 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -188,7 +188,7 @@ struct ProfileView: View { return } - guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .pubkey(profile.pubkey)) else { + guard let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .user(profile.pubkey, nil)) else { return } diff --git a/damusTests/ListTests.swift b/damusTests/ListTests.swift index 4c9ae0c4..d457bffe 100644 --- a/damusTests/ListTests.swift +++ b/damusTests/ListTests.swift @@ -23,15 +23,13 @@ final class ListTests: XCTestCase { let pubkey = test_keypair_full.pubkey let to_mute = test_pubkey let keypair = FullKeypair(pubkey: pubkey, privkey: privkey) - let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(to_mute))! + let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .user(to_mute, nil))! XCTAssertEqual(mutelist.pubkey, pubkey) XCTAssertEqual(mutelist.content, "") - XCTAssertEqual(mutelist.tags.count, 2) - XCTAssertEqual(mutelist.tags[0][0].string(), "d") - XCTAssertEqual(mutelist.tags[0][1].string(), "mute") - XCTAssertEqual(mutelist.tags[1][0].string(), "p") - XCTAssertEqual(mutelist.tags[1][1].string(), to_mute.hex()) + XCTAssertEqual(mutelist.tags.count, 1) + XCTAssertEqual(mutelist.tags[0][0].string(), "p") + XCTAssertEqual(mutelist.tags[0][1].string(), to_mute.hex()) } func testCreateAndRemoveMuteList() throws { @@ -39,14 +37,12 @@ final class ListTests: XCTestCase { let pubkey = test_keypair_full.pubkey let to_mute = test_pubkey let keypair = FullKeypair(pubkey: pubkey, privkey: privkey) - let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(to_mute))! - let new = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .pubkey(to_mute))! + let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .user(to_mute, nil))! + let new = remove_from_mutelist(keypair: keypair, prev: mutelist, to_remove: .user(to_mute, nil))! XCTAssertEqual(new.pubkey, pubkey) XCTAssertEqual(new.content, "") - XCTAssertEqual(new.tags.count, 1) - XCTAssertEqual(new.tags[0][0].string(), "d") - XCTAssertEqual(new.tags[0][1].string(), "mute") + XCTAssertEqual(new.tags.count, 0) } func testAddToExistingMutelist() throws { @@ -55,17 +51,25 @@ final class ListTests: XCTestCase { let to_mute = test_pubkey let to_mute_2 = test_pubkey_2 let keypair = FullKeypair(pubkey: pubkey, privkey: privkey) - let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .pubkey(to_mute))! - let new = create_or_update_mutelist(keypair: keypair, mprev: mutelist, to_add: .pubkey(to_mute_2))! + let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .user(to_mute, nil))! + let new = create_or_update_mutelist(keypair: keypair, mprev: mutelist, to_add: .user(to_mute_2, nil))! XCTAssertEqual(new.pubkey, pubkey) XCTAssertEqual(new.content, "") - XCTAssertEqual(new.tags.count, 3) - XCTAssertEqual(new.tags[0][0].string(), "d") - XCTAssertEqual(new.tags[0][1].string(), "mute") + XCTAssertEqual(new.tags.count, 2) + XCTAssertEqual(new.tags[0][0].string(), "p") XCTAssertEqual(new.tags[1][0].string(), "p") - XCTAssertEqual(new.tags[1][1].string(), to_mute.hex()) - XCTAssertEqual(new.tags[2][0].string(), "p") - XCTAssertEqual(new.tags[2][1].string(), to_mute_2.hex()) + // This test failed once out of like 10 tries, due to the tags being in the incorrect order. So I decided to put the elements in an array and sort it. That way if the mutelist tags aren't in the expected order it won't fail the test. + XCTAssertEqual([new.tags[0][1].string(), new.tags[1][1].string()].sorted(), [to_mute.hex(), to_mute_2.hex()].sorted()) + } + + func testAddToExistingMutelistShouldNotOverrideContent() throws { + let privkey = test_keypair_full.privkey + let pubkey = test_keypair_full.pubkey + let keypair = FullKeypair(pubkey: pubkey, privkey: privkey) + let mutelist = NostrEvent(content: "random", keypair: keypair.to_keypair(), kind: NostrKind.mute_list.rawValue, tags: []) + let new = create_or_update_mutelist(keypair: keypair, mprev: mutelist, to_add: .user(test_pubkey, nil))! + + XCTAssertEqual(new.content, "random") } } diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift index 857e03b5..8ed33ee9 100644 --- a/nostrdb/NdbNote.swift +++ b/nostrdb/NdbNote.swift @@ -325,6 +325,10 @@ extension NdbNote { References(tags: self.tags) } + public var referenced_mute_items: References { + References(tags: self.tags) + } + public var references: References { References(tags: self.tags) } From 07b3146026207237c10e297448343f2c1cd6b66d Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Wed, 17 Jan 2024 18:17:39 -0700 Subject: [PATCH 04/18] mute: receiving New Mute List Type This patch depends on: Migrate Lists.swift to use new MuteItem - Makes request for new mute list type (kind:10000) - Processing new mute list type (kind:10000) Related: https://github.com/damus-io/damus/issues/1718 Related: https://github.com/damus-io/damus/issues/856 Lighting Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Models/HomeModel.swift | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index cd63f13a..850b6b7b 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -158,10 +158,9 @@ class HomeModel { // profile metadata processing is handled by nostrdb break case .list_deprecated: - handle_list_event(ev) + handle_old_list_event(ev) case .mute_list: - // @TODO: this will be implemented in a future patch - break + handle_mute_list_event(ev) case .boost: handle_boost_event(sub_id: sub_id, ev) case .like: @@ -464,10 +463,13 @@ class HomeModel { var our_contacts_filter = NostrFilter(kinds: [.contacts, .metadata]) our_contacts_filter.authors = [damus_state.pubkey] - var our_blocklist_filter = NostrFilter(kinds: [.list_deprecated]) - our_blocklist_filter.parameter = ["mute"] + var our_old_blocklist_filter = NostrFilter(kinds: [.list_deprecated]) + our_old_blocklist_filter.parameter = ["mute"] + our_old_blocklist_filter.authors = [damus_state.pubkey] + + var our_blocklist_filter = NostrFilter(kinds: [.mute_list]) our_blocklist_filter.authors = [damus_state.pubkey] - + var dms_filter = NostrFilter(kinds: [.dm]) var our_dms_filter = NostrFilter(kinds: [.dm]) @@ -491,7 +493,7 @@ class HomeModel { notifications_filter.limit = 500 var notifications_filters = [notifications_filter] - var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter] + var contacts_filters = [contacts_filter, our_contacts_filter, our_blocklist_filter, our_old_blocklist_filter] var dms_filters = [dms_filter, our_dms_filter] let last_of_kind = get_last_of_kind(relay_id: relay_id) @@ -560,12 +562,29 @@ class HomeModel { pool.send(.subscribe(sub), to: relay_ids) } - func handle_list_event(_ ev: NostrEvent) { + func handle_mute_list_event(_ ev: NostrEvent) { + // we only care about our mutelist + guard ev.pubkey == damus_state.pubkey else { + return + } + + // we only care about the most recent mutelist + if let mutelist = damus_state.contacts.mutelist { + if ev.created_at <= mutelist.created_at { + return + } + } + + damus_state.contacts.set_mutelist(ev) + } + + func handle_old_list_event(_ ev: NostrEvent) { // we only care about our lists guard ev.pubkey == damus_state.pubkey else { return } + // we only care about the most recent mutelist if let mutelist = damus_state.contacts.mutelist { if ev.created_at <= mutelist.created_at { return From 7aaea97de0d946ffadaf874c23ba52f137fe265c Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Wed, 17 Jan 2024 18:17:40 -0700 Subject: [PATCH 05/18] mute: adding filtering support for MuteItem events This patch depends on: Receiving New Mute List Type - Changes NewMutesNotify, NewUnmutesNotify & MuteNotify to use MuteItem instead of Pubkey - Changes is_muted in Contacts.swift to take in a MuteItem instead of a Pubkey - A lot of changes here were just modifying callers of that to accept the new parameter type Related: https://github.com/damus-io/damus/issues/1718 Related: https://github.com/damus-io/damus/issues/856 Lighting Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/ContentView.swift | 22 +++++++++---------- damus/Models/Contacts.swift | 18 +++++++-------- damus/Models/HomeModel.swift | 14 +++++------- damus/Models/NotificationsManager.swift | 11 +++------- damus/Notify/MuteNotify.swift | 6 ++--- damus/Notify/NewMutesNotify.swift | 4 ++-- damus/Notify/NewUnmutesNotify.swift | 4 ++-- damus/Views/DirectMessagesView.swift | 2 +- damus/Views/Events/EventMenu.swift | 2 +- .../Events/EventMutingContainerView.swift | 5 +++-- damus/Views/Profile/ProfileView.swift | 4 ++-- 11 files changed, 43 insertions(+), 49 deletions(-) diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 3fd0608d..92e08482 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -71,7 +71,7 @@ struct ContentView: View { @State var active_sheet: Sheets? = nil @State var damus_state: DamusState! @SceneStorage("ContentView.selected_timeline") var selected_timeline: Timeline = .home - @State var muting: Pubkey? = nil + @State var muting: MuteItem? = nil @State var confirm_mute: Bool = false @State var hide_bar: Bool = false @State var user_muted_confirm: Bool = false @@ -366,8 +366,8 @@ struct ContentView: View { .onReceive(handle_notify(.report)) { target in self.active_sheet = .report(target) } - .onReceive(handle_notify(.mute)) { pubkey in - self.muting = pubkey + .onReceive(handle_notify(.mute)) { mute_item in + self.muting = mute_item self.confirm_mute = true } .onReceive(handle_notify(.attached_wallet)) { nwc in @@ -526,7 +526,7 @@ struct ContentView: View { user_muted_confirm = false } }, message: { - if let pubkey = self.muting { + if case let .user(pubkey, _) = self.muting { let profile_txn = damus_state!.profiles.lookup(id: pubkey) let profile = profile_txn?.unsafeUnownedValue let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) @@ -544,13 +544,13 @@ struct ContentView: View { Button(NSLocalizedString("Yes, Overwrite", comment: "Text of button that confirms to overwrite the existing mutelist.")) { guard let ds = damus_state, let keypair = ds.keypair.to_full(), - let pubkey = muting, - let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: .user(pubkey, nil)) + let muting, + let mutelist = create_or_update_mutelist(keypair: keypair, mprev: nil, to_add: muting) else { return } - damus_state?.contacts.set_mutelist(mutelist) + ds.contacts.set_mutelist(mutelist) ds.postbox.send(mutelist) confirm_overwrite_mutelist = false @@ -573,21 +573,21 @@ struct ContentView: View { confirm_overwrite_mutelist = true } else { guard let keypair = ds.keypair.to_full(), - let pubkey = muting + let muting else { return } - guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: .user(pubkey, nil)) else { + guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: muting) else { return } - damus_state?.contacts.set_mutelist(ev) + ds.contacts.set_mutelist(ev) ds.postbox.send(ev) } } }, message: { - if let pubkey = muting { + if case let .user(pubkey, _) = muting { let profile_txn = damus_state?.profiles.lookup(id: pubkey) let profile = profile_txn?.unsafeUnownedValue let name = Profile.displayName(profile: profile, pubkey: pubkey).username.truncate(maxLength: 50) diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift index 5829ed9c..28e11438 100644 --- a/damus/Models/Contacts.swift +++ b/damus/Models/Contacts.swift @@ -13,7 +13,7 @@ class Contacts { private var friend_of_friends: Set = Set() /// Tracks which friends are friends of a given pubkey. private var pubkey_to_our_friends = [Pubkey : Set]() - private var muted: Set = Set() + private var muted: Set = Set() let our_pubkey: Pubkey var event: NostrEvent? @@ -23,20 +23,20 @@ class Contacts { self.our_pubkey = our_pubkey } - func is_muted(_ pk: Pubkey) -> Bool { - return muted.contains(pk) + func is_muted(_ item: MuteItem) -> Bool { + return muted.contains(item) } - + func set_mutelist(_ ev: NostrEvent) { let oldlist = self.mutelist self.mutelist = ev - let old = oldlist.map({ ev in Set(ev.referenced_pubkeys) }) ?? Set() - let new = Set(ev.referenced_pubkeys) + 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() + var new_mutes = Set() + var new_unmutes = Set() for d in diff { if new.contains(d) { @@ -47,7 +47,7 @@ class Contacts { } // TODO: set local mutelist here - self.muted = Set(ev.referenced_pubkeys) + self.muted = ev.mute_list ?? Set() if new_mutes.count > 0 { notify(.new_mutes(new_mutes)) diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index 850b6b7b..e44fb66c 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -278,11 +278,11 @@ class HomeModel { func filter_events() { events.filter { ev in - !damus_state.contacts.is_muted(ev.pubkey) + !damus_state.contacts.is_muted(.user(ev.pubkey, nil)) } self.dms.dms = dms.dms.filter { ev in - !damus_state.contacts.is_muted(ev.pubkey) + !damus_state.contacts.is_muted(.user(ev.pubkey, nil)) } notifications.filter { ev in @@ -290,7 +290,8 @@ class HomeModel { return false } - return !damus_state.contacts.is_muted(ev.pubkey) && !damus_state.muted_threads.isMutedThread(ev, keypair: damus_state.keypair) + let event_muted = damus_state.contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil + return !event_muted } } @@ -1093,11 +1094,8 @@ func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool { } func should_show_event(keypair: Keypair, hellthreads: MutedThreadsManager, contacts: Contacts, ev: NostrEvent) -> Bool { - if contacts.is_muted(ev.pubkey) { - return false - } - - if hellthreads.isMutedThread(ev, keypair: keypair) { + let event_muted = contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil + if event_muted { return false } diff --git a/damus/Models/NotificationsManager.swift b/damus/Models/NotificationsManager.swift index 6a832362..5718f56c 100644 --- a/damus/Models/NotificationsManager.swift +++ b/damus/Models/NotificationsManager.swift @@ -36,16 +36,11 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent return false } - // Don't show notifications from muted threads. - if state.muted_threads.isMutedThread(ev, keypair: state.keypair) { + // Don't show notifications that match mute list. + if state.contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil { return false } - - // Don't show notifications from muted users - if state.contacts.is_muted(ev.pubkey) { - return false - } - + // Don't show notifications for old events guard ev.age < EVENT_MAX_AGE_FOR_NOTIFICATION else { return false diff --git a/damus/Notify/MuteNotify.swift b/damus/Notify/MuteNotify.swift index db15e519..77263fbb 100644 --- a/damus/Notify/MuteNotify.swift +++ b/damus/Notify/MuteNotify.swift @@ -8,8 +8,8 @@ import Foundation struct MuteNotify: Notify { - typealias Payload = Pubkey - var payload: Payload + typealias Payload = MuteItem + var payload: MuteItem } extension NotifyHandler { @@ -19,7 +19,7 @@ extension NotifyHandler { } extension Notifications { - static func mute(_ target: Pubkey) -> Notifications { + static func mute(_ target: MuteItem) -> Notifications { .init(.init(payload: target)) } } diff --git a/damus/Notify/NewMutesNotify.swift b/damus/Notify/NewMutesNotify.swift index bd3ae75d..201ae4f3 100644 --- a/damus/Notify/NewMutesNotify.swift +++ b/damus/Notify/NewMutesNotify.swift @@ -8,7 +8,7 @@ import Foundation struct NewMutesNotify: Notify { - typealias Payload = Set + typealias Payload = Set var payload: Payload } @@ -19,7 +19,7 @@ extension NotifyHandler { } extension Notifications { - static func new_mutes(_ pubkeys: Set) -> Notifications { + static func new_mutes(_ pubkeys: Set) -> Notifications { .init(.init(payload: pubkeys)) } } diff --git a/damus/Notify/NewUnmutesNotify.swift b/damus/Notify/NewUnmutesNotify.swift index d5bf0f6c..a5c85ae1 100644 --- a/damus/Notify/NewUnmutesNotify.swift +++ b/damus/Notify/NewUnmutesNotify.swift @@ -8,7 +8,7 @@ import Foundation struct NewUnmutesNotify: Notify { - typealias Payload = Set + typealias Payload = Set var payload: Payload } @@ -19,7 +19,7 @@ extension NotifyHandler { } extension Notifications { - static func new_unmutes(_ pubkeys: Set) -> Notifications { + static func new_unmutes(_ pubkeys: Set) -> Notifications { .init(.init(payload: pubkeys)) } } diff --git a/damus/Views/DirectMessagesView.swift b/damus/Views/DirectMessagesView.swift index 2ff3da74..6d3cfacc 100644 --- a/damus/Views/DirectMessagesView.swift +++ b/damus/Views/DirectMessagesView.swift @@ -39,7 +39,7 @@ struct DirectMessagesView: View { func filter_dms(dms: [DirectMessageModel]) -> [DirectMessageModel] { return dms.filter({ dm in - return damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: dm.pubkey) && !damus_state.contacts.is_muted(dm.pubkey) + return damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: dm.pubkey) && !damus_state.contacts.is_muted(.user(dm.pubkey, nil)) }) } diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift index 8728c9a6..3d368e4a 100644 --- a/damus/Views/Events/EventMenu.swift +++ b/damus/Views/Events/EventMenu.swift @@ -142,7 +142,7 @@ struct MenuItems: View { } Button(role: .destructive) { - notify(.mute(target_pubkey)) + notify(.mute(.user(target_pubkey, nil))) } label: { Label(NSLocalizedString("Mute user", comment: "Context menu option for muting users."), image: "mute") } diff --git a/damus/Views/Events/EventMutingContainerView.swift b/damus/Views/Events/EventMutingContainerView.swift index e8b7cb41..69ac71fd 100644 --- a/damus/Views/Events/EventMutingContainerView.swift +++ b/damus/Views/Events/EventMutingContainerView.swift @@ -49,12 +49,13 @@ struct EventMutingContainerView: View { } } .onReceive(handle_notify(.new_mutes)) { mutes in - if mutes.contains(event.pubkey) { + let new_muted_event_reason = mutes.event_muted_reason(event) + if new_muted_event_reason != nil { shown = false } } .onReceive(handle_notify(.new_unmutes)) { unmutes in - if unmutes.contains(event.pubkey) { + if unmutes.event_muted_reason(event) != nil { shown = true } } diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index 647a8183..c858e3a3 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -179,7 +179,7 @@ struct ProfileView: View { notify(.report(.user(profile.pubkey))) } - if damus_state.contacts.is_muted(profile.pubkey) { + if damus_state.contacts.is_muted(.user(profile.pubkey, nil)) { Button(NSLocalizedString("Unmute", comment: "Button to unmute a profile.")) { guard let keypair = damus_state.keypair.to_full(), @@ -197,7 +197,7 @@ struct ProfileView: View { } } else { Button(NSLocalizedString("Mute", comment: "Button to mute a profile."), role: .destructive) { - notify(.mute(profile.pubkey)) + notify(.mute(.user(profile.pubkey, nil))) } } } From 6003a501c1ddba4d429184701a07052e2bb67440 Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Wed, 17 Jan 2024 18:17:41 -0700 Subject: [PATCH 06/18] mute: updating UI to support new mute list This patch depends on: Adding filtering support for MuteItem events - Gives more specific mute reason in EventMutedBoxView - Showing all types of mutes in MutelistView - Allowing for adding mutes directly from MutelistView - Allowing for choosing duration of mute in EventMenu Related: https://github.com/damus-io/damus/issues/1718 Related: https://github.com/damus-io/damus/issues/856 Lighting Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Util/Router.swift | 6 +- damus/Views/DMChatView.swift | 2 +- damus/Views/Events/EventMenu.swift | 53 ++++----- .../Events/EventMutingContainerView.swift | 28 +++-- damus/Views/Muting/MutelistView.swift | 108 ++++++++++++++---- damus/Views/Profile/ProfileView.swift | 7 +- damus/Views/Reposts/RepostedEvent.swift | 4 +- damus/Views/SideMenuView.swift | 2 +- damus/Views/ThreadView.swift | 4 +- 9 files changed, 141 insertions(+), 73 deletions(-) diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift index 292f90bd..fbcaf801 100644 --- a/damus/Util/Router.swift +++ b/damus/Util/Router.swift @@ -14,7 +14,7 @@ enum Route: Hashable { case Relay(relay: String, showActionButtons: Binding) case RelayDetail(relay: String, metadata: RelayMetadata?) case Following(following: FollowingModel) - case MuteList(users: [Pubkey]) + case MuteList(mutelist_items: Set) case RelayConfig case Script(script: ScriptModel) case Bookmarks @@ -58,8 +58,8 @@ enum Route: Hashable { RelayDetailView(state: damusState, relay: relay, nip11: metadata) case .Following(let following): FollowingView(damus_state: damusState, following: following) - case .MuteList(let users): - MutelistView(damus_state: damusState, users: users) + case .MuteList(let mutelist_items): + MutelistView(damus_state: damusState, mutelist_items: mutelist_items) case .RelayConfig: RelayConfigView(state: damusState) case .Bookmarks: diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index 9666ffd9..a6222aa8 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -22,7 +22,7 @@ struct DMChatView: View, KeyboardReadable { LazyVStack(alignment: .leading) { ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in DMView(event: dms.events[ind], damus_state: damus_state) - .contextMenu{MenuItems(event: ev, keypair: damus_state.keypair, target_pubkey: ev.pubkey, bookmarks: damus_state.bookmarks, muted_threads: damus_state.muted_threads, settings: damus_state.settings, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus_state))} + .contextMenu{MenuItems(damus_state: damus_state, event: ev, target_pubkey: ev.pubkey, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus_state))} } EndBlock(height: 1) } diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift index 3d368e4a..d3144b1d 100644 --- a/damus/Views/Events/EventMenu.swift +++ b/damus/Views/Events/EventMenu.swift @@ -8,21 +8,15 @@ import SwiftUI struct EventMenuContext: View { + let damus_state: DamusState let event: NostrEvent - let keypair: Keypair let target_pubkey: Pubkey - let bookmarks: BookmarksManager - let muted_threads: MutedThreadsManager let profileModel : ProfileModel - @ObservedObject var settings: UserSettingsStore init(damus: DamusState, event: NostrEvent) { + self.damus_state = damus self.event = event - self.keypair = damus.keypair self.target_pubkey = event.pubkey - self.bookmarks = damus.bookmarks - self.muted_threads = damus.muted_threads - self._settings = ObservedObject(wrappedValue: damus.settings) self.profileModel = ProfileModel(pubkey: target_pubkey, damus: damus) } @@ -34,7 +28,7 @@ struct EventMenuContext: View { // Add our Menu button inside an overlay modifier to avoid affecting the rest of the layout around us. .overlay( Menu { - MenuItems(event: event, keypair: keypair, target_pubkey: target_pubkey, bookmarks: bookmarks, muted_threads: muted_threads, settings: settings, profileModel: profileModel) + MenuItems(damus_state: damus_state, event: event, target_pubkey: target_pubkey, profileModel: profileModel) } label: { Color.clear } @@ -49,38 +43,31 @@ struct EventMenuContext: View { } struct MenuItems: View { + let damus_state: DamusState let event: NostrEvent - let keypair: Keypair let target_pubkey: Pubkey - let bookmarks: BookmarksManager - let muted_threads: MutedThreadsManager let profileModel: ProfileModel - @ObservedObject var settings: UserSettingsStore - @State private var isBookmarked: Bool = false @State private var isMutedThread: Bool = false - init(event: NostrEvent, keypair: Keypair, target_pubkey: Pubkey, bookmarks: BookmarksManager, muted_threads: MutedThreadsManager, settings: UserSettingsStore, profileModel: ProfileModel) { - let bookmarked = bookmarks.isBookmarked(event) + init(damus_state: DamusState, event: NostrEvent, target_pubkey: Pubkey, profileModel: ProfileModel) { + let bookmarked = damus_state.bookmarks.isBookmarked(event) self._isBookmarked = State(initialValue: bookmarked) - let muted_thread = muted_threads.isMutedThread(event, keypair: keypair) + let muted_thread = (damus_state.contacts.mutelist?.mute_list?.event_muted_reason(event) != nil) self._isMutedThread = State(initialValue: muted_thread) - self.bookmarks = bookmarks - self.muted_threads = muted_threads + self.damus_state = damus_state self.event = event - self.keypair = keypair self.target_pubkey = target_pubkey - self.settings = settings self.profileModel = profileModel } var body: some View { Group { Button { - UIPasteboard.general.string = event.get_content(keypair) + UIPasteboard.general.string = event.get_content(damus_state.keypair) } label: { Label(NSLocalizedString("Copy text", comment: "Context menu option for copying the text from an note."), image: "copy2") } @@ -97,7 +84,7 @@ struct MenuItems: View { Label(NSLocalizedString("Copy note ID", comment: "Context menu option for copying the ID of the note."), image: "note-book") } - if settings.developer_mode { + if damus_state.settings.developer_mode { Button { UIPasteboard.general.string = event_to_json(ev: event) } label: { @@ -106,8 +93,8 @@ struct MenuItems: View { } Button { - self.bookmarks.updateBookmark(event) - isBookmarked = self.bookmarks.isBookmarked(event) + self.damus_state.bookmarks.updateBookmark(event) + isBookmarked = self.damus_state.bookmarks.isBookmarked(event) } label: { let imageName = isBookmarked ? "bookmark.fill" : "bookmark" let removeBookmarkString = NSLocalizedString("Remove bookmark", comment: "Context menu option for removing a note bookmark.") @@ -122,9 +109,13 @@ struct MenuItems: View { } // Mute thread - relocated to below Broadcast, as to move further away from Add Bookmark to prevent accidental muted threads if event.known_kind != .dm { - Button { - self.muted_threads.updateMutedThread(event) - let muted = self.muted_threads.isMutedThread(event, keypair: self.keypair) + MuteDurationMenu { duration in + if let full_keypair = self.damus_state.keypair.to_full(), + let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.contacts.mutelist, to_toggle: .thread(event.thread_id(keypair: damus_state.keypair), duration?.date_from_now)) { + damus_state.contacts.set_mutelist(new_mutelist_ev) + damus_state.postbox.send(new_mutelist_ev) + } + let muted = (damus_state.contacts.mutelist?.mute_list?.event_muted_reason(event) != nil) isMutedThread = muted } label: { let imageName = isMutedThread ? "mute" : "mute" @@ -134,15 +125,15 @@ struct MenuItems: View { } } // Only allow reporting if logged in with private key and the currently viewed profile is not the logged in profile. - if keypair.pubkey != target_pubkey && keypair.privkey != nil { + if damus_state.keypair.pubkey != target_pubkey && damus_state.keypair.privkey != nil { Button(role: .destructive) { notify(.report(.note(ReportNoteTarget(pubkey: target_pubkey, note_id: event.id)))) } label: { Label(NSLocalizedString("Report", comment: "Context menu option for reporting content."), image: "raising-hand") } - Button(role: .destructive) { - notify(.mute(.user(target_pubkey, nil))) + MuteDurationMenu { duration in + notify(.mute(.user(target_pubkey, duration?.date_from_now))) } label: { Label(NSLocalizedString("Mute user", comment: "Context menu option for muting users."), image: "mute") } diff --git a/damus/Views/Events/EventMutingContainerView.swift b/damus/Views/Events/EventMutingContainerView.swift index 69ac71fd..83b7f6b0 100644 --- a/damus/Views/Events/EventMutingContainerView.swift +++ b/damus/Views/Events/EventMutingContainerView.swift @@ -9,15 +9,20 @@ import SwiftUI /// A container view that shows or hides provided content based on whether the given event should be muted or not, with built-in user controls to show or hide content, and an option to customize the muted box struct EventMutingContainerView: View { - typealias MuteBoxViewClosure = ((_ shown: Binding) -> AnyView) - + typealias MuteBoxViewClosure = ((_ shown: Binding, _ mutedReason: MuteItem?) -> AnyView) + let damus_state: DamusState let event: NostrEvent let content: Content var customMuteBox: MuteBoxViewClosure? + /// Represents if the note itself should be shown. + /// + /// By default this is the same as `should_show_event`. However, if the user taps the button to manually show a muted note, this can become out of sync with `should_show_event`. @State var shown: Bool - + + @State var muted_reason: MuteItem? + init(damus_state: DamusState, event: NostrEvent, @ViewBuilder content: () -> Content) { self.damus_state = damus_state self.event = event @@ -38,10 +43,10 @@ struct EventMutingContainerView: View { Group { if should_mute { if let customMuteBox { - customMuteBox($shown) + customMuteBox($shown, muted_reason) } else { - EventMutedBoxView(shown: $shown) + EventMutedBoxView(shown: $shown, reason: muted_reason) } } if shown { @@ -52,11 +57,13 @@ struct EventMutingContainerView: View { let new_muted_event_reason = mutes.event_muted_reason(event) if new_muted_event_reason != nil { shown = false + muted_reason = new_muted_event_reason } } .onReceive(handle_notify(.new_unmutes)) { unmutes in if unmutes.event_muted_reason(event) != nil { shown = true + muted_reason = nil } } } @@ -65,16 +72,21 @@ struct EventMutingContainerView: View { /// A box that instructs the user about a content that has been muted. struct EventMutedBoxView: View { @Binding var shown: Bool - + var reason: MuteItem? + var body: some View { ZStack { RoundedRectangle(cornerRadius: 20) .foregroundColor(DamusColors.adaptableGrey) HStack { - Text("Note from a user you've muted", comment: "Text to indicate that what is being shown is a note from a user who has been muted.") + if let reason { + Text("Note from a \(reason.title) you've muted", comment: "Text to indicate that what is being shown is a note which has been muted.") + } else { + Text("Note you've muted", comment: "Text to indicate that what is being shown is a note which has been muted.") + } Spacer() - Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a note from a user who has been muted.") : NSLocalizedString("Show", comment: "Button to show a note from a user who has been muted.")) { + Button(shown ? NSLocalizedString("Hide", comment: "Button to hide a note which has been muted.") : NSLocalizedString("Show", comment: "Button to show a note which has been muted.")) { shown.toggle() } } diff --git a/damus/Views/Muting/MutelistView.swift b/damus/Views/Muting/MutelistView.swift index 5c58c478..37813383 100644 --- a/damus/Views/Muting/MutelistView.swift +++ b/damus/Views/Muting/MutelistView.swift @@ -9,55 +9,117 @@ import SwiftUI struct MutelistView: View { let damus_state: DamusState - @State var users: [Pubkey] - - func RemoveAction(pubkey: Pubkey) -> some View { + @State var mutelist_items: Set = Set() + @State var show_add_muteitem: Bool = false + + func RemoveAction(item: MuteItem) -> some View { Button { guard let mutelist = damus_state.contacts.mutelist, let keypair = damus_state.keypair.to_full(), let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, - to_remove: .user(pubkey, nil)) + to_remove: item) else { return } - + damus_state.contacts.set_mutelist(new_ev) damus_state.postbox.send(new_ev) - users = get_mutelist_users(new_ev) + mutelist_items = new_ev.mute_list ?? Set() } label: { Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete") } .tint(.red) } - + var body: some View { - List(users, id: \.self) { pubkey in - UserViewRow(damus_state: damus_state, pubkey: pubkey) - .id(pubkey) - .swipeActions { - RemoveAction(pubkey: pubkey) + List { + Section(NSLocalizedString("Users", comment: "Section header title for a list of muted users.")) { + ForEach(mutelist_items.users, id: \.self) { pubkey in + UserViewRow(damus_state: damus_state, pubkey: pubkey) + .id(pubkey) + .swipeActions { + RemoveAction(item: .user(pubkey, nil)) + } + .onTapGesture { + damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) + } } - .onTapGesture { - damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) + } + Section(NSLocalizedString("Hashtags", comment: "Section header title for a list of hashtags that are muted.")) { + ForEach(mutelist_items.hashtags, id: \.hashtag) { hashtag in + Text("#\(hashtag.hashtag)") + .id(hashtag.hashtag) + .swipeActions { + RemoveAction(item: .hashtag(hashtag, nil)) + } + .onTapGesture { + damus_state.nav.push(route: Route.Search(search: SearchModel.init(state: damus_state, search: NostrFilter(hashtag: [hashtag.hashtag])))) + } } + } + Section(NSLocalizedString("Words", comment: "Section header title for a list of words that are muted.")) { + ForEach(mutelist_items.words, id: \.self) { word in + Text("\(word)") + .id(word) + .swipeActions { + RemoveAction(item: .word(word, nil)) + } + } + } + Section(NSLocalizedString("Threads", comment: "Section header title for a list of threads that are muted.")) { + ForEach(mutelist_items.threads, id: \.self) { note_id in + if let event = damus_state.events.lookup(note_id) { + EventView(damus: damus_state, event: event) + .id(note_id.hex()) + .swipeActions { + RemoveAction(item: .thread(note_id, nil)) + } + } else { + Text(NSLocalizedString("Error retrieving muted event", comment: "Text for an item that application failed to retrieve the muted event for.")) + } + } + } } - .navigationTitle(NSLocalizedString("Muted Users", comment: "Navigation title of view to see list of muted users.")) + .navigationTitle(NSLocalizedString("Muted", comment: "Navigation title of view to see list of muted users & phrases.")) .onAppear { - users = get_mutelist_users(damus_state.contacts.mutelist) + mutelist_items = damus_state.contacts.mutelist?.mute_list ?? Set() + } + .onReceive(handle_notify(.new_mutes)) { new_mutes in + mutelist_items = mutelist_items.union(new_mutes) + } + .onReceive(handle_notify(.new_unmutes)) { new_unmutes in + mutelist_items = mutelist_items.subtracting(new_unmutes) + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + self.show_add_muteitem = true + } label: { + Image(systemName: "plus") + } + } + } + .sheet(isPresented: $show_add_muteitem, onDismiss: { self.show_add_muteitem = false }) { + if #available(iOS 16.0, *) { + AddMuteItemView(state: damus_state) + .presentationDetents([.height(300)]) + .presentationDragIndicator(.visible) + } else { + AddMuteItemView(state: damus_state) + } } } } - -func get_mutelist_users(_ mutelist: NostrEvent?) -> Array { - guard let mutelist else { return [] } - return Array(mutelist.referenced_pubkeys) -} - struct MutelistView_Previews: PreviewProvider { static var previews: some View { - MutelistView(damus_state: test_damus_state, users: [test_note.pubkey, test_note.pubkey]) + MutelistView(damus_state: test_damus_state, mutelist_items: Set([ + .user(test_note.pubkey, nil), + .hashtag(Hashtag(hashtag: "test"), nil), + .word("test", nil), + .thread(test_note.id, nil) + ])) } } diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index c858e3a3..e8f3e2f9 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -196,8 +196,11 @@ struct ProfileView: View { damus_state.postbox.send(new_ev) } } else { - Button(NSLocalizedString("Mute", comment: "Button to mute a profile."), role: .destructive) { - notify(.mute(.user(profile.pubkey, nil))) + MuteDurationMenu { duration in + notify(.mute(.user(profile.pubkey, duration?.date_from_now))) + } label: { + Text(NSLocalizedString("Mute", comment: "Button to mute a profile.")) + .foregroundStyle(.red) } } } diff --git a/damus/Views/Reposts/RepostedEvent.swift b/damus/Views/Reposts/RepostedEvent.swift index 207ee295..6dc4e874 100644 --- a/damus/Views/Reposts/RepostedEvent.swift +++ b/damus/Views/Reposts/RepostedEvent.swift @@ -25,9 +25,9 @@ struct RepostedEvent: View { EventMutingContainerView( damus_state: damus, event: inner_ev, - muteBox: { event_shown in + muteBox: { event_shown, muted_reason in AnyView( - EventMutedBoxView(shown: event_shown) + EventMutedBoxView(shown: event_shown, reason: muted_reason) .padding(.horizontal, 5) // Add a bit of horizontal padding to avoid the mute box from touching the edges of the screen ) }) { diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift index 8a0e3cdd..fe3b6eca 100644 --- a/damus/Views/SideMenuView.swift +++ b/damus/Views/SideMenuView.swift @@ -66,7 +66,7 @@ struct SideMenuView: View { } } - NavigationLink(value: Route.MuteList(users: get_mutelist_users(damus_state.contacts.mutelist))) { + NavigationLink(value: Route.MuteList(mutelist_items: damus_state.contacts.mutelist?.mute_list ?? Set())) { navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), img: "mute") } diff --git a/damus/Views/ThreadView.swift b/damus/Views/ThreadView.swift index 7ef8a315..677107a9 100644 --- a/damus/Views/ThreadView.swift +++ b/damus/Views/ThreadView.swift @@ -70,9 +70,9 @@ struct ThreadView: View { EventMutingContainerView( damus_state: state, event: self.thread.event, - muteBox: { event_shown in + muteBox: { event_shown, muted_reason in AnyView( - EventMutedBoxView(shown: event_shown) + EventMutedBoxView(shown: event_shown, reason: muted_reason) .padding(5) ) } From 89f7c3ff30c48ca67c7c44d6a9c80c79ce07c2aa Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Wed, 17 Jan 2024 18:17:42 -0700 Subject: [PATCH 07/18] mute: adding ability to mute hashtag from SearchView This patch depends on: Updating UI to support new mute list - Adding the ability to mute a hashtag from SearchView Related: https://github.com/damus-io/damus/issues/1718 Related: https://github.com/damus-io/damus/issues/856 Lighting-address: fishcharlie@strike.me Changelog-Added: Add ability to mute hashtag from SearchView Signed-off-by: Charlie Fish Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Views/SearchView.swift | 64 +++++++++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/damus/Views/SearchView.swift b/damus/Views/SearchView.swift index eb55cd6a..3d5fddf5 100644 --- a/damus/Views/SearchView.swift +++ b/damus/Views/SearchView.swift @@ -11,7 +11,8 @@ struct SearchView: View { let appstate: DamusState @ObservedObject var search: SearchModel @Environment(\.dismiss) var dismiss - + @State var is_hashtag_muted: Bool = false + var content_filter: (NostrEvent) -> Bool { let filters = ContentFilters.defaults(damus_state: self.appstate) return ContentFilters(filters: filters).filter @@ -41,7 +42,68 @@ struct SearchView: View { } .onReceive(handle_notify(.new_mutes)) { notif in search.filter_muted() + + if let hashtag_string = search.search.hashtag?.first, + notif.contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil)) { + is_hashtag_muted = true + } } + .onReceive(handle_notify(.new_unmutes)) { unmutes in + if let hashtag_string = search.search.hashtag?.first, + unmutes.contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil)) { + is_hashtag_muted = false + } + } + .toolbar { + if let hashtag = search.search.hashtag?.first { + ToolbarItem(placement: .topBarTrailing) { + Menu { + if is_hashtag_muted { + Button { + guard + let full_keypair = appstate.keypair.to_full(), + let existing_mutelist = appstate.contacts.mutelist, + let mutelist = remove_from_mutelist(keypair: full_keypair, prev: existing_mutelist, to_remove: .hashtag(Hashtag(hashtag: hashtag), nil)) + else { + return + } + + appstate.contacts.set_mutelist(mutelist) + appstate.postbox.send(mutelist) + } label: { + Text("Unmute Hashtag", comment: "Label represnting a button that the user can tap to unmute a given hashtag so they start seeing it in their feed again.") + } + } else { + MuteDurationMenu { duration in + mute_hashtag(hashtag_string: hashtag, expiration_time: duration?.date_from_now) + } label: { + Text("Mute Hashtag", comment: "Label represnting a button that the user can tap to mute a given hashtag so they don't see it in their feed anymore.") + } + } + } label: { + Image(systemName: "ellipsis") + } + } + } + } + .onAppear { + if let hashtag_string = search.search.hashtag?.first { + is_hashtag_muted = (appstate.contacts.mutelist?.mute_list ?? []).contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil)) + } + } + } + + func mute_hashtag(hashtag_string: String, expiration_time: Date?) { + guard + let full_keypair = appstate.keypair.to_full(), + let existing_mutelist = appstate.contacts.mutelist, + let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: .hashtag(Hashtag(hashtag: hashtag_string), expiration_time)) + else { + return + } + + appstate.contacts.set_mutelist(mutelist) + appstate.postbox.send(mutelist) } var described_search: DescribedSearch { From 71c9bd63fcf61d256d4167fe6b4740073cdd2579 Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Wed, 17 Jan 2024 18:17:43 -0700 Subject: [PATCH 08/18] mute: migrating muted_threads to new mute list This patch depends on: Adding ability to mute hashtag from SearchView This is the last patch for the new mute list feature - Removing MutedThreadsManager - Adding system to migrate existing muted threads to new mute list Closes: https://github.com/damus-io/damus/issues/1718 Closes: https://github.com/damus-io/damus/issues/856 Lighting Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Signed-off-by: William Casarin --- .../NotificationExtensionState.swift | 2 - damus.xcodeproj/project.pbxproj | 2 - damus/ContentView.swift | 1 - damus/Models/ContentFilters.swift | 2 +- damus/Models/DamusState.swift | 5 +- damus/Models/HeadlessDamusState.swift | 1 - damus/Models/HomeModel.swift | 16 +++-- damus/Models/MutedThreadsManager.swift | 70 +++++-------------- damus/Models/SearchHomeModel.swift | 4 +- damus/Models/SearchModel.swift | 4 +- damus/TestData.swift | 1 - .../Events/EventMutingContainerView.swift | 4 +- damus/Views/SearchHomeView.swift | 3 +- damusTests/LongPostTests.swift | 2 +- damusTests/Mocking/MockDamusState.swift | 1 - 15 files changed, 37 insertions(+), 81 deletions(-) diff --git a/DamusNotificationService/NotificationExtensionState.swift b/DamusNotificationService/NotificationExtensionState.swift index d6db01b4..69acc020 100644 --- a/DamusNotificationService/NotificationExtensionState.swift +++ b/DamusNotificationService/NotificationExtensionState.swift @@ -11,7 +11,6 @@ struct NotificationExtensionState: HeadlessDamusState { let ndb: Ndb let settings: UserSettingsStore let contacts: Contacts - let muted_threads: MutedThreadsManager let keypair: Keypair let profiles: Profiles let zaps: Zaps @@ -28,7 +27,6 @@ struct NotificationExtensionState: HeadlessDamusState { self.settings = UserSettingsStore() self.contacts = Contacts(our_pubkey: keypair.pubkey) - self.muted_threads = MutedThreadsManager(keypair: keypair) self.keypair = keypair self.profiles = Profiles(ndb: ndb) self.zaps = Zaps(our_pubkey: keypair.pubkey) diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj index 7ec805f6..90cc34f9 100644 --- a/damus.xcodeproj/project.pbxproj +++ b/damus.xcodeproj/project.pbxproj @@ -510,7 +510,6 @@ D7CB5D402B116E8A00AD4105 /* UserSettingsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA693073295D649800ADDB87 /* UserSettingsStore.swift */; }; D7CB5D412B116F0900AD4105 /* StringCodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA5588229F33F5B00DC6A45 /* StringCodable.swift */; }; D7CB5D422B116F8900AD4105 /* Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C3AC79A28306D7B00E1F516 /* Contacts.swift */; }; - D7CB5D432B116F9B00AD4105 /* MutedThreadsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */; }; D7CB5D452B116FE800AD4105 /* Contacts+.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7CB5D442B116FE800AD4105 /* Contacts+.swift */; }; D7CB5D462B11703D00AD4105 /* Notify.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA3529F2A76AE80003BB08B /* Notify.swift */; }; D7CB5D472B11718700AD4105 /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; }; @@ -3530,7 +3529,6 @@ D7CCFC0F2B0587F600323D86 /* Keys.swift in Sources */, D7CB5D542B1174F700AD4105 /* NIP05.swift in Sources */, D798D2232B0859B700234419 /* KeychainStorage.swift in Sources */, - D7CB5D432B116F9B00AD4105 /* MutedThreadsManager.swift in Sources */, D74AAFC32B153395006CF0F4 /* HeadlessDamusState.swift in Sources */, D7CE1B272B0BE224002EDAD4 /* bech32_util.c in Sources */, D7CCFC102B05880F00323D86 /* Id.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift index 92e08482..b7625afa 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -674,7 +674,6 @@ struct ContentView: View { postbox: PostBox(pool: pool), bootstrap_relays: bootstrap_relays, replies: ReplyCounter(our_pubkey: pubkey), - muted_threads: MutedThreadsManager(keypair: keypair), wallet: WalletModel(settings: settings), nav: self.navigationCoordinator, music: MusicController(onChange: music_changed), diff --git a/damus/Models/ContentFilters.swift b/damus/Models/ContentFilters.swift index da3c6ba5..c876f3b6 100644 --- a/damus/Models/ContentFilters.swift +++ b/damus/Models/ContentFilters.swift @@ -33,7 +33,7 @@ func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEv guard ev.known_kind == .boost else { return true } // This needs to use cached because it can be way too slow otherwise guard let inner_ev = ev.get_cached_inner_event(cache: damus_state.events) else { return true } - return should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: inner_ev) + return should_show_event(contacts: damus_state.contacts, ev: inner_ev) } } diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift index 3e5b4120..0b35abbf 100644 --- a/damus/Models/DamusState.swift +++ b/damus/Models/DamusState.swift @@ -28,7 +28,6 @@ struct DamusState: HeadlessDamusState { let postbox: PostBox let bootstrap_relays: [String] let replies: ReplyCounter - let muted_threads: MutedThreadsManager let wallet: WalletModel let nav: NavigationCoordinator let music: MusicController? @@ -36,7 +35,7 @@ struct DamusState: HeadlessDamusState { let ndb: Ndb var purple: DamusPurple - init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, muted_threads: MutedThreadsManager, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) { + init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) { self.pool = pool self.keypair = keypair self.likes = likes @@ -56,7 +55,6 @@ struct DamusState: HeadlessDamusState { self.postbox = postbox self.bootstrap_relays = bootstrap_relays self.replies = replies - self.muted_threads = muted_threads self.wallet = wallet self.nav = nav self.music = music @@ -123,7 +121,6 @@ struct DamusState: HeadlessDamusState { postbox: PostBox(pool: RelayPool(ndb: .empty)), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: empty_pub), - muted_threads: MutedThreadsManager(keypair: kp), wallet: WalletModel(settings: UserSettingsStore()), nav: NavigationCoordinator(), music: nil, diff --git a/damus/Models/HeadlessDamusState.swift b/damus/Models/HeadlessDamusState.swift index 15f785f0..dcb9d032 100644 --- a/damus/Models/HeadlessDamusState.swift +++ b/damus/Models/HeadlessDamusState.swift @@ -15,7 +15,6 @@ protocol HeadlessDamusState { var ndb: Ndb { get } var settings: UserSettingsStore { get } var contacts: Contacts { get } - var muted_threads: MutedThreadsManager { get } var keypair: Keypair { get } var profiles: Profiles { get } var zaps: Zaps { get } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index e44fb66c..9ea9833d 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -244,7 +244,7 @@ class HomeModel { process_zap_event(state: damus_state, ev: ev) { zapres in guard case .done(let zap) = zapres, zap.target.pubkey == self.damus_state.keypair.pubkey, - should_show_event(keypair: self.damus_state.keypair, hellthreads: self.damus_state.muted_threads, contacts: self.damus_state.contacts, ev: zap.request.ev) else { + should_show_event(contacts: self.damus_state.contacts, ev: zap.request.ev) else { return } @@ -577,6 +577,8 @@ class HomeModel { } damus_state.contacts.set_mutelist(ev) + + migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state) } func handle_old_list_event(_ ev: NostrEvent) { @@ -597,6 +599,8 @@ class HomeModel { } damus_state.contacts.set_mutelist(ev) + + migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state) } func get_last_event_of_kind(relay_id: String, kind: UInt32) -> NostrEvent? { @@ -612,7 +616,7 @@ class HomeModel { // don't show notifications from ourselves guard ev.pubkey != damus_state.pubkey, event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey), - should_show_event(keypair: self.damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else { + should_show_event(contacts: damus_state.contacts, ev: ev) else { return } @@ -650,7 +654,7 @@ class HomeModel { func handle_text_event(sub_id: String, _ ev: NostrEvent) { - guard should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else { + guard should_show_event(contacts: damus_state.contacts, ev: ev) else { return } @@ -679,7 +683,7 @@ class HomeModel { } func handle_dm(_ ev: NostrEvent) { - guard should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) else { + guard should_show_event(contacts: damus_state.contacts, ev: ev) else { return } @@ -1086,14 +1090,12 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: Pubkey) -> Bool { func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool { return should_show_event( - keypair: damus_state.keypair, - hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: event ) } -func should_show_event(keypair: Keypair, hellthreads: MutedThreadsManager, contacts: Contacts, ev: NostrEvent) -> Bool { +func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool { let event_muted = contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil if event_muted { return false diff --git a/damus/Models/MutedThreadsManager.swift b/damus/Models/MutedThreadsManager.swift index 80903e4a..20c1903f 100644 --- a/damus/Models/MutedThreadsManager.swift +++ b/damus/Models/MutedThreadsManager.swift @@ -11,7 +11,7 @@ fileprivate func getMutedThreadsKey(pubkey: Pubkey) -> String { pk_setting_key(pubkey, key: "muted_threads") } -func loadMutedThreads(pubkey: Pubkey) -> [NoteId] { +func loadOldMutedThreads(pubkey: Pubkey) -> [NoteId] { let key = getMutedThreadsKey(pubkey: pubkey) let xs = UserDefaults.standard.stringArray(forKey: key) ?? [] return xs.reduce(into: [NoteId]()) { ids, k in @@ -20,56 +20,20 @@ func loadMutedThreads(pubkey: Pubkey) -> [NoteId] { } } -func saveMutedThreads(pubkey: Pubkey, currentValue: [NoteId], value: [NoteId]) -> Bool { - let uniqueMutedThreads = Array(Set(value)) - - if uniqueMutedThreads != currentValue { - let ids = uniqueMutedThreads.map { note_id in return note_id.hex() } - UserDefaults.standard.set(ids, forKey: getMutedThreadsKey(pubkey: pubkey)) - return true - } - - return false -} - -class MutedThreadsManager: ObservableObject { - - private let keypair: Keypair - - private var _mutedThreadsSet: Set - private var _mutedThreads: [NoteId] - var mutedThreads: [NoteId] { - get { - return _mutedThreads - } - set { - if saveMutedThreads(pubkey: keypair.pubkey, currentValue: _mutedThreads, value: newValue) { - self._mutedThreads = newValue - self.objectWillChange.send() - } - } - } - - init(keypair: Keypair) { - self._mutedThreads = loadMutedThreads(pubkey: keypair.pubkey) - self._mutedThreadsSet = Set(_mutedThreads) - self.keypair = keypair - } - - func isMutedThread(_ ev: NostrEvent, keypair: Keypair) -> Bool { - return _mutedThreadsSet.contains(ev.thread_id(keypair: keypair)) - } - - func updateMutedThread(_ ev: NostrEvent) { - let threadId = ev.thread_id(keypair: keypair) - if isMutedThread(ev, keypair: keypair) { - mutedThreads = mutedThreads.filter { $0 != threadId } - _mutedThreadsSet.remove(threadId) - notify(.unmute_thread(ev)) - } else { - mutedThreads.append(threadId) - _mutedThreadsSet.insert(threadId) - notify(.mute_thread(ev)) - } - } +// We need to still use it since existing users might have their muted threads stored in UserDefaults +// So now all it's doing is moving a users muted threads to the new kind:10000 system +// It should not be used for any purpose beyond that +func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: DamusState) { + // Ensure that keypair is fullkeypair + guard let fullKeypair = keypair.to_full() else { return } + // Load existing muted threads + let mutedThreads = loadOldMutedThreads(pubkey: fullKeypair.pubkey) + guard !mutedThreads.isEmpty else { return } + // Set new muted system for those existing threads + let previous_mute_list_event = damus_state.contacts.mutelist + guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return } + damus_state.contacts.set_mutelist(new_mutelist_event) + damus_state.postbox.send(new_mutelist_event) + // Set existing muted threads to an empty array + UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey)) } diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift index 84ae5911..63bcc810 100644 --- a/damus/Models/SearchHomeModel.swift +++ b/damus/Models/SearchHomeModel.swift @@ -35,7 +35,7 @@ class SearchHomeModel: ObservableObject { } func filter_muted() { - events.filter { should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: $0) } + events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) } self.objectWillChange.send() } @@ -60,7 +60,7 @@ class SearchHomeModel: ObservableObject { guard sub_id == self.base_subid || sub_id == self.profiles_subid else { return } - if ev.is_textlike && should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: ev) && !ev.is_reply(damus_state.keypair) + if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(damus_state.keypair) { if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) { return diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift index b61231af..1d107fc3 100644 --- a/damus/Models/SearchModel.swift +++ b/damus/Models/SearchModel.swift @@ -28,7 +28,7 @@ class SearchModel: ObservableObject { func filter_muted() { self.events.filter { - should_show_event(keypair: state.keypair, hellthreads: state.muted_threads, contacts: state.contacts, ev: $0) + should_show_event(contacts: state.contacts, ev: $0) } self.objectWillChange.send() } @@ -57,7 +57,7 @@ class SearchModel: ObservableObject { return } - guard should_show_event(keypair: state.keypair, hellthreads: state.muted_threads, contacts: state.contacts, ev: ev) else { + guard should_show_event(contacts: state.contacts, ev: ev) else { return } diff --git a/damus/TestData.swift b/damus/TestData.swift index 5b0a3b48..f9515bb5 100644 --- a/damus/TestData.swift +++ b/damus/TestData.swift @@ -87,7 +87,6 @@ var test_damus_state: DamusState = ({ postbox: .init(pool: pool), bootstrap_relays: .init(), replies: .init(our_pubkey: our_pubkey), - muted_threads: .init(keypair: test_keypair), wallet: .init(settings: settings), nav: .init(), music: .init(onChange: {_ in }), diff --git a/damus/Views/Events/EventMutingContainerView.swift b/damus/Views/Events/EventMutingContainerView.swift index 83b7f6b0..c030be24 100644 --- a/damus/Views/Events/EventMutingContainerView.swift +++ b/damus/Views/Events/EventMutingContainerView.swift @@ -27,7 +27,7 @@ struct EventMutingContainerView: View { self.damus_state = damus_state self.event = event self.content = content() - self._shown = State(initialValue: should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: event)) + self._shown = State(initialValue: should_show_event(contacts: damus_state.contacts, ev: event)) } init(damus_state: DamusState, event: NostrEvent, muteBox: @escaping MuteBoxViewClosure, @ViewBuilder content: () -> Content) { @@ -36,7 +36,7 @@ struct EventMutingContainerView: View { } var should_mute: Bool { - return !should_show_event(keypair: damus_state.keypair, hellthreads: damus_state.muted_threads, contacts: damus_state.contacts, ev: event) + return !should_show_event(contacts: damus_state.contacts, ev: event) } var body: some View { diff --git a/damus/Views/SearchHomeView.swift b/damus/Views/SearchHomeView.swift index 00ae46ea..91a15e2d 100644 --- a/damus/Views/SearchHomeView.swift +++ b/damus/Views/SearchHomeView.swift @@ -59,7 +59,8 @@ struct SearchHomeView: View { return false } - if damus_state.muted_threads.isMutedThread(ev, keypair: self.damus_state.keypair) { + let event_muted = damus_state.contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil + if event_muted { return false } diff --git a/damusTests/LongPostTests.swift b/damusTests/LongPostTests.swift index f4888c9f..70637607 100644 --- a/damusTests/LongPostTests.swift +++ b/damusTests/LongPostTests.swift @@ -34,7 +34,7 @@ final class LongPostTests: XCTestCase { XCTAssertEqual(subid, "subid") XCTAssertTrue(ev.should_show_event) XCTAssertTrue(!ev.too_big) - XCTAssertTrue(should_show_event(keypair: test_keypair, hellthreads: test_damus_state.muted_threads, contacts: contacts, ev: ev)) + XCTAssertTrue(should_show_event(contacts: contacts, ev: ev)) XCTAssertTrue(validate_event(ev: ev) == .ok ) } diff --git a/damusTests/Mocking/MockDamusState.swift b/damusTests/Mocking/MockDamusState.swift index ebdd587e..037c68db 100644 --- a/damusTests/Mocking/MockDamusState.swift +++ b/damusTests/Mocking/MockDamusState.swift @@ -44,7 +44,6 @@ func generate_test_damus_state( postbox: .init(pool: pool), bootstrap_relays: .init(), replies: .init(our_pubkey: our_pubkey), - muted_threads: .init(keypair: test_keypair), wallet: .init(settings: settings), nav: .init(), music: .init(onChange: {_ in }), From 3cce42eea18803e3ec8af4ff8ce897e8c06ceb80 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 25 Jan 2024 11:48:15 -0800 Subject: [PATCH 09/18] tags: add u64 decoding function This will be used for decoding expiries. --- nostrdb/NdbTagElem.swift | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/nostrdb/NdbTagElem.swift b/nostrdb/NdbTagElem.swift index 3ed18377..cf9e83e5 100644 --- a/nostrdb/NdbTagElem.swift +++ b/nostrdb/NdbTagElem.swift @@ -130,6 +130,22 @@ struct NdbTagElem: Sequence, Hashable, Equatable { return id.id } + func u64() -> UInt64? { + switch self.data() { + case .id: + return nil + case .str(let str): + var end_ptr = UnsafeMutablePointer(nil as OpaquePointer?) + let res = strtoull(str.str, &end_ptr, 10) + + if end_ptr?.pointee == 0 { + return res + } else { + return nil + } + } + } + func string() -> String { switch self.data() { case .id(let id): From e999e81e8fef2f0840e1edde8f95ab95619f3332 Mon Sep 17 00:00:00 2001 From: William Casarin Date: Thu, 25 Jan 2024 11:49:17 -0800 Subject: [PATCH 10/18] mute: implement fast MuteItem decoder Just using strings is really bad performance wise, and is not the proper way to implement a TagConvertible. The whole point of this protocol is to parse tags efficiently. --- damus/Models/MuteItem.swift | 50 ++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/damus/Models/MuteItem.swift b/damus/Models/MuteItem.swift index 136f7ff1..69bbe2c0 100644 --- a/damus/Models/MuteItem.swift +++ b/damus/Models/MuteItem.swift @@ -147,8 +147,56 @@ enum MuteItem: Hashable, Equatable { // - MARK: TagConvertible extension MuteItem: TagConvertible { + enum MuteKeys: String { + case p, t, word, e + + init?(tag: NdbTagElem) { + let len = tag.count + if len == 1 { + switch tag.single_char { + case "p": self = .p + case "t": self = .t + case "e": self = .e + default: return nil + } + } else if len == 4 && tag.matches_str("word", tag_len: 4) { + self = .word + } else { + return nil + } + } + + var description: String { self.rawValue } + } + static func from_tag(tag: TagSequence) -> MuteItem? { - return MuteItem(tag.strings()) + guard tag.count >= 2 else { return nil } + + var i = tag.makeIterator() + + guard let t0 = i.next(), + let mkey = MuteKeys(tag: t0), + let t1 = i.next() + else { + return nil + } + + var expiry: Date? = nil + if let expiry_str = i.next(), let ts = expiry_str.u64() { + expiry = Date(timeIntervalSince1970: Double(ts)) + } + + switch mkey { + case .p: + return t1.id().map({ .user(Pubkey($0), expiry) }) + case .t: + return .hashtag(Hashtag(hashtag: t1.string()), expiry) + case .word: + return .word(t1.string(), expiry) + case .e: + guard let id = t1.id() else { return nil } + return .thread(NoteId(id), expiry) + } } } From a4f0eeadecc3bad5016c7dac8658a127e26ec237 Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Sat, 27 Jan 2024 13:43:34 -0700 Subject: [PATCH 11/18] mute: don't mutate string when adding hashtag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently there is an issue where if you try to mute a hashtag, it will mutate the `new_text` variable. This patch fixes that so that we aren’t mutating `new_text`. Lighting-address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Views/Muting/AddMuteItemView.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/damus/Views/Muting/AddMuteItemView.swift b/damus/Views/Muting/AddMuteItemView.swift index 73a058d7..428611da 100644 --- a/damus/Views/Muting/AddMuteItemView.swift +++ b/damus/Views/Muting/AddMuteItemView.swift @@ -65,8 +65,7 @@ struct AddMuteItemView: View { } } else if new_text.starts(with: "#") { // Remove the starting `#` character - new_text.removeFirst() - return .hashtag(Hashtag(hashtag: new_text), expiration_date) + return .hashtag(Hashtag(hashtag: String("\(new_text)".dropFirst())), expiration_date) } else { return .word(new_text, expiration_date) } From a6b508c25a9c8f8c11e0ded2508e9b13075e9551 Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Sat, 27 Jan 2024 14:06:38 -0700 Subject: [PATCH 12/18] mute: fix issue with not being able to change mute duration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch fixes an issue where the user can’t change the mute duration when adding a new mute item. The problem was that `expiration` was nil for indefinite which isn’t really allowed for a Picker. I fixed this by adding an indefinite case to DamusDuration. Closes: https://github.com/damus-io/damus/issues/1907 Lighting-address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Types/DamusDuration.swift | 5 +++++ damus/Views/Muting/AddMuteItemView.swift | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/damus/Types/DamusDuration.swift b/damus/Types/DamusDuration.swift index fa499b41..22e68a0f 100644 --- a/damus/Types/DamusDuration.swift +++ b/damus/Types/DamusDuration.swift @@ -8,12 +8,15 @@ import Foundation enum DamusDuration: CaseIterable { + case indefinite case day case week case month var title: String { switch self { + case .indefinite: + return NSLocalizedString("Indefinite", comment: "Mute a given item indefinitly (until user unmutes it). As opposed to muting the item for a given period of time.") case .day: return NSLocalizedString("24 hours", comment: "A duration of 24 hours/1 day to be shown to the user. Most likely in the context of how long they want to mute a piece of content for.") case .week: @@ -27,6 +30,8 @@ enum DamusDuration: CaseIterable { let current_date = Date() switch self { + case .indefinite: + return nil case .day: return Calendar.current.date(byAdding: .day, value: 1, to: current_date) case .week: diff --git a/damus/Views/Muting/AddMuteItemView.swift b/damus/Views/Muting/AddMuteItemView.swift index 428611da..ad4e923d 100644 --- a/damus/Views/Muting/AddMuteItemView.swift +++ b/damus/Views/Muting/AddMuteItemView.swift @@ -9,7 +9,7 @@ import SwiftUI struct AddMuteItemView: View { let state: DamusState @State var new_text: String = "" - @State var expiration: DamusDuration? + @State var expiration: DamusDuration = .indefinite @Environment(\.dismiss) var dismiss @@ -23,7 +23,6 @@ struct AddMuteItemView: View { .padding(.bottom) Picker(selection: $expiration) { - Text("Indefinite", comment: "Mute a given item indefinitly (until user unmutes it). As opposed to muting the item for a given period of time.") ForEach(DamusDuration.allCases, id: \.self) { duration in Text(duration.title).tag(duration) } @@ -55,7 +54,7 @@ struct AddMuteItemView: View { .cornerRadius(10) Button(action: { - let expiration_date: Date? = self.expiration?.date_from_now + let expiration_date: Date? = self.expiration.date_from_now let mute_item: MuteItem? = { if new_text.starts(with: "npub") { if let pubkey: Pubkey = bech32_pubkey_decode(new_text) { From d5606aabca70643cccd9807e1b44769d5aca38bb Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Sat, 27 Jan 2024 13:43:58 -0700 Subject: [PATCH 13/18] mute: fix bug where mutes can't be added without existing mutelist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit There is currently an issue where if a user doesn’t have an existing mutelist it won’t let the user add new mute items. This patch fixes that by not including the existing mutelist variable in the guard statement. Lighting-address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Signed-off-by: William Casarin --- damus/Views/Muting/AddMuteItemView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/damus/Views/Muting/AddMuteItemView.swift b/damus/Views/Muting/AddMuteItemView.swift index ad4e923d..46699735 100644 --- a/damus/Views/Muting/AddMuteItemView.swift +++ b/damus/Views/Muting/AddMuteItemView.swift @@ -72,9 +72,10 @@ struct AddMuteItemView: View { // Actually update & relay the new mute list if let mute_item { + let existing_mutelist = state.contacts.mutelist + guard let full_keypair = state.keypair.to_full(), - let existing_mutelist = state.contacts.mutelist, let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: mute_item) else { return From 96ed6b7cc7d496d438a8ee7b6314b3a9542a2bf6 Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Sat, 10 Feb 2024 09:36:46 -0700 Subject: [PATCH 14/18] mute: add maybe_get_content function to NdbNote MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch adds a maybe_get_content method to NdbNote which returns an optional string instead of “*failed to decrypt content*” on DM decryption failure. This method will be used by the MutelistManager in future patches. Lighting-Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Link: 20240210163650.42884-2-contact@charlie.fish Signed-off-by: William Casarin --- nostrdb/NdbNote.swift | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/nostrdb/NdbNote.swift b/nostrdb/NdbNote.swift index 8ed33ee9..ba543a71 100644 --- a/nostrdb/NdbNote.swift +++ b/nostrdb/NdbNote.swift @@ -346,6 +346,14 @@ extension NdbNote { return content } + func maybe_get_content(_ keypair: Keypair) -> String? { + if known_kind == .dm { + return decrypted(keypair: keypair) + } + + return content + } + func blocks(_ keypair: Keypair) -> Blocks { return get_blocks(keypair: keypair) } From a9baef7a213eaa0f10b80d658e61b08fb95e7d1d Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Sat, 10 Feb 2024 09:36:47 -0700 Subject: [PATCH 15/18] mute: adding MutelistManager.swift This patch adds MutelistManager which contains separate sets for each type of muted item. Whenever we receive a new event we parse it into those sets. When checking to see if an event is muted, we simply check in each of those sets if it contains the given mute. For words/phrases we have to loop through each item in the set to see if the event contains the given word. In future patches I will be implementing and integrating this new MutelistManager. Lighting-Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Link: 20240210163650.42884-3-contact@charlie.fish Signed-off-by: William Casarin --- damus.xcodeproj/project.pbxproj | 6 ++ damus/Models/MutelistManager.swift | 162 +++++++++++++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 damus/Models/MutelistManager.swift 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 + } +} From 1d4d2b020443fd67f5ddf5678b176b94a8aab58b Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Sat, 10 Feb 2024 09:36:48 -0700 Subject: [PATCH 16/18] mute: integrate new MutelistManager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This patch is slightly large (I think still within the guidelines tho) , but also pretty straightforward. I thought for a while about how I could split this up in a straightforward way, and I couldn’t come up with anything without breaking intermediate builds. - Deleted a lot of old/unnecessary code (ie. the Collection extension for MuteItem, since we’re now using the MutelistManager sets) - Changed damus_state.contacts to damus_state.mutelist_manager for all mute list manager work - Updated MutelistView to take advantage of new code (this is probably the largest change in this patch) Lighting-Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Link: 20240210163650.42884-4-contact@charlie.fish Signed-off-by: William Casarin --- .../NotificationExtensionState.swift | 2 + damus/ContentView.swift | 9 +- damus/Models/Contacts.swift | 39 +------ damus/Models/ContentFilters.swift | 2 +- damus/Models/DamusState.swift | 7 +- damus/Models/HeadlessDamusState.swift | 1 + damus/Models/HomeModel.swift | 28 ++--- damus/Models/MuteItem.swift | 61 ---------- damus/Models/MutedThreadsManager.swift | 4 +- damus/Models/NotificationsManager.swift | 2 +- damus/Models/SearchHomeModel.swift | 4 +- damus/Models/SearchModel.swift | 4 +- damus/TestData.swift | 1 + damus/Util/Router.swift | 9 +- damus/Views/DMChatView.swift | 2 +- damus/Views/DirectMessagesView.swift | 4 +- damus/Views/Events/EventMenu.swift | 8 +- .../Events/EventMutingContainerView.swift | 8 +- damus/Views/Muting/AddMuteItemView.swift | 4 +- damus/Views/Muting/MutelistView.swift | 105 ++++++++++-------- damus/Views/Profile/ProfileView.swift | 6 +- damus/Views/SearchHomeView.swift | 2 +- damus/Views/SearchView.swift | 10 +- damus/Views/SideMenuView.swift | 2 +- 24 files changed, 123 insertions(+), 201 deletions(-) diff --git a/DamusNotificationService/NotificationExtensionState.swift b/DamusNotificationService/NotificationExtensionState.swift index 69acc020..d5cb989f 100644 --- a/DamusNotificationService/NotificationExtensionState.swift +++ b/DamusNotificationService/NotificationExtensionState.swift @@ -11,6 +11,7 @@ struct NotificationExtensionState: HeadlessDamusState { let ndb: Ndb let settings: UserSettingsStore let contacts: Contacts + let mutelist_manager: MutelistManager let keypair: Keypair let profiles: Profiles let zaps: Zaps @@ -27,6 +28,7 @@ struct NotificationExtensionState: HeadlessDamusState { self.settings = UserSettingsStore() self.contacts = Contacts(our_pubkey: keypair.pubkey) + self.mutelist_manager = MutelistManager() self.keypair = keypair self.profiles = Profiles(ndb: ndb) self.zaps = Zaps(our_pubkey: keypair.pubkey) diff --git a/damus/ContentView.swift b/damus/ContentView.swift index b7625afa..c10ce9c8 100644 --- a/damus/ContentView.swift +++ b/damus/ContentView.swift @@ -550,7 +550,7 @@ struct ContentView: View { return } - ds.contacts.set_mutelist(mutelist) + ds.mutelist_manager.set_mutelist(mutelist) ds.postbox.send(mutelist) confirm_overwrite_mutelist = false @@ -569,7 +569,7 @@ struct ContentView: View { return } - if ds.contacts.mutelist == nil { + if ds.mutelist_manager.event == nil { confirm_overwrite_mutelist = true } else { guard let keypair = ds.keypair.to_full(), @@ -578,11 +578,11 @@ struct ContentView: View { return } - guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.contacts.mutelist, to_add: muting) else { + guard let ev = create_or_update_mutelist(keypair: keypair, mprev: ds.mutelist_manager.event, to_add: muting) else { return } - ds.contacts.set_mutelist(ev) + ds.mutelist_manager.set_mutelist(ev) ds.postbox.send(ev) } } @@ -660,6 +660,7 @@ struct ContentView: View { likes: EventCounter(our_pubkey: pubkey), boosts: EventCounter(our_pubkey: pubkey), contacts: Contacts(our_pubkey: pubkey), + mutelist_manager: MutelistManager(), profiles: Profiles(ndb: ndb), dms: home.dms, previews: PreviewCache(), diff --git a/damus/Models/Contacts.swift b/damus/Models/Contacts.swift index 28e11438..e411ef0d 100644 --- a/damus/Models/Contacts.swift +++ b/damus/Models/Contacts.swift @@ -13,51 +13,14 @@ class Contacts { private var friend_of_friends: Set = Set() /// Tracks which friends are friends of a given pubkey. private var pubkey_to_our_friends = [Pubkey : Set]() - private var muted: Set = Set() let our_pubkey: Pubkey var event: NostrEvent? - var mutelist: NostrEvent? - + init(our_pubkey: Pubkey) { self.our_pubkey = our_pubkey } - - func is_muted(_ item: MuteItem) -> Bool { - return muted.contains(item) - } - func set_mutelist(_ ev: NostrEvent) { - let oldlist = self.mutelist - self.mutelist = 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) { - new_mutes.insert(d) - } else { - new_unmutes.insert(d) - } - } - - // TODO: set local mutelist here - self.muted = ev.mute_list ?? Set() - - if new_mutes.count > 0 { - notify(.new_mutes(new_mutes)) - } - - if new_unmutes.count > 0 { - notify(.new_unmutes(new_unmutes)) - } - } - func remove_friend(_ pubkey: Pubkey) { friends.remove(pubkey) diff --git a/damus/Models/ContentFilters.swift b/damus/Models/ContentFilters.swift index c876f3b6..63a23a36 100644 --- a/damus/Models/ContentFilters.swift +++ b/damus/Models/ContentFilters.swift @@ -33,7 +33,7 @@ func get_repost_of_muted_user_filter(damus_state: DamusState) -> ((_ ev: NostrEv guard ev.known_kind == .boost else { return true } // This needs to use cached because it can be way too slow otherwise guard let inner_ev = ev.get_cached_inner_event(cache: damus_state.events) else { return true } - return should_show_event(contacts: damus_state.contacts, ev: inner_ev) + return should_show_event(state: damus_state, ev: inner_ev) } } diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift index 0b35abbf..32575bec 100644 --- a/damus/Models/DamusState.swift +++ b/damus/Models/DamusState.swift @@ -14,6 +14,7 @@ struct DamusState: HeadlessDamusState { let likes: EventCounter let boosts: EventCounter let contacts: Contacts + let mutelist_manager: MutelistManager let profiles: Profiles let dms: DirectMessagesModel let previews: PreviewCache @@ -34,13 +35,14 @@ struct DamusState: HeadlessDamusState { let video: VideoController let ndb: Ndb var purple: DamusPurple - - init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) { + + init(pool: RelayPool, keypair: Keypair, likes: EventCounter, boosts: EventCounter, contacts: Contacts, mutelist_manager: MutelistManager, profiles: Profiles, dms: DirectMessagesModel, previews: PreviewCache, zaps: Zaps, lnurls: LNUrls, settings: UserSettingsStore, relay_filters: RelayFilters, relay_model_cache: RelayModelCache, drafts: Drafts, events: EventCache, bookmarks: BookmarksManager, postbox: PostBox, bootstrap_relays: [String], replies: ReplyCounter, wallet: WalletModel, nav: NavigationCoordinator, music: MusicController?, video: VideoController, ndb: Ndb, purple: DamusPurple? = nil) { self.pool = pool self.keypair = keypair self.likes = likes self.boosts = boosts self.contacts = contacts + self.mutelist_manager = mutelist_manager self.profiles = profiles self.dms = dms self.previews = previews @@ -107,6 +109,7 @@ struct DamusState: HeadlessDamusState { likes: EventCounter(our_pubkey: empty_pub), boosts: EventCounter(our_pubkey: empty_pub), contacts: Contacts(our_pubkey: empty_pub), + mutelist_manager: MutelistManager(), profiles: Profiles(ndb: .empty), dms: DirectMessagesModel(our_pubkey: empty_pub), previews: PreviewCache(), diff --git a/damus/Models/HeadlessDamusState.swift b/damus/Models/HeadlessDamusState.swift index dcb9d032..d888c09d 100644 --- a/damus/Models/HeadlessDamusState.swift +++ b/damus/Models/HeadlessDamusState.swift @@ -15,6 +15,7 @@ protocol HeadlessDamusState { var ndb: Ndb { get } var settings: UserSettingsStore { get } var contacts: Contacts { get } + var mutelist_manager: MutelistManager { get } var keypair: Keypair { get } var profiles: Profiles { get } var zaps: Zaps { get } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift index 9ea9833d..25f894bd 100644 --- a/damus/Models/HomeModel.swift +++ b/damus/Models/HomeModel.swift @@ -244,7 +244,7 @@ class HomeModel { process_zap_event(state: damus_state, ev: ev) { zapres in guard case .done(let zap) = zapres, zap.target.pubkey == self.damus_state.keypair.pubkey, - should_show_event(contacts: self.damus_state.contacts, ev: zap.request.ev) else { + should_show_event(state: self.damus_state, ev: zap.request.ev) else { return } @@ -278,11 +278,11 @@ class HomeModel { func filter_events() { events.filter { ev in - !damus_state.contacts.is_muted(.user(ev.pubkey, nil)) + !damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil)) } self.dms.dms = dms.dms.filter { ev in - !damus_state.contacts.is_muted(.user(ev.pubkey, nil)) + !damus_state.mutelist_manager.is_muted(.user(ev.pubkey, nil)) } notifications.filter { ev in @@ -290,7 +290,7 @@ class HomeModel { return false } - let event_muted = damus_state.contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil + let event_muted = damus_state.mutelist_manager.is_event_muted(ev) return !event_muted } } @@ -570,13 +570,13 @@ class HomeModel { } // we only care about the most recent mutelist - if let mutelist = damus_state.contacts.mutelist { + if let mutelist = damus_state.mutelist_manager.event { if ev.created_at <= mutelist.created_at { return } } - damus_state.contacts.set_mutelist(ev) + damus_state.mutelist_manager.set_mutelist(ev) migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state) } @@ -588,7 +588,7 @@ class HomeModel { } // we only care about the most recent mutelist - if let mutelist = damus_state.contacts.mutelist { + if let mutelist = damus_state.mutelist_manager.event { if ev.created_at <= mutelist.created_at { return } @@ -598,7 +598,7 @@ class HomeModel { return } - damus_state.contacts.set_mutelist(ev) + damus_state.mutelist_manager.set_mutelist(ev) migrate_old_muted_threads_to_new_mutelist(keypair: damus_state.keypair, damus_state: damus_state) } @@ -616,7 +616,7 @@ class HomeModel { // don't show notifications from ourselves guard ev.pubkey != damus_state.pubkey, event_has_our_pubkey(ev, our_pubkey: self.damus_state.pubkey), - should_show_event(contacts: damus_state.contacts, ev: ev) else { + should_show_event(state: damus_state, ev: ev) else { return } @@ -654,7 +654,7 @@ class HomeModel { func handle_text_event(sub_id: String, _ ev: NostrEvent) { - guard should_show_event(contacts: damus_state.contacts, ev: ev) else { + guard should_show_event(state: damus_state, ev: ev) else { return } @@ -683,7 +683,7 @@ class HomeModel { } func handle_dm(_ ev: NostrEvent) { - guard should_show_event(contacts: damus_state.contacts, ev: ev) else { + guard should_show_event(state: damus_state, ev: ev) else { return } @@ -1090,13 +1090,13 @@ func event_has_our_pubkey(_ ev: NostrEvent, our_pubkey: Pubkey) -> Bool { func should_show_event(event: NostrEvent, damus_state: DamusState) -> Bool { return should_show_event( - contacts: damus_state.contacts, + state: damus_state, ev: event ) } -func should_show_event(contacts: Contacts, ev: NostrEvent) -> Bool { - let event_muted = contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil +func should_show_event(state: DamusState, ev: NostrEvent, keypair: Keypair? = nil) -> Bool { + let event_muted = state.mutelist_manager.is_event_muted(ev, keypair: keypair) if event_muted { return false } diff --git a/damus/Models/MuteItem.swift b/damus/Models/MuteItem.swift index 69bbe2c0..8df15a1d 100644 --- a/damus/Models/MuteItem.swift +++ b/damus/Models/MuteItem.swift @@ -200,64 +200,3 @@ extension MuteItem: TagConvertible { } } -extension Collection where Element == MuteItem { - /// 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) -> MuteItem? { - return self.first { muted_item in - switch muted_item { - case .user(let pubkey, let expiration_date): - return pubkey == ev.pubkey && !muted_item.is_expired() - case .hashtag(let hashtag, let expiration_date): - return ev.referenced_hashtags.contains(hashtag) && !muted_item.is_expired() - case .word(let word, let expiration_date): - return ev.content.lowercased().contains(word.lowercased()) && !muted_item.is_expired() - case .thread(let note_id, let expiration_date): - return ev.referenced_ids.contains(note_id) && !muted_item.is_expired() - } - } - } - - var users: [Pubkey] { - return self.compactMap { muted_item in - if case .user(let pubkey, _) = muted_item, - !muted_item.is_expired() { - return pubkey - } else { - return nil - } - } - } - var hashtags: [Hashtag] { - return self.compactMap { muted_item in - if case .hashtag(let hashtag, _) = muted_item, - !muted_item.is_expired() { - return hashtag - } else { - return nil - } - } - } - var words: [String] { - return self.compactMap { muted_item in - if case .word(let str, _) = muted_item, - !muted_item.is_expired() { - return str - } else { - return nil - } - } - } - var threads: [NoteId] { - return self.compactMap { muted_item in - if case .thread(let note_id, _) = muted_item, - !muted_item.is_expired() { - return note_id - } else { - return nil - } - } - } -} diff --git a/damus/Models/MutedThreadsManager.swift b/damus/Models/MutedThreadsManager.swift index 20c1903f..7a4e2c25 100644 --- a/damus/Models/MutedThreadsManager.swift +++ b/damus/Models/MutedThreadsManager.swift @@ -30,9 +30,9 @@ func migrate_old_muted_threads_to_new_mutelist(keypair: Keypair, damus_state: Da let mutedThreads = loadOldMutedThreads(pubkey: fullKeypair.pubkey) guard !mutedThreads.isEmpty else { return } // Set new muted system for those existing threads - let previous_mute_list_event = damus_state.contacts.mutelist + let previous_mute_list_event = damus_state.mutelist_manager.event guard let new_mutelist_event = create_or_update_mutelist(keypair: fullKeypair, mprev: previous_mute_list_event, to_add: Set(mutedThreads.map { MuteItem.thread($0, nil) })) else { return } - damus_state.contacts.set_mutelist(new_mutelist_event) + damus_state.mutelist_manager.set_mutelist(new_mutelist_event) damus_state.postbox.send(new_mutelist_event) // Set existing muted threads to an empty array UserDefaults.standard.set([], forKey: getMutedThreadsKey(pubkey: keypair.pubkey)) diff --git a/damus/Models/NotificationsManager.swift b/damus/Models/NotificationsManager.swift index 5718f56c..d0a9f313 100644 --- a/damus/Models/NotificationsManager.swift +++ b/damus/Models/NotificationsManager.swift @@ -37,7 +37,7 @@ func should_display_notification(state: HeadlessDamusState, event ev: NostrEvent } // Don't show notifications that match mute list. - if state.contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil { + if state.mutelist_manager.is_event_muted(ev, keypair: state.keypair) { return false } diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift index 63bcc810..33f4b608 100644 --- a/damus/Models/SearchHomeModel.swift +++ b/damus/Models/SearchHomeModel.swift @@ -35,7 +35,7 @@ class SearchHomeModel: ObservableObject { } func filter_muted() { - events.filter { should_show_event(contacts: damus_state.contacts, ev: $0) } + events.filter { should_show_event(state: damus_state, ev: $0) } self.objectWillChange.send() } @@ -60,7 +60,7 @@ class SearchHomeModel: ObservableObject { guard sub_id == self.base_subid || sub_id == self.profiles_subid else { return } - if ev.is_textlike && should_show_event(contacts: damus_state.contacts, ev: ev) && !ev.is_reply(damus_state.keypair) + if ev.is_textlike && should_show_event(state: damus_state, ev: ev) && !ev.is_reply(damus_state.keypair) { if !damus_state.settings.multiple_events_per_pubkey && seen_pubkey.contains(ev.pubkey) { return diff --git a/damus/Models/SearchModel.swift b/damus/Models/SearchModel.swift index 1d107fc3..faebe284 100644 --- a/damus/Models/SearchModel.swift +++ b/damus/Models/SearchModel.swift @@ -28,7 +28,7 @@ class SearchModel: ObservableObject { func filter_muted() { self.events.filter { - should_show_event(contacts: state.contacts, ev: $0) + should_show_event(state: state, ev: $0) } self.objectWillChange.send() } @@ -57,7 +57,7 @@ class SearchModel: ObservableObject { return } - guard should_show_event(contacts: state.contacts, ev: ev) else { + guard should_show_event(state: state, ev: ev) else { return } diff --git a/damus/TestData.swift b/damus/TestData.swift index f9515bb5..127468a7 100644 --- a/damus/TestData.swift +++ b/damus/TestData.swift @@ -73,6 +73,7 @@ var test_damus_state: DamusState = ({ likes: .init(our_pubkey: our_pubkey), boosts: .init(our_pubkey: our_pubkey), contacts: .init(our_pubkey: our_pubkey), + mutelist_manager: MutelistManager(), profiles: .init(ndb: ndb), dms: .init(our_pubkey: our_pubkey), previews: .init(), diff --git a/damus/Util/Router.swift b/damus/Util/Router.swift index fbcaf801..6ac44882 100644 --- a/damus/Util/Router.swift +++ b/damus/Util/Router.swift @@ -14,7 +14,7 @@ enum Route: Hashable { case Relay(relay: String, showActionButtons: Binding) case RelayDetail(relay: String, metadata: RelayMetadata?) case Following(following: FollowingModel) - case MuteList(mutelist_items: Set) + case MuteList case RelayConfig case Script(script: ScriptModel) case Bookmarks @@ -58,8 +58,8 @@ enum Route: Hashable { RelayDetailView(state: damusState, relay: relay, nip11: metadata) case .Following(let following): FollowingView(damus_state: damusState, following: following) - case .MuteList(let mutelist_items): - MutelistView(damus_state: damusState, mutelist_items: mutelist_items) + case .MuteList: + MutelistView(damus_state: damusState) case .RelayConfig: RelayConfigView(state: damusState) case .Bookmarks: @@ -139,9 +139,8 @@ enum Route: Hashable { hasher.combine(relay) case .Following: hasher.combine("following") - case .MuteList(let users): + case .MuteList: hasher.combine("muteList") - hasher.combine(users) case .RelayConfig: hasher.combine("relayConfig") case .Bookmarks: diff --git a/damus/Views/DMChatView.swift b/damus/Views/DMChatView.swift index a6222aa8..32a39fbc 100644 --- a/damus/Views/DMChatView.swift +++ b/damus/Views/DMChatView.swift @@ -20,7 +20,7 @@ struct DMChatView: View, KeyboardReadable { ScrollViewReader { scroller in ScrollView { LazyVStack(alignment: .leading) { - ForEach(Array(zip(dms.events, dms.events.indices)), id: \.0.id) { (ev, ind) in + ForEach(Array(zip(dms.events, dms.events.indices)).filter { should_show_event(state: damus_state, ev: $0.0, keypair: damus_state.keypair)}, id: \.0.id) { (ev, ind) in DMView(event: dms.events[ind], damus_state: damus_state) .contextMenu{MenuItems(damus_state: damus_state, event: ev, target_pubkey: ev.pubkey, profileModel: ProfileModel(pubkey: ev.pubkey, damus: damus_state))} } diff --git a/damus/Views/DirectMessagesView.swift b/damus/Views/DirectMessagesView.swift index 6d3cfacc..a0874a98 100644 --- a/damus/Views/DirectMessagesView.swift +++ b/damus/Views/DirectMessagesView.swift @@ -39,7 +39,7 @@ struct DirectMessagesView: View { func filter_dms(dms: [DirectMessageModel]) -> [DirectMessageModel] { return dms.filter({ dm in - return damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: dm.pubkey) && !damus_state.contacts.is_muted(.user(dm.pubkey, nil)) + return damus_state.settings.friend_filter.filter(contacts: damus_state.contacts, pubkey: dm.pubkey) && !damus_state.mutelist_manager.is_muted(.user(dm.pubkey, nil)) }) } @@ -53,7 +53,7 @@ struct DirectMessagesView: View { func MaybeEvent(_ model: DirectMessageModel) -> some View { Group { - if let ev = model.events.last { + if let ev = model.events.last(where: { should_show_event(state: damus_state, ev: $0, keypair: damus_state.keypair) }) { EventView(damus: damus_state, event: ev, pubkey: model.pubkey, options: options) .onTapGesture { self.model.set_active_dm_model(model) diff --git a/damus/Views/Events/EventMenu.swift b/damus/Views/Events/EventMenu.swift index d3144b1d..af267989 100644 --- a/damus/Views/Events/EventMenu.swift +++ b/damus/Views/Events/EventMenu.swift @@ -55,7 +55,7 @@ struct MenuItems: View { let bookmarked = damus_state.bookmarks.isBookmarked(event) self._isBookmarked = State(initialValue: bookmarked) - let muted_thread = (damus_state.contacts.mutelist?.mute_list?.event_muted_reason(event) != nil) + let muted_thread = damus_state.mutelist_manager.is_event_muted(event) self._isMutedThread = State(initialValue: muted_thread) self.damus_state = damus_state @@ -111,11 +111,11 @@ struct MenuItems: View { if event.known_kind != .dm { MuteDurationMenu { duration in if let full_keypair = self.damus_state.keypair.to_full(), - let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.contacts.mutelist, to_toggle: .thread(event.thread_id(keypair: damus_state.keypair), duration?.date_from_now)) { - damus_state.contacts.set_mutelist(new_mutelist_ev) + let new_mutelist_ev = toggle_from_mutelist(keypair: full_keypair, prev: damus_state.mutelist_manager.event, to_toggle: .thread(event.thread_id(keypair: damus_state.keypair), duration?.date_from_now)) { + damus_state.mutelist_manager.set_mutelist(new_mutelist_ev) damus_state.postbox.send(new_mutelist_ev) } - let muted = (damus_state.contacts.mutelist?.mute_list?.event_muted_reason(event) != nil) + let muted = damus_state.mutelist_manager.is_event_muted(event) isMutedThread = muted } label: { let imageName = isMutedThread ? "mute" : "mute" diff --git a/damus/Views/Events/EventMutingContainerView.swift b/damus/Views/Events/EventMutingContainerView.swift index c030be24..44887ac8 100644 --- a/damus/Views/Events/EventMutingContainerView.swift +++ b/damus/Views/Events/EventMutingContainerView.swift @@ -27,7 +27,7 @@ struct EventMutingContainerView: View { self.damus_state = damus_state self.event = event self.content = content() - self._shown = State(initialValue: should_show_event(contacts: damus_state.contacts, ev: event)) + self._shown = State(initialValue: should_show_event(state: damus_state, ev: event)) } init(damus_state: DamusState, event: NostrEvent, muteBox: @escaping MuteBoxViewClosure, @ViewBuilder content: () -> Content) { @@ -36,7 +36,7 @@ struct EventMutingContainerView: View { } var should_mute: Bool { - return !should_show_event(contacts: damus_state.contacts, ev: event) + return !should_show_event(state: damus_state, ev: event) } var body: some View { @@ -54,14 +54,14 @@ struct EventMutingContainerView: View { } } .onReceive(handle_notify(.new_mutes)) { mutes in - let new_muted_event_reason = mutes.event_muted_reason(event) + let new_muted_event_reason = damus_state.mutelist_manager.event_muted_reason(event) if new_muted_event_reason != nil { shown = false muted_reason = new_muted_event_reason } } .onReceive(handle_notify(.new_unmutes)) { unmutes in - if unmutes.event_muted_reason(event) != nil { + if damus_state.mutelist_manager.event_muted_reason(event) != nil { shown = true muted_reason = nil } diff --git a/damus/Views/Muting/AddMuteItemView.swift b/damus/Views/Muting/AddMuteItemView.swift index 46699735..cbc4f974 100644 --- a/damus/Views/Muting/AddMuteItemView.swift +++ b/damus/Views/Muting/AddMuteItemView.swift @@ -72,7 +72,7 @@ struct AddMuteItemView: View { // Actually update & relay the new mute list if let mute_item { - let existing_mutelist = state.contacts.mutelist + let existing_mutelist = state.mutelist_manager.event guard let full_keypair = state.keypair.to_full(), @@ -81,7 +81,7 @@ struct AddMuteItemView: View { return } - state.contacts.set_mutelist(mutelist) + state.mutelist_manager.set_mutelist(mutelist) state.postbox.send(mutelist) } diff --git a/damus/Views/Muting/MutelistView.swift b/damus/Views/Muting/MutelistView.swift index 37813383..2091c929 100644 --- a/damus/Views/Muting/MutelistView.swift +++ b/damus/Views/Muting/MutelistView.swift @@ -9,12 +9,16 @@ import SwiftUI struct MutelistView: View { let damus_state: DamusState - @State var mutelist_items: Set = Set() @State var show_add_muteitem: Bool = false + @State var users: [MuteItem] = [] + @State var hashtags: [MuteItem] = [] + @State var threads: [MuteItem] = [] + @State var words: [MuteItem] = [] + func RemoveAction(item: MuteItem) -> some View { Button { - guard let mutelist = damus_state.contacts.mutelist, + guard let mutelist = damus_state.mutelist_manager.event, let keypair = damus_state.keypair.to_full(), let new_ev = remove_from_mutelist(keypair: keypair, prev: mutelist, @@ -23,74 +27,88 @@ struct MutelistView: View { return } - damus_state.contacts.set_mutelist(new_ev) + damus_state.mutelist_manager.set_mutelist(new_ev) damus_state.postbox.send(new_ev) - mutelist_items = new_ev.mute_list ?? Set() + updateMuteItems() } label: { Label(NSLocalizedString("Delete", comment: "Button to remove a user from their mutelist."), image: "delete") } .tint(.red) } + func updateMuteItems() { + users = Array(damus_state.mutelist_manager.users) + hashtags = Array(damus_state.mutelist_manager.hashtags) + threads = Array(damus_state.mutelist_manager.threads) + words = Array(damus_state.mutelist_manager.words) + } var body: some View { List { Section(NSLocalizedString("Users", comment: "Section header title for a list of muted users.")) { - ForEach(mutelist_items.users, id: \.self) { pubkey in - UserViewRow(damus_state: damus_state, pubkey: pubkey) - .id(pubkey) - .swipeActions { - RemoveAction(item: .user(pubkey, nil)) - } - .onTapGesture { - damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) - } + ForEach(users, id: \.self) { user in + if case let MuteItem.user(pubkey, _) = user { + UserViewRow(damus_state: damus_state, pubkey: pubkey) + .id(pubkey) + .swipeActions { + RemoveAction(item: .user(pubkey, nil)) + } + .onTapGesture { + damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) + } + } } } Section(NSLocalizedString("Hashtags", comment: "Section header title for a list of hashtags that are muted.")) { - ForEach(mutelist_items.hashtags, id: \.hashtag) { hashtag in - Text("#\(hashtag.hashtag)") - .id(hashtag.hashtag) - .swipeActions { - RemoveAction(item: .hashtag(hashtag, nil)) - } - .onTapGesture { - damus_state.nav.push(route: Route.Search(search: SearchModel.init(state: damus_state, search: NostrFilter(hashtag: [hashtag.hashtag])))) - } + ForEach(hashtags, id: \.self) { item in + if case let MuteItem.hashtag(hashtag, _) = item { + Text("#\(hashtag.hashtag)") + .id(hashtag.hashtag) + .swipeActions { + RemoveAction(item: .hashtag(hashtag, nil)) + } + .onTapGesture { + damus_state.nav.push(route: Route.Search(search: SearchModel.init(state: damus_state, search: NostrFilter(hashtag: [hashtag.hashtag])))) + } + } } } Section(NSLocalizedString("Words", comment: "Section header title for a list of words that are muted.")) { - ForEach(mutelist_items.words, id: \.self) { word in - Text("\(word)") - .id(word) - .swipeActions { - RemoveAction(item: .word(word, nil)) - } + ForEach(words, id: \.self) { item in + if case let MuteItem.word(word, _) = item { + Text("\(word)") + .id(word) + .swipeActions { + RemoveAction(item: .word(word, nil)) + } + } } } Section(NSLocalizedString("Threads", comment: "Section header title for a list of threads that are muted.")) { - ForEach(mutelist_items.threads, id: \.self) { note_id in - if let event = damus_state.events.lookup(note_id) { - EventView(damus: damus_state, event: event) - .id(note_id.hex()) - .swipeActions { - RemoveAction(item: .thread(note_id, nil)) - } - } else { - Text(NSLocalizedString("Error retrieving muted event", comment: "Text for an item that application failed to retrieve the muted event for.")) + ForEach(threads, id: \.self) { item in + if case let MuteItem.thread(note_id, _) = item { + if let event = damus_state.events.lookup(note_id) { + EventView(damus: damus_state, event: event) + .id(note_id.hex()) + .swipeActions { + RemoveAction(item: .thread(note_id, nil)) + } + } else { + Text(NSLocalizedString("Error retrieving muted event", comment: "Text for an item that application failed to retrieve the muted event for.")) + } } } } } .navigationTitle(NSLocalizedString("Muted", comment: "Navigation title of view to see list of muted users & phrases.")) .onAppear { - mutelist_items = damus_state.contacts.mutelist?.mute_list ?? Set() + updateMuteItems() } .onReceive(handle_notify(.new_mutes)) { new_mutes in - mutelist_items = mutelist_items.union(new_mutes) + updateMuteItems() } .onReceive(handle_notify(.new_unmutes)) { new_unmutes in - mutelist_items = mutelist_items.subtracting(new_unmutes) + updateMuteItems() } .toolbar { ToolbarItem(placement: .topBarTrailing) { @@ -115,11 +133,6 @@ struct MutelistView: View { struct MutelistView_Previews: PreviewProvider { static var previews: some View { - MutelistView(damus_state: test_damus_state, mutelist_items: Set([ - .user(test_note.pubkey, nil), - .hashtag(Hashtag(hashtag: "test"), nil), - .word("test", nil), - .thread(test_note.id, nil) - ])) + MutelistView(damus_state: test_damus_state) } } diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift index e8f3e2f9..c3ff327a 100644 --- a/damus/Views/Profile/ProfileView.swift +++ b/damus/Views/Profile/ProfileView.swift @@ -179,11 +179,11 @@ struct ProfileView: View { notify(.report(.user(profile.pubkey))) } - if damus_state.contacts.is_muted(.user(profile.pubkey, nil)) { + if damus_state.mutelist_manager.is_muted(.user(profile.pubkey, nil)) { Button(NSLocalizedString("Unmute", comment: "Button to unmute a profile.")) { guard let keypair = damus_state.keypair.to_full(), - let mutelist = damus_state.contacts.mutelist + let mutelist = damus_state.mutelist_manager.event else { return } @@ -192,7 +192,7 @@ struct ProfileView: View { return } - damus_state.contacts.set_mutelist(new_ev) + damus_state.mutelist_manager.set_mutelist(new_ev) damus_state.postbox.send(new_ev) } } else { diff --git a/damus/Views/SearchHomeView.swift b/damus/Views/SearchHomeView.swift index 91a15e2d..3aa13c8d 100644 --- a/damus/Views/SearchHomeView.swift +++ b/damus/Views/SearchHomeView.swift @@ -59,7 +59,7 @@ struct SearchHomeView: View { return false } - let event_muted = damus_state.contacts.mutelist?.mute_list?.event_muted_reason(ev) != nil + let event_muted = damus_state.mutelist_manager.is_event_muted(ev) if event_muted { return false } diff --git a/damus/Views/SearchView.swift b/damus/Views/SearchView.swift index 3d5fddf5..083225ab 100644 --- a/damus/Views/SearchView.swift +++ b/damus/Views/SearchView.swift @@ -62,13 +62,13 @@ struct SearchView: View { Button { guard let full_keypair = appstate.keypair.to_full(), - let existing_mutelist = appstate.contacts.mutelist, + let existing_mutelist = appstate.mutelist_manager.event, let mutelist = remove_from_mutelist(keypair: full_keypair, prev: existing_mutelist, to_remove: .hashtag(Hashtag(hashtag: hashtag), nil)) else { return } - appstate.contacts.set_mutelist(mutelist) + appstate.mutelist_manager.set_mutelist(mutelist) appstate.postbox.send(mutelist) } label: { Text("Unmute Hashtag", comment: "Label represnting a button that the user can tap to unmute a given hashtag so they start seeing it in their feed again.") @@ -88,7 +88,7 @@ struct SearchView: View { } .onAppear { if let hashtag_string = search.search.hashtag?.first { - is_hashtag_muted = (appstate.contacts.mutelist?.mute_list ?? []).contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil)) + is_hashtag_muted = (appstate.mutelist_manager.event?.mute_list ?? []).contains(MuteItem.hashtag(Hashtag(hashtag: hashtag_string), nil)) } } } @@ -96,13 +96,13 @@ struct SearchView: View { func mute_hashtag(hashtag_string: String, expiration_time: Date?) { guard let full_keypair = appstate.keypair.to_full(), - let existing_mutelist = appstate.contacts.mutelist, + let existing_mutelist = appstate.mutelist_manager.event, let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: .hashtag(Hashtag(hashtag: hashtag_string), expiration_time)) else { return } - appstate.contacts.set_mutelist(mutelist) + appstate.mutelist_manager.set_mutelist(mutelist) appstate.postbox.send(mutelist) } diff --git a/damus/Views/SideMenuView.swift b/damus/Views/SideMenuView.swift index fe3b6eca..abe5adbe 100644 --- a/damus/Views/SideMenuView.swift +++ b/damus/Views/SideMenuView.swift @@ -66,7 +66,7 @@ struct SideMenuView: View { } } - NavigationLink(value: Route.MuteList(mutelist_items: damus_state.contacts.mutelist?.mute_list ?? Set())) { + NavigationLink(value: Route.MuteList) { navLabel(title: NSLocalizedString("Muted", comment: "Sidebar menu label for muted users view."), img: "mute") } From a18f50f2502ea616ee2a9c8fe26e0d60e4dc5896 Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Sat, 10 Feb 2024 09:36:49 -0700 Subject: [PATCH 17/18] mute: fix mute hashtag from search view if no existing mutelist There is a bug where if a user creates a new account and tries to mute a hashtag from the search view it will fail silently. This is due to the fact that the user has no existing mutelist. Lighting-Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Link: 20240210163650.42884-5-contact@charlie.fish Signed-off-by: William Casarin --- damus/Views/SearchView.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/damus/Views/SearchView.swift b/damus/Views/SearchView.swift index 083225ab..fc7d582e 100644 --- a/damus/Views/SearchView.swift +++ b/damus/Views/SearchView.swift @@ -94,9 +94,10 @@ struct SearchView: View { } func mute_hashtag(hashtag_string: String, expiration_time: Date?) { + let existing_mutelist = appstate.mutelist_manager.event + guard let full_keypair = appstate.keypair.to_full(), - let existing_mutelist = appstate.mutelist_manager.event, let mutelist = create_or_update_mutelist(keypair: full_keypair, mprev: existing_mutelist, to_add: .hashtag(Hashtag(hashtag: hashtag_string), expiration_time)) else { return From 68a18f5e405caabda234363f9d75e94e6eb98dc1 Mon Sep 17 00:00:00 2001 From: Charlie Fish Date: Sat, 10 Feb 2024 09:36:50 -0700 Subject: [PATCH 18/18] mute: fix bug with duplicate Indefinite items in MuteDurationMenu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `Indefinite` was added to DamusDuration in "Fixing issue with not being able to change mute duration” so this needs to be removed so that there isn’t a repeative item in the menu. Lighting-Address: fishcharlie@strike.me Signed-off-by: Charlie Fish Reviewed-by: William Casarin Link: 20240210163650.42884-6-contact@charlie.fish Signed-off-by: William Casarin --- damus/Views/Muting/MuteDurationMenu.swift | 5 ----- 1 file changed, 5 deletions(-) diff --git a/damus/Views/Muting/MuteDurationMenu.swift b/damus/Views/Muting/MuteDurationMenu.swift index 11498264..04a24bfa 100644 --- a/damus/Views/Muting/MuteDurationMenu.swift +++ b/damus/Views/Muting/MuteDurationMenu.swift @@ -13,11 +13,6 @@ struct MuteDurationMenu: View { var body: some View { Menu { - Button { - action(nil) - } label: { - Text("Indefinite", comment: "Mute a given item indefinitly (until user unmutes it). As opposed to muting the item for a given period of time.") - } ForEach(DamusDuration.allCases, id: \.self) { duration in Button { action(duration)