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 }