damus

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

commit 0f05123ef8d3c9943ae8ca2dc682bc167e4cc634
parent 9f332a148fe4fc5af70b2e2a6206d8efb4e78dce
Author: Charlie Fish <contact@charlie.fish>
Date:   Wed, 17 Jan 2024 18:17:38 -0700

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 <contact@charlie.fish>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus/ContentView.swift | 4++--
Mdamus/Models/HomeModel.swift | 7+++++--
Mdamus/Models/MuteItem.swift | 7+++++++
Mdamus/Nostr/NostrEvent.swift | 12++++++++++++
Mdamus/Nostr/NostrKind.swift | 3++-
Mdamus/Util/Lists.swift | 70++++++++++++++++++----------------------------------------------------
Mdamus/Views/Muting/AddMuteItemView.swift | 14+++++++++++++-
Mdamus/Views/Muting/MutelistView.swift | 2+-
Mdamus/Views/Profile/ProfileView.swift | 2+-
MdamusTests/ListTests.swift | 42+++++++++++++++++++++++-------------------
Mnostrdb/NdbNote.swift | 4++++
11 files changed, 88 insertions(+), 79 deletions(-)

diff --git 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 @@ -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 @@ -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 @@ -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<MuteItem>? { + 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 @@ -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 @@ -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<MuteItem>) -> NostrEvent? { + let muted_items: Set<MuteItem> = (mprev?.mute_list ?? Set<MuteItem>()).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) - } - - 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 remove_from_mutelist(keypair: FullKeypair, prev: NostrEvent?, to_remove: MuteItem) -> NostrEvent? { + let muted_items: Set<MuteItem> = (prev?.mute_list ?? Set<MuteItem>()).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 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) +func toggle_from_mutelist(keypair: FullKeypair, prev: NostrEvent?, to_toggle: MuteItem) -> NostrEvent? { + let existing_muted_items: Set<MuteItem> = (prev?.mute_list ?? Set<MuteItem>()) - 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) - } + 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) } - - return false } diff --git 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 @@ -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 @@ -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 @@ -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 @@ -325,6 +325,10 @@ extension NdbNote { References<ReplaceableParam>(tags: self.tags) } + public var referenced_mute_items: References<MuteItem> { + References<MuteItem>(tags: self.tags) + } + public var references: References<RefId> { References<RefId>(tags: self.tags) }