damus

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

commit 139df33cb7ffa317ea0969638b13ba59cd168789
parent a324523b85527d14834189979f8cb8aea0924cd9
Author: Daniel D’Aquino <daniel@daquino.me>
Date:   Mon, 23 Oct 2023 23:32:43 +0000

Rename ZapButton to NoteZapButton and ZapButtonView to ProfileZapLinkView (no-op)

This is a non-functional refactor to rename two views with similar names, to avoid confusion.

Signed-off-by: Daniel D’Aquino <daniel@daquino.me>
Reviewed-by: William Casarin <jb55@jb55.com>
Signed-off-by: William Casarin <jb55@jb55.com>

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 21++++++++++-----------
Adamus/Components/NoteZapButton.swift | 300+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddamus/Components/ZapButton.swift | 300-------------------------------------------------------------------------------
Mdamus/Views/ActionBar/EventActionBar.swift | 2+-
Mdamus/Views/Profile/ProfileView.swift | 2+-
Mdamus/Views/ProfileActionSheetView.swift | 2+-
Adamus/Views/Zaps/ProfileZapLinkView.swift | 92+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ddamus/Views/Zaps/ZapButtonView.swift | 92-------------------------------------------------------------------------------
8 files changed, 405 insertions(+), 406 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -299,7 +299,7 @@ 4CB883A82975FC1800DC99E7 /* Zaps.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A72975FC1800DC99E7 /* Zaps.swift */; }; 4CB883AA297612FF00DC99E7 /* ZapTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883A9297612FF00DC99E7 /* ZapTests.swift */; }; 4CB883AE2976FA9300DC99E7 /* FormatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */; }; - 4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* ZapButton.swift */; }; + 4CB883B0297705DD00DC99E7 /* NoteZapButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883AF297705DD00DC99E7 /* NoteZapButton.swift */; }; 4CB883B6297730E400DC99E7 /* LNUrls.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB883B5297730E400DC99E7 /* LNUrls.swift */; }; 4CB8FC232A41ABA800763C51 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB8FC222A41ABA500763C51 /* AboutView.swift */; }; 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */; }; @@ -434,12 +434,11 @@ D72A2D022AD9C136002AFF62 /* EventViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2CFF2AD9B66B002AFF62 /* EventViewTests.swift */; }; D72A2D052AD9C1B5002AFF62 /* MockDamusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D042AD9C1B5002AFF62 /* MockDamusState.swift */; }; D72A2D072AD9C1FB002AFF62 /* MockProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */; }; - D723C38E2AB8D83400065664 /* ContentFilters.swift in Sources */ = {isa = PBXBuildFile; fileRef = D723C38D2AB8D83400065664 /* ContentFilters.swift */; }; D7315A2A2ACDF3B70036E30A /* DamusCacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */; }; D7315A2C2ACDF4DA0036E30A /* DamusCacheManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */; }; - D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; }; - D76874F32AE3632B00FB0F68 /* ZapButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */; }; + D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */; }; D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */; }; + D783A63F2AD4E53D00658DDA /* SuggestedHashtagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */; }; D78525252A7B2EA4002FA637 /* NoteContentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */; }; D7870BC12AC4750B0080BA88 /* MentionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC02AC4750B0080BA88 /* MentionView.swift */; }; D7870BC32AC47EBC0080BA88 /* EventLoaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */; }; @@ -996,7 +995,7 @@ 4CB883A72975FC1800DC99E7 /* Zaps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Zaps.swift; sourceTree = "<group>"; }; 4CB883A9297612FF00DC99E7 /* ZapTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapTests.swift; sourceTree = "<group>"; }; 4CB883AD2976FA9300DC99E7 /* FormatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FormatTests.swift; sourceTree = "<group>"; }; - 4CB883AF297705DD00DC99E7 /* ZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButton.swift; sourceTree = "<group>"; }; + 4CB883AF297705DD00DC99E7 /* NoteZapButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteZapButton.swift; sourceTree = "<group>"; }; 4CB883B5297730E400DC99E7 /* LNUrls.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LNUrls.swift; sourceTree = "<group>"; }; 4CB8FC222A41ABA500763C51 /* AboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; }; 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNameView.swift; sourceTree = "<group>"; }; @@ -1138,9 +1137,9 @@ D72A2D062AD9C1FB002AFF62 /* MockProfiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockProfiles.swift; sourceTree = "<group>"; }; D7315A292ACDF3B70036E30A /* DamusCacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManager.swift; sourceTree = "<group>"; }; D7315A2B2ACDF4DA0036E30A /* DamusCacheManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusCacheManagerTests.swift; sourceTree = "<group>"; }; - D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = "<group>"; }; - D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButtonView.swift; sourceTree = "<group>"; }; + D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileZapLinkView.swift; sourceTree = "<group>"; }; D77BFA0A2AE3051200621634 /* ProfileActionSheetView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileActionSheetView.swift; sourceTree = "<group>"; }; + D783A63E2AD4E53D00658DDA /* SuggestedHashtagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestedHashtagsView.swift; sourceTree = "<group>"; }; D78525242A7B2EA4002FA637 /* NoteContentViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteContentViewTests.swift; sourceTree = "<group>"; }; D7870BC02AC4750B0080BA88 /* MentionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MentionView.swift; sourceTree = "<group>"; }; D7870BC22AC47EBC0080BA88 /* EventLoaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventLoaderView.swift; sourceTree = "<group>"; }; @@ -2091,7 +2090,7 @@ 5C513FB9297F72980072348F /* CustomPicker.swift */, 4CF0ABE22981BC7D00D66079 /* UserView.swift */, 7C902AE22981D55B002AB16E /* ZoomableScrollView.swift */, - 4CB883AF297705DD00DC99E7 /* ZapButton.swift */, + 4CB883AF297705DD00DC99E7 /* NoteZapButton.swift */, 4C42812B298C848200DBF26F /* TranslateView.swift */, 7CFF6316299FEFE5005D382A /* SelectableText.swift */, 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */, @@ -2242,7 +2241,7 @@ 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */, 4CA3FA0F29F593D000FDB3C3 /* ZapTypePicker.swift */, 4C73C5132A4437C10062CAC0 /* ZapUserView.swift */, - D76874F22AE3632B00FB0F68 /* ZapButtonView.swift */, + D76874F22AE3632B00FB0F68 /* ProfileZapLinkView.swift */, ); path = Zaps; sourceTree = "<group>"; @@ -2896,7 +2895,7 @@ 4C5E54062A9671F800FF6E60 /* UserStatusSheet.swift in Sources */, F71694F42A6732B7001F4053 /* GradientFollowButton.swift in Sources */, 4C3AC7A728369BA200E1F516 /* SearchHomeView.swift in Sources */, - 4CB883B0297705DD00DC99E7 /* ZapButton.swift in Sources */, + 4CB883B0297705DD00DC99E7 /* NoteZapButton.swift in Sources */, 4C363A922825FCF2006E126D /* ProfileUpdate.swift in Sources */, 4C3BEFDA281DCA1400B3DE84 /* LikeCounter.swift in Sources */, 4C32B9502A9AD44700DC3548 /* FlatBufferBuilder.swift in Sources */, @@ -2967,7 +2966,7 @@ E4FA1C032A24BB7F00482697 /* SearchSettingsView.swift in Sources */, 4C75EFBB2804A34C0006080F /* ProofOfWork.swift in Sources */, 4C3AC7A52836987600E1F516 /* MainTabView.swift in Sources */, - D76874F32AE3632B00FB0F68 /* ZapButtonView.swift in Sources */, + D76874F32AE3632B00FB0F68 /* ProfileZapLinkView.swift in Sources */, D77BFA0B2AE3051200621634 /* ProfileActionSheetView.swift in Sources */, 4C1A9A1F29DDD24B00516EAC /* AppearanceSettingsView.swift in Sources */, 3AA59D1D2999B0400061C48E /* DraftsModel.swift in Sources */, diff --git a/damus/Components/NoteZapButton.swift b/damus/Components/NoteZapButton.swift @@ -0,0 +1,300 @@ +// +// NoteZapButton.swift +// damus +// +// Created by William Casarin on 2023-01-17. +// + +import SwiftUI + +enum ZappingEventType { + case failed(ZappingError) + case got_zap_invoice(String) + case sent_from_nwc +} + +enum ZappingError { + case fetching_invoice + case bad_lnurl + case canceled + case send_failed +} + +struct ZappingEvent { + let is_custom: Bool + let type: ZappingEventType + let target: ZapTarget +} + +struct NoteZapButton: View { + let damus_state: DamusState + let target: ZapTarget + let lnurl: String + + @ObservedObject var zaps: ZapsDataModel + + var our_zap: Zapping? { + zaps.zaps.first(where: { z in z.request.ev.pubkey == damus_state.pubkey }) + } + + var zap_img: String { + switch our_zap { + case .none: + return "zap" + case .zap: + return "zap.fill" + case .pending: + return "zap.fill" + } + } + + var zap_color: Color { + if our_zap == nil { + return Color.gray + } + + // always orange ! + return Color.orange + } + + func tap() { + guard let our_zap else { + send_zap(damus_state: damus_state, target: target, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) + return + } + + // 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 + } + + UIImpactFeedbackGenerator(style: .rigid).impactOccurred() + + 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 { + HStack(spacing: 4) { + if !damus_state.settings.nozaps || zaps.zap_total > 0 { + Button(action: { + }, label: { + Image(zap_img) + .resizable() + .foregroundColor(zap_color) + .font(.footnote.weight(.medium)) + .aspectRatio(contentMode: .fit) + .frame(width:20, height: 20) + }) + } + + if zaps.zap_total > 0 { + Text(verbatim: format_msats_abbrev(zaps.zap_total)) + .font(.footnote) + .foregroundColor(zap_color) + } + } + .accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button")) + .simultaneousGesture(LongPressGesture().onEnded {_ in + guard !damus_state.settings.nozaps else { return } + + present_sheet(.zap(target: target, lnurl: lnurl)) + }) + .highPriorityGesture(TapGesture().onEnded { + guard !damus_state.settings.nozaps else { return } + + tap() + }) + } +} + + +struct ZapButton_Previews: PreviewProvider { + static var previews: some View { + let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice))) + let zaps = ZapsDataModel([.pending(pending_zap)]) + + NoteZapButton(damus_state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), 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, target: ZapTarget, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) { + guard let keypair = damus_state.keypair.to_full() else { + return + } + + // Only take the first 10 because reasons + let relays = Array(damus_state.pool.our_descriptors.prefix(10)) + let content = comment ?? "" + + guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else { + // this should never happen + return + } + + let amount_msat = Int64(amount_sats ?? damus_state.settings.default_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: mzapreq, type: zap_type, state: pending_zap_state) + let zapreq = mzapreq.potentially_anon_outer_request.ev + let reqid = ZapRequestId(from_makezap: mzapreq) + + UIImpactFeedbackGenerator(style: .heavy).impactOccurred() + damus_state.add_zap(zap: .pending(pending_zap)) + + Task { @MainActor in + guard let payreq = await damus_state.lnurls.lookup_or_fetch(pubkey: target.pubkey, lnurl: lnurl) else { + // TODO: show error + remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events) + let typ = ZappingEventType.failed(.bad_lnurl) + let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target) + notify(.zapping(ev)) + return + } + + guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, msats: amount_msat, zap_type: zap_type, comment: comment) else { + remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events) + let typ = ZappingEventType.failed(.fetching_invoice) + let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target) + notify(.zapping(ev)) + return + } + + 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: reqid, zapcache: damus_state.zaps, evcache: damus_state.events) + let typ = ZappingEventType.failed(.canceled) + let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target) + notify(.zapping(ev)) + return + } + + var flusher: OnFlush? = nil + + // donations are only enabled on one-tap zaps and off appstore + if !damus_state.settings.nozaps && !is_custom && damus_state.settings.donation_percent > 0 { + flusher = .once({ pe in + // send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation + Task { @MainActor in + await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat) + } + }) + } + + // we don't have a delay on one-tap nozaps (since this will be from customize zap view) + let delay = damus_state.settings.nozaps ? nil : 5.0 + + let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher) + + guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else { + print("nwc: failed to send nwc request for zapreq \(reqid.reqid)") + + let typ = ZappingEventType.failed(.send_failed) + let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target) + notify(.zapping(ev)) + return + } + + print("nwc: sending request \(nwc_req.id) zap_req_id \(reqid.reqid)") + + if pzap_state.update_state(state: .postbox_pending(nwc_req)) { + // we don't need to trigger a ZapsDataModel update here + } + + let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target) + notify(.zapping(ev)) + + case .external(let pending_ext): + pending_ext.state = .done + let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target) + notify(.zapping(ev)) + } + } + + 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: + if nwc_state.update_state(state: .cancel_fetching_invoice) { + // we don't need to update the ZapsDataModel here + } + // 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) + } + let reqid = ZapRequestId(from_pending: zap) + remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache) + + case .failed: + let reqid = ZapRequestId(from_pending: zap) + remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache) + } + + return nil +} diff --git a/damus/Components/ZapButton.swift b/damus/Components/ZapButton.swift @@ -1,300 +0,0 @@ -// -// ZapButton.swift -// damus -// -// Created by William Casarin on 2023-01-17. -// - -import SwiftUI - -enum ZappingEventType { - case failed(ZappingError) - case got_zap_invoice(String) - case sent_from_nwc -} - -enum ZappingError { - case fetching_invoice - case bad_lnurl - case canceled - case send_failed -} - -struct ZappingEvent { - let is_custom: Bool - let type: ZappingEventType - let target: ZapTarget -} - -struct ZapButton: View { - let damus_state: DamusState - let target: ZapTarget - let lnurl: String - - @ObservedObject var zaps: ZapsDataModel - - var our_zap: Zapping? { - zaps.zaps.first(where: { z in z.request.ev.pubkey == damus_state.pubkey }) - } - - var zap_img: String { - switch our_zap { - case .none: - return "zap" - case .zap: - return "zap.fill" - case .pending: - return "zap.fill" - } - } - - var zap_color: Color { - if our_zap == nil { - return Color.gray - } - - // always orange ! - return Color.orange - } - - func tap() { - guard let our_zap else { - send_zap(damus_state: damus_state, target: target, lnurl: lnurl, is_custom: false, comment: nil, amount_sats: nil, zap_type: damus_state.settings.default_zap_type) - return - } - - // 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 - } - - UIImpactFeedbackGenerator(style: .rigid).impactOccurred() - - 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 { - HStack(spacing: 4) { - if !damus_state.settings.nozaps || zaps.zap_total > 0 { - Button(action: { - }, label: { - Image(zap_img) - .resizable() - .foregroundColor(zap_color) - .font(.footnote.weight(.medium)) - .aspectRatio(contentMode: .fit) - .frame(width:20, height: 20) - }) - } - - if zaps.zap_total > 0 { - Text(verbatim: format_msats_abbrev(zaps.zap_total)) - .font(.footnote) - .foregroundColor(zap_color) - } - } - .accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button")) - .simultaneousGesture(LongPressGesture().onEnded {_ in - guard !damus_state.settings.nozaps else { return } - - present_sheet(.zap(target: target, lnurl: lnurl)) - }) - .highPriorityGesture(TapGesture().onEnded { - guard !damus_state.settings.nozaps else { return } - - tap() - }) - } -} - - -struct ZapButton_Previews: PreviewProvider { - static var previews: some View { - let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), request: .normal(test_zap_request), type: .pub, state: .external(.init(state: .fetching_invoice))) - let zaps = ZapsDataModel([.pending(pending_zap)]) - - ZapButton(damus_state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), 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, target: ZapTarget, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) { - guard let keypair = damus_state.keypair.to_full() else { - return - } - - // Only take the first 10 because reasons - let relays = Array(damus_state.pool.our_descriptors.prefix(10)) - let content = comment ?? "" - - guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else { - // this should never happen - return - } - - let amount_msat = Int64(amount_sats ?? damus_state.settings.default_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: mzapreq, type: zap_type, state: pending_zap_state) - let zapreq = mzapreq.potentially_anon_outer_request.ev - let reqid = ZapRequestId(from_makezap: mzapreq) - - UIImpactFeedbackGenerator(style: .heavy).impactOccurred() - damus_state.add_zap(zap: .pending(pending_zap)) - - Task { @MainActor in - guard let payreq = await damus_state.lnurls.lookup_or_fetch(pubkey: target.pubkey, lnurl: lnurl) else { - // TODO: show error - remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events) - let typ = ZappingEventType.failed(.bad_lnurl) - let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target) - notify(.zapping(ev)) - return - } - - guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, msats: amount_msat, zap_type: zap_type, comment: comment) else { - remove_zap(reqid: reqid, zapcache: damus_state.zaps, evcache: damus_state.events) - let typ = ZappingEventType.failed(.fetching_invoice) - let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target) - notify(.zapping(ev)) - return - } - - 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: reqid, zapcache: damus_state.zaps, evcache: damus_state.events) - let typ = ZappingEventType.failed(.canceled) - let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target) - notify(.zapping(ev)) - return - } - - var flusher: OnFlush? = nil - - // donations are only enabled on one-tap zaps and off appstore - if !damus_state.settings.nozaps && !is_custom && damus_state.settings.donation_percent > 0 { - flusher = .once({ pe in - // send donation zap when the pending zap is flushed, this allows user to cancel and not send a donation - Task { @MainActor in - await send_donation_zap(pool: damus_state.pool, postbox: damus_state.postbox, nwc: nwc_state.url, percent: damus_state.settings.donation_percent, base_msats: amount_msat) - } - }) - } - - // we don't have a delay on one-tap nozaps (since this will be from customize zap view) - let delay = damus_state.settings.nozaps ? nil : 5.0 - - let nwc_req = nwc_pay(url: nwc_state.url, pool: damus_state.pool, post: damus_state.postbox, invoice: inv, delay: delay, on_flush: flusher) - - guard let nwc_req, case .nwc(let pzap_state) = pending_zap_state else { - print("nwc: failed to send nwc request for zapreq \(reqid.reqid)") - - let typ = ZappingEventType.failed(.send_failed) - let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target) - notify(.zapping(ev)) - return - } - - print("nwc: sending request \(nwc_req.id) zap_req_id \(reqid.reqid)") - - if pzap_state.update_state(state: .postbox_pending(nwc_req)) { - // we don't need to trigger a ZapsDataModel update here - } - - let ev = ZappingEvent(is_custom: is_custom, type: .sent_from_nwc, target: target) - notify(.zapping(ev)) - - case .external(let pending_ext): - pending_ext.state = .done - let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target) - notify(.zapping(ev)) - } - } - - 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: - if nwc_state.update_state(state: .cancel_fetching_invoice) { - // we don't need to update the ZapsDataModel here - } - // 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) - } - let reqid = ZapRequestId(from_pending: zap) - remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache) - - case .failed: - let reqid = ZapRequestId(from_pending: zap) - remove_zap(reqid: reqid, zapcache: zapcache, evcache: evcache) - } - - return nil -} diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift @@ -84,7 +84,7 @@ struct EventActionBar: View { if let lnurl = self.lnurl { Spacer() - ZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model) + NoteZapButton(damus_state: damus_state, target: ZapTarget.note(id: event.id, author: event.pubkey), lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model) } Spacer() diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift @@ -222,7 +222,7 @@ struct ProfileView: View { } func lnButton(unownedProfile: Profile?, record: ProfileRecord?) -> some View { - return ZapButtonView(unownedProfileRecord: record, profileModel: self.profile) { reactions_enabled, lud16, lnurl in + return ProfileZapLinkView(unownedProfileRecord: record, profileModel: self.profile) { reactions_enabled, lud16, lnurl in Image(reactions_enabled ? "zap.fill" : "zap") .foregroundColor(reactions_enabled ? .orange : Color.primary) .profile_button_style(scheme: colorScheme) diff --git a/damus/Views/ProfileActionSheetView.swift b/damus/Views/ProfileActionSheetView.swift @@ -61,7 +61,7 @@ struct ProfileActionSheetView: View { if let lnurl = self.profile_data()?.lnurl, lnurl != "" { return AnyView( VStack(alignment: .center, spacing: 10) { - ZapButtonView(damus_state: damus_state, pubkey: self.profile.pubkey, action: { dismiss() }) { reactions_enabled, lud16, lnurl in + ProfileZapLinkView(damus_state: damus_state, pubkey: self.profile.pubkey, action: { dismiss() }) { reactions_enabled, lud16, lnurl in Image(reactions_enabled ? "zap.fill" : "zap") .foregroundColor(reactions_enabled ? .orange : Color.primary) .profile_button_style(scheme: colorScheme) diff --git a/damus/Views/Zaps/ProfileZapLinkView.swift b/damus/Views/Zaps/ProfileZapLinkView.swift @@ -0,0 +1,92 @@ +// +// ProfileZapLinkView.swift +// damus +// +// Created by Daniel D’Aquino on 2023-10-20. +// + +import SwiftUI + +struct ProfileZapLinkView<Content: View>: View { + typealias ContentViewFunction = (_ reactions_enabled: Bool, _ lud16: String?, _ lnurl: String?) -> Content + typealias ActionFunction = () -> Void + + let pubkey: Pubkey + @ViewBuilder let label: ContentViewFunction + let action: ActionFunction? + + let reactions_enabled: Bool + let lud16: String? + let lnurl: String? + + init(pubkey: Pubkey, reactions_enabled: Bool, lud16: String?, lnurl: String?, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { + self.pubkey = pubkey + self.label = label + self.action = action + self.reactions_enabled = reactions_enabled + self.lud16 = lud16 + self.lnurl = lnurl + } + + init(damus_state: DamusState, pubkey: Pubkey, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { + self.pubkey = pubkey + self.label = label + self.action = action + + let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey) + let record = profile_txn.unsafeUnownedValue + self.reactions_enabled = record?.profile?.reactions ?? true + self.lud16 = record?.profile?.lud06 + self.lnurl = record?.lnurl + } + + init(unownedProfileRecord: ProfileRecord?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { + self.pubkey = profileModel.pubkey + self.label = label + self.action = action + + self.reactions_enabled = unownedProfileRecord?.profile?.reactions ?? true + self.lud16 = unownedProfileRecord?.profile?.lud16 + self.lnurl = unownedProfileRecord?.lnurl + } + + var body: some View { + Button( + action: { + if let lnurl { + present_sheet(.zap(target: .profile(self.pubkey), lnurl: lnurl)) + } + action?() + }, + label: { + self.label(self.reactions_enabled, self.lud16, self.lnurl) + } + ) + .contextMenu { + if self.reactions_enabled == false { + Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.") + } + + if let lud16 { + Button { + UIPasteboard.general.string = lud16 + } label: { + Label(lud16, image: "copy2") + } + } else { + Button { + UIPasteboard.general.string = lnurl + } label: { + Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), image: "copy") + } + } + } + .disabled(lnurl == nil) + } +} + +#Preview { + ProfileZapLinkView(pubkey: test_pubkey, reactions_enabled: true, lud16: make_test_profile().lud16, lnurl: "test@sendzaps.lol", label: { reactions_enabled, lud16, lnurl in + Image("zap.fill") + }) +} diff --git a/damus/Views/Zaps/ZapButtonView.swift b/damus/Views/Zaps/ZapButtonView.swift @@ -1,92 +0,0 @@ -// -// ZapButtonView.swift -// damus -// -// Created by Daniel D’Aquino on 2023-10-20. -// - -import SwiftUI - -struct ZapButtonView<Content: View>: View { - typealias ContentViewFunction = (_ reactions_enabled: Bool, _ lud16: String?, _ lnurl: String?) -> Content - typealias ActionFunction = () -> Void - - let pubkey: Pubkey - @ViewBuilder let label: ContentViewFunction - let action: ActionFunction? - - let reactions_enabled: Bool - let lud16: String? - let lnurl: String? - - init(pubkey: Pubkey, reactions_enabled: Bool, lud16: String?, lnurl: String?, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { - self.pubkey = pubkey - self.label = label - self.action = action - self.reactions_enabled = reactions_enabled - self.lud16 = lud16 - self.lnurl = lnurl - } - - init(damus_state: DamusState, pubkey: Pubkey, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { - self.pubkey = pubkey - self.label = label - self.action = action - - let profile_txn = damus_state.profiles.lookup_with_timestamp(pubkey) - let record = profile_txn.unsafeUnownedValue - self.reactions_enabled = record?.profile?.reactions ?? true - self.lud16 = record?.profile?.lud06 - self.lnurl = record?.lnurl - } - - init(unownedProfileRecord: ProfileRecord?, profileModel: ProfileModel, action: ActionFunction? = nil, @ViewBuilder label: @escaping ContentViewFunction) { - self.pubkey = profileModel.pubkey - self.label = label - self.action = action - - self.reactions_enabled = unownedProfileRecord?.profile?.reactions ?? true - self.lud16 = unownedProfileRecord?.profile?.lud16 - self.lnurl = unownedProfileRecord?.lnurl - } - - var body: some View { - Button( - action: { - if let lnurl { - present_sheet(.zap(target: .profile(self.pubkey), lnurl: lnurl)) - } - action?() - }, - label: { - self.label(self.reactions_enabled, self.lud16, self.lnurl) - } - ) - .contextMenu { - if self.reactions_enabled == false { - Text("OnlyZaps Enabled", comment: "Non-tappable text in context menu that shows up when the zap button on profile is long pressed to indicate that the user has enabled OnlyZaps, meaning that they would like to be only zapped and not accept reactions to their notes.") - } - - if let lud16 { - Button { - UIPasteboard.general.string = lud16 - } label: { - Label(lud16, image: "copy2") - } - } else { - Button { - UIPasteboard.general.string = lnurl - } label: { - Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), image: "copy") - } - } - } - .disabled(lnurl == nil) - } -} - -#Preview { - ZapButtonView(pubkey: test_pubkey, reactions_enabled: true, lud16: make_test_profile().lud16, lnurl: "test@sendzaps.lol", label: { reactions_enabled, lud16, lnurl in - Image("zap.fill") - }) -}