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:
committed by
Daniel D’Aquino
parent
f0b5162205
commit
abfe0f642f
12
damus/Assets.xcassets/damoose.imageset/Contents.json
vendored
Normal file
12
damus/Assets.xcassets/damoose.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "damoose.jpeg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
damus/Assets.xcassets/damoose.imageset/damoose.jpeg
vendored
Normal file
BIN
damus/Assets.xcassets/damoose.imageset/damoose.jpeg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 122 KiB |
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user