SimpleX-Chat/apps/ios/Shared/Views/UserSettings/UserProfile.swift
Evgeny 8d54acef92
ios: only handle taps on messages with links or secrets, use image for secret markdown (#5885)
* ios: use image for secret markdown

* remove unnecessary ViewBuilders
2025-05-11 14:15:14 +01:00

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)
}
}