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 }