commit 952d6746d55eaf573fe2aa44e31aba24141323b2
parent b3b335f91719d0c7e1936cab77b3ce08e00a5b83
Author: Terry Yiu <963907+tyiu@users.noreply.github.com>
Date: Thu, 1 Jun 2023 20:51:50 -0400
Add profile zaps
Refactor profile zaps to reuse same BOLT11 Lightning invoice logic as
note zaps, which fixes profile zaps from Cash App and Muun wallets
Changelog-Added: Add profile zaps
Changelog-Fixed: Fix profile zapping for Muun and Strike wallets
Closes: #1236
Fixes: #1067
Diffstat:
9 files changed, 130 insertions(+), 36 deletions(-)
diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj
@@ -11,6 +11,7 @@
3169CAE6294E69C000EE4006 /* EmptyTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAE5294E69C000EE4006 /* EmptyTimelineView.swift */; };
3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3169CAEC294FCCFC00EE4006 /* Constants.swift */; };
31D2E847295218AF006D67F8 /* Shimmer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D2E846295218AF006D67F8 /* Shimmer.swift */; };
+ 3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */; };
3A3040ED29A5CB86008A0F29 /* ReplyDescriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EC29A5CB86008A0F29 /* ReplyDescriptionTests.swift */; };
3A3040EF29A8FEE9008A0F29 /* EventDetailBarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040EE29A8FEE9008A0F29 /* EventDetailBarTests.swift */; };
3A3040F129A8FF97008A0F29 /* LocalizationUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A3040F029A8FF97008A0F29 /* LocalizationUtil.swift */; };
@@ -337,6 +338,7 @@
3A185A04297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A185A05297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "lv-LV"; path = "lv-LV.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A185A06297F2C3800F4BDC0 /* lv-LV */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "lv-LV"; path = "lv-LV.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
+ 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ZapButtonModel.swift; sourceTree = "<group>"; };
3A25EF132992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
3A25EF142992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "el-GR"; path = "el-GR.lproj/Localizable.strings"; sourceTree = "<group>"; };
3A25EF152992DA5D008ABE69 /* el-GR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "el-GR"; path = "el-GR.lproj/Localizable.stringsdict"; sourceTree = "<group>"; };
@@ -903,6 +905,7 @@
4CD348EE29C3659D00497EB2 /* ImageUploadModel.swift */,
3A48E7AF29DFBE9D006E787E /* MutedThreadsManager.swift */,
4C7D09772A0B0CC900943473 /* WalletModel.swift */,
+ 3A23838D2A297DD200E5AA2E /* ZapButtonModel.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -1790,6 +1793,7 @@
4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */,
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */,
+ 3A23838E2A297DD200E5AA2E /* ZapButtonModel.swift in Sources */,
F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */,
4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */,
4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */,
diff --git a/damus/Components/ZapButton.swift b/damus/Components/ZapButton.swift
@@ -10,29 +10,25 @@ 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 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
+ let target: ZapTarget
}
struct ZapButton: View {
let damus_state: DamusState
- let event: NostrEvent
+ let target: ZapTarget
let lnurl: String
@ObservedObject var zaps: ZapsDataModel
@@ -71,7 +67,7 @@ struct ZapButton: View {
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)
+ 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
}
@@ -142,7 +138,7 @@ struct ZapButton: View {
tap()
})
.sheet(isPresented: $button.showing_zap_customizer) {
- CustomizeZapView(state: damus_state, event: event, lnurl: lnurl)
+ CustomizeZapView(state: damus_state, target: target, lnurl: lnurl)
}
.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 ?? "")
@@ -150,7 +146,7 @@ struct ZapButton: View {
.onReceive(handle_notify(.zapping)) { notif in
let zap_ev = notif.object as! ZappingEvent
- guard zap_ev.event.id == self.event.id else {
+ guard zap_ev.target.id == self.target.id else {
return
}
@@ -169,6 +165,8 @@ struct ZapButton: View {
let wallet = damus_state.settings.default_wallet.model
open_with_wallet(wallet: wallet, invoice: inv)
}
+ case .sent_from_nwc:
+ break
}
}
}
@@ -180,7 +178,7 @@ struct ZapButton_Previews: PreviewProvider {
let pending_zap = PendingZap(amount_msat: 1000, target: ZapTarget.note(id: "noteid", author: "author"), 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(), event: test_event, lnurl: "lnurl", zaps: zaps)
+ ZapButton(damus_state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "lnurl", zaps: zaps)
}
}
@@ -196,14 +194,13 @@ func initial_pending_zap_state(settings: UserSettingsStore) -> PendingZapState {
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, 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 target = ZapTarget.note(id: event.id, author: event.pubkey)
let content = comment ?? ""
guard let mzapreq = make_zap_request_event(keypair: keypair, content: content, relays: relays, target: target, zap_type: zap_type) else {
@@ -231,7 +228,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
DispatchQueue.main.async {
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, event: event)
+ let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
}
return
@@ -245,7 +242,7 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
DispatchQueue.main.async {
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, event: event)
+ let ev = ZappingEvent(is_custom: is_custom, type: typ, target: target)
notify(.zapping, ev)
}
return
@@ -258,6 +255,9 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
// 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
}
@@ -276,6 +276,10 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
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
}
@@ -284,9 +288,13 @@ func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_cust
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), event: event)
+ let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), target: target)
notify(.zapping, ev)
}
}
diff --git a/damus/ContentView.swift b/damus/ContentView.swift
@@ -456,6 +456,11 @@ struct ContentView: View {
let damus_state else {
return
}
+
+ if local.type == .profile_zap {
+ open_profile(id: local.event_id)
+ return
+ }
guard let target = damus_state.events.lookup(local.event_id) else {
return
@@ -471,6 +476,9 @@ struct ContentView: View {
case .mention: fallthrough
case .repost:
open_event(ev: target)
+ case .profile_zap:
+ // Handled separately above.
+ break
}
}
.onReceive(handle_notify(.onlyzaps_mode)) { notif in
diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift
@@ -187,7 +187,12 @@ class HomeModel: ObservableObject {
}
if damus_state.settings.zap_notification {
// Create in-app local notification for zap received.
- create_in_app_zap_notification(profiles: profiles, zap: zap, evId: ev.referenced_ids.first?.id ?? "")
+ switch zap.target {
+ case .profile(let profile_id):
+ create_in_app_profile_zap_notification(profiles: profiles, zap: zap, profile_id: profile_id)
+ case .note(let note_target):
+ create_in_app_event_zap_notification(profiles: profiles, zap: zap, evId: note_target.note_id)
+ }
}
}
@@ -1118,7 +1123,28 @@ func zap_notification_body(profiles: Profiles, zap: Zap, locale: Locale = Locale
}
}
-func create_in_app_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
+func create_in_app_profile_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, profile_id: String) {
+ let content = UNMutableNotificationContent()
+
+ content.title = zap_notification_title(zap)
+ content.body = zap_notification_body(profiles: profiles, zap: zap, locale: locale)
+ content.sound = UNNotificationSound.default
+ content.userInfo = LossyLocalNotification(type: .profile_zap, event_id: profile_id).to_user_info()
+
+ let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false)
+
+ let request = UNNotificationRequest(identifier: "myZapNotification", content: content, trigger: trigger)
+
+ UNUserNotificationCenter.current().add(request) { error in
+ if let error = error {
+ print("Error: \(error)")
+ } else {
+ print("Local notification scheduled")
+ }
+ }
+}
+
+func create_in_app_event_zap_notification(profiles: Profiles, zap: Zap, locale: Locale = Locale.current, evId: String) {
let content = UNMutableNotificationContent()
content.title = zap_notification_title(zap)
@@ -1202,7 +1228,7 @@ func create_local_notification(profiles: Profiles, notify: LocalNotification) {
case .dm:
title = displayName
identifier = "myDMNotification"
- case .zap:
+ case .zap, .profile_zap:
// not handled here
break
}
diff --git a/damus/Models/ZapButtonModel.swift b/damus/Models/ZapButtonModel.swift
@@ -0,0 +1,15 @@
+//
+// ZapButtonModel.swift
+// damus
+//
+// Created by Terry Yiu on 6/1/23.
+//
+
+import Foundation
+
+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
+}
diff --git a/damus/Util/LocalNotification.swift b/damus/Util/LocalNotification.swift
@@ -44,4 +44,5 @@ enum LocalNotificationType: String {
case mention
case repost
case zap
+ case profile_zap
}
diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift
@@ -88,7 +88,7 @@ struct EventActionBar: View {
if let lnurl = self.lnurl {
Spacer()
- ZapButton(damus_state: damus_state, event: event, lnurl: lnurl, zaps: self.damus_state.events.get_cache_data(self.event.id).zaps_model)
+ 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)
}
Spacer()
diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift
@@ -106,6 +106,7 @@ struct ProfileView: View {
@StateObject var profile: ProfileModel
@StateObject var followers: FollowersModel
+ @StateObject var zap_button_model: ZapButtonModel = ZapButtonModel()
init(damus_state: DamusState, profile: ProfileModel, followers: FollowersModel) {
self.damus_state = damus_state
@@ -244,11 +245,7 @@ struct ProfileView: View {
func lnButton(lnurl: String, profile: Profile) -> some View {
let button_img = profile.reactions == false ? "zap.fill" : "zap"
return Button(action: {
- if damus_state.settings.show_wallet_selector {
- showing_select_wallet = true
- } else {
- open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl)
- }
+ zap_button_model.showing_zap_customizer = true
}) {
Image(button_img)
.foregroundColor(button_img == "zap.fill" ? .orange : Color.primary)
@@ -275,8 +272,37 @@ struct ProfileView: View {
}
.cornerRadius(24)
- .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: lnurl)
+ .sheet(isPresented: $zap_button_model.showing_zap_customizer) {
+ CustomizeZapView(state: damus_state, target: ZapTarget.profile(self.profile.pubkey), lnurl: lnurl)
+ }
+ .sheet(isPresented: $zap_button_model.showing_select_wallet, onDismiss: {zap_button_model.showing_select_wallet = false}) {
+ SelectWalletView(default_wallet: damus_state.settings.default_wallet, showingSelectWallet: $zap_button_model.showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: zap_button_model.invoice ?? "")
+ }
+ .onReceive(handle_notify(.zapping)) { notif in
+ let zap_ev = notif.object as! ZappingEvent
+
+ guard zap_ev.target.id == self.profile.pubkey else {
+ return
+ }
+
+ guard !zap_ev.is_custom else {
+ return
+ }
+
+ switch zap_ev.type {
+ case .failed:
+ break
+ case .got_zap_invoice(let inv):
+ if damus_state.settings.show_wallet_selector {
+ zap_button_model.invoice = inv
+ zap_button_model.showing_select_wallet = true
+ } else {
+ let wallet = damus_state.settings.default_wallet.model
+ open_with_wallet(wallet: wallet, invoice: inv)
+ }
+ case .sent_from_nwc:
+ break
+ }
}
}
diff --git a/damus/Views/Zaps/CustomizeZapView.swift b/damus/Views/Zaps/CustomizeZapView.swift
@@ -46,7 +46,7 @@ func satsString(_ count: Int, locale: Locale = Locale.current) -> String {
struct CustomizeZapView: View {
let state: DamusState
- let event: NostrEvent
+ let target: ZapTarget
let lnurl: String
@State var comment: String
@State var custom_amount: String
@@ -71,9 +71,9 @@ struct CustomizeZapView: View {
colorScheme == .light ? DamusColors.black : DamusColors.white
}
- init(state: DamusState, event: NostrEvent, lnurl: String) {
+ init(state: DamusState, target: ZapTarget, lnurl: String) {
self._comment = State(initialValue: "")
- self.event = event
+ self.target = target
self.zap_amounts = get_zap_amount_items(state.settings.default_zap_amount)
self._error = State(initialValue: nil)
self._invoice = State(initialValue: "")
@@ -184,7 +184,7 @@ struct CustomizeZapView: View {
} else {
Button(NSLocalizedString("Zap", comment: "Button to send a zap.")) {
let amount = custom_amount_sats
- send_zap(damus_state: state, event: event, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type)
+ send_zap(damus_state: state, target: target, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type)
self.zapping = true
}
.disabled(custom_amount_sats == 0 || custom_amount.isEmpty)
@@ -208,7 +208,7 @@ struct CustomizeZapView: View {
guard zap_ev.is_custom else {
return
}
- guard zap_ev.event.id == event.id else {
+ guard zap_ev.target.id == target.id else {
return
}
@@ -221,6 +221,10 @@ struct CustomizeZapView: View {
self.error = NSLocalizedString("Error fetching lightning invoice", comment: "Message to display when there was an error fetching a lightning invoice while attempting to zap.")
case .bad_lnurl:
self.error = NSLocalizedString("Invalid lightning address", comment: "Message to display when there was an error attempting to zap due to an invalid lightning address.")
+ case .canceled:
+ self.error = NSLocalizedString("Zap attempt from connected wallet was canceled.", comment: "Message to display when a zap from the user's connected wallet was canceled.")
+ case .send_failed:
+ self.error = NSLocalizedString("Zap attempt from connected wallet failed.", comment: "Message to display when sending a zap from the user's connected wallet failed.")
}
break
case .got_zap_invoice(let inv):
@@ -234,6 +238,8 @@ struct CustomizeZapView: View {
self.showing_wallet_selector = false
dismiss()
}
+ case .sent_from_nwc:
+ dismiss()
}
}
@@ -309,7 +315,7 @@ struct CustomizeZapView: View {
}
var ZapPicker: some View {
- ZapTypePicker(zap_type: $zap_type, settings: state.settings, profiles: state.profiles, pubkey: event.pubkey)
+ ZapTypePicker(zap_type: $zap_type, settings: state.settings, profiles: state.profiles, pubkey: target.pubkey)
}
var MainContent: some View {
@@ -326,7 +332,7 @@ extension View {
struct CustomizeZapView_Previews: PreviewProvider {
static var previews: some View {
- CustomizeZapView(state: test_damus_state(), event: test_event, lnurl: "")
+ CustomizeZapView(state: test_damus_state(), target: ZapTarget.note(id: test_event.id, author: test_event.pubkey), lnurl: "")
.frame(width: 400, height: 600)
}
}