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
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if !zapping {
|
||||
return nil
|
||||
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
|
||||
}
|
||||
|
||||
return Color.yellow
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,10 +61,10 @@ class ZapsModel: ObservableObject {
|
||||
}
|
||||
|
||||
if let zap = state.zaps.zaps[ev.id] {
|
||||
if state.events.store_zap(zap: zap) {
|
||||
objectWillChange.send()
|
||||
state.events.store_zap(zap: zap)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
|
||||
guard let zapper = state.profiles.lookup_zapper(pubkey: target.pubkey) else {
|
||||
return
|
||||
}
|
||||
@ -73,10 +73,7 @@ class ZapsModel: ObservableObject {
|
||||
return
|
||||
}
|
||||
|
||||
if self.state.add_zap(zap: zap) {
|
||||
objectWillChange.send()
|
||||
}
|
||||
}
|
||||
self.state.add_zap(zap: .zap(zap))
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
}
|
||||
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
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
|
||||
if new_zap.event.id == zap.event.id {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
||||
if after == nil {
|
||||
flush_event(posted_ev)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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]
|
||||
@ -23,14 +23,41 @@ class Zaps {
|
||||
self.event_totals = [:]
|
||||
}
|
||||
|
||||
func add_zap(zap: Zap) {
|
||||
if zaps[zap.event.id] != nil {
|
||||
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: 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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -181,7 +181,9 @@ struct TextEvent: View {
|
||||
VStack(alignment: .leading) {
|
||||
TopPart(is_anon: is_anon)
|
||||
|
||||
if !options.contains(.no_replying_to) {
|
||||
ReplyPart
|
||||
}
|
||||
|
||||
EvBody(options: self.options)
|
||||
|
||||
|
@ -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 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: priv, pubkey: priv.pubkey, options: [.no_action_bar, .no_replying_to])
|
||||
TextEvent(damus: damus, event: zap.request, pubkey: zap.request.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_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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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])
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user