ux: Profile Edit Improvements (#2376)

This PR adds improvements to the profile edit view.  The banner image is
changed from the old ostrich image to the fresh new damoose. The image and
banner url text entries have been removed from the edit form and now live under
the image selector menu. Selecting the Image URL menu option presents a sheet
where a user can update the image URL. There are now safe guards in place for
users who update their profile, if they make any changes and try to navigate
back to home they will get an alert asking if they want to discard changes. The
Save button is also more prominent.

Changelog-Changed: Changed the default banner from ostriches to damoose
Changelog-Added: Added profile edit safe guards
Changelog-Changed: Changed image and banner url text fields to new sheet view

Signed-off-by: ericholguin <ericholguin@apache.org>
This commit is contained in:
Eric Holguin
2024-08-12 12:54:32 -06:00
committed by Daniel D’Aquino
parent f0b5162205
commit abfe0f642f
5 changed files with 194 additions and 34 deletions

View File

@@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "damoose.jpeg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

View File

@@ -13,7 +13,7 @@ struct EditBannerImageView: View {
var damus_state: DamusState var damus_state: DamusState
@ObservedObject var viewModel: ImageUploadingObserver @ObservedObject var viewModel: ImageUploadingObserver
let callback: (URL?) -> Void let callback: (URL?) -> Void
let defaultImage = UIImage(named: "profile-banner") ?? UIImage() let defaultImage = UIImage(named: "damoose") ?? UIImage()
@State var banner_image: URL? = nil @State var banner_image: URL? = nil
@@ -38,7 +38,7 @@ struct EditBannerImageView: View {
struct InnerBannerImageView: View { struct InnerBannerImageView: View {
let disable_animation: Bool let disable_animation: Bool
let url: URL? let url: URL?
let defaultImage = UIImage(named: "profile-banner") ?? UIImage() let defaultImage = UIImage(named: "damoose") ?? UIImage()
var body: some View { var body: some View {
ZStack { ZStack {

View File

@@ -21,13 +21,15 @@ struct EditMetadataView: View {
@State var ln: String @State var ln: String
@State var website: String @State var website: String
@Environment(\.dismiss) var dismiss
@State var confirm_ln_address: Bool = false @State var confirm_ln_address: Bool = false
@State var confirm_save_alert: Bool = false
@StateObject var profileUploadObserver = ImageUploadingObserver() @StateObject var profileUploadObserver = ImageUploadingObserver()
@StateObject var bannerUploadObserver = ImageUploadingObserver() @StateObject var bannerUploadObserver = ImageUploadingObserver()
@Environment(\.dismiss) var dismiss
@Environment(\.presentationMode) var presentationMode
init(damus_state: DamusState) { init(damus_state: DamusState) {
self.damus_state = damus_state self.damus_state = damus_state
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey) let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
@@ -77,7 +79,7 @@ struct EditMetadataView: View {
var TopSection: some View { var TopSection: some View {
ZStack(alignment: .top) { ZStack(alignment: .top) {
GeometryReader { geo in GeometryReader { geo in
EditBannerImageView(damus_state: damus_state, viewModel: bannerUploadObserver, callback: uploadedBanner(image_url:)) EditBannerImageView(damus_state: damus_state, viewModel: bannerUploadObserver, callback: uploadedBanner(image_url:), banner_image: URL(string: banner))
.aspectRatio(contentMode: .fill) .aspectRatio(contentMode: .fill)
.frame(width: geo.size.width, height: BANNER_HEIGHT) .frame(width: geo.size.width, height: BANNER_HEIGHT)
.clipped() .clipped()
@@ -86,7 +88,7 @@ struct EditMetadataView: View {
let pfp_size: CGFloat = 90.0 let pfp_size: CGFloat = 90.0
HStack(alignment: .center) { HStack(alignment: .center) {
EditProfilePictureView(pubkey: damus_state.pubkey, damus_state: damus_state, size: pfp_size, uploadObserver: profileUploadObserver, callback: uploadedProfilePicture(image_url:)) EditProfilePictureView(profile_url: URL(string: picture), 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 .offset(y: -(pfp_size/2.0)) // Increase if set a frame
Spacer() Spacer()
@@ -97,6 +99,28 @@ struct EditMetadataView: View {
} }
} }
func navImage(img: String) -> some View {
Image(img)
.frame(width: 33, height: 33)
.background(Color.black.opacity(0.6))
.clipShape(Circle())
}
var navBackButton: some View {
HStack {
Button {
if didChange() {
confirm_save_alert.toggle()
} else {
presentationMode.wrappedValue.dismiss()
}
} label: {
navImage(img: "chevron-left")
}
Spacer()
}
}
var body: some View { var body: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
TopSection TopSection
@@ -116,18 +140,6 @@ struct EditMetadataView: View {
} }
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.")) { 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) TextField(NSLocalizedString("https://jb55.com", comment: "Placeholder example text for website URL for user profile."), text: $website)
.autocorrectionDisabled(true) .autocorrectionDisabled(true)
@@ -139,10 +151,10 @@ struct EditMetadataView: View {
ZStack(alignment: .topLeading) { ZStack(alignment: .topLeading) {
TextEditor(text: $about) TextEditor(text: $about)
.textInputAutocapitalization(.sentences) .textInputAutocapitalization(.sentences)
.frame(minHeight: 20, alignment: .leading) .frame(minHeight: 45, alignment: .leading)
.multilineTextAlignment(.leading) .multilineTextAlignment(.leading)
Text(about.isEmpty ? placeholder : about) Text(about.isEmpty ? placeholder : about)
.padding(.leading, 4) .padding(4)
.opacity(about.isEmpty ? 1 : 0) .opacity(about.isEmpty ? 1 : 0)
.foregroundColor(Color(uiColor: .placeholderText)) .foregroundColor(Color(uiColor: .placeholderText))
} }
@@ -175,25 +187,48 @@ struct EditMetadataView: View {
} }
}) })
Button(NSLocalizedString("Save", comment: "Button for saving profile.")) {
if !ln.isEmpty && !is_ln_valid(ln: ln) { }
confirm_ln_address = true
} else { Button(action: {
save() if !ln.isEmpty && !is_ln_valid(ln: ln) {
dismiss() confirm_ln_address = true
} } else {
save()
dismiss()
} }
.disabled(profileUploadObserver.isLoading || bannerUploadObserver.isLoading) }, label: {
.alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) { Text(NSLocalizedString("Save", comment: "Button for saving profile."))
Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) { .frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
} })
} message: { .buttonStyle(GradientButtonStyle(padding: 15))
Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.") .padding(.horizontal, 10)
.padding(.bottom, 10)
.disabled(!didChange())
.opacity(!didChange() ? 0.5 : 1)
.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) .ignoresSafeArea(edges: .top)
.background(Color(.systemGroupedBackground)) .background(Color(.systemGroupedBackground))
.navigationBarBackButtonHidden()
.toolbar {
ToolbarItem(placement: .principal) {
navBackButton
}
}
.alert(NSLocalizedString("Discard changes?", comment: "Alert user that changes have been made."), isPresented: $confirm_save_alert) {
Button(NSLocalizedString("No", comment: "Do not discard changes."), role: .cancel) {
}
Button(NSLocalizedString("Yes", comment: "Agree to discard changes made to profile.")) {
dismiss()
}
}
} }
func uploadedProfilePicture(image_url: URL?) { func uploadedProfilePicture(image_url: URL?) {
@@ -203,6 +238,45 @@ struct EditMetadataView: View {
func uploadedBanner(image_url: URL?) { func uploadedBanner(image_url: URL?) {
banner = image_url?.absoluteString ?? "" banner = image_url?.absoluteString ?? ""
} }
func didChange() -> Bool {
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
let data = profile_txn?.unsafeUnownedValue
if data?.name ?? "" != name {
return true
}
if data?.display_name ?? "" != display_name {
return true
}
if data?.about ?? "" != about {
return true
}
if data?.website ?? "" != website {
return true
}
if data?.picture ?? "" != picture {
return true
}
if data?.banner ?? "" != banner {
return true
}
if data?.nip05 ?? "" != nip05 {
return true
}
if data?.lud16 ?? data?.lud06 ?? "" != ln {
return true
}
return false
}
} }
struct EditMetadataView_Previews: PreviewProvider { struct EditMetadataView_Previews: PreviewProvider {

View File

@@ -18,6 +18,7 @@ struct EditPictureControl: View {
var size: CGFloat? = 25 var size: CGFloat? = 25
var setup: Bool? = false var setup: Bool? = false
@Binding var image_url: URL? @Binding var image_url: URL?
@State var image_url_temp: URL?
@ObservedObject var uploadObserver: ImageUploadingObserver @ObservedObject var uploadObserver: ImageUploadingObserver
let callback: (URL?) -> Void let callback: (URL?) -> Void
@@ -25,12 +26,21 @@ struct EditPictureControl: View {
@State private var show_camera = false @State private var show_camera = false
@State private var show_library = false @State private var show_library = false
@State private var show_url_sheet = false
@State var image_upload_confirm: Bool = false @State var image_upload_confirm: Bool = false
@State var preUploadedMedia: PreUploadedMedia? = nil @State var preUploadedMedia: PreUploadedMedia? = nil
@Environment(\.dismiss) var dismiss
var body: some View { var body: some View {
Menu { Menu {
Button(action: {
self.show_url_sheet = true
}) {
Text("Image URL", comment: "Option to enter a url")
}
Button(action: { Button(action: {
self.show_library = true self.show_library = true
}) { }) {
@@ -51,7 +61,7 @@ struct EditPictureControl: View {
.background(DamusColors.white.opacity(0.7)) .background(DamusColors.white.opacity(0.7))
.clipShape(Circle()) .clipShape(Circle())
.shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0) .shadow(color: DamusColors.purple, radius: 15, x: 0, y: 0)
} else if let url = image_url { } else if let url = image_url, setup ?? false {
KFAnimatedImage(url) KFAnimatedImage(url)
.imageContext(.pfp, disable_animation: false) .imageContext(.pfp, disable_animation: false)
.onFailure(fallbackUrl: URL(string: robohash(pubkey)), cacheKey: url.absoluteString) .onFailure(fallbackUrl: URL(string: robohash(pubkey)), cacheKey: url.absoluteString)
@@ -115,6 +125,70 @@ struct EditPictureControl: View {
Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {} Button(NSLocalizedString("Cancel", comment: "Button to cancel the upload."), role: .cancel) {}
} }
} }
.sheet(isPresented: $show_url_sheet) {
ZStack {
DamusColors.adaptableWhite.edgesIgnoringSafeArea(.all)
VStack {
Text("Image URL")
.bold()
Divider()
.padding(.horizontal)
HStack {
Image(systemName: "doc.on.clipboard")
.foregroundColor(.gray)
.onTapGesture {
if let pastedURL = UIPasteboard.general.string {
image_url_temp = URL(string: pastedURL)
}
}
TextField(image_url_temp?.absoluteString ?? "", text: Binding(
get: { image_url_temp?.absoluteString ?? "" },
set: { image_url_temp = URL(string: $0) }
))
}
.padding(12)
.background {
RoundedRectangle(cornerRadius: 12)
.stroke(.gray.opacity(0.5), lineWidth: 1)
.background {
RoundedRectangle(cornerRadius: 12)
.foregroundColor(.damusAdaptableWhite)
}
}
.padding(10)
Button(action: {
show_url_sheet.toggle()
}, label: {
Text("Cancel", comment: "Cancel button text for dismissing updating image url.")
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
.padding(10)
})
.buttonStyle(NeutralButtonStyle())
.padding(10)
Button(action: {
image_url = image_url_temp
callback(image_url)
show_url_sheet.toggle()
}, label: {
Text("Update", comment: "Update button text for updating image url.")
.frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
})
.buttonStyle(GradientButtonStyle(padding: 10))
.padding(.horizontal, 10)
.disabled(image_url_temp == image_url)
.opacity(image_url_temp == image_url ? 0.5 : 1)
}
}
.onAppear {
image_url_temp = image_url
}
.presentationDetents([.height(300)])
.presentationDragIndicator(.visible)
}
} }
private func handle_upload(media: MediaUpload) { private func handle_upload(media: MediaUpload) {