From b3617a58d411fcbbfdcead3998234e787bd4b84a Mon Sep 17 00:00:00 2001 From: cheykrym Date: Thu, 11 Dec 2025 03:30:39 +0300 Subject: [PATCH] add edit contact --- yobble/Resources/Localizable.xcstrings | 93 ++++++++-- yobble/Views/Chat/MessageProfileView.swift | 16 +- yobble/Views/Contacts/ContactEditView.swift | 185 ++++++++++++++++++++ 3 files changed, 275 insertions(+), 19 deletions(-) create mode 100644 yobble/Views/Contacts/ContactEditView.swift diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 1bb003c..e016220 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -809,6 +809,17 @@ } } }, + "Изменение фото недоступно" : { + "comment" : "Contact edit avatar unavailable title", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Photo change unavailable" + } + } + } + }, "Изменить" : { "comment" : "Message profile edit contact button", "localizations" : { @@ -827,7 +838,15 @@ }, "Изменить фото" : { - + "comment" : "Edit avatar button title", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Change photo" + } + } + } }, "Изображение" : { "comment" : "Image message placeholder" @@ -905,6 +924,17 @@ "Коды восстановления" : { "comment" : "Раздел кодов восстановления 2FA" }, + "Контакт" : { + "comment" : "Contact edit title", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Contact" + } + } + } + }, "Контактов пока нет" : { "comment" : "Contacts empty state title" }, @@ -1140,6 +1170,17 @@ "Мы отправим письмо, как только функция будет готова." : { "comment" : "Сообщение при недоступной отправке письма" }, + "Мы пока не можем обновить фото контакта." : { + "comment" : "Contact edit avatar unavailable message", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "We can’t update the contact photo yet." + } + } + } + }, "Мы постараемся всё исправить. Напишите, что смутило." : { "comment" : "feedback: rating description 2", "localizations" : { @@ -1302,6 +1343,17 @@ "Не удалось завершить сессию." : { "comment" : "Sessions service revoke unexpected status" }, + "Не удалось загрузить данные контакта." : { + "comment" : "Contact edit missing profile message", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to load contact data." + } + } + } + }, "Не удалось загрузить историю чата." : { }, @@ -1723,7 +1775,7 @@ "comment" : "Common cancel\nОбщий текст кнопки отмены" }, "Отображаемое имя" : { - + "comment" : "Display name field placeholder" }, "Отправить код ещё раз" : { @@ -2407,7 +2459,7 @@ }, "Публичная информация" : { - + "comment" : "Profile info section title" }, "Публичное имя" : { @@ -2524,16 +2576,6 @@ } } }, - "Редактировать профиль" : { - "localizations" : { - "en" : { - "stringUnit" : { - "state" : "translated", - "value" : "Edit profile" - } - } - } - }, "Редактирование контакта появится позже." : { "comment" : "Message profile edit contact alert message", "localizations" : { @@ -2545,6 +2587,16 @@ } } }, + "Редактировать профиль" : { + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Edit profile" + } + } + } + }, "Редактор контактов скоро появится. Мы сохраним имя, телефон и заметку." : { "comment" : "Message profile add contact alert message" }, @@ -2671,7 +2723,7 @@ "comment" : "Кнопка копирования кода восстановления" }, "Скоро" : { - "comment" : "Add blocked user placeholder title\nContacts placeholder title\nЗаголовок заглушки" + "comment" : "Add blocked user placeholder title\nCommon soon title\nContacts placeholder title\nЗаголовок заглушки" }, "Скоро можно будет искать сообщения, ссылки и файлы в этом чате." : { "comment" : "Message profile search action description" @@ -2753,6 +2805,17 @@ "Сохраните секретный ключ и введите код из приложения, чтобы завершить настройку." : { "comment" : "Сообщение после активации 2FA" }, + "Сохранить" : { + "comment" : "Contact edit save button", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Save" + } + } + } + }, "Сохранить изменения" : { "localizations" : { "en" : { @@ -3099,4 +3162,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/yobble/Views/Chat/MessageProfileView.swift b/yobble/Views/Chat/MessageProfileView.swift index b283349..532b348 100644 --- a/yobble/Views/Chat/MessageProfileView.swift +++ b/yobble/Views/Chat/MessageProfileView.swift @@ -46,8 +46,16 @@ struct MessageProfileView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { if canEditContact { - Button(NSLocalizedString("Изменить", comment: "Message profile edit contact button")) { - handleEditContactTap() + if let profile = currentChatProfile { + NavigationLink { + ContactEditView(contact: ContactEditInfo(profile: profile)) + } label: { + Text(NSLocalizedString("Изменить", comment: "Message profile edit contact button")) + } + } else { + Button(NSLocalizedString("Изменить", comment: "Message profile edit contact button")) { + handleEditContactTap() + } } } } @@ -537,8 +545,8 @@ struct MessageProfileView: View { private func handleEditContactTap() { showPlaceholderAction( - title: NSLocalizedString("Изменить контакт", comment: "Contacts context action edit"), - message: NSLocalizedString("Редактирование контакта появится позже.", comment: "Message profile edit contact alert message") + title: NSLocalizedString("Ошибка", comment: "Common error title"), + message: NSLocalizedString("Не удалось загрузить данные контакта.", comment: "Contact edit missing profile message") ) } diff --git a/yobble/Views/Contacts/ContactEditView.swift b/yobble/Views/Contacts/ContactEditView.swift new file mode 100644 index 0000000..6e93b8f --- /dev/null +++ b/yobble/Views/Contacts/ContactEditView.swift @@ -0,0 +1,185 @@ +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 custom = customName?.trimmedNonEmpty { + return custom + } + if let full = fullName?.trimmedNonEmpty { + return full + } + if let login, !login.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + return "@\(login)" + } + return NSLocalizedString("Неизвестный пользователь", comment: "Message profile fallback title") + } +} + +struct ContactEditView: View { + let contact: ContactEditInfo + + @State private var displayName: String + @State private var originalDisplayName: String + @State private var activeAlert: ContactEditAlert? + + init(contact: ContactEditInfo) { + self.contact = contact + let initialName = contact.preferredName + _displayName = State(initialValue: initialName) + _originalDisplayName = State(initialValue: initialName) + } + + var body: some View { + Form { + avatarSection + + Section(header: Text(NSLocalizedString("Публичная информация", comment: "Profile info section title"))) { + TextField(NSLocalizedString("Отображаемое имя", comment: "Display name field placeholder"), text: $displayName) + } + } + .navigationTitle(NSLocalizedString("Контакт", comment: "Contact edit title")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .confirmationAction) { + Button(NSLocalizedString("Сохранить", comment: "Contact edit 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 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 { + displayName.trimmingCharacters(in: .whitespacesAndNewlines) != originalDisplayName.trimmingCharacters(in: .whitespacesAndNewlines) + } + + private func showAvatarUnavailableAlert() { + activeAlert = ContactEditAlert( + title: NSLocalizedString("Изменение фото недоступно", comment: "Contact edit avatar unavailable title"), + message: NSLocalizedString("Мы пока не можем обновить фото контакта.", comment: "Contact edit avatar unavailable message") + ) + } + + private func handleSaveTap() { + activeAlert = ContactEditAlert( + title: NSLocalizedString("Скоро", comment: "Common soon title"), + message: NSLocalizedString("Редактирование контакта появится позже.", comment: "Message profile edit contact alert message") + ) + } +} + +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 + } +}