commit 8a75537ea3573a6989151c3f6cee5e78d48df2ce
parent 49c8d63d0b5699cd10c85ba223ed634177b7cd64
Author: Eric Holguin <14004132+ericholguin@users.noreply.github.com>
Date: Mon, 12 Aug 2024 12:54:32 -0600
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>
Diffstat:
5 files changed, 194 insertions(+), 34 deletions(-)
diff --git a/damus/Assets.xcassets/damoose.imageset/Contents.json b/damus/Assets.xcassets/damoose.imageset/Contents.json
@@ -0,0 +1,12 @@
+{
+ "images" : [
+ {
+ "filename" : "damoose.jpeg",
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/damus/Assets.xcassets/damoose.imageset/damoose.jpeg b/damus/Assets.xcassets/damoose.imageset/damoose.jpeg
Binary files differ.
diff --git a/damus/Views/BannerImageView.swift b/damus/Views/BannerImageView.swift
@@ -13,7 +13,7 @@ struct EditBannerImageView: View {
var damus_state: DamusState
@ObservedObject var viewModel: ImageUploadingObserver
let callback: (URL?) -> Void
- let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
+ let defaultImage = UIImage(named: "damoose") ?? UIImage()
@State var banner_image: URL? = nil
@@ -38,7 +38,7 @@ struct EditBannerImageView: View {
struct InnerBannerImageView: View {
let disable_animation: Bool
let url: URL?
- let defaultImage = UIImage(named: "profile-banner") ?? UIImage()
+ let defaultImage = UIImage(named: "damoose") ?? UIImage()
var body: some View {
ZStack {
diff --git a/damus/Views/Profile/EditMetadataView.swift b/damus/Views/Profile/EditMetadataView.swift
@@ -21,13 +21,15 @@ struct EditMetadataView: View {
@State var ln: String
@State var website: String
- @Environment(\.dismiss) var dismiss
-
@State var confirm_ln_address: Bool = false
+ @State var confirm_save_alert: Bool = false
@StateObject var profileUploadObserver = ImageUploadingObserver()
@StateObject var bannerUploadObserver = ImageUploadingObserver()
+ @Environment(\.dismiss) var dismiss
+ @Environment(\.presentationMode) var presentationMode
+
init(damus_state: DamusState) {
self.damus_state = damus_state
let profile_txn = damus_state.profiles.lookup(id: damus_state.pubkey)
@@ -77,7 +79,7 @@ struct EditMetadataView: View {
var TopSection: some View {
ZStack(alignment: .top) {
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)
.frame(width: geo.size.width, height: BANNER_HEIGHT)
.clipped()
@@ -86,7 +88,7 @@ struct EditMetadataView: View {
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:))
+ 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
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 {
VStack(alignment: .leading) {
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.")) {
TextField(NSLocalizedString("https://jb55.com", comment: "Placeholder example text for website URL for user profile."), text: $website)
.autocorrectionDisabled(true)
@@ -139,10 +151,10 @@ struct EditMetadataView: View {
ZStack(alignment: .topLeading) {
TextEditor(text: $about)
.textInputAutocapitalization(.sentences)
- .frame(minHeight: 20, alignment: .leading)
+ .frame(minHeight: 45, alignment: .leading)
.multilineTextAlignment(.leading)
Text(about.isEmpty ? placeholder : about)
- .padding(.leading, 4)
+ .padding(4)
.opacity(about.isEmpty ? 1 : 0)
.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 {
- save()
- dismiss()
- }
+
+ }
+
+ Button(action: {
+ 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.")
+ }, label: {
+ Text(NSLocalizedString("Save", comment: "Button for saving profile."))
+ .frame(minWidth: 300, maxWidth: .infinity, alignment: .center)
+ })
+ .buttonStyle(GradientButtonStyle(padding: 15))
+ .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)
.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?) {
@@ -203,6 +238,45 @@ struct EditMetadataView: View {
func uploadedBanner(image_url: URL?) {
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 {
diff --git a/damus/Views/Profile/EditPictureControl.swift b/damus/Views/Profile/EditPictureControl.swift
@@ -18,6 +18,7 @@ struct EditPictureControl: View {
var size: CGFloat? = 25
var setup: Bool? = false
@Binding var image_url: URL?
+ @State var image_url_temp: URL?
@ObservedObject var uploadObserver: ImageUploadingObserver
let callback: (URL?) -> Void
@@ -25,13 +26,22 @@ struct EditPictureControl: View {
@State private var show_camera = false
@State private var show_library = false
+ @State private var show_url_sheet = false
@State var image_upload_confirm: Bool = false
@State var preUploadedMedia: PreUploadedMedia? = nil
+
+ @Environment(\.dismiss) var dismiss
var body: some View {
Menu {
Button(action: {
+ self.show_url_sheet = true
+ }) {
+ Text("Image URL", comment: "Option to enter a url")
+ }
+
+ Button(action: {
self.show_library = true
}) {
Text("Choose from Library", comment: "Option to select photo from library")
@@ -51,7 +61,7 @@ struct EditPictureControl: View {
.background(DamusColors.white.opacity(0.7))
.clipShape(Circle())
.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)
.imageContext(.pfp, disable_animation: false)
.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) {}
}
}
+ .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) {