damus

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

commit cfe14fac23c3cd70285af0afef1cb7a0207ca0a1
parent 2046fe5502322b5469ef5fe01baffc4680136146
Author: Terry Yiu <963907+tyiu@users.noreply.github.com>
Date:   Sun, 25 Jun 2023 23:35:01 -0400

Deduplicate users in group notifications

Changelog-Fixed: Deduplicate users in notifications
Closes: #1326

Diffstat:
Mdamus/Models/HomeModel.swift | 2+-
Mdamus/Util/DisplayName.swift | 2+-
Mdamus/Util/Keys.swift | 1+
Mdamus/Views/Events/TextEvent.swift | 2+-
Mdamus/Views/Notifications/EventGroupView.swift | 71++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mdamus/Views/Profile/MaybeAnonPfpView.swift | 2+-
Mdamus/Views/Zaps/ZapUserView.swift | 2+-
MdamusTests/EventGroupViewTests.swift | 38++++++++++++++++++++++++++++++--------
8 files changed, 84 insertions(+), 36 deletions(-)

diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -1076,7 +1076,7 @@ func zap_notification_title(_ zap: Zap) -> String { func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String { let src = zap.request.ev - let pk = zap.is_anon ? "anon" : src.pubkey + let pk = zap.is_anon ? ANON_PUBKEY : src.pubkey let profile = profiles.lookup(id: pk) let sats = NSNumber(value: (Double(zap.invoice.amount) / 1000.0)) let formattedSats = format_msats_abbrev(zap.invoice.amount) diff --git a/damus/Util/DisplayName.swift b/damus/Util/DisplayName.swift @@ -38,7 +38,7 @@ enum DisplayName { func parse_display_name(profile: Profile?, pubkey: String) -> DisplayName { - if pubkey == "anon" { + if pubkey == ANON_PUBKEY { return .one(NSLocalizedString("Anonymous", comment: "Placeholder display name of anonymous user.")) } diff --git a/damus/Util/Keys.swift b/damus/Util/Keys.swift @@ -9,6 +9,7 @@ import Foundation import secp256k1 let PUBKEY_HRP = "npub" +let ANON_PUBKEY = "anon" struct FullKeypair: Equatable { let pubkey: String diff --git a/damus/Views/Events/TextEvent.swift b/damus/Views/Events/TextEvent.swift @@ -132,7 +132,7 @@ struct TextEvent: View { func ProfileName(is_anon: Bool) -> some View { let profile = damus.profiles.lookup(id: pubkey) - let pk = is_anon ? "anon" : pubkey + let pk = is_anon ? ANON_PUBKEY : pubkey return EventProfileName(pubkey: pk, profile: profile, damus: damus, size: .normal) } diff --git a/damus/Views/Notifications/EventGroupView.swift b/damus/Views/Notifications/EventGroupView.swift @@ -78,19 +78,42 @@ func event_author_name(profiles: Profiles, pubkey: String) -> String { return Profile.displayName(profile: alice_prof, pubkey: pubkey).username.truncate(maxLength: 50) } -func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType) -> String { +func event_group_unique_pubkeys(profiles: Profiles, group: EventGroupType) -> [String] { + var seen = Set<String>() + var sorted = [String]() + if let zapgrp = group.zap_group { - let zap = zapgrp.zaps[ind] - - if zap.is_anon { - return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.") + let zaps = zapgrp.zaps + + for i in 0..<zaps.count { + let zap = zapgrp.zaps[i] + let pubkey: String + + if zap.is_anon { + pubkey = ANON_PUBKEY + } else { + pubkey = zap.request.ev.pubkey + } + + if !seen.contains(pubkey) { + seen.insert(pubkey) + sorted.append(pubkey) + } } - - return event_author_name(profiles: profiles, pubkey: zap.request.ev.pubkey) } else { - let ev = group.events[ind] - return event_author_name(profiles: profiles, pubkey: ev.pubkey) + let events = group.events + + for i in 0..<events.count { + let ev = events[i] + let pubkey = ev.pubkey + if !seen.contains(pubkey) { + seen.insert(pubkey) + sorted.append(pubkey) + } + } } + + return sorted } /** @@ -130,29 +153,29 @@ func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType "zapped_your_profile_2" - returned when 2 zaps occurred to the current user's profile "zapped_your_profile_3" - returned when 3 or more zaps occurred to the current user's profile */ -func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, nozaps: Bool, locale: Locale? = nil) -> String { - if group.events.count == 0 { +func reacting_to_text(profiles: Profiles, our_pubkey: String, group: EventGroupType, ev: NostrEvent?, nozaps: Bool, pubkeys: [String], locale: Locale? = nil) -> String { + if pubkeys.count == 0 { return "??" } let verb = reacting_to_verb(group: group) let reacting_to = determine_reacting_to(our_pubkey: our_pubkey, ev: ev, group: group, nozaps: nozaps) - let localization_key = "\(verb)_\(reacting_to)_\(min(group.events.count, 3))" + let localization_key = "\(verb)_\(reacting_to)_\(min(pubkeys.count, 3))" let format = localizedStringFormat(key: localization_key, locale: locale) - switch group.events.count { + switch pubkeys.count { case 1: - let display_name = event_group_author_name(profiles: profiles, ind: 0, group: group) + let display_name = event_author_name(profiles: profiles, pubkey: pubkeys[0]) return String(format: format, locale: locale, display_name) case 2: - let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group) - let bob_name = event_group_author_name(profiles: profiles, ind: 1, group: group) + let alice_name = event_author_name(profiles: profiles, pubkey: pubkeys[0]) + let bob_name = event_author_name(profiles: profiles, pubkey: pubkeys[1]) return String(format: format, locale: locale, alice_name, bob_name) default: - let alice_name = event_group_author_name(profiles: profiles, ind: 0, group: group) - let count = group.events.count - 1 + let alice_name = event_author_name(profiles: profiles, pubkey: pubkeys[0]) + let count = pubkeys.count - 1 return String(format: format, locale: locale, count, alice_name) } @@ -175,8 +198,8 @@ struct EventGroupView: View { let event: NostrEvent? let group: EventGroupType - var GroupDescription: some View { - Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event, nozaps: state.settings.nozaps))") + func GroupDescription(_ pubkeys: [String]) -> some View { + Text(verbatim: "\(reacting_to_text(profiles: state.profiles, our_pubkey: state.pubkey, group: group, ev: event, nozaps: state.settings.nozaps, pubkeys: pubkeys))") } func ZapIcon(_ zapgrp: ZapGroup) -> some View { @@ -216,12 +239,14 @@ struct EventGroupView: View { .frame(width: PFP_SIZE + 10) VStack(alignment: .leading) { - ProfilePicturesView(state: state, pubkeys: group.events.map { $0.pubkey }) + let unique_pubkeys = event_group_unique_pubkeys(profiles: state.profiles, group: group) + + ProfilePicturesView(state: state, pubkeys: unique_pubkeys) if let event { let thread = ThreadModel(event: event, damus_state: state) let dest = ThreadView(state: state, thread: thread) - GroupDescription + GroupDescription(unique_pubkeys) if !state.settings.nozaps || !group.is_note_zap { NavigationLink(destination: dest) { VStack(alignment: .leading) { @@ -234,7 +259,7 @@ struct EventGroupView: View { .buttonStyle(.plain) } } else { - GroupDescription + GroupDescription(unique_pubkeys) } } } diff --git a/damus/Views/Profile/MaybeAnonPfpView.swift b/damus/Views/Profile/MaybeAnonPfpView.swift @@ -38,6 +38,6 @@ struct MaybeAnonPfpView: View { struct MaybeAnonPfpView_Previews: PreviewProvider { static var previews: some View { - MaybeAnonPfpView(state: test_damus_state(), is_anon: true, pubkey: "anon", size: PFP_SIZE) + MaybeAnonPfpView(state: test_damus_state(), is_anon: true, pubkey: ANON_PUBKEY, size: PFP_SIZE) } } diff --git a/damus/Views/Zaps/ZapUserView.swift b/damus/Views/Zaps/ZapUserView.swift @@ -23,6 +23,6 @@ struct ZapUserView: View { struct ZapUserView_Previews: PreviewProvider { static var previews: some View { - ZapUserView(state: test_damus_state(), pubkey: "anon") + ZapUserView(state: test_damus_state(), pubkey: ANON_PUBKEY) } } diff --git a/damusTests/EventGroupViewTests.swift b/damusTests/EventGroupViewTests.swift @@ -18,6 +18,27 @@ final class EventGroupViewTests: XCTestCase { // Put teardown code here. This method is called after the invocation of each test method in the class. } + func testEventAuthorName() { + let damusState = test_damus_state() + XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: "pk1"), "pk1:pk1") + XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: "pk2"), "pk2:pk2") + XCTAssertEqual(event_author_name(profiles: damusState.profiles, pubkey: "anon"), "Anonymous") + } + + func testEventGroupUniquePubkeys() { + let damusState = test_damus_state() + + let encodedPost = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}" + let repost1 = NostrEvent(id: "", content: encodedPost, pubkey: "pk1", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) + let repost2 = NostrEvent(id: "", content: encodedPost, pubkey: "pk2", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) + let repost3 = NostrEvent(id: "", content: encodedPost, pubkey: "pk3", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) + + XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: []))), []) + XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1]))), ["pk1"]) + XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2]))), ["pk1", "pk2"]) + XCTAssertEqual(event_group_unique_pubkeys(profiles: damusState.profiles, group: .repost(EventGroup(events: [repost1, repost2, repost3]))), ["pk1", "pk2", "pk3"]) + } + func testReactingToText() throws { let enUsLocale = Locale(identifier: "en-US") let damusState = test_damus_state() @@ -25,18 +46,19 @@ final class EventGroupViewTests: XCTestCase { let encodedPost = "{\"id\": \"8ba545ab96959fe0ce7db31bc10f3ac3aa5353bc4428dbf1e56a7be7062516db\",\"pubkey\": \"7e27509ccf1e297e1df164912a43406218f8bd80129424c3ef798ca3ef5c8444\",\"created_at\": 1677013417,\"kind\": 1,\"tags\": [],\"content\": \"hello\",\"sig\": \"93684f15eddf11f42afbdd81828ee9fc35350344d8650c78909099d776e9ad8d959cd5c4bff7045be3b0b255144add43d0feef97940794a1bc9c309791bebe4a\"}" let repost1 = NostrEvent(id: "", content: encodedPost, pubkey: "pk1", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) let repost2 = NostrEvent(id: "", content: encodedPost, pubkey: "pk2", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) + let repost3 = NostrEvent(id: "", content: encodedPost, pubkey: "pk3", kind: NostrKind.boost.rawValue, tags: [], createdAt: 1) let nozaps = true - XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_event, nozaps: nozaps, locale: enUsLocale), "??") - XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, nozaps: nozaps, locale: enUsLocale), "pk1:pk1 reposted a note you were tagged in") - XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, nozaps: nozaps, locale: enUsLocale), "pk1:pk1 and pk2:pk2 reposted a note you were tagged in") - XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost2])), ev: test_event, nozaps: nozaps, locale: enUsLocale), "pk1:pk1 and 2 others reposted a note you were tagged in") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_event, nozaps: nozaps, pubkeys: [], locale: enUsLocale), "??") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, nozaps: nozaps, pubkeys: ["pk1"], locale: enUsLocale), "pk1:pk1 reposted a note you were tagged in") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, nozaps: nozaps, pubkeys: ["pk1", "pk2"], locale: enUsLocale), "pk1:pk1 and pk2:pk2 reposted a note you were tagged in") + XCTAssertEqual(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost2])), ev: test_event, nozaps: nozaps, pubkeys: ["pk1", "pk2", "pk3"], locale: enUsLocale), "pk1:pk1 and 2 others reposted a note you were tagged in") Bundle.main.localizations.map { Locale(identifier: $0) }.forEach { - XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_event, nozaps: nozaps, locale: $0), "??") - XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, nozaps: nozaps, locale: $0)) - XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, nozaps: nozaps, locale: $0)) - XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost2])), ev: test_event, nozaps: nozaps, locale: $0)) + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [])), ev: test_event, nozaps: nozaps, pubkeys: [], locale: $0), "??") + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1])), ev: test_event, nozaps: nozaps, pubkeys: ["pk1"], locale: $0)) + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2])), ev: test_event, nozaps: nozaps, pubkeys: ["pk1", "pk2"], locale: $0)) + XCTAssertNoThrow(reacting_to_text(profiles: damusState.profiles, our_pubkey: damusState.pubkey, group: .repost(EventGroup(events: [repost1, repost2, repost3])), ev: test_event, nozaps: nozaps, pubkeys: ["pk1", "pk2", "pk3"], locale: $0)) } }