mirror of
git://jb55.com/damus
synced 2024-10-06 03:33:22 +00:00
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:
parent
1518a0a16c
commit
03691d0369
2
.envrc
2
.envrc
@ -1,4 +1,4 @@
|
|||||||
use nix
|
#use nix
|
||||||
|
|
||||||
export TODO_FILE=$PWD/TODO
|
export TODO_FILE=$PWD/TODO
|
||||||
|
|
||||||
|
@ -23,45 +23,90 @@ struct ZappingEvent {
|
|||||||
let event: NostrEvent
|
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 {
|
struct ZapButton: View {
|
||||||
let damus_state: DamusState
|
let damus_state: DamusState
|
||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let lnurl: String
|
let lnurl: String
|
||||||
|
|
||||||
@ObservedObject var bar: ActionBarModel
|
@ObservedObject var zaps: ZapsDataModel
|
||||||
|
@StateObject var button: ZapButtonModel = ZapButtonModel()
|
||||||
|
|
||||||
@State var zapping: Bool = false
|
var our_zap: Zapping? {
|
||||||
@State var invoice: String = ""
|
zaps.zaps.first(where: { z in z.request.pubkey == damus_state.pubkey })
|
||||||
@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 zap_color: Color? {
|
var zap_img: String {
|
||||||
if bar.zapped {
|
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
|
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 {
|
// we've tapped and we have a zap already... cancel if we can
|
||||||
return nil
|
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 {
|
var body: some View {
|
||||||
@ -69,37 +114,32 @@ struct ZapButton: View {
|
|||||||
Button(action: {
|
Button(action: {
|
||||||
}, label: {
|
}, label: {
|
||||||
Image(systemName: zap_img)
|
Image(systemName: zap_img)
|
||||||
.foregroundColor(zap_color == nil ? Color.gray : zap_color!)
|
.foregroundColor(zap_color)
|
||||||
.font(.footnote.weight(.medium))
|
.font(.footnote.weight(.medium))
|
||||||
})
|
})
|
||||||
.simultaneousGesture(LongPressGesture().onEnded {_ in
|
.simultaneousGesture(LongPressGesture().onEnded {_ in
|
||||||
guard !zapping else {
|
guard our_zap == nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.showing_zap_customizer = true
|
button.showing_zap_customizer = true
|
||||||
})
|
})
|
||||||
.highPriorityGesture(TapGesture().onEnded {_ in
|
.highPriorityGesture(TapGesture().onEnded {
|
||||||
guard !zapping else {
|
tap()
|
||||||
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
|
|
||||||
})
|
})
|
||||||
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
.accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button"))
|
||||||
|
|
||||||
if bar.zap_total > 0 {
|
if zaps.zap_total > 0 {
|
||||||
Text(verbatim: format_msats_abbrev(bar.zap_total))
|
Text(verbatim: format_msats_abbrev(zaps.zap_total))
|
||||||
.font(.footnote)
|
.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)
|
CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) {
|
.sheet(isPresented: $button.showing_select_wallet, onDismiss: {button.showing_select_wallet = false}) {
|
||||||
SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice)
|
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
|
.onReceive(handle_notify(.zapping)) { notif in
|
||||||
let zap_ev = notif.object as! ZappingEvent
|
let zap_ev = notif.object as! ZappingEvent
|
||||||
@ -117,15 +157,13 @@ struct ZapButton: View {
|
|||||||
break
|
break
|
||||||
case .got_zap_invoice(let inv):
|
case .got_zap_invoice(let inv):
|
||||||
if damus_state.settings.show_wallet_selector {
|
if damus_state.settings.show_wallet_selector {
|
||||||
self.invoice = inv
|
self.button.invoice = inv
|
||||||
self.showing_select_wallet = true
|
self.button.showing_select_wallet = true
|
||||||
} else {
|
} else {
|
||||||
let wallet = damus_state.settings.default_wallet.model
|
let wallet = damus_state.settings.default_wallet.model
|
||||||
open_with_wallet(wallet: wallet, invoice: inv)
|
open_with_wallet(wallet: wallet, invoice: inv)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.zapping = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -133,13 +171,25 @@ struct ZapButton: View {
|
|||||||
|
|
||||||
struct ZapButton_Previews: PreviewProvider {
|
struct ZapButton_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
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)
|
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)))
|
||||||
ZapButton(damus_state: test_damus_state(), event: test_event, lnurl: "lnurl", bar: bar)
|
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) {
|
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 {
|
guard let keypair = damus_state.keypair.to_full() else {
|
||||||
return
|
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 target = ZapTarget.note(id: event.id, author: event.pubkey)
|
||||||
let content = comment ?? ""
|
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 {
|
Task {
|
||||||
var mpayreq = damus_state.lnurls.lookup(target.pubkey)
|
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 {
|
guard let payreq = mpayreq else {
|
||||||
// TODO: show error
|
// TODO: show error
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
remove_zap(reqid: zapreq.id, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||||
let typ = ZappingEventType.failed(.bad_lnurl)
|
let typ = ZappingEventType.failed(.bad_lnurl)
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||||
notify(.zapping, ev)
|
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
|
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 {
|
guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
remove_zap(reqid: zapreq.id, zapcache: damus_state.zaps, evcache: damus_state.events)
|
||||||
let typ = ZappingEventType.failed(.fetching_invoice)
|
let typ = ZappingEventType.failed(.fetching_invoice)
|
||||||
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event)
|
||||||
notify(.zapping, ev)
|
notify(.zapping, ev)
|
||||||
@ -184,10 +245,24 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
|||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let url = damus_state.settings.nostr_wallet_connect,
|
|
||||||
let nwc = WalletConnectURL(str: url) {
|
switch pending_zap_state {
|
||||||
nwc_pay(url: nwc, pool: damus_state.pool, post: damus_state.postbox, invoice: inv)
|
case .nwc(let nwc_state):
|
||||||
} else {
|
// 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)
|
let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event)
|
||||||
notify(.zapping, ev)
|
notify(.zapping, ev)
|
||||||
}
|
}
|
||||||
@ -196,3 +271,41 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
|
|||||||
|
|
||||||
return
|
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
|
||||||
|
}
|
||||||
|
@ -581,7 +581,8 @@ struct ContentView: View {
|
|||||||
let new_relay_filters = load_relay_filters(pubkey) == nil
|
let new_relay_filters = load_relay_filters(pubkey) == nil
|
||||||
for relay in bootstrap_relays {
|
for relay in bootstrap_relays {
|
||||||
if let url = RelayURL(relay) {
|
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()
|
let settings = UserSettingsStore()
|
||||||
UserSettingsStore.shared = settings
|
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,
|
self.damus_state = DamusState(pool: pool,
|
||||||
keypair: keypair,
|
keypair: keypair,
|
||||||
likes: EventCounter(our_pubkey: pubkey),
|
likes: EventCounter(our_pubkey: pubkey),
|
||||||
|
@ -7,12 +7,17 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
enum Zapped {
|
||||||
|
case not_zapped
|
||||||
|
case pending
|
||||||
|
case zapped
|
||||||
|
}
|
||||||
|
|
||||||
class ActionBarModel: ObservableObject {
|
class ActionBarModel: ObservableObject {
|
||||||
@Published var our_like: NostrEvent?
|
@Published var our_like: NostrEvent?
|
||||||
@Published var our_boost: NostrEvent?
|
@Published var our_boost: NostrEvent?
|
||||||
@Published var our_reply: NostrEvent?
|
@Published var our_reply: NostrEvent?
|
||||||
@Published var our_zap: Zap?
|
@Published var our_zap: Zapping?
|
||||||
@Published var likes: Int
|
@Published var likes: Int
|
||||||
@Published var boosts: Int
|
@Published var boosts: Int
|
||||||
@Published var zaps: Int
|
@Published var zaps: Int
|
||||||
@ -35,7 +40,7 @@ class ActionBarModel: ObservableObject {
|
|||||||
self.replies = 0
|
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.likes = likes
|
||||||
self.boosts = boosts
|
self.boosts = boosts
|
||||||
self.zaps = zaps
|
self.zaps = zaps
|
||||||
@ -64,10 +69,6 @@ class ActionBarModel: ObservableObject {
|
|||||||
return likes == 0 && boosts == 0 && zaps == 0
|
return likes == 0 && boosts == 0 && zaps == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
var zapped: Bool {
|
|
||||||
return our_zap != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var liked: Bool {
|
var liked: Bool {
|
||||||
return our_like != nil
|
return our_like != nil
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ struct DamusState {
|
|||||||
let wallet: WalletModel
|
let wallet: WalletModel
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func add_zap(zap: Zap) -> Bool {
|
func add_zap(zap: Zapping) -> Bool {
|
||||||
// store generic zap mapping
|
// store generic zap mapping
|
||||||
self.zaps.add_zap(zap: zap)
|
self.zaps.add_zap(zap: zap)
|
||||||
// associate with events as well
|
// associate with events as well
|
||||||
|
@ -129,6 +129,25 @@ class HomeModel: ObservableObject {
|
|||||||
handle_zap_event(ev)
|
handle_zap_event(ev)
|
||||||
case .zap_request:
|
case .zap_request:
|
||||||
break
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
damus_state.add_zap(zap: zap)
|
damus_state.add_zap(zap: .zap(zap))
|
||||||
|
|
||||||
guard zap.target.pubkey == our_keypair.pubkey else {
|
guard zap.target.pubkey == our_keypair.pubkey else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !notifications.insert_zap(zap) {
|
if !notifications.insert_zap(.zap(zap)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -301,6 +320,16 @@ class HomeModel: ObservableObject {
|
|||||||
//remove_bootstrap_nodes(damus_state)
|
//remove_bootstrap_nodes(damus_state)
|
||||||
send_home_filters(relay_id: relay_id)
|
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):
|
case .error(let merr):
|
||||||
let desc = String(describing: merr)
|
let desc = String(describing: merr)
|
||||||
if desc.contains("Software caused connection abort") {
|
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])
|
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: 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: contacts_filters, sub_id: contacts_subid)), to: [relay_id])
|
||||||
pool.send(.subscribe(.init(filters: notifications_filters, sub_id: notifications_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
|
changed = true
|
||||||
if new.contains(d) {
|
if new.contains(d) {
|
||||||
if let url = RelayURL(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 {
|
} else {
|
||||||
state.pool.remove_relay(d)
|
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) {
|
func add_new_relay(relay_filters: RelayFilters, metadatas: RelayMetadatas, pool: RelayPool, descriptor: RelayDescriptor, new_relay_filters: Bool) {
|
||||||
try? pool.add_relay(url, info: info)
|
try? pool.add_relay(descriptor)
|
||||||
|
let url = descriptor.url
|
||||||
|
|
||||||
let relay_id = url.id
|
let relay_id = url.id
|
||||||
guard metadatas.lookup(relay_id: relay_id) == nil else {
|
guard metadatas.lookup(relay_id: relay_id) == nil else {
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class ZapGroup {
|
class ZapGroup {
|
||||||
var zaps: [Zap]
|
var zaps: [Zapping]
|
||||||
var msat_total: Int64
|
var msat_total: Int64
|
||||||
var zappers: Set<String>
|
var zappers: Set<String>
|
||||||
|
|
||||||
@ -17,22 +17,16 @@ class ZapGroup {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return first.event.created_at
|
return first.created_at
|
||||||
}
|
}
|
||||||
|
|
||||||
func zap_requests() -> [NostrEvent] {
|
func zap_requests() -> [NostrEvent] {
|
||||||
zaps.map { z in
|
zaps.map { z in z.request }
|
||||||
if let priv = z.private_request {
|
|
||||||
return priv
|
|
||||||
} else {
|
|
||||||
return z.request.ev
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
|
func would_filter(_ isIncluded: (NostrEvent) -> Bool) -> Bool {
|
||||||
for zap in zaps {
|
for zap in zaps {
|
||||||
if !isIncluded(zap.request_ev) {
|
if !isIncluded(zap.request) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -41,7 +35,7 @@ class ZapGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func filter(_ isIncluded: (NostrEvent) -> Bool) -> 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 {
|
guard new_zaps.count > 0 else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -59,15 +53,15 @@ class ZapGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func insert(_ zap: Zap) -> Bool {
|
func insert(_ zap: Zapping) -> Bool {
|
||||||
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
|
if !insert_uniq_sorted_zap_by_created(zaps: &zaps, new_zap: zap) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
msat_total += zap.invoice.amount
|
msat_total += zap.amount
|
||||||
|
|
||||||
if !zappers.contains(zap.request.ev.pubkey) {
|
if !zappers.contains(zap.request.pubkey) {
|
||||||
zappers.insert(zap.request.ev.pubkey)
|
zappers.insert(zap.request.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
@ -99,7 +99,7 @@ enum NotificationItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class NotificationsModel: ObservableObject, ScrollQueue {
|
class NotificationsModel: ObservableObject, ScrollQueue {
|
||||||
var incoming_zaps: [Zap]
|
var incoming_zaps: [Zapping]
|
||||||
var incoming_events: [NostrEvent]
|
var incoming_events: [NostrEvent]
|
||||||
var should_queue: Bool
|
var should_queue: Bool
|
||||||
|
|
||||||
@ -150,7 +150,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for zap in incoming_zaps {
|
for zap in incoming_zaps {
|
||||||
pks.insert(zap.request.ev.pubkey)
|
pks.insert(zap.request.pubkey)
|
||||||
}
|
}
|
||||||
|
|
||||||
return Array(pks)
|
return Array(pks)
|
||||||
@ -249,7 +249,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
private func insert_zap_immediate(_ zap: Zap) -> Bool {
|
private func insert_zap_immediate(_ zap: Zapping) -> Bool {
|
||||||
switch zap.target {
|
switch zap.target {
|
||||||
case .note(let notezt):
|
case .note(let notezt):
|
||||||
let id = notezt.note_id
|
let id = notezt.note_id
|
||||||
@ -285,7 +285,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func insert_zap(_ zap: Zap) -> Bool {
|
func insert_zap(_ zap: Zapping) -> Bool {
|
||||||
if should_queue {
|
if should_queue {
|
||||||
return insert_uniq_sorted_zap_by_created(zaps: &incoming_zaps, new_zap: zap)
|
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
|
changed = changed || incoming_events.count != count
|
||||||
|
|
||||||
count = profile_zaps.zaps.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
|
changed = changed || profile_zaps.zaps.count != count
|
||||||
|
|
||||||
for el in reactions {
|
for el in reactions {
|
||||||
@ -325,7 +325,7 @@ class NotificationsModel: ObservableObject, ScrollQueue {
|
|||||||
for el in zaps {
|
for el in zaps {
|
||||||
count = el.value.zaps.count
|
count = el.value.zaps.count
|
||||||
el.value.zaps = el.value.zaps.filter {
|
el.value.zaps = el.value.zaps.filter {
|
||||||
isIncluded($0.request.ev)
|
isIncluded($0.request)
|
||||||
}
|
}
|
||||||
changed = changed || el.value.zaps.count != count
|
changed = changed || el.value.zaps.count != count
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ class ZapsModel: ObservableObject {
|
|||||||
self.target = target
|
self.target = target
|
||||||
}
|
}
|
||||||
|
|
||||||
var zaps: [Zap] {
|
var zaps: [Zapping] {
|
||||||
return state.events.lookup_zaps(target: target)
|
return state.events.lookup_zaps(target: target)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ class ZapsModel: ObservableObject {
|
|||||||
case .notice:
|
case .notice:
|
||||||
break
|
break
|
||||||
case .eose:
|
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)
|
load_profiles(profiles_subid: profiles_subid, relay_id: relay_id, load: .from_events(events), damus_state: state)
|
||||||
case .event(_, let ev):
|
case .event(_, let ev):
|
||||||
guard ev.kind == 9735 else {
|
guard ev.kind == 9735 else {
|
||||||
@ -61,22 +61,19 @@ class ZapsModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let zap = state.zaps.zaps[ev.id] {
|
if let zap = state.zaps.zaps[ev.id] {
|
||||||
if state.events.store_zap(zap: zap) {
|
state.events.store_zap(zap: zap)
|
||||||
objectWillChange.send()
|
return
|
||||||
}
|
|
||||||
} 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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -22,4 +22,6 @@ enum NostrKind: Int {
|
|||||||
case list = 30000
|
case list = 30000
|
||||||
case zap = 9735
|
case zap = 9735
|
||||||
case zap_request = 9734
|
case zap_request = 9734
|
||||||
|
case nwc_request = 23194
|
||||||
|
case nwc_response = 23195
|
||||||
}
|
}
|
||||||
|
@ -10,21 +10,46 @@ import Foundation
|
|||||||
public struct RelayInfo: Codable {
|
public struct RelayInfo: Codable {
|
||||||
let read: Bool?
|
let read: Bool?
|
||||||
let write: Bool?
|
let write: Bool?
|
||||||
let ephemeral: Bool?
|
|
||||||
|
|
||||||
init(read: Bool, write: Bool, ephemeral: Bool = false) {
|
init(read: Bool, write: Bool) {
|
||||||
self.read = read
|
self.read = read
|
||||||
self.write = write
|
self.write = write
|
||||||
self.ephemeral = ephemeral
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static let rw = RelayInfo(read: true, write: true, ephemeral: false)
|
static let rw = RelayInfo(read: true, write: true)
|
||||||
static let ephemeral = RelayInfo(read: true, write: true, ephemeral: true)
|
}
|
||||||
|
|
||||||
|
enum RelayVariant {
|
||||||
|
case regular
|
||||||
|
case ephemeral
|
||||||
|
case nwc
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct RelayDescriptor {
|
public struct RelayDescriptor {
|
||||||
public let url: RelayURL
|
let url: RelayURL
|
||||||
public let info: RelayInfo
|
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 {
|
enum RelayFlags: Int {
|
||||||
|
@ -43,7 +43,7 @@ class RelayPool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var our_descriptors: [RelayDescriptor] {
|
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] {
|
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)
|
let relay_id = get_relay_id(url)
|
||||||
if get_relay(relay_id) != nil {
|
if get_relay(relay_id) != nil {
|
||||||
throw RelayError.RelayAlreadyExists
|
throw RelayError.RelayAlreadyExists
|
||||||
@ -99,8 +100,7 @@ class RelayPool {
|
|||||||
let conn = RelayConnection(url: url) { event in
|
let conn = RelayConnection(url: url) { event in
|
||||||
self.handle_event(relay_id: relay_id, event: event)
|
self.handle_event(relay_id: relay_id, event: event)
|
||||||
}
|
}
|
||||||
let descriptor = RelayDescriptor(url: url, info: info)
|
let relay = Relay(descriptor: desc, connection: conn)
|
||||||
let relay = Relay(descriptor: descriptor, connection: conn)
|
|
||||||
self.relays.append(relay)
|
self.relays.append(relay)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -196,7 +196,7 @@ class RelayPool {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (relay.descriptor.info.ephemeral ?? false) && skip_ephemeral {
|
if relay.descriptor.ephemeral && skip_ephemeral {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -266,7 +266,7 @@ func add_rw_relay(_ pool: RelayPool, _ url: String) {
|
|||||||
guard let url = RelayURL(url) else {
|
guard let url = RelayURL(url) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try? pool.add_relay(url, info: RelayInfo.rw)
|
try? pool.add_relay(RelayDescriptor(url: url, info: .rw))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,11 +55,42 @@ class PreviewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ZapsDataModel: ObservableObject {
|
class ZapsDataModel: ObservableObject {
|
||||||
@Published var zaps: [Zap]
|
@Published var zaps: [Zapping]
|
||||||
|
|
||||||
init(_ zaps: [Zap]) {
|
init(_ zaps: [Zapping]) {
|
||||||
self.zaps = zaps
|
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 {
|
class RelativeTimeModel: ObservableObject {
|
||||||
@ -86,7 +117,7 @@ class EventData {
|
|||||||
return preview_model.state
|
return preview_model.state
|
||||||
}
|
}
|
||||||
|
|
||||||
init(zaps: [Zap] = []) {
|
init(zaps: [Zapping] = []) {
|
||||||
self.translations_model = .init(state: .havent_tried)
|
self.translations_model = .init(state: .havent_tried)
|
||||||
self.artifacts_model = .init(state: .not_loaded)
|
self.artifacts_model = .init(state: .not_loaded)
|
||||||
self.zaps_model = .init(zaps)
|
self.zaps_model = .init(zaps)
|
||||||
@ -131,12 +162,23 @@ class EventCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func store_zap(zap: Zap) -> Bool {
|
func store_zap(zap: Zapping) -> Bool {
|
||||||
let data = get_cache_data(zap.target.id).zaps_model
|
let data = get_cache_data(zap.target.id).zaps_model
|
||||||
return insert_uniq_sorted_zap_by_amount(zaps: &data.zaps, new_zap: zap)
|
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
|
return get_cache_data(target.id).zaps_model.zaps
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,12 +7,17 @@
|
|||||||
|
|
||||||
import Foundation
|
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
|
var i: Int = 0
|
||||||
|
|
||||||
for zap in zaps {
|
for zap in zaps {
|
||||||
// don't insert duplicate events
|
if new_zap.request.id == zap.request.id {
|
||||||
if new_zap.event.id == zap.event.id {
|
// replace pending
|
||||||
|
if !new_zap.is_pending && zap.is_pending {
|
||||||
|
zaps[i] = new_zap
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// don't insert duplicate events
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,16 +33,16 @@ func insert_uniq_sorted_zap(zaps: inout [Zap], new_zap: Zap, cmp: (Zap, Zap) ->
|
|||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult
|
@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
|
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
|
@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
|
return insert_uniq_sorted_zap(zaps: &zaps, new_zap: new_zap) { (a, b) in
|
||||||
a.invoice.amount > b.invoice.amount
|
a.amount > b.amount
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -26,16 +26,24 @@ class PostedEvent {
|
|||||||
let event: NostrEvent
|
let event: NostrEvent
|
||||||
let skip_ephemeral: Bool
|
let skip_ephemeral: Bool
|
||||||
var remaining: [Relayer]
|
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.event = event
|
||||||
self.skip_ephemeral = skip_ephemeral
|
self.skip_ephemeral = skip_ephemeral
|
||||||
|
self.flush_after = flush_after
|
||||||
self.remaining = remaining.map {
|
self.remaining = remaining.map {
|
||||||
Relayer(relay: $0, attempts: 0, retry_after: 2.0)
|
Relayer(relay: $0, attempts: 0, retry_after: 2.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum CancelSendErr {
|
||||||
|
case nothing_to_cancel
|
||||||
|
case not_delayed
|
||||||
|
case too_late
|
||||||
|
}
|
||||||
|
|
||||||
class PostBox {
|
class PostBox {
|
||||||
let pool: RelayPool
|
let pool: RelayPool
|
||||||
var events: [String: PostedEvent]
|
var events: [String: PostedEvent]
|
||||||
@ -46,12 +54,37 @@ class PostBox {
|
|||||||
pool.register_handler(sub_id: "postbox", handler: handle_event)
|
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() {
|
func try_flushing_events() {
|
||||||
let now = Int64(Date().timeIntervalSince1970)
|
let now = Int64(Date().timeIntervalSince1970)
|
||||||
for kv in events {
|
for kv in events {
|
||||||
let event = kv.value
|
let event = kv.value
|
||||||
|
|
||||||
|
// some are delayed
|
||||||
|
if let after = event.flush_after, Date.now.timeIntervalSince1970 < after.timeIntervalSince1970 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
for relayer in event.remaining {
|
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")
|
print("attempt #\(relayer.attempts) to flush event '\(event.event.content)' to \(relayer.relay) after \(relayer.retry_after) seconds")
|
||||||
flush_event(event, to_relay: relayer)
|
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
|
// Don't add event if we already have it
|
||||||
if events[event.id] != nil {
|
if events[event.id] != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let remaining = to ?? pool.our_descriptors.map { $0.url.id }
|
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
|
events[event.id] = posted_ev
|
||||||
|
|
||||||
flush_event(posted_ev)
|
if after == nil {
|
||||||
|
flush_event(posted_ev)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,6 +67,80 @@ struct WalletRequest<T: Codable>: Codable {
|
|||||||
let params: T?
|
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> {
|
func make_wallet_pay_invoice_request(invoice: String) -> WalletRequest<PayInvoiceRequest> {
|
||||||
let data = PayInvoiceRequest(invoice: invoice)
|
let data = PayInvoiceRequest(invoice: invoice)
|
||||||
return WalletRequest(method: "pay_invoice", params: data)
|
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)
|
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)
|
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 {
|
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)
|
try? pool.add_relay(.nwc(url: url.relay))
|
||||||
post.send(ev, to: [url.relay.id], skip_ephemeral: false)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct NoteZapTarget: Equatable {
|
public struct NoteZapTarget: Equatable, Hashable {
|
||||||
public let note_id: String
|
public let note_id: String
|
||||||
public let author: String
|
public let author: String
|
||||||
}
|
}
|
||||||
@ -41,6 +41,148 @@ public enum ZapTarget: Equatable {
|
|||||||
|
|
||||||
struct ZapRequest {
|
struct ZapRequest {
|
||||||
let ev: NostrEvent
|
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 {
|
struct Zap {
|
||||||
@ -246,7 +388,7 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? {
|
|||||||
return endpoint
|
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 {
|
guard var base_url = payreq.callback.flatMap({ URLComponents(string: $0) }) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -256,7 +398,7 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent?, sats: Int
|
|||||||
|
|
||||||
var query = [URLQueryItem(name: "amount", value: "\(amount)")]
|
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)")
|
print("zapreq json: \(json)")
|
||||||
query.append(URLQueryItem(name: "nostr", value: json))
|
query.append(URLQueryItem(name: "nostr", value: json))
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,9 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
class Zaps {
|
class Zaps {
|
||||||
var zaps: [String: Zap]
|
var zaps: [String: Zapping]
|
||||||
let our_pubkey: String
|
let our_pubkey: String
|
||||||
var our_zaps: [String: [Zap]]
|
var our_zaps: [String: [Zapping]]
|
||||||
|
|
||||||
var event_counts: [String: Int]
|
var event_counts: [String: Int]
|
||||||
var event_totals: [String: Int64]
|
var event_totals: [String: Int64]
|
||||||
@ -22,15 +22,42 @@ class Zaps {
|
|||||||
self.event_counts = [:]
|
self.event_counts = [:]
|
||||||
self.event_totals = [:]
|
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) {
|
func add_zap(zap: Zapping) {
|
||||||
if zaps[zap.event.id] != nil {
|
if zaps[zap.request.id] != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.zaps[zap.event.id] = zap
|
self.zaps[zap.request.id] = zap
|
||||||
|
|
||||||
// record our zaps for an event
|
// record our zaps for an event
|
||||||
if zap.request.ev.pubkey == our_pubkey {
|
if zap.request.pubkey == our_pubkey {
|
||||||
switch zap.target {
|
switch zap.target {
|
||||||
case .note(let note_target):
|
case .note(let note_target):
|
||||||
if our_zaps[note_target.note_id] == nil {
|
if our_zaps[note_target.note_id] == nil {
|
||||||
@ -44,7 +71,7 @@ class Zaps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// don't count tips to self. lame.
|
// 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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,8 +85,15 @@ class Zaps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
event_counts[id] = event_counts[id]! + 1
|
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)
|
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)
|
||||||
|
}
|
||||||
|
@ -88,7 +88,7 @@ struct EventActionBar: View {
|
|||||||
|
|
||||||
if let lnurl = self.lnurl {
|
if let lnurl = self.lnurl {
|
||||||
Spacer()
|
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()
|
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 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 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 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) {
|
VStack(spacing: 50) {
|
||||||
EventActionBar(damus_state: ds, event: ev, bar: bar)
|
EventActionBar(damus_state: ds, event: ev, bar: bar)
|
||||||
|
@ -181,7 +181,9 @@ struct TextEvent: View {
|
|||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
TopPart(is_anon: is_anon)
|
TopPart(is_anon: is_anon)
|
||||||
|
|
||||||
ReplyPart
|
if !options.contains(.no_replying_to) {
|
||||||
|
ReplyPart
|
||||||
|
}
|
||||||
|
|
||||||
EvBody(options: self.options)
|
EvBody(options: self.options)
|
||||||
|
|
||||||
|
@ -9,30 +9,30 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ZapEvent: View {
|
struct ZapEvent: View {
|
||||||
let damus: DamusState
|
let damus: DamusState
|
||||||
let zap: Zap
|
let zap: Zapping
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
HStack(alignment: .center) {
|
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)
|
.font(.headline)
|
||||||
.padding([.top], 2)
|
.padding([.top], 2)
|
||||||
|
|
||||||
if zap.private_request != nil {
|
if zap.is_private {
|
||||||
Image(systemName: "lock.fill")
|
Image(systemName: "lock.fill")
|
||||||
.foregroundColor(DamusColors.green)
|
.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."))
|
.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 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."))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let priv = zap.private_request {
|
TextEvent(damus: damus, event: zap.request, pubkey: zap.request.pubkey, options: [.no_action_bar, .no_replying_to])
|
||||||
|
.padding([.top], 1)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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_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 {
|
struct ZapEvent_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VStack {
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -68,15 +68,11 @@ func event_group_author_name(profiles: Profiles, ind: Int, group: EventGroupType
|
|||||||
if let zapgrp = group.zap_group {
|
if let zapgrp = group.zap_group {
|
||||||
let zap = zapgrp.zaps[ind]
|
let zap = zapgrp.zaps[ind]
|
||||||
|
|
||||||
if let privzap = zap.private_request {
|
|
||||||
return event_author_name(profiles: profiles, pubkey: privzap.pubkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
if zap.is_anon {
|
if zap.is_anon {
|
||||||
return NSLocalizedString("Anonymous", comment: "Placeholder author name of the anonymous person who zapped an event.")
|
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 {
|
} else {
|
||||||
let ev = group.events[ind]
|
let ev = group.events[ind]
|
||||||
return event_author_name(profiles: profiles, pubkey: ev.pubkey)
|
return event_author_name(profiles: profiles, pubkey: ev.pubkey)
|
||||||
|
@ -88,8 +88,8 @@ struct RelayConfigView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let info = RelayInfo.rw
|
let info = RelayInfo.rw
|
||||||
|
let descriptor = RelayDescriptor(url: url, info: info)
|
||||||
guard (try? state.pool.add_relay(url, info: info)) != nil else {
|
guard (try? state.pool.add_relay(descriptor)) != nil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,17 +9,20 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ZapsView: View {
|
struct ZapsView: View {
|
||||||
let state: DamusState
|
let state: DamusState
|
||||||
@StateObject var model: ZapsModel
|
var model: ZapsModel
|
||||||
|
|
||||||
|
@ObservedObject var zaps: ZapsDataModel
|
||||||
|
|
||||||
init(state: DamusState, target: ZapTarget) {
|
init(state: DamusState, target: ZapTarget) {
|
||||||
self.state = state
|
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 {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
LazyVStack {
|
LazyVStack {
|
||||||
ForEach(model.zaps, id: \.event.id) { zap in
|
ForEach(zaps.zaps, id: \.request.id) { zap in
|
||||||
ZapEvent(damus: state, zap: zap)
|
ZapEvent(damus: state, zap: zap)
|
||||||
.padding([.horizontal])
|
.padding([.horizontal])
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user