Files
Kecalek_python/ios_client 0.8.5/Kecalek/Views/Profile/ProfileView.swift
2026-03-14 12:43:56 +01:00

278 lines
11 KiB
Swift

import SwiftUI
import PhotosUI
import UIKit
struct ProfileView: View {
var appState: AppState
var isOwnProfile: Bool
var userId: String?
@State private var viewModel = ProfileViewModel()
@State private var showLogoutConfirm = false
@State private var showAvatarPicker = false
@State private var showAuthorizeDevice = false
@State private var showRotateKeys = false
@State private var rotatePassword = ""
@State private var isRotating = false
@State private var rotateMessage: String?
@State private var rotateIsError = false
@State private var showChangeUsername = false
@State private var newUsername = ""
@State private var showChangePassword = false
@State private var oldPassword = ""
@State private var newPassword = ""
@State private var confirmNewPassword = ""
@State private var showVerification = false
@Environment(\.dismiss) private var dismiss
var body: some View {
NavigationStack {
Form {
// Avatar
Section {
HStack {
Spacer()
VStack(spacing: 8) {
if let avatarData = viewModel.avatarData,
let uiImage = UIImage(data: avatarData) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80)
.clipShape(Circle())
} else {
CircularAvatarView(
name: viewModel.profile?.username ?? "?",
size: 80,
isGroup: false
)
}
if isOwnProfile {
Button("Change Photo") {
showAvatarPicker = true
}
.font(.caption)
}
}
Spacer()
}
.listRowBackground(Color.clear)
}
// Info
Section("Info") {
if let username = viewModel.profile?.username {
LabeledContent("Username", value: username)
}
if let email = viewModel.profile?.email {
LabeledContent("Email", value: email)
}
}
if isOwnProfile {
// Editable fields
Section("Contact") {
TextField("Phone", text: $viewModel.phone)
.keyboardType(.phonePad)
Toggle("Phone visible to contacts", isOn: $viewModel.phoneVisible)
TextField("Location", text: $viewModel.location)
Toggle("Location visible to contacts", isOn: $viewModel.locationVisible)
}
} else {
// Read-only view
if let phone = viewModel.profile?.phone, viewModel.profile?.phoneVisible == true {
Section("Contact") {
LabeledContent("Phone", value: phone)
}
}
if let location = viewModel.profile?.location, viewModel.profile?.locationVisible == true {
Section("Location") {
LabeledContent("Location", value: location)
}
}
}
if !isOwnProfile, let uid = userId {
Section("Security") {
NavigationLink {
SafetyNumberView(
peerUserId: uid,
peerUsername: viewModel.profile?.username ?? "User",
chatClient: appState.chatClient
)
} label: {
Label("Verify Identity", systemImage: "checkmark.shield")
}
}
}
if let error = viewModel.errorMessage {
Section {
Text(error)
.foregroundStyle(.red)
}
}
if isOwnProfile {
Section("Account") {
Button {
newUsername = viewModel.profile?.username ?? ""
showChangeUsername = true
} label: {
Label("Change Username", systemImage: "person.text.rectangle")
}
Button {
showChangePassword = true
} label: {
Label("Change Password", systemImage: "key")
}
}
Section("Security") {
Button {
showAuthorizeDevice = true
} label: {
Label("Authorize New Device", systemImage: "iphone.badge.checkmark")
}
Button {
showRotateKeys = true
} label: {
Label("Rotate Keys", systemImage: "arrow.triangle.2.circlepath")
}
}
Section {
Button(role: .destructive) {
showLogoutConfirm = true
} label: {
HStack {
Spacer()
Text("Logout")
Spacer()
}
}
}
}
}
.navigationTitle(isOwnProfile ? "My Profile" : "Profile")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
if isOwnProfile {
Button("Save") {
Task {
let success = await viewModel.saveProfile(chatClient: appState.chatClient)
if success {
dismiss()
}
}
}
.disabled(viewModel.isSaving)
}
}
ToolbarItem(placement: .topBarLeading) {
if !isOwnProfile {
Button {
dismiss()
} label: {
Image(systemName: "xmark")
}
}
}
}
.alert("Logout", isPresented: $showLogoutConfirm) {
Button("Cancel", role: .cancel) {}
Button("Logout", role: .destructive) {
Task {
await appState.logout()
}
}
} message: {
Text("Are you sure you want to logout?")
}
.sheet(isPresented: $showAvatarPicker) {
ImagePickerView { data in
Task {
await viewModel.uploadAvatar(imageData: data, chatClient: appState.chatClient)
}
}
}
.sheet(isPresented: $showAuthorizeDevice) {
AuthorizeDeviceView(appState: appState)
}
.alert("Rotate Keys", isPresented: $showRotateKeys) {
SecureField("Password", text: $rotatePassword)
Button("Cancel", role: .cancel) { rotatePassword = "" }
Button("Rotate") {
Task {
isRotating = true
let (success, msg) = await appState.chatClient.rotateKeys(password: rotatePassword)
rotatePassword = ""
isRotating = false
rotateMessage = msg
rotateIsError = !success
}
}
} message: {
Text("Enter your password to generate new keys. All other devices will be disconnected.")
}
.alert(rotateIsError ? "Error" : "Success", isPresented: Binding(
get: { rotateMessage != nil },
set: { if !$0 { rotateMessage = nil } }
)) {
Button("OK") { rotateMessage = nil }
} message: {
Text(rotateMessage ?? "")
}
.alert("Change Username", isPresented: $showChangeUsername) {
TextField("New username", text: $newUsername)
Button("Cancel", role: .cancel) { newUsername = "" }
Button("Change") {
Task {
let success = await viewModel.changeUsername(newUsername: newUsername, chatClient: appState.chatClient)
if success {
await viewModel.loadProfile(chatClient: appState.chatClient)
}
newUsername = ""
}
}
} message: {
Text("Enter a new display name.")
}
.alert("Change Password", isPresented: $showChangePassword) {
SecureField("Current password", text: $oldPassword)
SecureField("New password", text: $newPassword)
SecureField("Confirm new password", text: $confirmNewPassword)
Button("Cancel", role: .cancel) {
oldPassword = ""
newPassword = ""
confirmNewPassword = ""
}
Button("Change") {
Task {
guard newPassword == confirmNewPassword else {
viewModel.errorMessage = "Passwords don't match"
return
}
_ = await viewModel.changePassword(
oldPassword: oldPassword, newPassword: newPassword,
chatClient: appState.chatClient
)
oldPassword = ""
newPassword = ""
confirmNewPassword = ""
}
}
} message: {
Text("Enter your current password and a new password.")
}
.task {
await viewModel.loadProfile(userId: userId, chatClient: appState.chatClient)
}
}
}
}