339 lines
12 KiB
Swift
339 lines
12 KiB
Swift
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
|
||
}
|
||
}
|