2022-01-31 21:28:07 +00:00
|
|
|
//
|
2022-02-01 17:34:06 +00:00
|
|
|
// UserProfile.swift
|
2022-01-31 21:28:07 +00:00
|
|
|
// SimpleX
|
|
|
|
//
|
|
|
|
// Created by Evgeny Poberezkin on 31/01/2022.
|
|
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import SwiftUI
|
2022-05-31 07:55:13 +01:00
|
|
|
import SimpleXChat
|
2022-01-31 21:28:07 +00:00
|
|
|
|
2022-02-01 17:34:06 +00:00
|
|
|
struct UserProfile: View {
|
2022-01-31 21:28:07 +00:00
|
|
|
@EnvironmentObject var chatModel: ChatModel
|
2024-09-16 15:28:45 +03:00
|
|
|
@EnvironmentObject var theme: AppTheme
|
|
|
|
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
|
2022-01-31 21:28:07 +00:00
|
|
|
@State private var profile = Profile(displayName: "", fullName: "")
|
2024-09-16 15:28:45 +03:00
|
|
|
@State private var currentProfileHash: Int?
|
|
|
|
// Modals
|
2022-03-25 22:13:01 +04:00
|
|
|
@State private var showChooseSource = false
|
|
|
|
@State private var showImagePicker = false
|
2022-05-18 21:32:30 +04:00
|
|
|
@State private var showTakePhoto = false
|
2022-04-04 19:19:54 +01:00
|
|
|
@State private var chosenImage: UIImage? = nil
|
2023-10-04 17:45:39 +01:00
|
|
|
@State private var alert: UserProfileAlert?
|
|
|
|
@FocusState private var focusDisplayName
|
2022-01-31 21:28:07 +00:00
|
|
|
|
|
|
|
var body: some View {
|
2024-09-16 15:28:45 +03:00
|
|
|
List {
|
2025-05-26 16:57:18 +01:00
|
|
|
EditProfileImage(profileImage: $profile.image, showChooseSource: $showChooseSource)
|
|
|
|
.padding(.top)
|
2024-09-16 15:28:45 +03:00
|
|
|
|
|
|
|
Section {
|
|
|
|
HStack {
|
|
|
|
TextField("Enter your name…", text: $profile.displayName)
|
|
|
|
.focused($focusDisplayName)
|
|
|
|
if !validDisplayName(profile.displayName) {
|
|
|
|
Button {
|
|
|
|
alert = .invalidNameError(validName: mkValidName(profile.displayName))
|
|
|
|
} label: {
|
|
|
|
Image(systemName: "exclamationmark.circle").foregroundColor(.red)
|
2022-05-12 15:07:28 +01:00
|
|
|
}
|
2022-01-31 21:28:07 +00:00
|
|
|
}
|
|
|
|
}
|
2024-09-16 15:28:45 +03:00
|
|
|
if let user = chatModel.currentUser, showFullName(user) {
|
|
|
|
TextField("Full name (optional)", text: $profile.fullName)
|
2022-03-25 22:13:01 +04:00
|
|
|
}
|
2024-09-16 15:28:45 +03:00
|
|
|
} footer: {
|
|
|
|
Text("Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.")
|
|
|
|
}
|
2022-03-25 22:13:01 +04:00
|
|
|
|
2024-09-16 15:28:45 +03:00
|
|
|
Section {
|
|
|
|
Button(action: getCurrentProfile) {
|
|
|
|
Text("Reset")
|
|
|
|
}
|
|
|
|
.disabled(currentProfileHash == profile.hashValue)
|
|
|
|
Button(action: saveProfile) {
|
|
|
|
Text("Save (and notify contacts)")
|
2022-01-31 21:28:07 +00:00
|
|
|
}
|
2024-09-16 15:28:45 +03:00
|
|
|
.disabled(!canSaveProfile)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Lifecycle
|
|
|
|
.onAppear {
|
|
|
|
getCurrentProfile()
|
|
|
|
}
|
|
|
|
.onDisappear {
|
|
|
|
if canSaveProfile {
|
|
|
|
showAlert(
|
|
|
|
title: NSLocalizedString("Save your profile?", comment: "alert title"),
|
|
|
|
message: NSLocalizedString("Your profile was changed. If you save it, the updated profile will be sent to all your contacts.", comment: "alert message"),
|
|
|
|
buttonTitle: NSLocalizedString("Save (and notify contacts)", comment: "alert button"),
|
|
|
|
buttonAction: saveProfile,
|
|
|
|
cancelButton: true
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.onChange(of: chosenImage) { image in
|
2024-10-05 22:11:57 +03:00
|
|
|
Task {
|
|
|
|
let resized: String? = if let image {
|
|
|
|
await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
|
|
|
|
} else {
|
|
|
|
nil
|
|
|
|
}
|
|
|
|
await MainActor.run { profile.image = resized }
|
2022-01-31 21:28:07 +00:00
|
|
|
}
|
|
|
|
}
|
2024-09-16 15:28:45 +03:00
|
|
|
// Modals
|
2022-03-25 22:13:01 +04:00
|
|
|
.confirmationDialog("Profile image", isPresented: $showChooseSource, titleVisibility: .visible) {
|
|
|
|
Button("Take picture") {
|
2022-05-18 21:32:30 +04:00
|
|
|
showTakePhoto = true
|
2022-03-25 22:13:01 +04:00
|
|
|
}
|
|
|
|
Button("Choose from library") {
|
|
|
|
showImagePicker = true
|
|
|
|
}
|
2023-02-27 17:46:10 +00:00
|
|
|
if UIPasteboard.general.hasImages {
|
|
|
|
Button("Paste image") {
|
|
|
|
chosenImage = UIPasteboard.general.image
|
|
|
|
}
|
|
|
|
}
|
2022-03-25 22:13:01 +04:00
|
|
|
}
|
2022-05-18 21:32:30 +04:00
|
|
|
.fullScreenCover(isPresented: $showTakePhoto) {
|
|
|
|
ZStack {
|
|
|
|
Color.black.edgesIgnoringSafeArea(.all)
|
2022-04-04 19:19:54 +01:00
|
|
|
CameraImagePicker(image: $chosenImage)
|
|
|
|
}
|
2022-03-25 22:13:01 +04:00
|
|
|
}
|
2022-12-03 21:42:12 +00:00
|
|
|
.sheet(isPresented: $showImagePicker) {
|
2023-12-12 09:04:48 +00:00
|
|
|
LibraryImagePicker(image: $chosenImage) { _ in
|
|
|
|
await MainActor.run {
|
|
|
|
showImagePicker = false
|
|
|
|
}
|
2022-05-18 21:32:30 +04:00
|
|
|
}
|
|
|
|
}
|
2023-10-04 17:45:39 +01:00
|
|
|
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
|
2022-03-25 22:13:01 +04:00
|
|
|
}
|
|
|
|
|
2023-10-04 17:45:39 +01:00
|
|
|
private func showFullName(_ user: User) -> Bool {
|
|
|
|
user.profile.fullName != "" && user.profile.fullName != user.profile.displayName
|
|
|
|
}
|
2024-09-16 15:28:45 +03:00
|
|
|
|
|
|
|
private var canSaveProfile: Bool {
|
|
|
|
currentProfileHash != profile.hashValue &&
|
|
|
|
profile.displayName.trimmingCharacters(in: .whitespaces) != "" &&
|
|
|
|
validDisplayName(profile.displayName)
|
2023-10-04 17:45:39 +01:00
|
|
|
}
|
|
|
|
|
2024-09-16 15:28:45 +03:00
|
|
|
private func saveProfile() {
|
|
|
|
focusDisplayName = false
|
2022-02-24 17:16:41 +00:00
|
|
|
Task {
|
|
|
|
do {
|
2023-10-04 17:45:39 +01:00
|
|
|
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
|
2023-08-23 20:43:06 +03:00
|
|
|
if let (newProfile, _) = try await apiUpdateProfile(profile: profile) {
|
2024-09-16 15:28:45 +03:00
|
|
|
await MainActor.run {
|
2023-01-23 15:48:29 +00:00
|
|
|
chatModel.updateCurrentUser(newProfile)
|
2024-09-16 15:28:45 +03:00
|
|
|
getCurrentProfile()
|
2022-02-24 17:16:41 +00:00
|
|
|
}
|
2023-10-25 06:01:47 +08:00
|
|
|
} else {
|
|
|
|
alert = .duplicateUserError
|
2022-02-24 17:16:41 +00:00
|
|
|
}
|
|
|
|
} catch {
|
2022-07-30 13:03:44 +01:00
|
|
|
logger.error("UserProfile apiUpdateProfile error: \(responseError(error))")
|
2022-01-31 21:28:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2024-09-16 15:28:45 +03:00
|
|
|
|
|
|
|
private func getCurrentProfile() {
|
|
|
|
if let user = chatModel.currentUser {
|
|
|
|
profile = fromLocalProfile(user.profile)
|
|
|
|
currentProfileHash = profile.hashValue
|
|
|
|
}
|
|
|
|
}
|
2022-01-31 21:28:07 +00:00
|
|
|
}
|
|
|
|
|
2025-05-26 16:57:18 +01:00
|
|
|
struct EditProfileImage: View {
|
|
|
|
@EnvironmentObject var theme: AppTheme
|
|
|
|
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
|
|
|
|
@Binding var profileImage: String?
|
|
|
|
@Binding var showChooseSource: Bool
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
Group {
|
|
|
|
if profileImage != nil {
|
|
|
|
ZStack(alignment: .bottomTrailing) {
|
|
|
|
ZStack(alignment: .topTrailing) {
|
|
|
|
ProfileImage(imageStr: profileImage, size: 160)
|
|
|
|
.onTapGesture { showChooseSource = true }
|
|
|
|
overlayButton("multiply", edge: .top) { profileImage = nil }
|
|
|
|
}
|
|
|
|
overlayButton("camera", edge: .bottom) { showChooseSource = true }
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ZStack(alignment: .center) {
|
|
|
|
ProfileImage(imageStr: profileImage, size: 160)
|
|
|
|
editImageButton { showChooseSource = true }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
|
|
.listRowBackground(Color.clear)
|
|
|
|
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
|
|
|
.contentShape(Rectangle())
|
|
|
|
}
|
|
|
|
|
|
|
|
private func overlayButton(
|
|
|
|
_ systemName: String,
|
|
|
|
edge: Edge.Set,
|
|
|
|
action: @escaping () -> Void
|
|
|
|
) -> some View {
|
|
|
|
Image(systemName: systemName)
|
|
|
|
.resizable()
|
|
|
|
.aspectRatio(contentMode: .fit)
|
|
|
|
.frame(height: 12)
|
|
|
|
.foregroundColor(theme.colors.primary)
|
|
|
|
.padding(6)
|
|
|
|
.frame(width: 36, height: 36, alignment: .center)
|
|
|
|
.background(radius >= 20 ? Color.clear : theme.colors.background.opacity(0.5))
|
|
|
|
.clipShape(Circle())
|
|
|
|
.contentShape(Circle())
|
|
|
|
.padding([.trailing, edge], -12)
|
|
|
|
.onTapGesture(perform: action)
|
|
|
|
}
|
2022-07-28 11:49:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
func editImageButton(action: @escaping () -> Void) -> some View {
|
|
|
|
Button {
|
|
|
|
action()
|
|
|
|
} label: {
|
|
|
|
Image(systemName: "camera")
|
|
|
|
.resizable()
|
|
|
|
.aspectRatio(contentMode: .fit)
|
|
|
|
.frame(width: 48)
|
|
|
|
}
|
|
|
|
}
|