diff --git a/yobble/Network/ContactsService.swift b/yobble/Network/ContactsService.swift index a8256b7..f4f0e07 100644 --- a/yobble/Network/ContactsService.swift +++ b/yobble/Network/ContactsService.swift @@ -44,6 +44,11 @@ private struct ContactDeleteRequestPayload: Encodable { let userId: UUID } +private struct ContactUpdateRequestPayload: Encodable { + let userId: UUID + let customName: String? +} + final class ContactsService { private let client: NetworkClient private let decoder: JSONDecoder @@ -215,6 +220,58 @@ final class ContactsService { } } + func updateContact(userId: UUID, customName: String?, completion: @escaping (Result) -> Void) { + let request = ContactUpdateRequestPayload(userId: userId, customName: customName) + + guard let body = try? encoder.encode(request) else { + let message = NSLocalizedString("Не удалось подготовить данные запроса.", comment: "Contacts service encoding error") + completion(.failure(ContactsServiceError.encoding(message))) + return + } + + client.request( + path: "/v1/user/contact/update", + method: .patch, + body: body, + requiresAuth: true + ) { [decoder] result in + switch result { + case .success(let response): + do { + let apiResponse = try decoder.decode(APIResponse.self, from: response.data) + guard apiResponse.status == "fine" else { + let message = apiResponse.detail ?? NSLocalizedString("Не удалось обновить контакт.", comment: "Contacts service update unexpected status") + completion(.failure(ContactsServiceError.unexpectedStatus(message))) + return + } + completion(.success(())) + } catch { + let debugMessage = Self.describeDecodingError(error: error, data: response.data) + if AppConfig.DEBUG { + print("[ContactsService] decode contact update failed: \(debugMessage)") + } + completion(.failure(ContactsServiceError.decoding(debugDescription: debugMessage))) + } + case .failure(let error): + if case let NetworkError.server(_, data) = error, + let data, + let message = Self.errorMessage(from: data) { + completion(.failure(ContactsServiceError.unexpectedStatus(message))) + return + } + completion(.failure(error)) + } + } + } + + func updateContact(userId: UUID, customName: String?) async throws { + try await withCheckedThrowingContinuation { continuation in + updateContact(userId: userId, customName: customName) { result in + continuation.resume(with: result) + } + } + } + private static func decodeDate(from decoder: Decoder) throws -> Date { let container = try decoder.singleValueContainer() let string = try container.decode(String.self) diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index 79915bc..e63ed04 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -853,9 +853,12 @@ }, "Имя в чате" : { + }, + "Имя контакта должно быть короче 32 символов." : { + "comment" : "Contact edit name too long message" }, "Имя не может быть пустым." : { - "comment" : "Contact add empty name error" + "comment" : "Contact add empty name error\nContact edit empty name error" }, "Имя, логин и статус — как в профиле Telegram." : { "comment" : "Message profile about description" @@ -1410,6 +1413,9 @@ "Не удалось обновить аватар." : { "comment" : "Avatar upload unexpected status" }, + "Не удалось обновить контакт." : { + "comment" : "Contacts service update unexpected status" + }, "Не удалось обновить пароль." : { "localizations" : { "en" : { @@ -1445,7 +1451,7 @@ } }, "Не удалось определить контакт." : { - "comment" : "Contact delete invalid user id error" + "comment" : "Contact delete invalid user id error\nContact edit invalid user id error" }, "Не удалось определить пользователя для блокировки." : { "comment" : "Message profile missing user id error" @@ -2599,6 +2605,7 @@ }, "Редактирование контакта появится позже." : { "comment" : "Message profile edit contact alert message", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -2744,7 +2751,7 @@ "comment" : "Кнопка копирования кода восстановления" }, "Скоро" : { - "comment" : "Add blocked user placeholder title\nCommon soon title\nContacts placeholder title\nЗаголовок заглушки" + "comment" : "Add blocked user placeholder title\nContacts placeholder title\nЗаголовок заглушки" }, "Скоро можно будет искать сообщения, ссылки и файлы в этом чате." : { "comment" : "Message profile search action description" @@ -2754,6 +2761,9 @@ }, "Скрыть" : { + }, + "Слишком длинное имя" : { + "comment" : "Contact edit name too long title" }, "Слишком много запросов." : { "localizations" : { diff --git a/yobble/Views/Chat/MessageProfileView.swift b/yobble/Views/Chat/MessageProfileView.swift index 9d7fa60..d945e7f 100644 --- a/yobble/Views/Chat/MessageProfileView.swift +++ b/yobble/Views/Chat/MessageProfileView.swift @@ -48,9 +48,15 @@ struct MessageProfileView: View { if canEditContact { if let profile = currentChatProfile { NavigationLink { - ContactEditView(contact: ContactEditInfo(profile: profile)) { - handleContactDeleted() - } + ContactEditView( + contact: ContactEditInfo(profile: profile), + onContactDeleted: { + handleContactDeleted() + }, + onContactUpdated: { newName in + handleContactUpdated(newName) + } + ) } label: { Text(NSLocalizedString("Изменить", comment: "Message profile edit contact button")) } @@ -598,6 +604,29 @@ struct MessageProfileView: View { chatProfile = updatedProfile } + private func handleContactUpdated(_ newName: String) { + guard let profile = currentChatProfile else { return } + + let updatedProfile = ChatProfile( + userId: profile.userId, + login: profile.login, + fullName: profile.fullName, + customName: newName, + bio: profile.bio, + lastSeen: profile.lastSeen, + createdAt: profile.createdAt, + avatars: profile.avatars, + stories: profile.stories, + permissions: profile.permissions, + profilePermissions: profile.profilePermissions, + relationship: profile.relationship, + rating: profile.rating, + isOfficial: profile.isOfficial + ) + + chatProfile = updatedProfile + } + private func handleContactDeleted() { guard let profile = currentChatProfile else { return } diff --git a/yobble/Views/Contacts/ContactEditView.swift b/yobble/Views/Contacts/ContactEditView.swift index 3d67d78..e3ac816 100644 --- a/yobble/Views/Contacts/ContactEditView.swift +++ b/yobble/Views/Contacts/ContactEditView.swift @@ -56,18 +56,25 @@ struct ContactEditInfo { struct ContactEditView: View { let contact: ContactEditInfo let onContactDeleted: (() -> Void)? + let onContactUpdated: ((String) -> Void)? @Environment(\.dismiss) private var dismiss private let contactsService = ContactsService() @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) { + init( + contact: ContactEditInfo, + onContactDeleted: (() -> Void)? = nil, + onContactUpdated: ((String) -> Void)? = nil + ) { self.contact = contact self.onContactDeleted = onContactDeleted + self.onContactUpdated = onContactUpdated let initialName = contact.preferredName _displayName = State(initialValue: initialName) } @@ -78,6 +85,7 @@ struct ContactEditView: View { Section(header: Text(NSLocalizedString("Публичная информация", comment: "Profile info section title"))) { TextField(NSLocalizedString("Отображаемое имя", comment: "Display name field placeholder"), text: $displayName) + .disabled(isSaving || isDeleting) } Section { @@ -93,10 +101,14 @@ struct ContactEditView: View { .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .confirmationAction) { - Button(NSLocalizedString("Сохранить", comment: "Contact edit save button")) { - handleSaveTap() + if isSaving { + ProgressView() + } else { + Button(NSLocalizedString("Сохранить", comment: "Contact edit save button")) { + handleSaveTap() + } + .disabled(!hasChanges || isDeleting) } - .disabled(!hasChanges) } } .alert(item: $activeAlert) { item in @@ -216,10 +228,53 @@ struct ContactEditView: View { } private func handleSaveTap() { - activeAlert = ContactEditAlert( - title: NSLocalizedString("Скоро", comment: "Common soon title"), - message: NSLocalizedString("Редактирование контакта появится позже.", comment: "Message profile edit contact alert message") - ) + 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() {