ios_app_v2/yobble/Views/Contacts/ContactEditView.swift

339 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import SwiftUI
struct ContactEditInfo {
let userId: String
let login: String?
let fullName: String?
let customName: String?
let avatarFileId: String?
init(userId: String, login: String?, fullName: String?, customName: String?, avatarFileId: String?) {
self.userId = userId
self.login = login
self.fullName = fullName
self.customName = customName
self.avatarFileId = avatarFileId
}
init(userId: UUID, login: String?, fullName: String?, customName: String?, avatarFileId: String?) {
self.init(userId: userId.uuidString, login: login, fullName: fullName, customName: customName, avatarFileId: avatarFileId)
}
init(profile: ChatProfile) {
self.init(
userId: profile.userId,
login: profile.login,
fullName: profile.fullName,
customName: profile.customName,
avatarFileId: profile.avatars?.current?.fileId
)
}
init(payload: ContactPayload) {
self.init(
userId: payload.userId,
login: payload.login,
fullName: payload.fullName,
customName: payload.customName,
avatarFileId: nil
)
}
var preferredName: String {
if let full = fullName?.trimmedNonEmpty {
return full
}
if let login, !login.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
return "@\(login)"
}
return NSLocalizedString("Неизвестный пользователь", comment: "Message profile fallback title")
}
var loadCustomName: String {
if let custom = customName?.trimmedNonEmpty {
return custom
} else {
return ""
}
}
}
struct ContactEditView: View {
let contact: ContactEditInfo
let onContactDeleted: (() -> Void)?
let onContactUpdated: ((String) -> Void)?
@Environment(\.dismiss) private var dismiss
private let contactsService = ContactsService()
private let initialName: String
@State private var displayName: String
@State private var activeAlert: ContactEditAlert?
@State private var isSaving = false
@State private var isDeleting = false
@State private var showDeleteConfirmation = false
init(
contact: ContactEditInfo,
onContactDeleted: (() -> Void)? = nil,
onContactUpdated: ((String) -> Void)? = nil
) {
self.contact = contact //TODO
self.onContactDeleted = onContactDeleted
self.onContactUpdated = onContactUpdated
self.initialName = contact.preferredName
let initialCustomName = contact.loadCustomName
_displayName = State(initialValue: initialCustomName)
}
var body: some View {
Form {
avatarSection
Section(header: Text(NSLocalizedString("Публичная информация", comment: "Profile info section title"))) {
// TextField(NSLocalizedString("Отображаемое имя", comment: "Display name field placeholder"), text: $displayName)
TextField(NSLocalizedString("\(self.initialName)", comment: "Display name field placeholder"), text: $displayName)
.disabled(isSaving || isDeleting)
}
Section {
Button(role: .destructive) {
handleDeleteTap()
} label: {
deleteButtonLabel
}
.disabled(isDeleting)
}
}
.navigationTitle(NSLocalizedString("Контакт", comment: "Contact edit title"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .confirmationAction) {
if isSaving {
ProgressView()
} else {
Button(NSLocalizedString("Сохранить", comment: "Contact edit save button")) {
handleSaveTap()
}
.disabled(!hasChanges || isDeleting)
}
}
}
.alert(item: $activeAlert) { item in
Alert(
title: Text(item.title),
message: Text(item.message),
dismissButton: .default(Text(NSLocalizedString("Понятно", comment: "Placeholder alert dismiss")))
)
}
.confirmationDialog(
NSLocalizedString("Удалить контакт?", comment: "Contact delete confirmation title"),
isPresented: $showDeleteConfirmation,
titleVisibility: .visible
) {
Button(NSLocalizedString("Удалить", comment: "Contact delete confirm action"), role: .destructive) {
confirmDelete()
}
Button(NSLocalizedString("Отмена", comment: "Common cancel"), role: .cancel) {
showDeleteConfirmation = false
}
} message: {
Text(String(
format: NSLocalizedString("Контакт \"%1$@\" будет удалён из списка.", comment: "Contact delete confirmation message"),
contact.preferredName
))
}
}
@ViewBuilder
private var deleteButtonLabel: some View {
if isDeleting {
HStack(spacing: 8) {
ProgressView()
Text(NSLocalizedString("Удаляем...", comment: "Contact delete in progress"))
}
.frame(maxWidth: .infinity, alignment: .center)
} else {
Text(NSLocalizedString("Удалить контакт", comment: "Contact edit delete action"))
.frame(maxWidth: .infinity, alignment: .center)
}
}
private var avatarSection: some View {
Section {
HStack {
Spacer()
VStack(spacing: 8) {
avatarView
.frame(width: 120, height: 120)
.clipShape(Circle())
Button(NSLocalizedString("Изменить фото", comment: "Edit avatar button title")) {
showAvatarUnavailableAlert()
}
}
Spacer()
}
}
.listRowBackground(Color(UIColor.systemGroupedBackground))
}
@ViewBuilder
private var avatarView: some View {
if let url = avatarURL,
let fileId = contact.avatarFileId {
CachedAvatarView(url: url, fileId: fileId, userId: contact.userId) {
avatarPlaceholder
}
.aspectRatio(contentMode: .fill)
} else {
avatarPlaceholder
}
}
private var avatarPlaceholder: some View {
Circle()
.fill(Color.accentColor.opacity(0.15))
.overlay(
Text(avatarInitial)
.font(.system(size: 48, weight: .semibold))
.foregroundColor(.accentColor)
)
}
private var avatarInitial: String {
let trimmedName = displayName.trimmedNonEmpty ?? contact.preferredName
if let first = trimmedName.trimmingCharacters(in: .whitespacesAndNewlines).first {
return String(first).uppercased()
}
if let login = contact.login?.trimmingCharacters(in: .whitespacesAndNewlines), !login.isEmpty {
return String(login.prefix(1)).uppercased()
}
return "?"
}
private var avatarURL: URL? {
guard let fileId = contact.avatarFileId else { return nil }
return URL(string: "\(AppConfig.API_SERVER)/v1/storage/download/avatar/\(contact.userId)?file_id=\(fileId)")
}
private var hasChanges: Bool {
let trimmed = displayName.trimmingCharacters(in: .whitespacesAndNewlines)
// guard !trimmed.isEmpty else { return false }
if let existing = contact.customName?.trimmedNonEmpty {
return trimmed != existing
}
return true
}
private func showAvatarUnavailableAlert() {
activeAlert = ContactEditAlert(
title: NSLocalizedString("Изменение фото недоступно", comment: "Contact edit avatar unavailable title"),
message: NSLocalizedString("Мы пока не можем обновить фото контакта.", comment: "Contact edit avatar unavailable message")
)
}
private func handleSaveTap() {
guard !isSaving, !isDeleting else { return }
guard let userId = UUID(uuidString: contact.userId) else {
activeAlert = ContactEditAlert(
title: NSLocalizedString("Ошибка", comment: "Common error title"),
message: NSLocalizedString("Не удалось определить контакт.", comment: "Contact edit invalid user id error")
)
return
}
let trimmed = displayName.trimmingCharacters(in: .whitespacesAndNewlines)
// guard !trimmed.isEmpty else {
// activeAlert = ContactEditAlert(
// title: NSLocalizedString("Ошибка", comment: "Common error title"),
// message: NSLocalizedString("Имя не может быть пустым.", comment: "Contact edit empty name error")
// )
// return
// }
if trimmed.count > 32 {
activeAlert = ContactEditAlert(
title: NSLocalizedString("Слишком длинное имя", comment: "Contact edit name too long title"),
message: NSLocalizedString("Имя контакта должно быть короче 32 символов.", comment: "Contact edit name too long message")
)
return
}
isSaving = true
Task {
do {
try await contactsService.updateContact(userId: userId, customName: trimmed)
await MainActor.run {
isSaving = false
onContactUpdated?(trimmed)
dismiss()
}
} catch {
await MainActor.run {
isSaving = false
activeAlert = ContactEditAlert(
title: NSLocalizedString("Ошибка", comment: "Common error title"),
message: error.localizedDescription
)
}
}
}
}
private func handleDeleteTap() {
guard !isDeleting else { return }
showDeleteConfirmation = true
}
private func confirmDelete() {
guard !isDeleting else { return }
guard let userId = UUID(uuidString: contact.userId) else {
activeAlert = ContactEditAlert(
title: NSLocalizedString("Ошибка", comment: "Common error title"),
message: NSLocalizedString("Не удалось определить контакт.", comment: "Contact delete invalid user id error")
)
return
}
isDeleting = true
showDeleteConfirmation = false
Task {
do {
try await contactsService.removeContact(userId: userId)
await MainActor.run {
isDeleting = false
onContactDeleted?()
dismiss()
}
} catch {
await MainActor.run {
isDeleting = false
activeAlert = ContactEditAlert(
title: NSLocalizedString("Ошибка", comment: "Common error title"),
message: error.localizedDescription
)
}
}
}
}
}
private struct ContactEditAlert: Identifiable {
let id = UUID()
let title: String
let message: String
}
private extension String {
var trimmedNonEmpty: String? {
let value = trimmingCharacters(in: .whitespacesAndNewlines)
return value.isEmpty ? nil : value
}
}