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