damus

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

commit 043eb5b4360eba658b5c5a28759f32ee84dbd77a
parent 8f237b47ebfc49c47ae2eb2d38f5b68af7d75e17
Author: William Casarin <jb55@jb55.com>
Date:   Fri,  9 Jun 2023 10:10:33 +0200

Show zap comments in threads and show top zap

Changelog-Added: Top zaps
Changelog-Added: Show zap comments in threads

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 12++++++------
Mdamus/Components/ZapButton.swift | 2+-
Mdamus/Models/ActionBarModel.swift | 2+-
Mdamus/Models/DamusState.swift | 10+++++++++-
Mdamus/Models/HomeModel.swift | 146++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Mdamus/Models/Notifications/ZapGroup.swift | 10+++++-----
Mdamus/Models/NotificationsModel.swift | 6+++---
Mdamus/Models/ThreadModel.swift | 17+++++++++++++++++
Mdamus/Models/ZapsModel.swift | 7+------
Mdamus/Util/EventCache.swift | 13++++++++-----
Mdamus/Util/InsertSort.swift | 4++--
Mdamus/Util/Zap.swift | 37++++++++++++++++++++++++++++---------
Mdamus/Util/Zaps.swift | 19+++++++++++--------
Mdamus/Views/EventView.swift | 3++-
Mdamus/Views/Events/TextEvent.swift | 1+
Mdamus/Views/Events/ZapEvent.swift | 24+++++++++++++++++-------
Mdamus/Views/Notifications/EventGroupView.swift | 2+-
Mdamus/Views/ThreadView.swift | 67++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mdamus/Views/Zaps/ZapsView.swift | 4++--
19 files changed, 252 insertions(+), 134 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -270,12 +270,12 @@ 4CFF8F6D29CD022E008DB934 /* WideEventView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */; }; 4FE60CDD295E1C5E00105A1F /* Wallet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4FE60CDC295E1C5E00105A1F /* Wallet.swift */; }; 50088DA129E8271A008A1FDF /* WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50088DA029E8271A008A1FDF /* WebSocket.swift */; }; - 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; }; - 501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; }; 5019CADD2A0FB0A9000069E1 /* ProfileDatabaseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */; }; 501F8C5529FF5EF6001AFC1D /* PersistedProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */; }; 501F8C5829FF5FC5001AFC1D /* Damus.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5629FF5FC5001AFC1D /* Damus.xcdatamodeld */; }; 501F8C5A29FF70F5001AFC1D /* ProfileDatabase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */; }; + 501F8C802A0220E1001AFC1D /* KeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */; }; + 501F8C822A0224EB001AFC1D /* KeychainStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */; }; 50A50A8D29A09E1C00C01BE7 /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */; }; 50B5685329F97CB400A23243 /* CredentialHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50B5685229F97CB400A23243 /* CredentialHandler.swift */; }; 50DA11262A16A23F00236234 /* Launch.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50DA11252A16A23F00236234 /* Launch.storyboard */; }; @@ -714,12 +714,12 @@ 4CFF8F6C29CD022E008DB934 /* WideEventView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WideEventView.swift; sourceTree = "<group>"; }; 4FE60CDC295E1C5E00105A1F /* Wallet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Wallet.swift; sourceTree = "<group>"; }; 50088DA029E8271A008A1FDF /* WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocket.swift; sourceTree = "<group>"; }; - 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; }; - 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; }; 5019CADC2A0FB0A9000069E1 /* ProfileDatabaseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabaseTests.swift; sourceTree = "<group>"; }; 501F8C5429FF5EF6001AFC1D /* PersistedProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistedProfile.swift; sourceTree = "<group>"; }; 501F8C5729FF5FC5001AFC1D /* Damus.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Damus.xcdatamodel; sourceTree = "<group>"; }; 501F8C5929FF70F5001AFC1D /* ProfileDatabase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileDatabase.swift; sourceTree = "<group>"; }; + 501F8C7F2A0220E1001AFC1D /* KeychainStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorage.swift; sourceTree = "<group>"; }; + 501F8C812A0224EB001AFC1D /* KeychainStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainStorageTests.swift; sourceTree = "<group>"; }; 50A50A8C29A09E1C00C01BE7 /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; }; 50B5685229F97CB400A23243 /* CredentialHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CredentialHandler.swift; sourceTree = "<group>"; }; 50DA11252A16A23F00236234 /* Launch.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Launch.storyboard; sourceTree = "<group>"; }; @@ -2208,7 +2208,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; @@ -2256,7 +2256,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = damus/damus.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 3; + CURRENT_PROJECT_VERSION = 4; DEVELOPMENT_ASSET_PATHS = "\"damus/Preview Content\""; DEVELOPMENT_TEAM = XK7H4JAB3D; ENABLE_PREVIEWS = YES; diff --git a/damus/Components/ZapButton.swift b/damus/Components/ZapButton.swift @@ -35,7 +35,7 @@ struct ZapButton: View { @StateObject var button: ZapButtonModel = ZapButtonModel() var our_zap: Zapping? { - zaps.zaps.first(where: { z in z.request.pubkey == damus_state.pubkey }) + zaps.zaps.first(where: { z in z.request.ev.pubkey == damus_state.pubkey }) } var zap_img: String { diff --git a/damus/Models/ActionBarModel.swift b/damus/Models/ActionBarModel.swift @@ -20,7 +20,7 @@ class ActionBarModel: ObservableObject { @Published var our_zap: Zapping? @Published var likes: Int @Published var boosts: Int - @Published var zaps: Int + @Published private(set) var zaps: Int @Published var zap_total: Int64 @Published var replies: Int diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift @@ -35,8 +35,16 @@ struct DamusState { func add_zap(zap: Zapping) -> Bool { // store generic zap mapping self.zaps.add_zap(zap: zap) + let stored = self.events.store_zap(zap: zap) + + // thread zaps + if let ev = zap.event, zap.is_in_thread { + replies.count_replies(ev) + events.add_replies(ev: ev) + } + // associate with events as well - return self.events.store_zap(zap: zap) + return stored } var pubkey: String { diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -164,70 +164,38 @@ class HomeModel: ObservableObject { } } - func handle_zap_event_with_zapper(profiles: Profiles, ev: NostrEvent, our_keypair: Keypair, zapper: String) { - - guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else { - return - } - - damus_state.add_zap(zap: .zap(zap)) - - guard zap.target.pubkey == our_keypair.pubkey else { - return - } + func handle_zap_event(_ ev: NostrEvent) { + process_zap_event(damus_state: damus_state, ev: ev) { zapres in + guard case .done(let zap) = zapres else { return } + + guard zap.target.pubkey == self.damus_state.keypair.pubkey else { + return + } - if !notifications.insert_zap(.zap(zap)) { - return - } + if !self.notifications.insert_zap(.zap(zap)) { + return + } - if handle_last_event(ev: ev, timeline: .notifications) { - if damus_state.settings.zap_vibration { + guard let new_bits = handle_last_events(new_events: self.new_events, ev: ev, timeline: .notifications, shouldNotify: true) else { + return + } + + if self.damus_state.settings.zap_vibration { // Generate zap vibration zap_vibrate(zap_amount: zap.invoice.amount) } - if damus_state.settings.zap_notification { + + if self.damus_state.settings.zap_notification { // Create in-app local notification for zap received. switch zap.target { case .profile(let profile_id): - create_in_app_profile_zap_notification(profiles: profiles, zap: zap, profile_id: profile_id) + create_in_app_profile_zap_notification(profiles: self.damus_state.profiles, zap: zap, profile_id: profile_id) case .note(let note_target): - create_in_app_event_zap_notification(profiles: profiles, zap: zap, evId: note_target.note_id) + create_in_app_event_zap_notification(profiles: self.damus_state.profiles, zap: zap, evId: note_target.note_id) } } - } - - return - } - - func handle_zap_event(_ ev: NostrEvent) { - // These are zap notifications - guard let ptag = event_tag(ev, name: "p") else { - return - } - - let our_keypair = damus_state.keypair - if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) { - handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: local_zapper) - return - } - - guard let profile = damus_state.profiles.lookup(id: ptag) else { - return - } - - guard let lnurl = profile.lnurl else { - return - } - - Task { - guard let zapper = await fetch_zapper_from_lnurl(lnurl) else { - return - } - DispatchQueue.main.async { - self.damus_state.profiles.zappers[ptag] = zapper - self.handle_zap_event_with_zapper(profiles: self.damus_state.profiles, ev: ev, our_keypair: our_keypair, zapper: zapper) - } + self.new_events = new_bits } } @@ -1106,7 +1074,7 @@ func zap_notification_title(_ zap: Zap) -> String { } func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale.current) -> String { - let src = zap.private_request ?? zap.request.ev + let src = zap.request.ev let anon = event_is_anonymous(ev: src) let pk = anon ? "anon" : src.pubkey let profile = profiles.lookup(id: pk) @@ -1250,3 +1218,75 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) { } } + +enum ProcessZapResult { + case already_processed(Zap) + case done(Zap) + case failed +} + +func process_zap_event(damus_state: DamusState, ev: NostrEvent, completion: @escaping (ProcessZapResult) -> Void) { + // These are zap notifications + guard let ptag = event_tag(ev, name: "p") else { + completion(.failed) + return + } + + // just return the zap if we already have it + if let zap = damus_state.zaps.zaps[ev.id], case .zap(let z) = zap { + completion(.already_processed(z)) + return + } + + if let local_zapper = damus_state.profiles.lookup_zapper(pubkey: ptag) { + guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: local_zapper) else { + completion(.failed) + return + } + damus_state.add_zap(zap: .zap(zap)) + completion(.done(zap)) + return + } + + guard let profile = damus_state.profiles.lookup(id: ptag) else { + completion(.failed) + return + } + + guard let lnurl = profile.lnurl else { + completion(.failed) + return + } + + Task { + guard let zapper = await fetch_zapper_from_lnurl(lnurl) else { + completion(.failed) + return + } + + DispatchQueue.main.async { + damus_state.profiles.zappers[ptag] = zapper + guard let zap = process_zap_event_with_zapper(damus_state: damus_state, ev: ev, zapper: zapper) else { + completion(.failed) + return + } + damus_state.add_zap(zap: .zap(zap)) + completion(.done(zap)) + } + } + + +} + +fileprivate func process_zap_event_with_zapper(damus_state: DamusState, ev: NostrEvent, zapper: String) -> Zap? { + let our_keypair = damus_state.keypair + + guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: our_keypair.privkey) else { + return nil + } + + damus_state.add_zap(zap: .zap(zap)) + + return zap +} + diff --git a/damus/Models/Notifications/ZapGroup.swift b/damus/Models/Notifications/ZapGroup.swift @@ -21,12 +21,12 @@ class ZapGroup { } func zap_requests() -> [NostrEvent] { - zaps.map { z in z.request } + zaps.map { z in z.request.ev } } func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool { for zap in zaps { - if !isIncluded(zap.request) { + if !isIncluded(zap.request.ev) { return true } } @@ -35,7 +35,7 @@ class ZapGroup { } func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? { - let new_zaps = zaps.filter { isIncluded($0.request) } + let new_zaps = zaps.filter { isIncluded($0.request.ev) } guard new_zaps.count > 0 else { return nil } @@ -60,8 +60,8 @@ class ZapGroup { msat_total += zap.amount - if !zappers.contains(zap.request.pubkey) { - zappers.insert(zap.request.pubkey) + if !zappers.contains(zap.request.ev.pubkey) { + zappers.insert(zap.request.ev.pubkey) } return true diff --git a/damus/Models/NotificationsModel.swift b/damus/Models/NotificationsModel.swift @@ -150,7 +150,7 @@ class NotificationsModel: ObservableObject, ScrollQueue { } for zap in incoming_zaps { - pks.insert(zap.request.pubkey) + pks.insert(zap.request.ev.pubkey) } return Array(pks) @@ -307,7 +307,7 @@ class NotificationsModel: ObservableObject, ScrollQueue { changed = changed || incoming_events.count != count count = profile_zaps.zaps.count - profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request) } + profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request.ev) } changed = changed || profile_zaps.zaps.count != count for el in reactions { @@ -325,7 +325,7 @@ class NotificationsModel: ObservableObject, ScrollQueue { for el in zaps { count = el.value.zaps.count el.value.zaps = el.value.zaps.filter { - isIncluded($0.request) + isIncluded($0.request.ev) } changed = changed || el.value.zaps.count != count } diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -10,15 +10,21 @@ import Foundation /// manages the lifetime of a thread class ThreadModel: ObservableObject { @Published var event: NostrEvent + let original_event: NostrEvent var event_map: Set<NostrEvent> init(event: NostrEvent, damus_state: DamusState) { self.damus_state = damus_state self.event_map = Set() self.event = event + self.original_event = event add_event(event) } + var is_original: Bool { + return original_event.id == event.id + } + let damus_state: DamusState let profiles_subid = UUID().description @@ -101,6 +107,10 @@ class ThreadModel: ObservableObject { if ev.known_kind == .metadata { process_metadata_event(events: damus_state.events, our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) + } else if ev.known_kind == .zap { + process_zap_event(damus_state: damus_state, ev: ev) { zap in + + } } else if ev.is_textlike { self.add_event(ev) } @@ -116,3 +126,10 @@ class ThreadModel: ObservableObject { } } + + +func get_top_zap(events: EventCache, evid: String) -> Zapping? { + return events.get_cache_data(evid).zaps_model.zaps.first(where: { zap in + !zap.request.marked_hidden + }) +} diff --git a/damus/Models/ZapsModel.swift b/damus/Models/ZapsModel.swift @@ -53,18 +53,13 @@ class ZapsModel: ObservableObject { case .notice: break case .eose: - let events = state.events.lookup_zaps(target: target).map { $0.request } + let events = state.events.lookup_zaps(target: target).map { $0.request.ev } load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state) case .event(_, let ev): guard ev.kind == 9735 else { return } - if let zap = state.zaps.zaps[ev.id] { - state.events.store_zap(zap: zap) - return - } - guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else { return } diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift @@ -62,7 +62,7 @@ class ZapsDataModel: ObservableObject { } func confirm_nwc(reqid: String) { - guard let zap = zaps.first(where: { z in z.request.id == reqid }), + guard let zap = zaps.first(where: { z in z.request.ev.id == reqid }), case .pending(let pzap) = zap else { return @@ -83,16 +83,16 @@ class ZapsDataModel: ObservableObject { } func from(_ pubkey: String) -> [Zapping] { - return self.zaps.filter { z in z.request.pubkey == pubkey } + return self.zaps.filter { z in z.request.ev.pubkey == pubkey } } @discardableResult func remove(reqid: String) -> Bool { - guard zaps.first(where: { z in z.request.id == reqid }) != nil else { + guard zaps.first(where: { z in z.request.ev.id == reqid }) != nil else { return false } - self.zaps = zaps.filter { z in z.request.id != reqid } + self.zaps = zaps.filter { z in z.request.ev.id != reqid } return true } } @@ -175,6 +175,9 @@ class EventCache { @discardableResult func store_zap(zap: Zapping) -> Bool { let data = get_cache_data(zap.target.id).zaps_model + if let ev = zap.event { + insert(ev) + } return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap) } @@ -182,7 +185,7 @@ class EventCache { switch zap.target { case .note(let note_target): let zaps = get_cache_data(note_target.note_id).zaps_model - zaps.remove(reqid: zap.request.id) + zaps.remove(reqid: zap.request.ev.id) case .profile: // these aren't stored anywhere yet break diff --git a/damus/Util/InsertSort.swift b/damus/Util/InsertSort.swift @@ -11,10 +11,10 @@ func insert_uniq_sorted_zap(zaps: inout [Zapping], new_zap: Zapping, cmp: (Zappi var i: Int = 0 for zap in zaps { - if new_zap.request.id == zap.request.id { + if new_zap.request.ev.id == zap.request.ev.id { // replace pending if !new_zap.is_pending && zap.is_pending { - print("nwc: replacing pending with real zap \(new_zap.request.id)") + print("nwc: replacing pending with real zap \(new_zap.request.ev.id)") zaps[i] = new_zap return true } diff --git a/damus/Util/Zap.swift b/damus/Util/Zap.swift @@ -41,7 +41,16 @@ public enum ZapTarget: Equatable { struct ZapRequest { let ev: NostrEvent + let marked_hidden: Bool + var is_in_thread: Bool { + return !self.ev.content.isEmpty && !marked_hidden + } + + init(ev: NostrEvent) { + self.ev = ev + self.marked_hidden = ev.tags.first(where: { t in t.count > 0 && t[0] == "hidden" }) != nil + } } enum ExtPendingZapStateType { @@ -129,7 +138,7 @@ struct ZapRequestId: Equatable { let reqid: String init(from_zap: Zapping) { - self.reqid = from_zap.request.id + self.reqid = from_zap.request.ev.id } init(from_makezap: MakeZapRequest) { @@ -198,12 +207,12 @@ enum Zapping { } } - var request: NostrEvent { + var request: ZapRequest { switch self { case .zap(let zap): - return zap.request_ev + return zap.request case .pending(let pzap): - return pzap.request.ev + return pzap.request } } @@ -227,6 +236,15 @@ enum Zapping { } } + var is_in_thread: Bool { + switch self { + case .zap(let zap): + return zap.request.is_in_thread + case .pending(let pzap): + return pzap.request.is_in_thread + } + } + var is_anon: Bool { switch self { case .zap(let zap): @@ -242,12 +260,12 @@ struct Zap { public let invoice: ZapInvoice public let zapper: String /// zap authorizer public let target: ZapTarget - public let request: ZapRequest + public let raw_request: ZapRequest public let is_anon: Bool - public let private_request: NostrEvent? + public let private_request: ZapRequest? - var request_ev: NostrEvent { - return private_request ?? self.request.ev + var request: ZapRequest { + return private_request ?? self.raw_request } public static func from_zap_event(zap_ev: NostrEvent, zapper: String, our_privkey: String?) -> Zap? { @@ -295,8 +313,9 @@ struct Zap { } let is_anon = private_request == nil && event_is_anonymous(ev: zap_req) + let preq = private_request.map { pr in ZapRequest(ev: pr) } - return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: private_request) + return Zap(event: zap_ev, invoice: zap_invoice, zapper: zapper, target: target, raw_request: ZapRequest(ev: zap_req), is_anon: is_anon, private_request: preq) } } diff --git a/damus/Util/Zaps.swift b/damus/Util/Zaps.swift @@ -12,8 +12,8 @@ class Zaps { let our_pubkey: String var our_zaps: [String: [Zapping]] - var event_counts: [String: Int] - var event_totals: [String: Int64] + private(set) var event_counts: [String: Int] + private(set) var event_totals: [String: Int64] init(our_pubkey: String) { self.zaps = [:] @@ -27,13 +27,13 @@ class Zaps { var res: Zapping? = nil for kv in our_zaps { let ours = kv.value - guard let zap = ours.first(where: { z in z.request.id == reqid }) else { + guard let zap = ours.first(where: { z in z.request.ev.id == reqid }) else { continue } res = zap - our_zaps[kv.key] = ours.filter { z in z.request.id != reqid } + our_zaps[kv.key] = ours.filter { z in z.request.ev.id != reqid } if let count = event_counts[zap.target.id] { event_counts[zap.target.id] = count - 1 @@ -51,13 +51,16 @@ class Zaps { } func add_zap(zap: Zapping) { - if zaps[zap.request.id] != nil { + if zaps[zap.request.ev.id] != nil { return } - self.zaps[zap.request.id] = zap + self.zaps[zap.request.ev.id] = zap + if let zap_id = zap.event?.id { + self.zaps[zap_id] = zap + } // record our zaps for an event - if zap.request.pubkey == our_pubkey { + if zap.request.ev.pubkey == our_pubkey { switch zap.target { case .note(let note_target): if our_zaps[note_target.note_id] == nil { @@ -71,7 +74,7 @@ class Zaps { } // don't count tips to self. lame. - guard zap.request.pubkey != zap.target.pubkey else { + guard zap.request.ev.pubkey != zap.target.pubkey else { return } diff --git a/damus/Views/EventView.swift b/damus/Views/EventView.swift @@ -38,8 +38,9 @@ struct EventView: View { } } else if event.known_kind == .zap { if let zap = damus.zaps.zaps[event.id] { - ZapEvent(damus: damus, zap: zap) + ZapEvent(damus: damus, zap: zap, is_top_zap: options.contains(.top_zap)) } else { + Text("Invalid Zap") EmptyView() } } else { diff --git a/damus/Views/Events/TextEvent.swift b/damus/Views/Events/TextEvent.swift @@ -18,6 +18,7 @@ struct EventViewOptions: OptionSet { static let no_translate = EventViewOptions(rawValue: 1 << 6) static let small_pfp = EventViewOptions(rawValue: 1 << 7) static let nested = EventViewOptions(rawValue: 1 << 8) + static let top_zap = EventViewOptions(rawValue: 1 << 9) static let embedded: EventViewOptions = [.no_action_bar, .small_pfp, .wide, .truncate_content, .nested] } diff --git a/damus/Views/Events/ZapEvent.swift b/damus/Views/Events/ZapEvent.swift @@ -10,13 +10,23 @@ import SwiftUI struct ZapEvent: View { let damus: DamusState let zap: Zapping + let is_top_zap: Bool var body: some View { VStack(alignment: .leading) { HStack(alignment: .center) { - Text("⚡️ \(format_msats(zap.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user") + Image("zap.fill") + .foregroundColor(.orange) + + Text("\(format_msats(zap.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user") .font(.headline) - .padding([.top], 2) + + if is_top_zap { + Text("Top Zap") + .font(.caption) + .foregroundColor(.gray) + .padding([.top], 2) + } if zap.is_private { Image("lock") @@ -31,7 +41,7 @@ struct ZapEvent: View { } } - TextEvent(damus: damus, event: zap.request, pubkey: zap.request.pubkey, options: [.no_action_bar, .no_replying_to]) + TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, options: [.no_action_bar, .no_replying_to]) .padding([.top], 1) } } @@ -41,18 +51,18 @@ struct ZapEvent: View { let test_zap_invoice = ZapInvoice(description: .description("description"), amount: 10000, string: "lnbc1", expiry: 1000000, payment_hash: Data(), created_at: 1000000) let test_zap_request_ev = NostrEvent(content: "hi", pubkey: "pk", kind: 9734) let test_zap_request = ZapRequest(ev: test_zap_request_ev) -let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: nil) +let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), raw_request: test_zap_request, is_anon: false, private_request: nil) -let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), request: test_zap_request, is_anon: false, private_request: test_event) +let test_private_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper", target: .profile("pk"), raw_request: test_zap_request, is_anon: false, private_request: .init(ev: test_event)) let test_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: "id", author: "pk"), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice))) struct ZapEvent_Previews: PreviewProvider { static var previews: some View { VStack { - ZapEvent(damus: test_damus_state(), zap: .zap(test_zap)) + ZapEvent(damus: test_damus_state(), zap: .zap(test_zap), is_top_zap: true) - ZapEvent(damus: test_damus_state(), zap: .zap(test_private_zap)) + ZapEvent(damus: test_damus_state(), zap: .zap(test_private_zap), is_top_zap: false) } } } diff --git a/damus/Views/Notifications/EventGroupView.swift b/damus/Views/Notifications/EventGroupView.swift @@ -72,7 +72,7 @@ func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.") } - return event_author_name(profiles: profiles, pubkey: zap.request.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) diff --git a/damus/Views/ThreadView.swift b/damus/Views/ThreadView.swift @@ -10,9 +10,18 @@ import SwiftUI struct ThreadView: View { let state: DamusState - @StateObject var thread: ThreadModel + @ObservedObject var thread: ThreadModel + @ObservedObject var zaps: ZapsDataModel + @Environment(\.dismiss) var dismiss + init(state: DamusState, thread: ThreadModel) { + self.state = state + self._thread = ObservedObject(wrappedValue: thread) + let zaps = state.events.get_cache_data(thread.event.id).zaps_model + self._zaps = ObservedObject(wrappedValue: zaps) + } + var parent_events: [NostrEvent] { state.events.parent_events(event: thread.event) } @@ -22,23 +31,28 @@ struct ThreadView: View { } var body: some View { + let top_zap = get_top_zap(events: state.events, evid: thread.event.id) ScrollViewReader { reader in ScrollView { LazyVStack { // MARK: - Parents events view ForEach(parent_events, id: \.id) { parent_event in - MutedEventView(damus_state: state, - event: parent_event, - selected: false) - .padding(.horizontal) - .onTapGesture { - thread.set_active_event(parent_event) - scroll_to_event(scroller: reader, id: parent_event.id, delay: 0.1, animate: false) + if top_zap?.event?.id != parent_event.id { + + MutedEventView(damus_state: state, + event: parent_event, + selected: false) + .padding(.horizontal) + .onTapGesture { + thread.set_active_event(parent_event) + scroll_to_event(scroller: reader, id: parent_event.id, delay: 0.1, animate: false) + } + + Divider() + .padding(.top, 4) + .padding(.leading, 25 * 2) } - Divider() - .padding(.top, 4) - .padding(.leading, 25 * 2) }.background(GeometryReader { geometry in // get the height and width of the EventView view let eventHeight = geometry.frame(in: .global).height @@ -59,20 +73,27 @@ struct ThreadView: View { ) .id(self.thread.event.id) + if let top_zap { + ZapEvent(damus: state, zap: top_zap, is_top_zap: true) + .padding(.horizontal) + } + ForEach(child_events, id: \.id) { child_event in - MutedEventView( - damus_state: state, - event: child_event, - selected: false - ) - .padding(.horizontal) - .onTapGesture { - thread.set_active_event(child_event) - scroll_to_event(scroller: reader, id: child_event.id, delay: 0.1, animate: false) + if top_zap?.event?.id != child_event.id { + MutedEventView( + damus_state: state, + event: child_event, + selected: false + ) + .padding(.horizontal) + .onTapGesture { + thread.set_active_event(child_event) + scroll_to_event(scroller: reader, id: child_event.id, delay: 0.1, animate: false) + } + + Divider() + .padding([.top], 4) } - - Divider() - .padding([.top], 4) } } }.navigationBarTitle(NSLocalizedString("Thread", comment: "Navigation bar title for note thread.")) diff --git a/damus/Views/Zaps/ZapsView.swift b/damus/Views/Zaps/ZapsView.swift @@ -22,8 +22,8 @@ struct ZapsView: View { var body: some View { ScrollView { LazyVStack { - ForEach(zaps.zaps, id: \.request.id) { zap in - ZapEvent(damus: state, zap: zap) + ForEach(zaps.zaps, id: \.request.ev.id) { zap in + ZapEvent(damus: state, zap: zap, is_top_zap: false) .padding([.horizontal]) } }