damus

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

EditMetadataView.swift (10141B)


      1 //
      2 //  EditMetadataView.swift
      3 //  damus
      4 //
      5 //  Created by Thomas Tastet on 23/12/2022.
      6 //
      7 
      8 import SwiftUI
      9 import Combine
     10 
     11 let BANNER_HEIGHT: CGFloat = 150.0;
     12 
     13 struct EditMetadataView: View {
     14     let damus_state: DamusState
     15     @State var display_name: String
     16     @State var about: String
     17     @State var picture: String
     18     @State var banner: String
     19     @State var nip05: String
     20     @State var name: String
     21     @State var ln: String
     22     @State var website: String
     23 
     24     @Environment(\.dismiss) var dismiss
     25 
     26     @State var confirm_ln_address: Bool = false
     27     
     28     @StateObject var profileUploadObserver = ImageUploadingObserver()
     29     @StateObject var bannerUploadObserver = ImageUploadingObserver()
     30     
     31     init(damus_state: DamusState) {
     32         self.damus_state = damus_state
     33         let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
     34         let data = profile_txn?.unsafeUnownedValue
     35 
     36         _name = State(initialValue: data?.name ?? "")
     37         _display_name = State(initialValue: data?.display_name ?? "")
     38         _about = State(initialValue: data?.about ?? "")
     39         _website = State(initialValue: data?.website ?? "")
     40         _picture = State(initialValue: data?.picture ?? "")
     41         _banner = State(initialValue: data?.banner ?? "")
     42         _nip05 = State(initialValue: data?.nip05 ?? "")
     43         _ln = State(initialValue: data?.lud16 ?? data?.lud06 ?? "")
     44     }
     45     
     46     func to_profile() -> Profile {
     47         let new_nip05 = nip05.isEmpty ? nil : nip05
     48         let new_picture = picture.isEmpty ? nil : picture
     49         let new_banner = banner.isEmpty ? nil : banner
     50         let new_lud06 = ln.contains("@") ? nil : ln
     51         let new_lud16 = ln.contains("@") ? ln : nil
     52 
     53         let profile = Profile(name: name, display_name: display_name, about: about, picture: new_picture, banner: new_banner, website: website, lud06: new_lud06, lud16: new_lud16, nip05: new_nip05, damus_donation: nil)
     54 
     55         return profile
     56     }
     57     
     58     func save() {
     59         let profile = to_profile()
     60         guard let keypair = damus_state.keypair.to_full(),
     61               let metadata_ev = make_metadata_event(keypair: keypair, metadata: profile)
     62         else {
     63             return
     64         }
     65 
     66         damus_state.postbox.send(metadata_ev)
     67     }
     68 
     69     func is_ln_valid(ln: String) -> Bool {
     70         return ln.contains("@") || ln.lowercased().starts(with: "lnurl")
     71     }
     72     
     73     var nip05_parts: NIP05? {
     74         return NIP05.parse(nip05)
     75     }
     76     
     77     var TopSection: some View {
     78         ZStack(alignment: .top) {
     79             GeometryReader { geo in
     80                 EditBannerImageView(damus_state: damus_state, viewModel: bannerUploadObserver, callback: uploadedBanner(image_url:))
     81                     .aspectRatio(contentMode: .fill)
     82                     .frame(width: geo.size.width, height: BANNER_HEIGHT)
     83                     .clipped()
     84             }.frame(height: BANNER_HEIGHT)
     85             VStack(alignment: .leading) {
     86                 let pfp_size: CGFloat = 90.0
     87 
     88                 HStack(alignment: .center) {
     89                     EditProfilePictureView(pubkey: damus_state.pubkey, damus_state: damus_state, size: pfp_size, uploadObserver: profileUploadObserver, callback: uploadedProfilePicture(image_url:))
     90                         .offset(y: -(pfp_size/2.0)) // Increase if set a frame
     91 
     92                    Spacer()
     93                 }.padding(.bottom,-(pfp_size/2.0))
     94             }
     95             .padding(.horizontal,18)
     96             .padding(.top,BANNER_HEIGHT)
     97         }
     98     }
     99     
    100     var body: some View {
    101         VStack(alignment: .leading) {
    102             TopSection
    103             Form {
    104                 Section(NSLocalizedString("Your Name", comment: "Label for Your Name section of user profile form.")) {
    105                     let display_name_placeholder = "Satoshi Nakamoto"
    106                     TextField(display_name_placeholder, text: $display_name)
    107                         .autocorrectionDisabled(true)
    108                         .textInputAutocapitalization(.never)
    109                 }
    110                 
    111                 Section(NSLocalizedString("Username", comment: "Label for Username section of user profile form.")) {
    112                     let username_placeholder = "satoshi"
    113                     TextField(username_placeholder, text: $name)
    114                         .autocorrectionDisabled(true)
    115                         .textInputAutocapitalization(.never)
    116 
    117                 }
    118                 
    119                 Section (NSLocalizedString("Profile Picture", comment: "Label for Profile Picture section of user profile form.")) {
    120                     TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $picture)
    121                         .autocorrectionDisabled(true)
    122                         .textInputAutocapitalization(.never)
    123                 }
    124                 
    125                 Section (NSLocalizedString("Banner Image", comment: "Label for Banner Image section of user profile form.")) {
    126                                     TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $banner)
    127                                         .autocorrectionDisabled(true)
    128                                         .textInputAutocapitalization(.never)
    129                                 }
    130                 
    131                 Section(NSLocalizedString("Website", comment: "Label for Website section of user profile form.")) {
    132                     TextField(NSLocalizedString("https://jb55.com", comment: "Placeholder example text for website URL for user profile."), text: $website)
    133                         .autocorrectionDisabled(true)
    134                         .textInputAutocapitalization(.never)
    135                 }
    136                 
    137                 Section(NSLocalizedString("About Me", comment: "Label for About Me section of user profile form.")) {
    138                     let placeholder = NSLocalizedString("Absolute Boss", comment: "Placeholder text for About Me description.")
    139                     ZStack(alignment: .topLeading) {
    140                         TextEditor(text: $about)
    141                             .textInputAutocapitalization(.sentences)
    142                             .frame(minHeight: 20, alignment: .leading)
    143                             .multilineTextAlignment(.leading)
    144                         Text(about.isEmpty ? placeholder : about)
    145                             .padding(.leading, 4)
    146                             .opacity(about.isEmpty ? 1 : 0)
    147                             .foregroundColor(Color(uiColor: .placeholderText))
    148                     }
    149                 }
    150                 
    151                 Section(NSLocalizedString("Bitcoin Lightning Tips", comment: "Label for Bitcoin Lightning Tips section of user profile form.")) {
    152                     TextField(NSLocalizedString("Lightning Address or LNURL", comment: "Placeholder text for entry of Lightning Address or LNURL."), text: $ln)
    153                         .autocorrectionDisabled(true)
    154                         .textInputAutocapitalization(.never)
    155                 }
    156                                 
    157                 Section(content: {
    158                     TextField(NSLocalizedString("jb55@jb55.com", comment: "Placeholder example text for identifier used for Nostr addresses."), text: $nip05)
    159                         .autocorrectionDisabled(true)
    160                         .textInputAutocapitalization(.never)
    161                         .onReceive(Just(nip05)) { newValue in
    162                             self.nip05 = newValue.trimmingCharacters(in: .whitespaces)
    163                         }
    164                 }, header: {
    165                     Text("Nostr Address", comment: "Label for the Nostr Address section of user profile form.")
    166                 }, footer: {
    167                     switch validate_nostr_address(nip05: nip05_parts, nip05_str: nip05) {
    168                     case .empty:
    169                         // without this, the keyboard dismisses unnecessarily when the footer changes state
    170                         Text("")
    171                     case .valid:
    172                         Text("")
    173                     case .invalid:
    174                         Text("'\(nip05)' is an invalid Nostr address. It should look like an email address.", comment: "Description of why the Nostr address is invalid.")
    175                     }
    176                 })
    177 
    178                 Button(NSLocalizedString("Save", comment: "Button for saving profile.")) {
    179                     if !ln.isEmpty && !is_ln_valid(ln: ln) {
    180                         confirm_ln_address = true
    181                     } else {
    182                         save()
    183                         dismiss()
    184                     }
    185                 }
    186                 .disabled(profileUploadObserver.isLoading || bannerUploadObserver.isLoading)
    187                 .alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) {
    188                     Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) {
    189                     }
    190                 } message: {
    191                     Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.")
    192                 }
    193             }
    194         }
    195         .ignoresSafeArea(edges: .top)
    196         .background(Color(.systemGroupedBackground))
    197     }
    198     
    199     func uploadedProfilePicture(image_url: URL?) {
    200         picture = image_url?.absoluteString ?? ""
    201     }
    202     
    203     func uploadedBanner(image_url: URL?) {
    204         banner = image_url?.absoluteString ?? ""
    205     }
    206 }
    207 
    208 struct EditMetadataView_Previews: PreviewProvider {
    209     static var previews: some View {
    210         EditMetadataView(damus_state: test_damus_state)
    211     }
    212 }
    213 
    214 enum NIP05ValidationResult {
    215     case empty
    216     case invalid
    217     case valid
    218 }
    219 
    220 func validate_nostr_address(nip05: NIP05?, nip05_str: String) -> NIP05ValidationResult {
    221     guard nip05 != nil else {
    222         // couldn't parse
    223         if nip05_str.isEmpty {
    224             return .empty
    225         } else {
    226             return .invalid
    227         }
    228     }
    229 
    230     // could parse so we valid.
    231     return .valid
    232 }