damus

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

commit 03691d03699091318a26ba9a8140c2632fadd997
parent 1518a0a16c7ac0fbe3b790173047b5fc28e02c37
Author: William Casarin <jb55@jb55.com>
Date:   Sat, 13 May 2023 21:33:34 -0700

Pending Zaps

A fairly large change that replaces Zaps in the codebase with "Zapping"
which is a tagged union consisting of a resolved Zap and a Pending Zap.
These are both counted as Zaps everywhere in Damus, except pending zaps
can be cancelled (most of the time).

Diffstat:
M.envrc | 2+-
Mdamus/Components/ZapButton.swift | 215++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mdamus/ContentView.swift | 8+++++++-
Mdamus/Models/ActionBarModel.swift | 13+++++++------
Mdamus/Models/DamusState.swift | 2+-
Mdamus/Models/HomeModel.swift | 43+++++++++++++++++++++++++++++++++++++------
Mdamus/Models/Notifications/ZapGroup.swift | 24+++++++++---------------
Mdamus/Models/NotificationsModel.swift | 12++++++------
Mdamus/Models/ZapsModel.swift | 31++++++++++++++-----------------
Mdamus/Nostr/NostrKind.swift | 2++
Mdamus/Nostr/Relay.swift | 39++++++++++++++++++++++++++++++++-------
Mdamus/Nostr/RelayPool.swift | 12++++++------
Mdamus/Util/EventCache.swift | 52+++++++++++++++++++++++++++++++++++++++++++++++-----
Mdamus/Util/InsertSort.swift | 19++++++++++++-------
Mdamus/Util/PostBox.swift | 47++++++++++++++++++++++++++++++++++++++++++-----
Mdamus/Util/WalletConnect.swift | 135++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mdamus/Util/Zap.swift | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mdamus/Util/Zaps.swift | 50++++++++++++++++++++++++++++++++++++++++++--------
Mdamus/Views/ActionBar/EventActionBar.swift | 4++--
Mdamus/Views/Events/TextEvent.swift | 4+++-
Mdamus/Views/Events/ZapEvent.swift | 28+++++++++++++++-------------
Mdamus/Views/Notifications/EventGroupView.swift | 6+-----
Mdamus/Views/Relays/RelayConfigView.swift | 4++--
Mdamus/Views/Zaps/ZapsView.swift | 9++++++---
24 files changed, 734 insertions(+), 175 deletions(-)

diff --git a/.envrc b/.envrc @@ -1,4 +1,4 @@ -use nix +#use nix export TODO_FILE=$PWD/TODO diff --git a/damus/Components/ZapButton.swift b/damus/Components/ZapButton.swift @@ -23,45 +23,90 @@ struct ZappingEvent { let event: NostrEvent } +class ZapButtonModel: ObservableObject { + var invoice: String? = nil + @Published var zapping: String = "" + @Published var showing_select_wallet: Bool = false + @Published var showing_zap_customizer: Bool = false +} + struct ZapButton: View { let damus_state: DamusState let event: NostrEvent let lnurl: String - @ObservedObject var bar: ActionBarModel + @ObservedObject var zaps: ZapsDataModel + @StateObject var button: ZapButtonModel = ZapButtonModel() - @State var zapping: Bool = false - @State var invoice: String = "" - @State var showing_select_wallet: Bool = false - @State var showing_zap_customizer: Bool = false - @State var is_charging: Bool = false + var our_zap: Zapping? { + zaps.zaps.first(where: { z in z.request.pubkey == damus_state.pubkey }) + } var zap_img: String { - if bar.zapped { - return "bolt.fill" - } - - if !zapping { + switch our_zap { + case .none: return "bolt" + case .zap: + return "bolt.fill" + case .pending: + return "bolt.fill" } - - return "bolt.fill" } - var zap_color: Color? { - if bar.zapped { + var zap_color: Color { + switch our_zap { + case .none: + return Color.gray + case .pending: + return Color.yellow + case .zap: return Color.orange } - - if is_charging { - return Color.yellow + } + + func tap() { + guard let our_zap else { + send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) + return } - if !zapping { - return nil + // we've tapped and we have a zap already... cancel if we can + switch our_zap { + case .zap: + // can't undo a zap we've already sent + // if we want to send more zaps we will need to long-press + print("cancel_zap: we already have a real zap, can't cancel") + break + case .pending(let pzap): + guard let res = cancel_zap(zap: pzap, box: damus_state.postbox, zapcache: damus_state.zaps, evcache: damus_state.events) else { + + UIImpactFeedbackGenerator(style: .soft).impactOccurred() + return + } + + switch res { + case .send_err(let cancel_err): + switch cancel_err { + case .nothing_to_cancel: + print("cancel_zap: got nothing_to_cancel in pending") + break + case .not_delayed: + print("cancel_zap: got not_delayed in pending") + break + case .too_late: + print("cancel_zap: got too_late in pending") + break + } + case .already_confirmed: + print("cancel_zap: got already_confirmed in pending") + break + case .not_nwc: + print("cancel_zap: got not_nwc in pending") + break + } } - - return Color.yellow + + } var body: some View { @@ -69,37 +114,32 @@ struct ZapButton: View { Button(action: { }, label: { Image(systemName: zap_img) - .foregroundColor(zap_color == nil ? Color.gray : zap_color!) + .foregroundColor(zap_color) .font(.footnote.weight(.medium)) }) .simultaneousGesture(LongPressGesture().onEnded {_ in - guard !zapping else { + guard our_zap == nil else { return } - self.showing_zap_customizer = true + button.showing_zap_customizer = true }) - .highPriorityGesture(TapGesture().onEnded {_ in - guard !zapping else { - return - } - - send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) - self.zapping = true + .highPriorityGesture(TapGesture().onEnded { + tap() }) .accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button")) - if bar.zap_total > 0 { - Text(verbatim: format_msats_abbrev(bar.zap_total)) + if zaps.zap_total > 0 { + Text(verbatim: format_msats_abbrev(zaps.zap_total)) .font(.footnote) - .foregroundColor(bar.zapped ? Color.orange : Color.gray) + .foregroundColor(zap_color) } } - .sheet(isPresented: $showing_zap_customizer) { + .sheet(isPresented: $button.showing_zap_customizer) { CustomizeZapView(state: damus_state, event: event, lnurl: lnurl) } - .sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) { - SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice) + .sheet(isPresented: $button.showing_select_wallet, onDismiss: {button.showing_select_wallet = false}) { + SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $button.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: button.invoice ?? "") } .onReceive(handle_notify(.zapping)) { notif in let zap_ev = notif.object as! ZappingEvent @@ -117,15 +157,13 @@ struct ZapButton: View { break case .got_zap_invoice(let inv): if damus_state.settings.show_wallet_selector { - self.invoice = inv - self.showing_select_wallet = true + self.button.invoice = inv + self.button.showing_select_wallet = true } else { let wallet = damus_state.settings.default_wallet.model open_with_wallet(wallet: wallet, invoice: inv) } } - - self.zapping = false } } } @@ -133,13 +171,25 @@ struct ZapButton: View { struct ZapButton_Previews: PreviewProvider { static var previews: some View { - 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) + let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: "noteid", author: "author"), request: test_zap_request, type: .pub, state: .external(.init(state: .fetching_invoice))) + let zaps = ZapsDataModel([.pending(pending_zap)]) + + ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", zaps: zaps) } } +func initial_pending_zap_state(settings: UserSettingsStore) -> PendingZapState { + if let url = settings.nostr_wallet_connect, + let nwc = WalletConnectURL(str: url) + { + return .nwc(NWCPendingZapState(state: .fetching_invoice, url: nwc)) + } + + return .external(ExtPendingZapState(state: .fetching_invoice)) +} + func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) { guard let keypair = damus_state.keypair.to_full() else { return @@ -150,7 +200,18 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust let target = ZapTarget.note(id: event.id, author: event.pubkey) let content = comment ?? "" - let zapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) + guard let zapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else { + // this should never happen + return + } + + let zap_amount = amount_sats ?? damus_state.settings.default_zap_amount + let amount_msat = Int64(zap_amount) * 1000 + let pending_zap_state = initial_pending_zap_state(settings: damus_state.settings) + let pending_zap = PendingZap(amount_msat: amount_msat, target: target, request: ZapRequest(ev: zapreq), type: zap_type, state: pending_zap_state) + + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() + damus_state.add_zap(zap: .pending(pending_zap)) Task { var mpayreq = damus_state.lnurls.lookup(target.pubkey) @@ -161,6 +222,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust guard let payreq = mpayreq else { // TODO: show error DispatchQueue.main.async { + remove_zap(reqid: zapreq.id, zapcache: damus_state.zaps, evcache: damus_state.events) let typ = ZappingEventType.failed(.bad_lnurl) let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event) notify(.zapping, ev) @@ -172,10 +234,9 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust damus_state.lnurls.endpoints[target.pubkey] = payreq } - let zap_amount = amount_sats ?? damus_state.settings.default_zap_amount - guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else { DispatchQueue.main.async { + remove_zap(reqid: zapreq.id, zapcache: damus_state.zaps, evcache: damus_state.events) let typ = ZappingEventType.failed(.fetching_invoice) let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event) notify(.zapping, ev) @@ -184,10 +245,24 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust } DispatchQueue.main.async { - if let url = damus_state.settings.nostr_wallet_connect, - let nwc = WalletConnectURL(str: url) { - nwc_pay(url: nwc, pool: damus_state.pool, post: damus_state.postbox, invoice: inv) - } else { + + switch pending_zap_state { + case .nwc(let nwc_state): + // don't both continuing, user has canceled + if case .cancel_fetching_invoice = nwc_state.state { + remove_zap(reqid: zapreq.id, zapcache: damus_state.zaps, evcache: damus_state.events) + return + } + + guard let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv), + case .nwc(let pzap_state) = pending_zap_state + else { + return + } + + pzap_state.state = .postbox_pending(nwc_req) + case .external(let pending_ext): + pending_ext.state = .done let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event) notify(.zapping, ev) } @@ -196,3 +271,41 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust return } + +enum CancelZapErr { + case send_err(CancelSendErr) + case already_confirmed + case not_nwc +} + +func cancel_zap(zap: PendingZap, box: PostBox, zapcache: Zaps, evcache: EventCache) -> CancelZapErr? { + guard case .nwc(let nwc_state) = zap.state else { + return .not_nwc + } + + switch nwc_state.state { + case .fetching_invoice: + nwc_state.state = .cancel_fetching_invoice + // let the code that retrieves the invoice remove the zap, because + // it still needs access to this pending zap to know to cancel + + case .cancel_fetching_invoice: + // already cancelling? + print("cancel_zap: already cancelling") + return nil + + case .confirmed: + return .already_confirmed + + case .postbox_pending(let nwc_req): + if let err = box.cancel_send(evid: nwc_req.id) { + return .send_err(err) + } + remove_zap(reqid: zap.request.ev.id, zapcache: zapcache, evcache: evcache) + + case .failed: + remove_zap(reqid: zap.request.ev.id, zapcache: zapcache, evcache: evcache) + } + + return nil +} diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -581,7 +581,8 @@ struct ContentView: View { let new_relay_filters = load_relay_filters(pubkey) == nil for relay in bootstrap_relays { if let url = RelayURL(relay) { - add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, url: url, info: .rw, new_relay_filters: new_relay_filters) + let descriptor = RelayDescriptor(url: url, info: .rw) + add_new_relay(relay_filters: relay_filters, metadatas: metadatas, pool: pool, descriptor: descriptor, new_relay_filters: new_relay_filters) } } @@ -592,6 +593,11 @@ struct ContentView: View { let settings = UserSettingsStore() UserSettingsStore.shared = settings + if let nwc_str = settings.nostr_wallet_connect, + let nwc = WalletConnectURL(str: nwc_str) { + try? pool.add_relay(.nwc(url: nwc.relay)) + } + self.damus_state = DamusState(pool: pool, keypair: keypair, likes: EventCounter(our_pubkey: pubkey), diff --git a/damus/Models/ActionBarModel.swift b/damus/Models/ActionBarModel.swift @@ -7,12 +7,17 @@ import Foundation +enum Zapped { + case not_zapped + case pending + case zapped +} 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 our_zap: Zapping? @Published var likes: Int @Published var boosts: Int @Published var zaps: Int @@ -35,7 +40,7 @@ class ActionBarModel: ObservableObject { self.replies = 0 } - init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zap?, our_reply: NostrEvent?) { + init(likes: Int, boosts: Int, zaps: Int, zap_total: Int64, replies: Int, our_like: NostrEvent?, our_boost: NostrEvent?, our_zap: Zapping?, our_reply: NostrEvent?) { self.likes = likes self.boosts = boosts self.zaps = zaps @@ -64,10 +69,6 @@ class ActionBarModel: ObservableObject { return likes == 0 && boosts == 0 && zaps == 0 } - var zapped: Bool { - return our_zap != nil - } - var liked: Bool { return our_like != nil } diff --git a/damus/Models/DamusState.swift b/damus/Models/DamusState.swift @@ -32,7 +32,7 @@ struct DamusState { let wallet: WalletModel @discardableResult - func add_zap(zap: Zap) -> Bool { + func add_zap(zap: Zapping) -> Bool { // store generic zap mapping self.zaps.add_zap(zap: zap) // associate with events as well diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -129,6 +129,25 @@ class HomeModel: ObservableObject { handle_zap_event(ev) case .zap_request: break + case .nwc_request: + break + case .nwc_response: + handle_nwc_response(ev) + } + } + + func handle_nwc_response(_ ev: NostrEvent) { + Task { @MainActor in + guard let resp = await FullWalletResponse(from: ev) else { + return + } + + if resp.response.error == nil { + nwc_success(zapcache: self.damus_state.zaps, resp: resp) + return + } + + nwc_error(zapcache: self.damus_state.zaps, evcache: self.damus_state.events, resp: resp) } } @@ -137,13 +156,13 @@ class HomeModel: ObservableObject { return } - damus_state.add_zap(zap: zap) + damus_state.add_zap(zap: .zap(zap)) guard zap.target.pubkey == our_keypair.pubkey else { return } - if !notifications.insert_zap(zap) { + if !notifications.insert_zap(.zap(zap)) { return } @@ -301,6 +320,16 @@ class HomeModel: ObservableObject { //remove_bootstrap_nodes(damus_state) send_home_filters(relay_id: relay_id) } + + // connect to nwc relays when connected + if let nwc_str = damus_state.settings.nostr_wallet_connect, + let r = pool.get_relay(relay_id), + r.descriptor.variant == .nwc, + let nwc = WalletConnectURL(str: nwc_str), + nwc.relay.id == relay_id + { + subscribe_to_nwc(url: nwc, pool: pool) + } case .error(let merr): let desc = String(describing: merr) if desc.contains("Software caused connection abort") { @@ -431,7 +460,7 @@ class HomeModel: ObservableObject { print_filters(relay_id: relay_id, filters: [home_filters, contacts_filters, notifications_filters, dms_filters]) - if let relay_id = relay_id { + if let relay_id { pool.send(.subscribe(.init(filters: home_filters, sub_id: home_subid)), to: [relay_id]) pool.send(.subscribe(.init(filters: contacts_filters, sub_id: contacts_subid)), to: [relay_id]) pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_subid)), to: [relay_id]) @@ -836,7 +865,8 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) { changed = true if new.contains(d) { if let url = RelayURL(d) { - add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, url: url, info: decoded[d] ?? .rw, new_relay_filters: new_relay_filters) + let descriptor = RelayDescriptor(url: url, info: decoded[d] ?? .rw) + add_new_relay(relay_filters: state.relay_filters, metadatas: state.relay_metadata, pool: state.pool, descriptor: descriptor, new_relay_filters: new_relay_filters) } } else { state.pool.remove_relay(d) @@ -849,8 +879,9 @@ func load_our_relays(state: DamusState, m_old_ev: NostrEvent?, ev: NostrEvent) { } } -func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, url: RelayURL, info: RelayInfo, new_relay_filters: Bool) { - try? pool.add_relay(url, info: info) +func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool) { + try? pool.add_relay(descriptor) + let url = descriptor.url let relay_id = url.id guard metadatas.lookup(relay_id: relay_id) == nil else { diff --git a/damus/Models/Notifications/ZapGroup.swift b/damus/Models/Notifications/ZapGroup.swift @@ -8,7 +8,7 @@ import Foundation class ZapGroup { - var zaps: [Zap] + var zaps: [Zapping] var msat_total: Int64 var zappers: Set<String> @@ -17,22 +17,16 @@ class ZapGroup { return 0 } - return first.event.created_at + return first.created_at } func zap_requests() -> [NostrEvent] { - zaps.map { z in - if let priv = z.private_request { - return priv - } else { - return z.request.ev - } - } + zaps.map { z in z.request } } func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool { for zap in zaps { - if !isIncluded(zap.request_ev) { + if !isIncluded(zap.request) { return true } } @@ -41,7 +35,7 @@ class ZapGroup { } func filter(_ isIncluded: (NostrEvent) -> Bool) -> ZapGroup? { - let new_zaps = zaps.filter { isIncluded($0.request_ev) } + let new_zaps = zaps.filter { isIncluded($0.request) } guard new_zaps.count > 0 else { return nil } @@ -59,15 +53,15 @@ class ZapGroup { } @discardableResult - func insert(_ zap: Zap) -> Bool { + func insert(_ zap: Zapping) -> Bool { if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) { return false } - msat_total += zap.invoice.amount + msat_total += zap.amount - if !zappers.contains(zap.request.ev.pubkey) { - zappers.insert(zap.request.ev.pubkey) + if !zappers.contains(zap.request.pubkey) { + zappers.insert(zap.request.pubkey) } return true diff --git a/damus/Models/NotificationsModel.swift b/damus/Models/NotificationsModel.swift @@ -99,7 +99,7 @@ enum NotificationItem { } class NotificationsModel: ObservableObject, ScrollQueue { - var incoming_zaps: [Zap] + var incoming_zaps: [Zapping] var incoming_events: [NostrEvent] var should_queue: Bool @@ -150,7 +150,7 @@ class NotificationsModel: ObservableObject, ScrollQueue { } for zap in incoming_zaps { - pks.insert(zap.request.ev.pubkey) + pks.insert(zap.request.pubkey) } return Array(pks) @@ -249,7 +249,7 @@ class NotificationsModel: ObservableObject, ScrollQueue { return false } - private func insert_zap_immediate(_ zap: Zap) -> Bool { + private func insert_zap_immediate(_ zap: Zapping) -> Bool { switch zap.target { case .note(let notezt): let id = notezt.note_id @@ -285,7 +285,7 @@ class NotificationsModel: ObservableObject, ScrollQueue { return false } - func insert_zap(_ zap: Zap) -> Bool { + func insert_zap(_ zap: Zapping) -> Bool { if should_queue { return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap) } @@ -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.ev) } + profile_zaps.zaps = profile_zaps.zaps.filter { zap in isIncluded(zap.request) } 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.ev) + isIncluded($0.request) } changed = changed || el.value.zaps.count != count } diff --git a/damus/Models/ZapsModel.swift b/damus/Models/ZapsModel.swift @@ -19,7 +19,7 @@ class ZapsModel: ObservableObject { self.target = target } - var zaps: [Zap] { + var zaps: [Zapping] { return state.events.lookup_zaps(target: target) } @@ -53,7 +53,7 @@ class ZapsModel: ObservableObject { case .notice: break case .eose: - let events = state.events.lookup_zaps(target: target).map { $0.request_ev } + let events = state.events.lookup_zaps(target: target).map { $0.request } 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 { @@ -61,22 +61,19 @@ class ZapsModel: ObservableObject { } if let zap = state.zaps.zaps[ev.id] { - if state.events.store_zap(zap: zap) { - objectWillChange.send() - } - } else { - guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else { - return - } - - guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else { - return - } - - if self.state.add_zap(zap: zap) { - objectWillChange.send() - } + state.events.store_zap(zap: zap) + return + } + + guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else { + return } + + guard let zap = Zap.from_zap_event(zap_ev: ev, zapper: zapper, our_privkey: state.keypair.privkey) else { + return + } + + self.state.add_zap(zap: .zap(zap)) } diff --git a/damus/Nostr/NostrKind.swift b/damus/Nostr/NostrKind.swift @@ -22,4 +22,6 @@ enum NostrKind: Int { case list = 30000 case zap = 9735 case zap_request = 9734 + case nwc_request = 23194 + case nwc_response = 23195 } diff --git a/damus/Nostr/Relay.swift b/damus/Nostr/Relay.swift @@ -10,21 +10,46 @@ import Foundation public struct RelayInfo: Codable { let read: Bool? let write: Bool? - let ephemeral: Bool? - init(read: Bool, write: Bool, ephemeral: Bool = false) { + init(read: Bool, write: Bool) { self.read = read self.write = write - self.ephemeral = ephemeral } - static let rw = RelayInfo(read: true, write: true, ephemeral: false) - static let ephemeral = RelayInfo(read: true, write: true, ephemeral: true) + static let rw = RelayInfo(read: true, write: true) +} + +enum RelayVariant { + case regular + case ephemeral + case nwc } public struct RelayDescriptor { - public let url: RelayURL - public let info: RelayInfo + let url: RelayURL + let info: RelayInfo + let variant: RelayVariant + + init(url: RelayURL, info: RelayInfo, variant: RelayVariant = .regular) { + self.url = url + self.info = info + self.variant = variant + } + + var ephemeral: Bool { + switch variant { + case .regular: + return false + case .ephemeral: + return true + case .nwc: + return true + } + } + + static func nwc(url: RelayURL) -> RelayDescriptor { + return RelayDescriptor(url: url, info: .rw, variant: .nwc) + } } enum RelayFlags: Int { diff --git a/damus/Nostr/RelayPool.swift b/damus/Nostr/RelayPool.swift @@ -43,7 +43,7 @@ class RelayPool { } var our_descriptors: [RelayDescriptor] { - return all_descriptors.filter { d in !(d.info.ephemeral ?? false) } + return all_descriptors.filter { d in !d.ephemeral } } var all_descriptors: [RelayDescriptor] { @@ -91,7 +91,8 @@ class RelayPool { } } - func add_relay(_ url: RelayURL, info: RelayInfo) throws { + func add_relay(_ desc: RelayDescriptor) throws { + let url = desc.url let relay_id = get_relay_id(url) if get_relay(relay_id) != nil { throw RelayError.RelayAlreadyExists @@ -99,8 +100,7 @@ class RelayPool { let conn = RelayConnection(url: url) { event in self.handle_event(relay_id: relay_id, event: event) } - let descriptor = RelayDescriptor(url: url, info: info) - let relay = Relay(descriptor: descriptor, connection: conn) + let relay = Relay(descriptor: desc, connection: conn) self.relays.append(relay) } @@ -196,7 +196,7 @@ class RelayPool { continue } - if (relay.descriptor.info.ephemeral ?? false) && skip_ephemeral { + if relay.descriptor.ephemeral && skip_ephemeral { continue } @@ -266,7 +266,7 @@ func add_rw_relay(_ pool: RelayPool, _ url: String) { guard let url = RelayURL(url) else { return } - try? pool.add_relay(url, info: RelayInfo.rw) + try? pool.add_relay(RelayDescriptor(url: url, info: .rw)) } diff --git a/damus/Util/EventCache.swift b/damus/Util/EventCache.swift @@ -55,11 +55,42 @@ class PreviewModel: ObservableObject { } class ZapsDataModel: ObservableObject { - @Published var zaps: [Zap] + @Published var zaps: [Zapping] - init(_ zaps: [Zap]) { + init(_ zaps: [Zapping]) { self.zaps = zaps } + + func update_state(reqid: String, state: PendingZapState) { + guard let zap = zaps.first(where: { z in z.request.id == reqid }), + case .pending(let pzap) = zap, + pzap.state != state + else { + return + } + + pzap.state = state + + self.objectWillChange.send() + } + + var zap_total: Int64 { + zaps.reduce(0) { total, zap in total + zap.amount } + } + + func from(_ pubkey: String) -> [Zapping] { + return self.zaps.filter { z in z.request.pubkey == pubkey } + } + + @discardableResult + func remove(reqid: String) -> Bool { + guard zaps.first(where: { z in z.request.id == reqid }) != nil else { + return false + } + + self.zaps = zaps.filter { z in z.request.id != reqid } + return true + } } class RelativeTimeModel: ObservableObject { @@ -86,7 +117,7 @@ class EventData { return preview_model.state } - init(zaps: [Zap] = []) { + init(zaps: [Zapping] = []) { self.translations_model = .init(state: .havent_tried) self.artifacts_model = .init(state: .not_loaded) self.zaps_model = .init(zaps) @@ -131,12 +162,23 @@ class EventCache { } @discardableResult - func store_zap(zap: Zap) -> Bool { + func store_zap(zap: Zapping) -> Bool { let data = get_cache_data(zap.target.id).zaps_model return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap) } - func lookup_zaps(target: ZapTarget) -> [Zap] { + func remove_zap(zap: Zapping) { + 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) + case .profile: + // these aren't stored anywhere yet + break + } + } + + func lookup_zaps(target: ZapTarget) -> [Zapping] { return get_cache_data(target.id).zaps_model.zaps } diff --git a/damus/Util/InsertSort.swift b/damus/Util/InsertSort.swift @@ -7,12 +7,17 @@ import Foundation -func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) -> Bool) -> Bool { +func insert_uniq_sorted_zap(zaps: inout [Zapping], new_zap: Zapping, cmp: (Zapping, Zapping) -> Bool) -> Bool { var i: Int = 0 for zap in zaps { - // don't insert duplicate events - if new_zap.event.id == zap.event.id { + if new_zap.request.id == zap.request.id { + // replace pending + if !new_zap.is_pending && zap.is_pending { + zaps[i] = new_zap + return true + } + // don't insert duplicate events return false } @@ -28,16 +33,16 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) -> } @discardableResult -func insert_uniq_sorted_zap_by_created(zaps: inout [Zap], new_zap: Zap) -> Bool { +func insert_uniq_sorted_zap_by_created(zaps: inout [Zapping], new_zap: Zapping) -> Bool { return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in - a.event.created_at > b.event.created_at + a.created_at > b.created_at } } @discardableResult -func insert_uniq_sorted_zap_by_amount(zaps: inout [Zap], new_zap: Zap) -> Bool { +func insert_uniq_sorted_zap_by_amount(zaps: inout [Zapping], new_zap: Zapping) -> Bool { return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in - a.invoice.amount > b.invoice.amount + a.amount > b.amount } } diff --git a/damus/Util/PostBox.swift b/damus/Util/PostBox.swift @@ -26,16 +26,24 @@ class PostedEvent { let event: NostrEvent let skip_ephemeral: Bool var remaining: [Relayer] + let flush_after: Date? - init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool) { + init(event: NostrEvent, remaining: [String], skip_ephemeral: Bool, flush_after: Date? = nil) { self.event = event self.skip_ephemeral = skip_ephemeral + self.flush_after = flush_after self.remaining = remaining.map { Relayer(relay: $0, attempts: 0, retry_after: 2.0) } } } +enum CancelSendErr { + case nothing_to_cancel + case not_delayed + case too_late +} + class PostBox { let pool: RelayPool var events: [String: PostedEvent] @@ -46,12 +54,37 @@ class PostBox { pool.register_handler(sub_id: "postbox", handler: handle_event) } + // only works reliably on delay-sent events + func cancel_send(evid: String) -> CancelSendErr? { + guard let ev = events[evid] else { + return .nothing_to_cancel + } + + guard let after = ev.flush_after else { + return .not_delayed + } + + guard Date.now < after else { + return .too_late + } + + events.removeValue(forKey: evid) + return nil + } + func try_flushing_events() { let now = Int64(Date().timeIntervalSince1970) for kv in events { let event = kv.value + + // some are delayed + if let after = event.flush_after, Date.now.timeIntervalSince1970 < after.timeIntervalSince1970 { + continue + } + for relayer in event.remaining { - if relayer.last_attempt == nil || (now >= (relayer.last_attempt! + Int64(relayer.retry_after))) { + if relayer.last_attempt == nil || + (now >= (relayer.last_attempt! + Int64(relayer.retry_after))) { print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds") flush_event(event, to_relay: relayer) } @@ -99,16 +132,20 @@ class PostBox { } } - func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true) { + func send(_ event: NostrEvent, to: [String]? = nil, skip_ephemeral: Bool = true, delay: TimeInterval? = nil) { // Don't add event if we already have it if events[event.id] != nil { return } let remaining = to ?? pool.our_descriptors.map { $0.url.id } - let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral) + let after = delay.map { d in Date.now.addingTimeInterval(d) } + let posted_ev = PostedEvent(event: event, remaining: remaining, skip_ephemeral: skip_ephemeral, flush_after: after) + events[event.id] = posted_ev - flush_event(posted_ev) + if after == nil { + flush_event(posted_ev) + } } } diff --git a/damus/Util/WalletConnect.swift b/damus/Util/WalletConnect.swift @@ -67,6 +67,80 @@ struct WalletRequest<T: Codable>: Codable { let params: T? } +struct WalletResponseErr: Codable { + let code: String? + let message: String? +} + +struct PayInvoiceResponse: Decodable { + let preimage: String +} + +enum WalletResponseResultType: String { + case pay_invoice +} + +enum WalletResponseResult { + case pay_invoice(PayInvoiceResponse) +} + +struct FullWalletResponse { + let req_id: String + let response: WalletResponse + + init?(from: NostrEvent) async { + guard let req_id = from.referenced_ids.first else { + return nil + } + + self.req_id = req_id.ref_id + + let ares = Task { + guard let resp: WalletResponse = decode_json(from.content) else { + let resp: WalletResponse? = nil + return resp + } + + return resp + } + + guard let res = await ares.value else { + return nil + } + + self.response = res + } + +} + +struct WalletResponse: Decodable { + let result_type: WalletResponseResultType + let error: WalletResponseErr? + let result: WalletResponseResult + + private enum CodingKeys: CodingKey { + case result_type, error, result + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let result_type_str = try container.decode(String.self, forKey: .result_type) + + guard let result_type = WalletResponseResultType(rawValue: result_type_str) else { + throw DecodingError.typeMismatch(WalletResponseResultType.self, .init(codingPath: decoder.codingPath, debugDescription: "result_type \(result_type_str) is unknown")) + } + + self.result_type = result_type + self.error = try container.decodeIfPresent(WalletResponseErr.self, forKey: .error) + + switch result_type { + case .pay_invoice: + let res = try container.decode(PayInvoiceResponse.self, forKey: .result) + self.result = .pay_invoice(res) + } + } +} + func make_wallet_pay_invoice_request(invoice: String) -> WalletRequest<PayInvoiceRequest> { let data = PayInvoiceRequest(invoice: invoice) return WalletRequest(method: "pay_invoice", params: data) @@ -92,12 +166,65 @@ func make_wallet_connect_request<T>(req: WalletRequest<T>, to_pk: String, keypai return create_encrypted_event(content, to_pk: to_pk, tags: tags, keypair: keypair, created_at: created_at, kind: 23194) } -func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String) { +func subscribe_to_nwc(url: WalletConnectURL, pool: RelayPool) { + var filter: NostrFilter = .filter_kinds([NostrKind.nwc_response.rawValue]) + filter.authors = [url.pubkey] + filter.limit = 0 + let sub = NostrSubscribe(filters: [filter], sub_id: "nwc") + + pool.send(.subscribe(sub), to: [url.relay.id]) +} + +func nwc_pay(url: WalletConnectURL, pool: RelayPool, post: PostBox, invoice: String) -> NostrEvent? { let req = make_wallet_pay_invoice_request(invoice: invoice) guard let ev = make_wallet_connect_request(req: req, to_pk: url.pubkey, keypair: url.keypair) else { - return + return nil } - try? pool.add_relay(url.relay, info: .ephemeral) - post.send(ev, to: [url.relay.id], skip_ephemeral: false) + try? pool.add_relay(.nwc(url: url.relay)) + subscribe_to_nwc(url: url, pool: pool) + post.send(ev, to: [url.relay.id], skip_ephemeral: false, delay: 5.0) + return ev +} + + +func nwc_success(zapcache: Zaps, resp: FullWalletResponse) { + // find the pending zap and mark it as pending-confirmed + for kv in zapcache.our_zaps { + let zaps = kv.value + + for zap in zaps { + guard case .pending(let pzap) = zap, + case .nwc(let nwc_state) = pzap.state, + case .postbox_pending(let nwc_req) = nwc_state.state, + nwc_req.id == resp.req_id + else { + continue + } + + nwc_state.state = .confirmed + return + } + } +} + +func nwc_error(zapcache: Zaps, evcache: EventCache, resp: FullWalletResponse) { + // find a pending zap with the nwc request id associated with this response and remove it + for kv in zapcache.our_zaps { + let zaps = kv.value + + for zap in zaps { + guard case .pending(let pzap) = zap, + case .nwc(let nwc_state) = pzap.state, + case .postbox_pending(let req) = nwc_state.state, + req.id == resp.req_id + else { + continue + } + + // remove the pending zap if there was an error + remove_zap(reqid: pzap.request.ev.id, zapcache: zapcache, evcache: evcache) + return + } + } } diff --git a/damus/Util/Zap.swift b/damus/Util/Zap.swift @@ -7,7 +7,7 @@ import Foundation -public struct NoteZapTarget: Equatable { +public struct NoteZapTarget: Equatable, Hashable { public let note_id: String public let author: String } @@ -41,6 +41,148 @@ public enum ZapTarget: Equatable { struct ZapRequest { let ev: NostrEvent + +} + +enum ExtPendingZapStateType { + case fetching_invoice + case done +} + +class ExtPendingZapState: Equatable { + static func == (lhs: ExtPendingZapState, rhs: ExtPendingZapState) -> Bool { + return lhs.state == rhs.state + } + + var state: ExtPendingZapStateType + + init(state: ExtPendingZapStateType) { + self.state = state + } +} + +enum PendingZapState: Equatable { + case nwc(NWCPendingZapState) + case external(ExtPendingZapState) +} + + +enum NWCStateType: Equatable { + case fetching_invoice + case cancel_fetching_invoice + case postbox_pending(NostrEvent) + case confirmed + case failed +} + +class NWCPendingZapState: Equatable { + var state: NWCStateType + let url: WalletConnectURL + + init(state: NWCStateType, url: WalletConnectURL) { + self.state = state + self.url = url + } + + static func == (lhs: NWCPendingZapState, rhs: NWCPendingZapState) -> Bool { + return lhs.state == rhs.state && lhs.url == rhs.url + } +} + +class PendingZap { + let amount_msat: Int64 + let target: ZapTarget + let request: ZapRequest + let type: ZapType + var state: PendingZapState + + init(amount_msat: Int64, target: ZapTarget, request: ZapRequest, type: ZapType, state: PendingZapState) { + self.amount_msat = amount_msat + self.target = target + self.request = request + self.type = type + self.state = state + } +} + + +enum Zapping { + case zap(Zap) + case pending(PendingZap) + + var is_pending: Bool { + switch self { + case .zap: + return false + case .pending: + return true + } + } + + var is_private: Bool { + switch self { + case .zap(let zap): + return zap.private_request != nil + case .pending(let pzap): + return pzap.type == .priv + } + } + + var amount: Int64 { + switch self { + case .zap(let zap): + return zap.invoice.amount + case .pending(let pzap): + return pzap.amount_msat + } + } + + var target: ZapTarget { + switch self { + case .zap(let zap): + return zap.target + case .pending(let pzap): + return pzap.target + } + } + + var request: NostrEvent { + switch self { + case .zap(let zap): + return zap.request_ev + case .pending(let pzap): + return pzap.request.ev + } + } + + var created_at: Int64 { + switch self { + case .zap(let zap): + return zap.event.created_at + case .pending(let pzap): + // pending zaps are created right away + return pzap.request.ev.created_at + } + } + + var event: NostrEvent? { + switch self { + case .zap(let zap): + return zap.event + case .pending: + // pending zaps don't have a zap event + return nil + } + } + + var is_anon: Bool { + switch self { + case .zap(let zap): + return zap.is_anon + case .pending(let pzap): + return pzap.type == .anon + } + } } struct Zap { @@ -246,7 +388,7 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? { return endpoint } -func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int, zap_type: ZapType, comment: String?) async -> String? { +func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int, zap_type: ZapType, comment: String?) async -> String? { guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else { return nil } @@ -256,7 +398,7 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int var query = [URLQueryItem(name: "amount", value: "\(amount)")] - if let zapreq, zappable && zap_type != .non_zap, let json = encode_json(zapreq) { + if zappable && zap_type != .non_zap, let json = encode_json(zapreq) { print("zapreq json: \(json)") query.append(URLQueryItem(name: "nostr", value: json)) } diff --git a/damus/Util/Zaps.swift b/damus/Util/Zaps.swift @@ -8,9 +8,9 @@ import Foundation class Zaps { - var zaps: [String: Zap] + var zaps: [String: Zapping] let our_pubkey: String - var our_zaps: [String: [Zap]] + var our_zaps: [String: [Zapping]] var event_counts: [String: Int] var event_totals: [String: Int64] @@ -22,15 +22,42 @@ class Zaps { self.event_counts = [:] self.event_totals = [:] } + + func remove_zap(reqid: String) -> Zapping? { + 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 { + continue + } + + res = zap + + our_zaps[kv.key] = ours.filter { z in z.request.id != reqid } + + if let count = event_counts[zap.target.id] { + event_counts[zap.target.id] = count - 1 + } + if let total = event_totals[zap.target.id] { + event_totals[zap.target.id] = total - zap.amount + } + + // we found the request id, we can stop looking + break + } + + self.zaps.removeValue(forKey: reqid) + return res + } - func add_zap(zap: Zap) { - if zaps[zap.event.id] != nil { + func add_zap(zap: Zapping) { + if zaps[zap.request.id] != nil { return } - self.zaps[zap.event.id] = zap + self.zaps[zap.request.id] = zap // record our zaps for an event - if zap.request.ev.pubkey == our_pubkey { + if zap.request.pubkey == our_pubkey { switch zap.target { case .note(let note_target): if our_zaps[note_target.note_id] == nil { @@ -44,7 +71,7 @@ class Zaps { } // don't count tips to self. lame. - guard zap.request.ev.pubkey != zap.target.pubkey else { + guard zap.request.pubkey != zap.target.pubkey else { return } @@ -58,8 +85,15 @@ class Zaps { } event_counts[id] = event_counts[id]! + 1 - event_totals[id] = event_totals[id]! + zap.invoice.amount + event_totals[id] = event_totals[id]! + zap.amount notify(.update_stats, zap.target.id) } } + +func remove_zap(reqid: String, zapcache: Zaps, evcache: EventCache) { + guard let zap = zapcache.remove_zap(reqid: reqid) else { + return + } + evcache.get_cache_data(zap.target.id).zaps_model.remove(reqid: reqid) +} diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift @@ -88,7 +88,7 @@ struct EventActionBar: View { if let lnurl = self.lnurl { Spacer() - ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, bar: bar) + ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model) } Spacer() @@ -227,7 +227,7 @@ struct EventActionBar_Previews: PreviewProvider { 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 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: .zap(test_zap), our_reply: test_event) VStack(spacing: 50) { EventActionBar(damus_state: ds, event: ev, bar: bar) diff --git a/damus/Views/Events/TextEvent.swift b/damus/Views/Events/TextEvent.swift @@ -181,7 +181,9 @@ struct TextEvent: View { VStack(alignment: .leading) { TopPart(is_anon: is_anon) - ReplyPart + if !options.contains(.no_replying_to) { + ReplyPart + } EvBody(options: self.options) diff --git a/damus/Views/Events/ZapEvent.swift b/damus/Views/Events/ZapEvent.swift @@ -9,30 +9,30 @@ import SwiftUI struct ZapEvent: View { let damus: DamusState - let zap: Zap + let zap: Zapping var body: some View { VStack(alignment: .leading) { HStack(alignment: .center) { - Text("⚡️ \(format_msats(zap.invoice.amount))", comment: "Text indicating the zap amount. i.e. number of satoshis that were tipped to a user") + 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 zap.private_request != nil { + if zap.is_private { Image(systemName: "lock.fill") .foregroundColor(DamusColors.green) .help(NSLocalizedString("Only you can see this message and who sent it.", comment: "Help text on green lock icon that explains that only the current user can see the message of a zap event and who sent the zap.")) } - } - - if let priv = zap.private_request { - TextEvent(damus: damus, event: priv, pubkey: priv.pubkey, options: [.no_action_bar, .no_replying_to]) - .padding([.top], 1) - } else { - TextEvent(damus: damus, event: zap.request.ev, pubkey: zap.request.ev.pubkey, options: [.no_action_bar, .no_replying_to]) - .padding([.top], 1) + if zap.is_pending { + Image(systemName: "clock.arrow.circlepath") + .foregroundColor(DamusColors.yellow) + .help(NSLocalizedString("Only you can see this message and who sent it.", comment: "Help text on green lock icon that explains that only the current user can see the message of a zap event and who sent the zap.")) + } } + + TextEvent(damus: damus, event: zap.request, pubkey: zap.request.pubkey, options: [.no_action_bar, .no_replying_to]) + .padding([.top], 1) } } } @@ -45,12 +45,14 @@ let test_zap = Zap(event: test_event, invoice: test_zap_invoice, zapper: "zapper 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_pending_zap = PendingZap(amount_msat: 10000, target: .note(id: "id", author: "pk"), request: 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: test_zap) + ZapEvent(damus: test_damus_state(), zap: .zap(test_zap)) - ZapEvent(damus: test_damus_state(), zap: test_private_zap) + ZapEvent(damus: test_damus_state(), zap: .zap(test_private_zap)) } } } diff --git a/damus/Views/Notifications/EventGroupView.swift b/damus/Views/Notifications/EventGroupView.swift @@ -68,15 +68,11 @@ func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType if let zapgrp = group.zap_group { let zap = zapgrp.zaps[ind] - if let privzap = zap.private_request { - return event_author_name(profiles: profiles, pubkey: privzap.pubkey) - } - if zap.is_anon { return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.") } - return event_author_name(profiles: profiles, pubkey: zap.request.ev.pubkey) + return event_author_name(profiles: profiles, pubkey: zap.request.pubkey) } else { let ev = group.events[ind] return event_author_name(profiles: profiles, pubkey: ev.pubkey) diff --git a/damus/Views/Relays/RelayConfigView.swift b/damus/Views/Relays/RelayConfigView.swift @@ -88,8 +88,8 @@ struct RelayConfigView: View { } let info = RelayInfo.rw - - guard (try? state.pool.add_relay(url, info: info)) != nil else { + let descriptor = RelayDescriptor(url: url, info: info) + guard (try? state.pool.add_relay(descriptor)) != nil else { return } diff --git a/damus/Views/Zaps/ZapsView.swift b/damus/Views/Zaps/ZapsView.swift @@ -9,17 +9,20 @@ import SwiftUI struct ZapsView: View { let state: DamusState - @StateObject var model: ZapsModel + var model: ZapsModel + + @ObservedObject var zaps: ZapsDataModel init(state: DamusState, target: ZapTarget) { self.state = state - self._model = StateObject(wrappedValue: ZapsModel(state: state, target: target)) + self.model = ZapsModel(state: state, target: target) + self._zaps = ObservedObject(wrappedValue: state.events.get_cache_data(target.id).zaps_model) } var body: some View { ScrollView { LazyVStack { - ForEach(model.zaps, id: \.event.id) { zap in + ForEach(zaps.zaps, id: \.request.id) { zap in ZapEvent(damus: state, zap: zap) .padding([.horizontal]) }