EditMetadataView.swift (10484B)
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 PPM_SIZE: CGFloat = 80.0 12 let BANNER_HEIGHT: CGFloat = 150.0; 13 14 func isHttpsUrl(_ string: String) -> Bool { 15 let urlRegEx = "^https://.*$" 16 let urlTest = NSPredicate(format:"SELF MATCHES %@", urlRegEx) 17 return urlTest.evaluate(with: string) 18 } 19 20 func isImage(_ urlString: String) -> Bool { 21 let imageTypes = ["image/jpg", "image/jpeg", "image/png", "image/gif", "image/tiff", "image/bmp", "image/webp"] 22 23 guard let url = URL(string: urlString) else { 24 return false 25 } 26 27 var result = false 28 let semaphore = DispatchSemaphore(value: 0) 29 30 let task = URLSession.shared.dataTask(with: url) { data, response, error in 31 if let error = error { 32 print(error) 33 semaphore.signal() 34 return 35 } 36 37 guard let httpResponse = response as? HTTPURLResponse, 38 let contentType = httpResponse.allHeaderFields["Content-Type"] as? String else { 39 semaphore.signal() 40 return 41 } 42 43 if imageTypes.contains(contentType.lowercased()) { 44 result = true 45 } 46 47 semaphore.signal() 48 } 49 50 task.resume() 51 semaphore.wait() 52 53 return result 54 } 55 56 struct EditMetadataView: View { 57 let damus_state: DamusState 58 @State var display_name: String 59 @State var about: String 60 @State var picture: String 61 @State var banner: String 62 @State var nip05: String 63 @State var name: String 64 @State var ln: String 65 @State var website: String 66 67 @Environment(\.dismiss) var dismiss 68 @Environment(\.colorScheme) var colorScheme 69 70 @State var confirm_ln_address: Bool = false 71 @StateObject var profileUploadViewModel = ProfileUploadingViewModel() 72 73 init (damus_state: DamusState) { 74 self.damus_state = damus_state 75 let data = damus_state.profiles.lookup(id: damus_state.pubkey) 76 77 _name = State(initialValue: data?.name ?? "") 78 _display_name = State(initialValue: data?.display_name ?? "") 79 _about = State(initialValue: data?.about ?? "") 80 _website = State(initialValue: data?.website ?? "") 81 _picture = State(initialValue: data?.picture ?? "") 82 _banner = State(initialValue: data?.banner ?? "") 83 _nip05 = State(initialValue: data?.nip05 ?? "") 84 _ln = State(initialValue: data?.lud16 ?? data?.lud06 ?? "") 85 } 86 87 func imageBorderColor() -> Color { 88 colorScheme == .light ? DamusColors.white : DamusColors.black 89 } 90 91 func save() { 92 let metadata = NostrMetadata( 93 display_name: display_name, 94 name: name, 95 about: about, 96 website: website, 97 nip05: nip05.isEmpty ? nil : nip05, 98 picture: picture.isEmpty ? nil : picture, 99 banner: banner.isEmpty ? nil : banner, 100 lud06: ln.contains("@") ? nil : ln, 101 lud16: ln.contains("@") ? ln : nil 102 ); 103 104 let m_metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: metadata) 105 106 if let metadata_ev = m_metadata_ev { 107 damus_state.postbox.send(metadata_ev) 108 } 109 } 110 111 func is_ln_valid(ln: String) -> Bool { 112 return ln.contains("@") || ln.lowercased().starts(with: "lnurl") 113 } 114 115 var nip05_parts: NIP05? { 116 return NIP05.parse(nip05) 117 } 118 119 var TopSection: some View { 120 ZStack(alignment: .top) { 121 GeometryReader { geo in 122 BannerImageView(pubkey: damus_state.pubkey, profiles: damus_state.profiles) 123 .aspectRatio(contentMode: .fill) 124 .frame(width: geo.size.width, height: BANNER_HEIGHT) 125 .clipped() 126 }.frame(height: BANNER_HEIGHT) 127 VStack(alignment: .leading) { 128 let pfp_size: CGFloat = 90.0 129 130 HStack(alignment: .center) { 131 ProfilePictureSelector(pubkey: damus_state.pubkey, damus_state: damus_state, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:)) 132 .offset(y: -(pfp_size/2.0)) // Increase if set a frame 133 134 Spacer() 135 }.padding(.bottom,-(pfp_size/2.0)) 136 } 137 .padding(.horizontal,18) 138 .padding(.top,BANNER_HEIGHT) 139 } 140 } 141 142 var body: some View { 143 VStack(alignment: .leading) { 144 TopSection 145 Form { 146 Section(NSLocalizedString("Your Name", comment: "Label for Your Name section of user profile form.")) { 147 TextField("Satoshi Nakamoto", text: $display_name) 148 .autocorrectionDisabled(true) 149 .textInputAutocapitalization(.never) 150 } 151 152 Section(NSLocalizedString("Username", comment: "Label for Username section of user profile form.")) { 153 TextField("satoshi", text: $name) 154 .autocorrectionDisabled(true) 155 .textInputAutocapitalization(.never) 156 157 } 158 159 Section (NSLocalizedString("Profile Picture", comment: "Label for Profile Picture section of user profile form.")) { 160 TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $picture) 161 .autocorrectionDisabled(true) 162 .textInputAutocapitalization(.never) 163 } 164 165 Section (NSLocalizedString("Banner Image", comment: "Label for Banner Image section of user profile form.")) { 166 TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $banner) 167 .autocorrectionDisabled(true) 168 .textInputAutocapitalization(.never) 169 } 170 171 Section(NSLocalizedString("Website", comment: "Label for Website section of user profile form.")) { 172 TextField(NSLocalizedString("https://jb55.com", comment: "Placeholder example text for website URL for user profile."), text: $website) 173 .autocorrectionDisabled(true) 174 .textInputAutocapitalization(.never) 175 } 176 177 Section(NSLocalizedString("About Me", comment: "Label for About Me section of user profile form.")) { 178 let placeholder = NSLocalizedString("Absolute Boss", comment: "Placeholder text for About Me description.") 179 ZStack(alignment: .topLeading) { 180 TextEditor(text: $about) 181 .textInputAutocapitalization(.sentences) 182 .frame(minHeight: 20, alignment: .leading) 183 .multilineTextAlignment(.leading) 184 Text(about.isEmpty ? placeholder : about) 185 .padding(.leading, 4) 186 .opacity(about.isEmpty ? 1 : 0) 187 .foregroundColor(Color(uiColor: .placeholderText)) 188 } 189 } 190 191 Section(NSLocalizedString("Bitcoin Lightning Tips", comment: "Label for Bitcoin Lightning Tips section of user profile form.")) { 192 TextField(NSLocalizedString("Lightning Address or LNURL", comment: "Placeholder text for entry of Lightning Address or LNURL."), text: $ln) 193 .autocorrectionDisabled(true) 194 .textInputAutocapitalization(.never) 195 } 196 197 Section(content: { 198 TextField(NSLocalizedString("jb55@jb55.com", comment: "Placeholder example text for identifier used for NIP-05 verification."), text: $nip05) 199 .autocorrectionDisabled(true) 200 .textInputAutocapitalization(.never) 201 .onReceive(Just(nip05)) { newValue in 202 self.nip05 = newValue.trimmingCharacters(in: .whitespaces) 203 } 204 }, header: { 205 Text("NIP-05 Verification", comment: "Label for NIP-05 Verification section of user profile form.") 206 }, footer: { 207 if let parts = nip05_parts { 208 Text("'\(parts.username)' at '\(parts.host)' will be used for verification", comment: "Description of how the nip05 identifier would be used for verification.") 209 } else if !nip05.isEmpty { 210 Text("'\(nip05)' is an invalid NIP-05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.") 211 } else { 212 Text("") // without this, the keyboard dismisses unnecessarily when the footer changes state 213 } 214 }) 215 216 Button(NSLocalizedString("Save", comment: "Button for saving profile.")) { 217 if !ln.isEmpty && !is_ln_valid(ln: ln) { 218 confirm_ln_address = true 219 } else { 220 save() 221 dismiss() 222 } 223 } 224 .disabled(profileUploadViewModel.isLoading) 225 .alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) { 226 Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) { 227 } 228 } message: { 229 Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.") 230 } 231 } 232 } 233 .ignoresSafeArea(edges: .top) 234 .background(Color(.systemGroupedBackground)) 235 } 236 237 func uploadedProfilePicture(image_url: URL?) { 238 picture = image_url?.absoluteString ?? "" 239 } 240 } 241 242 struct EditMetadataView_Previews: PreviewProvider { 243 static var previews: some View { 244 EditMetadataView(damus_state: test_damus_state()) 245 } 246 }