diff --git a/yobble/Network/ContactsService.swift b/yobble/Network/ContactsService.swift index ea2eb6b..a8256b7 100644 --- a/yobble/Network/ContactsService.swift +++ b/yobble/Network/ContactsService.swift @@ -40,6 +40,10 @@ private struct ContactCreateRequestPayload: Encodable { let customName: String? } +private struct ContactDeleteRequestPayload: Encodable { + let userId: UUID +} + final class ContactsService { private let client: NetworkClient private let decoder: JSONDecoder @@ -159,6 +163,58 @@ final class ContactsService { } } + func removeContact(userId: UUID, completion: @escaping (Result) -> Void) { + let request = ContactDeleteRequestPayload(userId: userId) + + 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/remove", + method: .delete, + 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 delete 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 delete 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 removeContact(userId: UUID) async throws { + try await withCheckedThrowingContinuation { continuation in + removeContact(userId: userId) { 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 aad5f36..79915bc 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -938,6 +938,9 @@ } } }, + "Контакт \"%1$@\" будет удалён из списка." : { + "comment" : "Contact delete confirmation message" + }, "Контактов пока нет" : { "comment" : "Contacts empty state title" }, @@ -1441,6 +1444,9 @@ } } }, + "Не удалось определить контакт." : { + "comment" : "Contact delete invalid user id error" + }, "Не удалось определить пользователя для блокировки." : { "comment" : "Message profile missing user id error" }, @@ -1484,6 +1490,9 @@ "Не удалось сохранить изменения профиля." : { "comment" : "Profile update unexpected status" }, + "Не удалось удалить контакт." : { + "comment" : "Contacts service delete unexpected status" + }, "Не удалось удалить пользователя из списка." : { "comment" : "Blocked users delete unexpected status" }, @@ -2998,6 +3007,7 @@ }, "Удаление контакта появится позже." : { "comment" : "Contact edit delete placeholder message", + "extractionState" : "stale", "localizations" : { "en" : { "stringUnit" : { @@ -3007,12 +3017,18 @@ } } }, + "Удалить" : { + "comment" : "Contact delete confirm action" + }, "Удалить из заблокированных?" : { "comment" : "Unblock confirmation title" }, "Удалить контакт" : { "comment" : "Contact edit delete action\nContacts context action delete" }, + "Удалить контакт?" : { + "comment" : "Contact delete confirmation title" + }, "Удалить фото" : { "comment" : "Avatar delete" }, @@ -3026,6 +3042,9 @@ } } }, + "Удаляем..." : { + "comment" : "Contact delete in progress" + }, "Удалять аккаунт через %lld дн." : { "localizations" : { "en" : { diff --git a/yobble/Views/Chat/MessageProfileView.swift b/yobble/Views/Chat/MessageProfileView.swift index 8c44e74..9d7fa60 100644 --- a/yobble/Views/Chat/MessageProfileView.swift +++ b/yobble/Views/Chat/MessageProfileView.swift @@ -48,7 +48,9 @@ struct MessageProfileView: View { if canEditContact { if let profile = currentChatProfile { NavigationLink { - ContactEditView(contact: ContactEditInfo(profile: profile)) + ContactEditView(contact: ContactEditInfo(profile: profile)) { + handleContactDeleted() + } } label: { Text(NSLocalizedString("Изменить", comment: "Message profile edit contact button")) } @@ -596,6 +598,37 @@ struct MessageProfileView: View { chatProfile = updatedProfile } + private func handleContactDeleted() { + guard let profile = currentChatProfile else { return } + + let existingRelationship = profile.relationship + let updatedRelationship = RelationshipStatus( + isTargetInContactsOfCurrentUser: false, + isCurrentUserInContactsOfTarget: existingRelationship?.isCurrentUserInContactsOfTarget ?? false, + isTargetUserBlockedByCurrentUser: existingRelationship?.isTargetUserBlockedByCurrentUser ?? false, + isCurrentUserInBlacklistOfTarget: existingRelationship?.isCurrentUserInBlacklistOfTarget ?? false + ) + + let updatedProfile = ChatProfile( + userId: profile.userId, + login: profile.login, + fullName: profile.fullName, + customName: nil, + bio: profile.bio, + lastSeen: profile.lastSeen, + createdAt: profile.createdAt, + avatars: profile.avatars, + stories: profile.stories, + permissions: profile.permissions, + profilePermissions: profile.profilePermissions, + relationship: updatedRelationship, + rating: profile.rating, + isOfficial: profile.isOfficial + ) + + chatProfile = updatedProfile + } + private func handleEditContactTap() { showPlaceholderAction( title: NSLocalizedString("Ошибка", comment: "Common error title"), diff --git a/yobble/Views/Contacts/ContactEditView.swift b/yobble/Views/Contacts/ContactEditView.swift index 971f135..3d67d78 100644 --- a/yobble/Views/Contacts/ContactEditView.swift +++ b/yobble/Views/Contacts/ContactEditView.swift @@ -55,12 +55,19 @@ struct ContactEditInfo { struct ContactEditView: View { let contact: ContactEditInfo + let onContactDeleted: (() -> Void)? + + @Environment(\.dismiss) private var dismiss + private let contactsService = ContactsService() @State private var displayName: String @State private var activeAlert: ContactEditAlert? + @State private var isDeleting = false + @State private var showDeleteConfirmation = false - init(contact: ContactEditInfo) { + init(contact: ContactEditInfo, onContactDeleted: (() -> Void)? = nil) { self.contact = contact + self.onContactDeleted = onContactDeleted let initialName = contact.preferredName _displayName = State(initialValue: initialName) } @@ -77,9 +84,9 @@ struct ContactEditView: View { Button(role: .destructive) { handleDeleteTap() } label: { - Text(NSLocalizedString("Удалить контакт", comment: "Contact edit delete action")) - .frame(maxWidth: .infinity, alignment: .center) + deleteButtonLabel } + .disabled(isDeleting) } } .navigationTitle(NSLocalizedString("Контакт", comment: "Contact edit title")) @@ -99,6 +106,37 @@ struct ContactEditView: View { 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 { @@ -185,10 +223,41 @@ struct ContactEditView: View { } private func handleDeleteTap() { - activeAlert = ContactEditAlert( - title: NSLocalizedString("Скоро", comment: "Common soon title"), - message: NSLocalizedString("Удаление контакта появится позже.", comment: "Contact edit delete placeholder message") - ) + 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 + ) + } + } + } } }