197 lines
7.0 KiB
Swift
197 lines
7.0 KiB
Swift
import SwiftUI
|
||
|
||
struct ContactAddView: View {
|
||
let contact: ContactEditInfo
|
||
let onContactAdded: ((ContactPayload) -> Void)?
|
||
|
||
@Environment(\.dismiss) private var dismiss
|
||
private let contactsService = ContactsService()
|
||
private let initialName: String
|
||
|
||
@State private var displayName: String
|
||
@State private var activeAlert: ContactAddAlert?
|
||
@State private var isSaving = false
|
||
|
||
init(contact: ContactEditInfo, onContactAdded: ((ContactPayload) -> Void)? = nil) {
|
||
self.contact = contact
|
||
self.onContactAdded = onContactAdded
|
||
// let initialName = contact.preferredName
|
||
self.initialName = contact.preferredName
|
||
_displayName = State(initialValue: "")
|
||
}
|
||
|
||
var body: some View {
|
||
Form {
|
||
avatarSection
|
||
|
||
Section(header: Text(NSLocalizedString("Публичная информация", comment: "Contact add public info section title"))) {
|
||
// TextField(NSLocalizedString("Отображаемое имя", comment: "Display name field placeholder"), text: $displayName)
|
||
TextField(NSLocalizedString("\(initialName)", comment: "Display name field placeholder"), text: $displayName)
|
||
.disabled(isSaving)
|
||
}
|
||
}
|
||
.navigationTitle(NSLocalizedString("Новый контакт", comment: "Contact add title"))
|
||
.navigationBarTitleDisplayMode(.inline)
|
||
.toolbar {
|
||
ToolbarItem(placement: .confirmationAction) {
|
||
if isSaving {
|
||
ProgressView()
|
||
} else {
|
||
Button(NSLocalizedString("Сохранить", comment: "Contact add save button")) {
|
||
handleSaveTap()
|
||
}
|
||
.disabled(!hasChanges)
|
||
}
|
||
}
|
||
}
|
||
.alert(item: $activeAlert) { item in
|
||
Alert(
|
||
title: Text(item.title),
|
||
message: Text(item.message),
|
||
dismissButton: .default(Text(NSLocalizedString("Понятно", comment: "Placeholder alert dismiss")))
|
||
)
|
||
}
|
||
}
|
||
|
||
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 initials = initials(from: trimmedName) {
|
||
return initials
|
||
}
|
||
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 = ContactAddAlert(
|
||
title: NSLocalizedString("Изменение фото недоступно", comment: "Contact add avatar unavailable title"),
|
||
message: NSLocalizedString("Мы пока не можем обновить фото контакта.", comment: "Contact add avatar unavailable message")
|
||
)
|
||
}
|
||
|
||
private func handleSaveTap() {
|
||
guard !isSaving else { return }
|
||
|
||
guard let userId = UUID(uuidString: contact.userId) else {
|
||
activeAlert = ContactAddAlert(
|
||
title: NSLocalizedString("Ошибка", comment: "Common error title"),
|
||
message: NSLocalizedString("Не удалось определить пользователя для добавления.", comment: "Contact add invalid user id error")
|
||
)
|
||
return
|
||
}
|
||
|
||
let trimmedName = displayName.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines)
|
||
// guard !trimmedName.isEmpty else {
|
||
// activeAlert = ContactAddAlert(
|
||
// title: NSLocalizedString("Ошибка", comment: "Common error title"),
|
||
// message: NSLocalizedString("Имя не может быть пустым.", comment: "Contact add empty name error")
|
||
// )
|
||
// return
|
||
// }
|
||
|
||
isSaving = true
|
||
let customName = trimmedName
|
||
|
||
Task {
|
||
do {
|
||
let payload = try await contactsService.addContact(userId: userId, customName: customName)
|
||
await MainActor.run {
|
||
isSaving = false
|
||
onContactAdded?(payload)
|
||
dismiss()
|
||
}
|
||
} catch {
|
||
await MainActor.run {
|
||
isSaving = false
|
||
activeAlert = ContactAddAlert(
|
||
title: NSLocalizedString("Ошибка", comment: "Common error title"),
|
||
message: error.localizedDescription
|
||
)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private struct ContactAddAlert: 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
|
||
}
|
||
}
|
||
|
||
private func initials(from text: String) -> String? {
|
||
let components = text
|
||
.split { $0.isWhitespace }
|
||
.filter { !$0.isEmpty }
|
||
let letters = components.prefix(2).compactMap { $0.first }
|
||
guard !letters.isEmpty else { return nil }
|
||
return letters.map { String($0).uppercased() }.joined()
|
||
}
|