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).
This commit is contained in:
@@ -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 zap_img: String {
|
||||
if bar.zapped {
|
||||
return "bolt.fill"
|
||||
}
|
||||
|
||||
if !zapping {
|
||||
return "bolt"
|
||||
}
|
||||
|
||||
return "bolt.fill"
|
||||
var our_zap: Zapping? {
|
||||
zaps.zaps.first(where: { z in z.request.pubkey == damus_state.pubkey })
|
||||
}
|
||||
|
||||
var zap_color: Color? {
|
||||
if bar.zapped {
|
||||
var zap_img: String {
|
||||
switch our_zap {
|
||||
case .none:
|
||||
return "bolt"
|
||||
case .zap:
|
||||
return "bolt.fill"
|
||||
case .pending:
|
||||
return "bolt.fill"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user