Files
damus/damus/Views/Profile/EditMetadataView.swift
William Casarin bfad2ab42d ndb/txn: make transactions failable
Since there may be situations where we close and re-open the database,
we need to make sure transactions fail when the database is not open.

Make NdbTxn an init?() constructor and check for ndb.closed. If it's
closed, then fail transaction construction.

This fixes crashes during high database activity when switching from
background to foreground and vice-versa.

Fixes: da2bdad18d ("nostrdb: close database when backgrounded")
2024-01-10 14:27:02 -08:00

233 lines
9.9 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// EditMetadataView.swift
// damus
//
// Created by Thomas Tastet on 23/12/2022.
//
import SwiftUI
import Combine
let BANNER_HEIGHT: CGFloat = 150.0;
struct EditMetadataView: View {
let damus_state: DamusState
@State var display_name: String
@State var about: String
@State var picture: String
@State var banner: String
@State var nip05: String
@State var name: String
@State var ln: String
@State var website: String
@Environment(\.dismiss) var dismiss
@State var confirm_ln_address: Bool = false
@StateObject var profileUploadObserver = ImageUploadingObserver()
@StateObject var bannerUploadObserver = ImageUploadingObserver()
init(damus_state: DamusState) {
self.damus_state = damus_state
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
let data = profile_txn?.unsafeUnownedValue
_name = State(initialValue: data?.name ?? "")
_display_name = State(initialValue: data?.display_name ?? "")
_about = State(initialValue: data?.about ?? "")
_website = State(initialValue: data?.website ?? "")
_picture = State(initialValue: data?.picture ?? "")
_banner = State(initialValue: data?.banner ?? "")
_nip05 = State(initialValue: data?.nip05 ?? "")
_ln = State(initialValue: data?.lud16 ?? data?.lud06 ?? "")
}
func to_profile() -> Profile {
let new_nip05 = nip05.isEmpty ? nil : nip05
let new_picture = picture.isEmpty ? nil : picture
let new_banner = banner.isEmpty ? nil : banner
let new_lud06 = ln.contains("@") ? nil : ln
let new_lud16 = ln.contains("@") ? ln : nil
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)
return profile
}
func save() {
let profile = to_profile()
guard let keypair = damus_state.keypair.to_full(),
let metadata_ev = make_metadata_event(keypair: keypair, metadata: profile)
else {
return
}
damus_state.postbox.send(metadata_ev)
}
func is_ln_valid(ln: String) -> Bool {
return ln.contains("@") || ln.lowercased().starts(with: "lnurl")
}
var nip05_parts: NIP05? {
return NIP05.parse(nip05)
}
var TopSection: some View {
ZStack(alignment: .top) {
GeometryReader { geo in
EditBannerImageView(damus_state: damus_state, viewModel: bannerUploadObserver, callback: uploadedBanner(image_url:))
.aspectRatio(contentMode: .fill)
.frame(width: geo.size.width, height: BANNER_HEIGHT)
.clipped()
}.frame(height: BANNER_HEIGHT)
VStack(alignment: .leading) {
let pfp_size: CGFloat = 90.0
HStack(alignment: .center) {
EditProfilePictureView(pubkey: damus_state.pubkey, damus_state: damus_state, size: pfp_size, uploadObserver: profileUploadObserver, callback: uploadedProfilePicture(image_url:))
.offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer()
}.padding(.bottom,-(pfp_size/2.0))
}
.padding(.horizontal,18)
.padding(.top,BANNER_HEIGHT)
}
}
var body: some View {
VStack(alignment: .leading) {
TopSection
Form {
Section(NSLocalizedString("Your Name", comment: "Label for Your Name section of user profile form.")) {
let display_name_placeholder = "Satoshi Nakamoto"
TextField(display_name_placeholder, text: $display_name)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
}
Section(NSLocalizedString("Username", comment: "Label for Username section of user profile form.")) {
let username_placeholder = "satoshi"
TextField(username_placeholder, text: $name)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
}
Section (NSLocalizedString("Profile Picture", comment: "Label for Profile Picture section of user profile form.")) {
TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $picture)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
}
Section (NSLocalizedString("Banner Image", comment: "Label for Banner Image section of user profile form.")) {
TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $banner)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
}
Section(NSLocalizedString("Website", comment: "Label for Website section of user profile form.")) {
TextField(NSLocalizedString("https://jb55.com", comment: "Placeholder example text for website URL for user profile."), text: $website)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
}
Section(NSLocalizedString("About Me", comment: "Label for About Me section of user profile form.")) {
let placeholder = NSLocalizedString("Absolute Boss", comment: "Placeholder text for About Me description.")
ZStack(alignment: .topLeading) {
TextEditor(text: $about)
.textInputAutocapitalization(.sentences)
.frame(minHeight: 20, alignment: .leading)
.multilineTextAlignment(.leading)
Text(about.isEmpty ? placeholder : about)
.padding(.leading, 4)
.opacity(about.isEmpty ? 1 : 0)
.foregroundColor(Color(uiColor: .placeholderText))
}
}
Section(NSLocalizedString("Bitcoin Lightning Tips", comment: "Label for Bitcoin Lightning Tips section of user profile form.")) {
TextField(NSLocalizedString("Lightning Address or LNURL", comment: "Placeholder text for entry of Lightning Address or LNURL."), text: $ln)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
}
Section(content: {
TextField(NSLocalizedString("jb55@jb55.com", comment: "Placeholder example text for identifier used for Nostr addresses."), text: $nip05)
.autocorrectionDisabled(true)
.textInputAutocapitalization(.never)
.onReceive(Just(nip05)) { newValue in
self.nip05 = newValue.trimmingCharacters(in: .whitespaces)
}
}, header: {
Text("Nostr Address", comment: "Label for the Nostr Address section of user profile form.")
}, footer: {
switch validate_nostr_address(nip05: nip05_parts, nip05_str: nip05) {
case .empty:
// without this, the keyboard dismisses unnecessarily when the footer changes state
Text("")
case .valid:
Text("")
case .invalid:
Text("'\(nip05)' is an invalid Nostr address. It should look like an email address.", comment: "Description of why the Nostr address is invalid.")
}
})
Button(NSLocalizedString("Save", comment: "Button for saving profile.")) {
if !ln.isEmpty && !is_ln_valid(ln: ln) {
confirm_ln_address = true
} else {
save()
dismiss()
}
}
.disabled(profileUploadObserver.isLoading || bannerUploadObserver.isLoading)
.alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) {
Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) {
}
} message: {
Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.")
}
}
}
.ignoresSafeArea(edges: .top)
.background(Color(.systemGroupedBackground))
}
func uploadedProfilePicture(image_url: URL?) {
picture = image_url?.absoluteString ?? ""
}
func uploadedBanner(image_url: URL?) {
banner = image_url?.absoluteString ?? ""
}
}
struct EditMetadataView_Previews: PreviewProvider {
static var previews: some View {
EditMetadataView(damus_state: test_damus_state)
}
}
enum NIP05ValidationResult {
case empty
case invalid
case valid
}
func validate_nostr_address(nip05: NIP05?, nip05_str: String) -> NIP05ValidationResult {
guard nip05 != nil else {
// couldn't parse
if nip05_str.isEmpty {
return .empty
} else {
return .invalid
}
}
// could parse so we valid.
return .valid
}