damus

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

commit bedf7e0648d3725241f141694076710ae903f031
parent 14802e334d6e66442a2be48056d44879992d1b0b
Author: William Casarin <jb55@jb55.com>
Date:   Tue,  4 Apr 2023 12:03:31 -0700

Add reply counts

Changelog-Added: Reply counts

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/Components/ZapButton.swift | 2+-
Mdamus/ContentView.swift | 3++-
Mdamus/Models/ActionBarModel.swift | 14++++++++++++--
Mdamus/Models/DamusState.swift | 4++--
Mdamus/Models/HomeModel.swift | 1+
Mdamus/Models/ThreadModel.swift | 1+
Adamus/Util/ReplyCounter.swift | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/ActionBar/EventActionBar.swift | 23++++++++++++++---------
Mdamus/Views/EventView.swift | 19+++----------------
10 files changed, 94 insertions(+), 31 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -37,6 +37,7 @@ 4C0A3F8F280F640A000448DE /* ThreadModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F8E280F640A000448DE /* ThreadModel.swift */; }; 4C0A3F91280F6528000448DE /* ChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F90280F6528000448DE /* ChatView.swift */; }; 4C0A3F93280F66F5000448DE /* ReplyMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C0A3F92280F66F5000448DE /* ReplyMap.swift */; }; + 4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */; }; 4C216F32286E388800040376 /* DMChatView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F31286E388800040376 /* DMChatView.swift */; }; 4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; }; 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; }; @@ -399,6 +400,7 @@ 4C0A3F8E280F640A000448DE /* ThreadModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadModel.swift; sourceTree = "<group>"; }; 4C0A3F90280F6528000448DE /* ChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatView.swift; sourceTree = "<group>"; }; 4C0A3F92280F66F5000448DE /* ReplyMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyMap.swift; sourceTree = "<group>"; }; + 4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyCounter.swift; sourceTree = "<group>"; }; 4C216F31286E388800040376 /* DMChatView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMChatView.swift; sourceTree = "<group>"; }; 4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; }; 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; }; @@ -955,6 +957,7 @@ 4C30AC7729A577AB00E2BD5A /* EventCache.swift */, 4C9BB83029C0ED4F00FC4E37 /* DisplayName.swift */, 4CE4F0F129D4FCFA005914DB /* DebouncedOnChange.swift */, + 4C1A9A1929DCA17E00516EAC /* ReplyCounter.swift */, ); path = Util; sourceTree = "<group>"; @@ -1622,6 +1625,7 @@ 4CE4F0F229D4FCFA005914DB /* DebouncedOnChange.swift in Sources */, 4CF0ABEC29844B4700D66079 /* AnyDecodable.swift in Sources */, 4C5F9118283D88E40052CD1C /* FollowingModel.swift in Sources */, + 4C1A9A1A29DCA17E00516EAC /* ReplyCounter.swift in Sources */, 643EA5C8296B764E005081BB /* RelayFilterView.swift in Sources */, 4C3EA67D28FFBBA300C48A62 /* InvoicesView.swift in Sources */, 4C363A8E28236FE4006E126D /* NoteContentView.swift in Sources */, diff --git a/damus/Components/ZapButton.swift b/damus/Components/ZapButton.swift @@ -134,7 +134,7 @@ struct ZapButton: View { struct ZapButton_Previews: PreviewProvider { static var previews: some View { - let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, our_like: nil, our_boost: nil, our_zap: nil) + let bar = ActionBarModel(likes: 0, boosts: 0, zaps: 10, zap_total: 15623414, replies: 2, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil) ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar) } } diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -627,7 +627,8 @@ struct ContentView: View { events: EventCache(), bookmarks: BookmarksManager(pubkey: pubkey), postbox: PostBox(pool: pool), - bootstrap_relays: bootstrap_relays + bootstrap_relays: bootstrap_relays, + replies: ReplyCounter(our_pubkey: pubkey) ) home.damus_state = self.damus_state! diff --git a/damus/Models/ActionBarModel.swift b/damus/Models/ActionBarModel.swift @@ -11,34 +11,40 @@ import Foundation class ActionBarModel: ObservableObject { @Published var our_like: NostrEvent? @Published var our_boost: NostrEvent? + @Published var our_reply: NostrEvent? @Published var our_zap: Zap? @Published var likes: Int @Published var boosts: Int @Published var zaps: Int @Published var zap_total: Int64 + @Published var replies: Int static func empty() -> ActionBarModel { - return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil) + return ActionBarModel(likes: 0, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil) } - init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?) { + init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?, our_reply: NostrEvent?) { self.likes = likes self.boosts = boosts self.zaps = zaps + self.replies = replies self.zap_total = zap_total self.our_like = our_like self.our_boost = our_boost self.our_zap = our_zap + self.our_reply = our_reply } func update(damus: DamusState, evid: String) { self.likes = damus.likes.counts[evid] ?? 0 self.boosts = damus.boosts.counts[evid] ?? 0 self.zaps = damus.zaps.event_counts[evid] ?? 0 + self.replies = damus.replies.get_replies(evid) self.zap_total = damus.zaps.event_totals[evid] ?? 0 self.our_like = damus.likes.our_events[evid] self.our_boost = damus.boosts.our_events[evid] self.our_zap = damus.zaps.our_zaps[evid]?.first + self.our_reply = damus.replies.our_reply(evid) self.objectWillChange.send() } @@ -54,6 +60,10 @@ class ActionBarModel: ObservableObject { return our_like != nil } + var replied: Bool { + return our_reply != nil + } + var boosted: Bool { return our_boost != nil } diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift @@ -27,8 +27,8 @@ struct DamusState { let events: EventCache let bookmarks: BookmarksManager let postbox: PostBox - let bootstrap_relays: [String] + let replies: ReplyCounter var pubkey: String { return keypair.pubkey @@ -39,6 +39,6 @@ struct DamusState { } static var empty: DamusState { - return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: []) + return DamusState.init(pool: RelayPool(), keypair: Keypair(pubkey: "", privkey: ""), likes: EventCounter(our_pubkey: ""), boosts: EventCounter(our_pubkey: ""), contacts: Contacts(our_pubkey: ""), tips: TipCounter(our_pubkey: ""), profiles: Profiles(), dms: DirectMessagesModel(our_pubkey: ""), previews: PreviewCache(), zaps: Zaps(our_pubkey: ""), lnurls: LNUrls(), settings: UserSettingsStore(), relay_filters: RelayFilters(our_pubkey: ""), relay_metadata: RelayMetadatas(), drafts: Drafts(), events: EventCache(), bookmarks: BookmarksManager(pubkey: ""), postbox: PostBox(pool: RelayPool()), bootstrap_relays: [], replies: ReplyCounter(our_pubkey: "")) } } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -517,6 +517,7 @@ class HomeModel: ObservableObject { return } + damus_state.replies.count_replies(ev) damus_state.events.insert(ev) if sub_id == home_subid { diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -114,6 +114,7 @@ class ThreadModel: ObservableObject { } let the_ev = damus_state.events.upsert(ev) + damus_state.replies.count_replies(the_ev) damus_state.events.add_replies(ev: the_ev) event_map.insert(ev) diff --git a/damus/Util/ReplyCounter.swift b/damus/Util/ReplyCounter.swift @@ -0,0 +1,54 @@ +// +// ReplyCounter.swift +// damus +// +// Created by William Casarin on 2023-04-04. +// + +import Foundation + +class ReplyCounter { + private var replies: [String: Int] + private var counted: Set<String> + private var our_replies: [String: NostrEvent] + private let our_pubkey: String + + init(our_pubkey: String) { + self.our_pubkey = our_pubkey + replies = [:] + counted = Set() + our_replies = [:] + } + + func our_reply(_ evid: String) -> NostrEvent? { + return our_replies[evid] + } + + func get_replies(_ evid: String) -> Int { + return replies[evid] ?? 0 + } + + func count_replies(_ event: NostrEvent) { + guard event.is_textlike else { + return + } + + if counted.contains(event.id) { + return + } + + counted.insert(event.id) + + for reply in event.direct_replies(nil) { + if event.pubkey == our_pubkey { + self.our_replies[reply.ref_id] = event + } + + if replies[reply.ref_id] != nil { + replies[reply.ref_id] = replies[reply.ref_id]! + 1 + } else { + replies[reply.ref_id] = 1 + } + } + } +} diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift @@ -47,10 +47,15 @@ struct EventActionBar: View { var body: some View { HStack { if damus_state.keypair.privkey != nil { - EventActionButton(img: "bubble.left", col: nil) { - notify(.reply, event) + HStack(spacing: 4) { + EventActionButton(img: "bubble.left", col: bar.replied ? Color.pink : Color.gray) { + notify(.reply, event) + } + .accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button")) + Text(verbatim: "\(bar.replies > 0 ? "\(bar.replies)" : "")") + .font(.footnote.weight(.medium)) + .foregroundColor(bar.replied ? Color.pink : Color.gray) } - .accessibilityLabel(NSLocalizedString("Reply", comment: "Accessibility label for reply button")) } Spacer() HStack(spacing: 4) { @@ -225,12 +230,12 @@ struct EventActionBar_Previews: PreviewProvider { let ev = NostrEvent(content: "hi", pubkey: pk) let bar = ActionBarModel.empty() - let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: nil, our_boost: nil, our_zap: nil) - let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: nil, our_zap: nil) - let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil) - let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil) - let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, our_like: NostrEvent(id: "", content: "", pubkey: ""), our_boost: NostrEvent(id: "", content: "", pubkey: ""), our_zap: nil) - let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, our_like: nil, our_boost: nil, our_zap: nil) + let likedbar = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil) + let likedbar_ours = ActionBarModel(likes: 10, boosts: 0, zaps: 0, zap_total: 0, replies: 0, our_like: test_event, our_boost: nil, our_zap: nil, our_reply: nil) + let maxed_bar = ActionBarModel(likes: 999, boosts: 999, zaps: 999, zap_total: 99999999, replies: 999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: nil) + let extra_max_bar = ActionBarModel(likes: 9999, boosts: 9999, zaps: 9999, zap_total: 99999999, replies: 9999, our_like: test_event, our_boost: test_event, our_zap: nil, our_reply: test_event) + let mega_max_bar = ActionBarModel(likes: 9999999, boosts: 99999, zaps: 9999, zap_total: 99999999, replies: 9999999, our_like: test_event, our_boost: test_event, our_zap: test_zap, our_reply: test_event) + let zapbar = ActionBarModel(likes: 0, boosts: 0, zaps: 5, zap_total: 10000000, replies: 0, our_like: nil, our_boost: nil, our_zap: nil, our_reply: nil) VStack(spacing: 50) { EventActionBar(damus_state: ds, event: ev, bar: bar) diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -151,22 +151,9 @@ func format_date(_ created_at: Int64) -> String { } func make_actionbar_model(ev: String, damus: DamusState) -> ActionBarModel { - let likes = damus.likes.counts[ev] - let boosts = damus.boosts.counts[ev] - let zaps = damus.zaps.event_counts[ev] - let zap_total = damus.zaps.event_totals[ev] - let our_like = damus.likes.our_events[ev] - let our_boost = damus.boosts.our_events[ev] - let our_zap = damus.zaps.our_zaps[ev] - - return ActionBarModel(likes: likes ?? 0, - boosts: boosts ?? 0, - zaps: zaps ?? 0, - zap_total: zap_total ?? 0, - our_like: our_like, - our_boost: our_boost, - our_zap: our_zap?.first - ) + let model = ActionBarModel.empty() + model.update(damus: damus, evid: ev) + return model }