damus

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

commit 71f7ea47df67f0a5ff8f85d5cfd72a610fe51242
parent 64b1a57918d0aeed621185103c69414827d3f637
Author: William Casarin <jb55@jb55.com>
Date:   Sat, 25 Feb 2023 12:10:37 -0800

Customized Zaps

Changelog-Added: Customized zaps

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 8++++++++
Mdamus/Components/ZapButton.swift | 181++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mdamus/Models/HomeModel.swift | 2++
Mdamus/Nostr/Nostr.swift | 3+++
Mdamus/Nostr/NostrEvent.swift | 17++++++++++++++---
Mdamus/Nostr/NostrKind.swift | 1+
Mdamus/Util/LNUrlPayRequest.swift | 2++
Mdamus/Util/Notifications.swift | 3+++
Mdamus/Util/Zap.swift | 10++++++++--
Mdamus/Views/ConfigView.swift | 33++++++++++++++++++---------------
Mdamus/Views/Events/TextEvent.swift | 23+++++++++++++++++++----
Adamus/Views/Profile/MaybeAnonPfpView.swift | 46++++++++++++++++++++++++++++++++++++++++++++++
Adamus/Views/Zaps/CustomizeZapView.swift | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 458 insertions(+), 86 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -134,6 +134,8 @@ 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; }; 4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; }; 4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; }; + 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */; }; + 4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */; }; 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; }; 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; }; 4CAAD8B029888AD200060CEA /* RelayConfigView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */; }; @@ -470,6 +472,8 @@ 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; }; 4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; }; 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = "<group>"; }; + 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomizeZapView.swift; sourceTree = "<group>"; }; + 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MaybeAnonPfpView.swift; sourceTree = "<group>"; }; 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; }; 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; }; 4CAAD8AF29888AD200060CEA /* RelayConfigView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RelayConfigView.swift; sourceTree = "<group>"; }; @@ -909,6 +913,7 @@ children = ( 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */, 4CB9D4A82992D2F400A9A7E4 /* FollowsYou.swift */, + 4C9F18E329ABDE6D008C55EC /* MaybeAnonPfpView.swift */, ); path = Profile; sourceTree = "<group>"; @@ -1066,6 +1071,7 @@ isa = PBXGroup; children = ( 4CE879572996C45300F758CC /* ZapsView.swift */, + 4C9F18E129AA9B6C008C55EC /* CustomizeZapView.swift */, ); path = Zaps; sourceTree = "<group>"; @@ -1304,6 +1310,7 @@ 4C363AA228296A7E006E126D /* SearchView.swift in Sources */, 4CC7AAED297F0B9E00430951 /* Highlight.swift in Sources */, 4C285C8A2838B985008A31F1 /* ProfilePictureSelector.swift in Sources */, + 4C9F18E429ABDE6D008C55EC /* MaybeAnonPfpView.swift in Sources */, 4C75EFB92804A2740006080F /* EventView.swift in Sources */, 3AA247FD297E3CFF0090C62D /* RepostsModel.swift in Sources */, F75BA12F29A18EF500E10810 /* BookmarksView.swift in Sources */, @@ -1398,6 +1405,7 @@ 4CE0E2B629A3ED5500DB4CA2 /* InnerTimelineView.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */, 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, + 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */, 4C42812C298C848200DBF26F /* TranslateView.swift in Sources */, diff --git a/damus/Components/ZapButton.swift b/damus/Components/ZapButton.swift @@ -7,6 +7,22 @@ import SwiftUI +enum ZappingEventType { + case failed(ZappingError) + case got_zap_invoice(String) +} + +enum ZappingError { + case fetching_invoice + case bad_lnurl +} + +struct ZappingEvent { + let is_custom: Bool + let type: ZappingEventType + let event: NostrEvent +} + struct ZapButton: View { let damus_state: DamusState let event: NostrEvent @@ -19,61 +35,8 @@ struct ZapButton: View { @State var slider_value: Double = 0.0 @State var slider_visible: Bool = false @State var showing_select_wallet: Bool = false - - func send_zap() { - guard let privkey = damus_state.keypair.privkey else { - return - } - - // Only take the first 10 because reasons - let relays = Array(damus_state.pool.descriptors.prefix(10)) - let target = ZapTarget.note(id: event.id, author: event.pubkey) - // TODO: gather comment? - let content = "" - let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target) - - zapping = true - - Task { - var mpayreq = damus_state.lnurls.lookup(target.pubkey) - if mpayreq == nil { - mpayreq = await fetch_static_payreq(lnurl) - } - - guard let payreq = mpayreq else { - // TODO: show error - DispatchQueue.main.async { - zapping = false - } - return - } - - DispatchQueue.main.async { - damus_state.lnurls.endpoints[target.pubkey] = payreq - } - - let zap_amount = get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000 - guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount) else { - DispatchQueue.main.async { - zapping = false - } - return - } - - DispatchQueue.main.async { - zapping = false - - if should_show_wallet_selector(damus_state.pubkey) { - self.invoice = inv - self.showing_select_wallet = true - } else { - open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv) - } - } - } - - //damus_state.pool.send(.event(zapreq)) - } + @State var showing_zap_customizer: Bool = false + @State var is_charging: Bool = false var zap_img: String { if bar.zapped { @@ -92,6 +55,10 @@ struct ZapButton: View { return Color.orange } + if is_charging { + return Color.yellow + } + if !zapping { return nil } @@ -101,14 +68,24 @@ struct ZapButton: View { var body: some View { HStack(spacing: 4) { - EventActionButton(img: zap_img, col: zap_color) { - if bar.zapped { - //notify(.delete, bar.our_tip) - } else if !zapping { - send_zap() + Image(systemName: zap_img) + .foregroundColor(zap_color == nil ? Color.gray : zap_color!) + .font(.footnote.weight(.medium)) + .onTapGesture { + if bar.zapped { + //notify(.delete, bar.our_tip) + } else if !zapping { + self.showing_zap_customizer = true + //send_zap(damus_state: damus_state, event: event, lnurl: lnurl, is_custom: false) + //self.zapping = true + } } - } - .accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button")) + .onLongPressGesture(minimumDuration: 0, pressing: { is_charing in + self.is_charging = is_charging + }, perform: { + self.showing_zap_customizer = true + }) + .accessibilityLabel(NSLocalizedString("Zap", comment: "Accessibility label for zap button")) if bar.zap_total > 0 { Text(verbatim: format_msats_abbrev(bar.zap_total)) @@ -116,9 +93,37 @@ struct ZapButton: View { .foregroundColor(bar.zapped ? Color.orange : Color.gray) } } + .sheet(isPresented: $showing_zap_customizer) { + CustomizeZapView(state: damus_state, event: event, lnurl: lnurl) + } .sheet(isPresented: $showing_select_wallet, onDismiss: {showing_select_wallet = false}) { SelectWalletView(showingSelectWallet: $showing_select_wallet, our_pubkey: damus_state.pubkey, invoice: invoice) } + .onReceive(handle_notify(.zapping)) { notif in + let zap_ev = notif.object as! ZappingEvent + + guard zap_ev.event.id == self.event.id else { + return + } + + guard !zap_ev.is_custom else { + return + } + + switch zap_ev.type { + case .failed: + break + case .got_zap_invoice(let inv): + if should_show_wallet_selector(damus_state.pubkey) { + self.invoice = inv + self.showing_select_wallet = true + } else { + open_with_wallet(wallet: get_default_wallet(damus_state.pubkey).model, invoice: inv) + } + } + + self.zapping = false + } } } @@ -130,3 +135,55 @@ struct ZapButton_Previews: PreviewProvider { } } + + +func send_zap(damus_state: DamusState, event: NostrEvent, lnurl: String, is_custom: Bool, comment: String?, amount_sats: Int?, zap_type: ZapType) { + guard let privkey = damus_state.keypair.privkey else { + return + } + + // Only take the first 10 because reasons + let relays = Array(damus_state.pool.descriptors.prefix(10)) + let target = ZapTarget.note(id: event.id, author: event.pubkey) + let content = comment ?? "" + let zapreq = make_zap_request_event(pubkey: damus_state.pubkey, privkey: privkey, content: content, relays: relays, target: target, is_anon: zap_type == .anon) + + Task { + var mpayreq = damus_state.lnurls.lookup(target.pubkey) + if mpayreq == nil { + mpayreq = await fetch_static_payreq(lnurl) + } + + guard let payreq = mpayreq else { + // TODO: show error + DispatchQueue.main.async { + let typ = ZappingEventType.failed(.bad_lnurl) + let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event) + notify(.zapping, ev) + } + return + } + + DispatchQueue.main.async { + damus_state.lnurls.endpoints[target.pubkey] = payreq + } + + let zap_amount = amount_sats ?? get_default_zap_amount(pubkey: damus_state.pubkey) ?? 1000 + + guard let inv = await fetch_zap_invoice(payreq, zapreq: zapreq, sats: zap_amount, zap_type: zap_type, comment: comment) else { + DispatchQueue.main.async { + let typ = ZappingEventType.failed(.fetching_invoice) + let ev = ZappingEvent(is_custom: is_custom, type: typ, event: event) + notify(.zapping, ev) + } + return + } + + DispatchQueue.main.async { + let ev = ZappingEvent(is_custom: is_custom, type: .got_zap_invoice(inv), event: event) + notify(.zapping, ev) + } + } + + return +} diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -125,6 +125,8 @@ class HomeModel: ObservableObject { handle_channel_meta(ev) case .zap: handle_zap_event(ev) + case .zap_request: + break } } diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift @@ -141,6 +141,9 @@ struct Profile: Codable { } static func displayName(profile: Profile?, pubkey: String) -> String { + if pubkey == "anon" { + return "Anonymous" + } let pk = bech32_nopre_pubkey(pubkey) ?? pubkey return profile?.name ?? abbrev_pubkey(pk) } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -577,14 +577,25 @@ func zap_target_to_tags(_ target: ZapTarget) -> [[String]] { } } -func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget) -> NostrEvent { +func make_zap_request_event(pubkey: String, privkey: String, content: String, relays: [RelayDescriptor], target: ZapTarget, is_anon: Bool) -> NostrEvent { var tags = zap_target_to_tags(target) var relay_tag = ["relays"] relay_tag.append(contentsOf: relays.map { $0.url.absoluteString }) tags.append(relay_tag) - let ev = NostrEvent(content: content, pubkey: pubkey, kind: 9734, tags: tags) + + var priv = privkey + var pub = pubkey + + if is_anon { + tags.append(["anon"]) + let kp = generate_new_keypair() + pub = kp.pubkey + priv = kp.privkey! + } + + let ev = NostrEvent(content: content, pubkey: pub, kind: 9734, tags: tags) ev.id = calculate_event_id(ev: ev) - ev.sig = sign_event(privkey: privkey, ev: ev) + ev.sig = sign_event(privkey: priv, ev: ev) return ev } diff --git a/damus/Nostr/NostrKind.swift b/damus/Nostr/NostrKind.swift @@ -21,4 +21,5 @@ enum NostrKind: Int { case chat = 42 case list = 30000 case zap = 9735 + case zap_request = 9734 } diff --git a/damus/Util/LNUrlPayRequest.swift b/damus/Util/LNUrlPayRequest.swift @@ -9,8 +9,10 @@ import Foundation struct LNUrlPayRequest: Decodable { let allowsNostr: Bool? + let commentAllowed: Int? let nostrPubkey: String? + let metadata: String? let minSendable: Int64? let maxSendable: Int64? let status: String? diff --git a/damus/Util/Notifications.swift b/damus/Util/Notifications.swift @@ -104,6 +104,9 @@ extension Notification.Name { static var update_bookmarks: Notification.Name { return Notification.Name("update_bookmarks") } + static var zapping: Notification.Name { + return Notification.Name("zapping") + } } func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher { diff --git a/damus/Util/Zap.swift b/damus/Util/Zap.swift @@ -285,7 +285,7 @@ func fetch_static_payreq(_ lnurl: String) async -> LNUrlPayRequest? { return endpoint } -func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int) 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 } @@ -295,12 +295,18 @@ func fetch_zap_invoice(_ payreq: LNUrlPayRequest, zapreq: NostrEvent, sats: Int) var query = [URLQueryItem(name: "amount", value: "\(amount)")] - if zappable { + if zappable && zap_type != .non_zap { if let json = encode_json(zapreq) { print("zapreq json: \(json)") query.append(URLQueryItem(name: "nostr", value: json)) } } + + // add a lud12 comment as well if we have it + if let comment, let limit = payreq.commentAllowed, limit != 0 { + let limited_comment = String(comment.prefix(limit)) + query.append(URLQueryItem(name: "comment", value: limited_comment)) + } base_url.queryItems = query diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift @@ -129,26 +129,14 @@ struct ConfigView: View { } } - Section(NSLocalizedString("Default Zap Amount in sats", comment: "Section title for zap configuration")) { TextField(String("1000"), text: $default_zap_amount) .keyboardType(.numberPad) .onReceive(Just(default_zap_amount)) { newValue in - let filtered = newValue.filter { Set("0123456789").contains($0) } - - if filtered != newValue { - default_zap_amount = filtered - } - - if filtered == "" { - set_default_zap_amount(pubkey: state.pubkey, amount: 1000) - return + + if let parsed = handle_string_amount(new_value: newValue) { + self.default_zap_amount = String(parsed) } - - guard let amt = Int(filtered) else { - return - } - set_default_zap_amount(pubkey: state.pubkey, amount: amt) } } @@ -346,3 +334,18 @@ struct ConfigView_Previews: PreviewProvider { } } } + + +func handle_string_amount(new_value: String) -> Int? { + let filtered = new_value.filter { Set("0123456789").contains($0) } + + if filtered == "" { + return nil + } + + guard let amt = Int(filtered) else { + return nil + } + + return amt +} diff --git a/damus/Views/Events/TextEvent.swift b/damus/Views/Events/TextEvent.swift @@ -18,17 +18,17 @@ struct TextEvent: View { HStack(alignment: .top) { let profile = damus.profiles.lookup(id: pubkey) + let is_anon = event_is_anonymous(ev: event) VStack { - NavigationLink(destination: ProfileView(damus_state: damus, pubkey: pubkey)) { - ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: damus.profiles) - } + MaybeAnonPfpView(state: damus, is_anon: is_anon, pubkey: pubkey) Spacer() } VStack(alignment: .leading) { HStack(alignment: .center) { - EventProfileName(pubkey: pubkey, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal) + let pk = is_anon ? "anon" : pubkey + EventProfileName(pubkey: pk, profile: profile, damus: damus, show_friend_confirmed: true, size: .normal) Text(verbatim: "\(format_relative_time(event.created_at))") .foregroundColor(.gray) @@ -65,3 +65,18 @@ struct TextEvent_Previews: PreviewProvider { TextEvent(damus: test_damus_state(), event: test_event, pubkey: "pk", has_action_bar: true, booster_pubkey: nil) } } + +func event_has_tag(ev: NostrEvent, tag: String) -> Bool { + for t in ev.tags { + if t.count >= 1 && t[0] == tag { + return true + } + } + + return false +} + + +func event_is_anonymous(ev: NostrEvent) -> Bool { + return ev.known_kind == .zap_request && event_has_tag(ev: ev, tag: "anon") +} diff --git a/damus/Views/Profile/MaybeAnonPfpView.swift b/damus/Views/Profile/MaybeAnonPfpView.swift @@ -0,0 +1,46 @@ +// +// MaybeAnonPfpView.swift +// damus +// +// Created by William Casarin on 2023-02-26. +// + +import SwiftUI + +struct MaybeAnonPfpView: View { + let state: DamusState + let is_anon: Bool + let pubkey: String + + init(state: DamusState, event: NostrEvent, pubkey: String) { + self.state = state + self.is_anon = event_is_anonymous(ev: event) + self.pubkey = pubkey + } + + init(state: DamusState, is_anon: Bool, pubkey: String) { + self.state = state + self.is_anon = is_anon + self.pubkey = pubkey + } + + var body: some View { + Group { + if is_anon { + Image(systemName: "person.fill.questionmark") + .font(.largeTitle) + .frame(width: PFP_SIZE, height: PFP_SIZE) + } else { + NavigationLink(destination: ProfileView(damus_state: state, pubkey: pubkey)) { + ProfilePicView(pubkey: pubkey, size: PFP_SIZE, highlight: .none, profiles: state.profiles) + } + } + } + } +} + +struct MaybeAnonPfpView_Previews: PreviewProvider { + static var previews: some View { + MaybeAnonPfpView(state: test_damus_state(), is_anon: true, pubkey: "anon") + } +} diff --git a/damus/Views/Zaps/CustomizeZapView.swift b/damus/Views/Zaps/CustomizeZapView.swift @@ -0,0 +1,215 @@ +// +// CustomizeZapView.swift +// damus +// +// Created by William Casarin on 2023-02-25. +// + +import SwiftUI +import Combine + +enum ZapType { + case pub + case anon + case non_zap +} + +struct ZapAmountItem: Identifiable, Hashable { + let amount: Int + let icon: String + + var id: String { + return icon + } +} + +func get_default_zap_amount_item(_ pubkey: String) -> ZapAmountItem { + let def = get_default_zap_amount(pubkey: pubkey) ?? 1000 + return ZapAmountItem(amount: def, icon: "🤙") +} + +func get_zap_amount_items(pubkey: String) -> [ZapAmountItem] { + let def_item = get_default_zap_amount_item(pubkey) + var entries = [ + ZapAmountItem(amount: 500, icon: "🙂"), + ZapAmountItem(amount: 5000, icon: "💜"), + ZapAmountItem(amount: 10_000, icon: "😍"), + ZapAmountItem(amount: 20_000, icon: "🤩"), + ZapAmountItem(amount: 50_000, icon: "🔥"), + ZapAmountItem(amount: 100_000, icon: "🚀"), + ZapAmountItem(amount: 1_000_000, icon: "🤯"), + ] + entries.append(def_item) + + entries.sort { $0.amount < $1.amount } + return entries +} + +struct CustomizeZapView: View { + let state: DamusState + let event: NostrEvent + let lnurl: String + @State var comment: String + @State var custom_amount: String + @State var custom_amount_sats: Int? + @State var selected_amount: ZapAmountItem + @State var zap_type: ZapType + @State var invoice: String + @State var error: String? + @State var showing_wallet_selector: Bool + @State var zapping: Bool + + let zap_amounts: [ZapAmountItem] + + @Environment(\.dismiss) var dismiss + + init(state: DamusState, event: NostrEvent, lnurl: String) { + self._comment = State(initialValue: "") + self.event = event + self.zap_amounts = get_zap_amount_items(pubkey: state.pubkey) + self._error = State(initialValue: nil) + self._invoice = State(initialValue: "") + self._showing_wallet_selector = State(initialValue: false) + self._custom_amount = State(initialValue: "") + self._zap_type = State(initialValue: .pub) + let selected = get_default_zap_amount_item(state.pubkey) + self._selected_amount = State(initialValue: selected) + self._custom_amount_sats = State(initialValue: nil) + self._zapping = State(initialValue: false) + self.lnurl = lnurl + self.state = state + } + + var ZapTypePicker: some View { + Picker("Zap Type", selection: $zap_type) { + Text("Public").tag(ZapType.pub) + Text("Anonymous").tag(ZapType.anon) + Text("Non-Zap").tag(ZapType.non_zap) + } + .pickerStyle(.segmented) + } + + var AmountPicker: some View { + Picker("Zap Amount", selection: $selected_amount) { + ForEach(zap_amounts) { entry in + let fmt = format_msats_abbrev(Int64(entry.amount) * 1000) + HStack(alignment: .firstTextBaseline) { + Text("\(entry.icon)") + .frame(width: 30) + Text("\(fmt)") + .frame(width: 50) + } + .tag(entry) + } + } + .pickerStyle(.wheel) + } + + func receive_zap(notif: Notification) { + let zap_ev = notif.object as! ZappingEvent + guard zap_ev.is_custom else { + return + } + guard zap_ev.event.id == event.id else { + return + } + + self.zapping = false + + switch zap_ev.type { + case .failed(let err): + switch err { + case .fetching_invoice: + self.error = "Error fetching lightning invoice" + case .bad_lnurl: + self.error = "Invalid lightning address" + } + break + case .got_zap_invoice(let inv): + if should_show_wallet_selector(state.pubkey) { + self.invoice = inv + self.showing_wallet_selector = true + } else { + open_with_wallet(wallet: get_default_wallet(state.pubkey).model, invoice: inv) + self.showing_wallet_selector = false + dismiss() + } + } + + +} + + var body: some View { + MainContent + .sheet(isPresented: $showing_wallet_selector) { + SelectWalletView(showingSelectWallet: $showing_wallet_selector, our_pubkey: state.pubkey, invoice: invoice) + } + .onReceive(handle_notify(.zapping)) { notif in + receive_zap(notif: notif) + } + .ignoresSafeArea() + } + + var MainContent: some View { + VStack(alignment: .leading) { + Form { + Section(content: { + AmountPicker + }, header: { + Text("Zap Amount in sats") + }) + + Section(content: { + TextField("100000", text: $custom_amount) + .keyboardType(.numberPad) + .onReceive(Just(custom_amount)) { newValue in + + if let parsed = handle_string_amount(new_value: newValue) { + self.custom_amount = String(parsed) + self.custom_amount_sats = parsed + } + } + }, header: { + Text("Custom Zap Amount") + }) + .dismissKeyboardOnTap() + + Section(content: { + TextField("Awesome post!", text: $comment) + }, header: { + Text("Comment") + }) + .dismissKeyboardOnTap() + + Section(content: { + ZapTypePicker + }, header: { + Text("Zap Type") + }) + + if zapping { + Text("Zapping...") + } else { + Button("Zap") { + let amount = custom_amount_sats ?? selected_amount.amount + send_zap(damus_state: state, event: event, lnurl: lnurl, is_custom: true, comment: comment, amount_sats: amount, zap_type: zap_type) + self.zapping = true + } + .zIndex(16) + } + + if let error { + Text(error) + .foregroundColor(.red) + } + } + } + } +} + +struct CustomizeZapView_Previews: PreviewProvider { + static var previews: some View { + CustomizeZapView(state: test_damus_state(), event: test_event, lnurl: "") + .frame(width: 400, height: 600) + } +}