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")
233 lines
9.9 KiB
Swift
233 lines
9.9 KiB
Swift
//
|
||
// 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
|
||
}
|