damus

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

CustomizeZapView.swift (10529B)


      1 //
      2 //  CustomizeZapView.swift
      3 //  damus
      4 //
      5 //  Created by William Casarin on 2023-02-25.
      6 //
      7 
      8 import SwiftUI
      9 import Combine
     10 
     11 struct ZapAmountItem: Identifiable, Hashable {
     12     let amount: Int
     13     let icon: String
     14     
     15     var id: String {
     16         return icon
     17     }
     18 }
     19 
     20 func get_default_zap_amount_item(_ def: Int) -> ZapAmountItem {
     21     return ZapAmountItem(amount: def, icon: "🤙")
     22 }
     23 
     24 func get_zap_amount_items(_ default_zap_amt: Int) -> [ZapAmountItem] {
     25     let def_item = get_default_zap_amount_item(default_zap_amt)
     26     var entries = [
     27         ZapAmountItem(amount: 69, icon: "😘"),
     28         ZapAmountItem(amount: 420, icon: "🌿"),
     29         ZapAmountItem(amount: 5000, icon: "💜"),
     30         ZapAmountItem(amount: 10_000, icon: "😍"),
     31         ZapAmountItem(amount: 20_000, icon: "🤩"),
     32         ZapAmountItem(amount: 50_000, icon: "🔥"),
     33         ZapAmountItem(amount: 100_000, icon: "🚀"),
     34         ZapAmountItem(amount: 1_000_000, icon: "🤯"),
     35     ]
     36     entries.append(def_item)
     37     
     38     entries.sort { $0.amount < $1.amount }
     39     return entries
     40 }
     41 
     42 enum ZapFields{
     43     case amount
     44     case comment
     45 }
     46 
     47 struct CustomizeZapView: View {
     48     let state: DamusState
     49     let target: ZapTarget
     50     let lnurl: String
     51     
     52     let zap_amounts: [ZapAmountItem]
     53     
     54     @FocusState var focusedTextField : ZapFields?
     55     
     56     @StateObject var model: CustomizeZapModel = CustomizeZapModel()
     57     @Environment(\.dismiss) var dismiss
     58     @Environment(\.colorScheme) var colorScheme
     59     
     60     func fillColor() -> Color {
     61         colorScheme == .light ? DamusColors.white : DamusColors.black
     62     }
     63     
     64     func fontColor() -> Color {
     65         colorScheme == .light ? DamusColors.black : DamusColors.white
     66     }
     67     
     68     init(state: DamusState, target: ZapTarget, lnurl: String) {
     69         self.target = target
     70         self.zap_amounts = get_zap_amount_items(state.settings.default_zap_amount)
     71         self.lnurl = lnurl
     72         self.state = state
     73     }
     74     
     75     func amount_parts(_ n: Int) -> [ZapAmountItem] {
     76         var i: Int = -1
     77         let start = n * 4
     78         let end = start + 4
     79         
     80         return zap_amounts.filter { _ in
     81             i += 1
     82             return i >= start && i < end
     83         }
     84     }
     85     
     86     func AmountsPart(n: Int) -> some View {
     87         HStack(alignment: .center, spacing: 15) {
     88             ForEach(amount_parts(n)) { entry in
     89                 ZapAmountButton(zapAmountItem: entry, action: {
     90                     model.custom_amount_sats = entry.amount
     91                     model.custom_amount = String(entry.amount)
     92                 })
     93             }
     94         }
     95     }
     96     
     97     var AmountPicker: some View {
     98         VStack {
     99             AmountsPart(n: 0)
    100             
    101             AmountsPart(n: 1)
    102         }
    103         .padding(10)
    104     }
    105     
    106     func ZapAmountButton(zapAmountItem: ZapAmountItem, action: @escaping () -> ()) -> some View {
    107         Button(action: action) {
    108             let fmt = format_msats_abbrev(Int64(zapAmountItem.amount) * 1000)
    109             Text(verbatim: "\(zapAmountItem.icon)\n\(fmt)")
    110                 .contentShape(Rectangle())
    111                 .font(.headline)
    112                 .frame(width: 70, height: 70)
    113                 .foregroundColor(fontColor())
    114                 .background(model.custom_amount_sats == zapAmountItem.amount ? fillColor() : DamusColors.adaptableGrey)
    115                 .cornerRadius(15)
    116                 .overlay(RoundedRectangle(cornerRadius: 15)
    117                     .stroke(DamusColors.purple.opacity(model.custom_amount_sats == zapAmountItem.amount ? 1.0 : 0.0), lineWidth: 2))
    118         }
    119     }
    120     
    121     var CustomZapTextField: some View {
    122         VStack(alignment: .center, spacing: 0) {
    123             TextField("", text: $model.custom_amount)
    124                 .focused($focusedTextField, equals: ZapFields.amount)
    125                 .task {
    126                     self.focusedTextField = .amount
    127                 }
    128                 .font(.system(size: 72, weight: .heavy))
    129                 .minimumScaleFactor(0.01)
    130                 .keyboardType(.numberPad)
    131                 .multilineTextAlignment(.center)
    132                 .onChange(of: model.custom_amount) { newValue in
    133                     if let parsed = handle_string_amount(new_value: newValue) {
    134                         model.custom_amount = parsed.formatted()
    135                         model.custom_amount_sats = parsed
    136                     } else {
    137                         model.custom_amount = "0"
    138                         model.custom_amount_sats = nil
    139                     }
    140                 }
    141             let noun = pluralizedString(key: "sats", count: model.custom_amount_sats ?? 0)
    142             Text(noun)
    143                 .font(.system(size: 18, weight: .heavy))
    144         }
    145     }
    146     
    147     var ZapReply: some View {
    148         HStack {
    149             TextField(NSLocalizedString("Send a message with your zap...", comment: "Placeholder text for a comment to send as part of a zap to the user."), text: $model.comment, axis: .vertical)
    150                 .focused($focusedTextField, equals: ZapFields.comment)
    151                 .task {
    152                             self.focusedTextField = .comment
    153                 }
    154                 .autocorrectionDisabled(true)
    155                 .textInputAutocapitalization(.never)
    156                 .lineLimit(5)
    157         }
    158         .frame(minHeight: 30)
    159         .padding(10)
    160         .background(.secondary.opacity(0.2))
    161         .cornerRadius(10)
    162         .padding(.horizontal, 10)
    163     }
    164     
    165     var ZapButton: some View {
    166         VStack {
    167             if model.zapping {
    168                 Text("Zapping...", comment: "Text to indicate that the app is in the process of sending a zap.")
    169             } else {
    170                 Button(action: {
    171                     let amount = model.custom_amount_sats
    172                     send_zap(damus_state: state, target: target, lnurl: lnurl, is_custom: true, comment: model.comment, amount_sats: amount, zap_type: model.zap_type)
    173                     model.zapping = true
    174                 }) {
    175                     HStack {
    176                         Text(NSLocalizedString("Zap User", comment: "Button to send a zap."))
    177                             .font(.system(size: 20, weight: .bold))
    178                     }
    179                     .frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
    180                 }
    181                 .buttonStyle(GradientButtonStyle())
    182                 .disabled(model.custom_amount_sats == 0 || model.custom_amount == "0")
    183                 .opacity(model.custom_amount_sats == 0 || model.custom_amount == "0" ? 0.5 : 1.0)
    184                 .padding(10)
    185             }
    186             
    187             if let error = model.error {
    188                 Text(error)
    189                     .foregroundColor(.red)
    190             }
    191         }
    192     }
    193     
    194     func receive_zap(zap_ev: ZappingEvent) {
    195         guard zap_ev.is_custom, zap_ev.target.id == target.id else {
    196             return
    197         }
    198         
    199         model.zapping = false
    200         
    201         switch zap_ev.type {
    202         case .failed(let err):
    203             model.error = err.humanReadableMessage()
    204             break
    205         case .got_zap_invoice(let inv):
    206             if state.settings.show_wallet_selector {
    207                 model.invoice = inv
    208                 present_sheet(.select_wallet(invoice: inv))
    209             } else {
    210                 end_editing()
    211                 let wallet = state.settings.default_wallet.model
    212                 do {
    213                     try open_with_wallet(wallet: wallet, invoice: inv)
    214                     dismiss()
    215                 }
    216                 catch {
    217                     present_sheet(.select_wallet(invoice: inv))
    218                 }
    219             }
    220         case .sent_from_nwc:
    221             dismiss()
    222         }
    223     }
    224     
    225     var body: some View {
    226         VStack(alignment: .center, spacing: 20) {
    227             ScrollView {
    228                 HStack(alignment: .center) {
    229                     UserView(damus_state: state, pubkey: target.pubkey)
    230                     
    231                     ZapTypeButton()
    232                 }
    233                 .padding([.horizontal, .top])
    234 
    235                 CustomZapTextField
    236                 
    237                 AmountPicker
    238                 
    239                 ZapReply
    240                 
    241                 ZapButton
    242                 
    243                 Spacer()
    244             }
    245         }
    246         .sheet(isPresented: $model.show_zap_types) {
    247             if #available(iOS 16.0, *) {
    248                 ZapPicker
    249                     .presentationDetents([.medium])
    250                     .presentationDragIndicator(.visible)
    251             } else {
    252                 ZapPicker
    253             }
    254         }
    255         .onAppear {
    256             model.set_defaults(settings: state.settings)
    257         }
    258         .onReceive(handle_notify(.zapping)) { zap_ev in
    259             receive_zap(zap_ev: zap_ev)
    260         }
    261         .background(fillColor().edgesIgnoringSafeArea(.all))
    262         .onTapGesture {
    263             hideKeyboard()
    264         }
    265     }
    266     
    267     func ZapTypeButton() -> some View {
    268         Button(action: {
    269             model.show_zap_types = true
    270         }) {
    271             switch model.zap_type {
    272             case .pub:
    273                 Image("globe")
    274                 Text("Public", comment: "Button text to indicate that the zap type is a public zap.")
    275             case .anon:
    276                 Image("question")
    277                 Text("Anonymous", comment: "Button text to indicate that the zap type is a anonymous zap.")
    278             case .priv:
    279                 Image("lock")
    280                 Text("Private", comment: "Button text to indicate that the zap type is a private zap.")
    281             case .non_zap:
    282                 Image("zap")
    283                 Text("None", comment: "Button text to indicate that the zap type is a private zap.")
    284             }
    285         }
    286         .font(.headline)
    287         .foregroundColor(fontColor())
    288         .padding(EdgeInsets(top: 5, leading: 15, bottom: 5, trailing: 15))
    289         .background(DamusColors.adaptableGrey)
    290         .cornerRadius(15)
    291     }
    292 
    293     var ZapPicker: some View {
    294         ZapTypePicker(zap_type: $model.zap_type, settings: state.settings, profiles: state.profiles, pubkey: target.pubkey)
    295     }
    296 }
    297 
    298 extension View {
    299     func hideKeyboard() {
    300         let resign = #selector(UIResponder.resignFirstResponder)
    301         UIApplication.shared.sendAction(resign, to: nil, from: nil, for: nil)
    302     }
    303 }
    304 
    305 struct CustomizeZapView_Previews: PreviewProvider {
    306     static var previews: some View {
    307         CustomizeZapView(state: test_damus_state, target: ZapTarget.note(id: test_note.id, author: test_note.pubkey), lnurl: "")
    308             .frame(width: 400, height: 600)
    309     }
    310 }