mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2025-06-28 12:19:54 +00:00
205 lines
7.4 KiB
Swift
205 lines
7.4 KiB
Swift
//
|
|
// UserProfile.swift
|
|
// SimpleX
|
|
//
|
|
// Created by Evgeny Poberezkin on 31/01/2022.
|
|
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
|
//
|
|
|
|
import SwiftUI
|
|
import SimpleXChat
|
|
|
|
struct UserProfile: View {
|
|
@EnvironmentObject var chatModel: ChatModel
|
|
@EnvironmentObject var theme: AppTheme
|
|
@AppStorage(DEFAULT_PROFILE_IMAGE_CORNER_RADIUS) private var radius = defaultProfileImageCorner
|
|
@State private var profile = Profile(displayName: "", fullName: "")
|
|
@State private var currentProfileHash: Int?
|
|
// Modals
|
|
@State private var showChooseSource = false
|
|
@State private var showImagePicker = false
|
|
@State private var showTakePhoto = false
|
|
@State private var chosenImage: UIImage? = nil
|
|
@State private var alert: UserProfileAlert?
|
|
@FocusState private var focusDisplayName
|
|
|
|
var body: some View {
|
|
List {
|
|
Group {
|
|
if profile.image != nil {
|
|
ZStack(alignment: .bottomTrailing) {
|
|
ZStack(alignment: .topTrailing) {
|
|
profileImageView(profile.image)
|
|
.onTapGesture { showChooseSource = true }
|
|
overlayButton("multiply", edge: .top) { profile.image = nil }
|
|
}
|
|
overlayButton("camera", edge: .bottom) { showChooseSource = true }
|
|
}
|
|
} else {
|
|
ZStack(alignment: .center) {
|
|
profileImageView(profile.image)
|
|
editImageButton { showChooseSource = true }
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.listRowBackground(Color.clear)
|
|
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
|
|
.padding(.top)
|
|
.contentShape(Rectangle())
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
if let user = chatModel.currentUser, showFullName(user) {
|
|
TextField("Full name (optional)", text: $profile.fullName)
|
|
}
|
|
} footer: {
|
|
Text("Your profile is stored on your device and shared only with your contacts. SimpleX servers cannot see your profile.")
|
|
}
|
|
|
|
Section {
|
|
Button(action: getCurrentProfile) {
|
|
Text("Reset")
|
|
}
|
|
.disabled(currentProfileHash == profile.hashValue)
|
|
Button(action: saveProfile) {
|
|
Text("Save (and notify contacts)")
|
|
}
|
|
.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
|
|
Task {
|
|
let resized: String? = if let image {
|
|
await resizeImageToStrSize(cropToSquare(image), maxDataSize: 12500)
|
|
} else {
|
|
nil
|
|
}
|
|
await MainActor.run { profile.image = resized }
|
|
}
|
|
}
|
|
// Modals
|
|
.confirmationDialog("Profile image", isPresented: $showChooseSource, titleVisibility: .visible) {
|
|
Button("Take picture") {
|
|
showTakePhoto = true
|
|
}
|
|
Button("Choose from library") {
|
|
showImagePicker = true
|
|
}
|
|
if UIPasteboard.general.hasImages {
|
|
Button("Paste image") {
|
|
chosenImage = UIPasteboard.general.image
|
|
}
|
|
}
|
|
}
|
|
.fullScreenCover(isPresented: $showTakePhoto) {
|
|
ZStack {
|
|
Color.black.edgesIgnoringSafeArea(.all)
|
|
CameraImagePicker(image: $chosenImage)
|
|
}
|
|
}
|
|
.sheet(isPresented: $showImagePicker) {
|
|
LibraryImagePicker(image: $chosenImage) { _ in
|
|
await MainActor.run {
|
|
showImagePicker = false
|
|
}
|
|
}
|
|
}
|
|
.alert(item: $alert) { a in userProfileAlert(a, $profile.displayName) }
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
private func showFullName(_ user: User) -> Bool {
|
|
user.profile.fullName != "" && user.profile.fullName != user.profile.displayName
|
|
}
|
|
|
|
private var canSaveProfile: Bool {
|
|
currentProfileHash != profile.hashValue &&
|
|
profile.displayName.trimmingCharacters(in: .whitespaces) != "" &&
|
|
validDisplayName(profile.displayName)
|
|
}
|
|
|
|
private func saveProfile() {
|
|
focusDisplayName = false
|
|
Task {
|
|
do {
|
|
profile.displayName = profile.displayName.trimmingCharacters(in: .whitespaces)
|
|
if let (newProfile, _) = try await apiUpdateProfile(profile: profile) {
|
|
await MainActor.run {
|
|
chatModel.updateCurrentUser(newProfile)
|
|
getCurrentProfile()
|
|
}
|
|
} else {
|
|
alert = .duplicateUserError
|
|
}
|
|
} catch {
|
|
logger.error("UserProfile apiUpdateProfile error: \(responseError(error))")
|
|
}
|
|
}
|
|
}
|
|
|
|
private func getCurrentProfile() {
|
|
if let user = chatModel.currentUser {
|
|
profile = fromLocalProfile(user.profile)
|
|
currentProfileHash = profile.hashValue
|
|
}
|
|
}
|
|
}
|
|
|
|
func profileImageView(_ imageStr: String?) -> some View {
|
|
ProfileImage(imageStr: imageStr, size: 192)
|
|
}
|
|
|
|
func editImageButton(action: @escaping () -> Void) -> some View {
|
|
Button {
|
|
action()
|
|
} label: {
|
|
Image(systemName: "camera")
|
|
.resizable()
|
|
.aspectRatio(contentMode: .fit)
|
|
.frame(width: 48)
|
|
}
|
|
}
|