damus

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

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:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/Components/ZapButton.swift | 44++++++++++++++++++++++++++------------------
Mdamus/ContentView.swift | 8++++++++
Mdamus/Models/HomeModel.swift | 32+++++++++++++++++++++++++++++---
Adamus/Models/ZapButtonModel.swift | 15+++++++++++++++
Mdamus/Util/LocalNotification.swift | 1+
Mdamus/Views/ActionBar/EventActionBar.swift | 2+-
Mdamus/Views/Profile/ProfileView.swift | 40+++++++++++++++++++++++++++++++++-------
Mdamus/Views/Zaps/CustomizeZapView.swift | 20+++++++++++++-------
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) } }