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:
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])
}