From 3c32ef6c70f7ea47e59fc3795d4f5e215fea5225 Mon Sep 17 00:00:00 2001 From: cheykrym Date: Thu, 11 Dec 2025 03:00:18 +0300 Subject: [PATCH] add block/unblock --- yobble/Network/BlockedUsersService.swift | 59 +++++++++++++++++++ yobble/Resources/Localizable.xcstrings | 23 ++++---- yobble/Views/Chat/MessageProfileView.swift | 66 ++++++++++++++++++---- 3 files changed, 128 insertions(+), 20 deletions(-) diff --git a/yobble/Network/BlockedUsersService.swift b/yobble/Network/BlockedUsersService.swift index 9c69a7e..0cbe2f0 100644 --- a/yobble/Network/BlockedUsersService.swift +++ b/yobble/Network/BlockedUsersService.swift @@ -133,6 +133,60 @@ final class BlockedUsersService { } } + func add(userId: UUID, completion: @escaping (Result) -> Void) { + let request = BlockedUserCreateRequest(userId: userId, login: nil) + let encoder = JSONEncoder() + encoder.keyEncodingStrategy = .convertToSnakeCase + + guard let body = try? encoder.encode(request) else { + let message = NSLocalizedString("Не удалось подготовить данные запроса.", comment: "Blocked users create encoding error") + completion(.failure(BlockedUsersServiceError.encoding(message))) + return + } + + client.request( + path: "/v1/user/blacklist/add", + method: .post, + 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: "Blocked users create unexpected status") + completion(.failure(BlockedUsersServiceError.unexpectedStatus(message))) + return + } + completion(.success(apiResponse.data)) + } catch { + let debugMessage = Self.describeDecodingError(error: error, data: response.data) + if AppConfig.DEBUG { + print("[BlockedUsersService] decode create response failed: \(debugMessage)") + } + completion(.failure(BlockedUsersServiceError.decoding(debugDescription: debugMessage))) + } + case .failure(let error): + if case let NetworkError.server(_, data) = error, + let data, + let message = Self.errorMessage(from: data) { + completion(.failure(BlockedUsersServiceError.unexpectedStatus(message))) + return + } + completion(.failure(error)) + } + } + } + + func add(userId: UUID) async throws -> BlockedUserInfo { + try await withCheckedThrowingContinuation { continuation in + add(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) @@ -229,3 +283,8 @@ final class BlockedUsersService { private struct BlockedUserDeleteRequest: Encodable { let userId: UUID } + +private struct BlockedUserCreateRequest: Encodable { + let userId: UUID? + let login: String? +} diff --git a/yobble/Resources/Localizable.xcstrings b/yobble/Resources/Localizable.xcstrings index f3831ec..b5a7ad7 100644 --- a/yobble/Resources/Localizable.xcstrings +++ b/yobble/Resources/Localizable.xcstrings @@ -280,9 +280,6 @@ "Блокировка контакта \"%1$@\" появится позже." : { "comment" : "Contacts block placeholder message" }, - "Блокировка чата пока в дизайне. Готовим отдельный экран со статусом и жалобой." : { - "comment" : "Message profile block alert message" - }, "Бот" : { "comment" : "Тип сессии — бот" }, @@ -639,7 +636,7 @@ }, "Заблокировать" : { - "comment" : "Message profile block alert title\nMessage profile block title" + "comment" : "Message profile block title" }, "Заблокировать контакт" : { "comment" : "Contacts context action block" @@ -1285,6 +1282,9 @@ "Не удалось выполнить поиск." : { "comment" : "Search error fallback\nSearch service decoding error" }, + "Не удалось заблокировать пользователя." : { + "comment" : "Blocked users create unexpected status" + }, "Не удалось завершить другие сессии." : { "comment" : "Sessions service revoke-all unexpected status" }, @@ -1369,6 +1369,9 @@ } } }, + "Не удалось определить пользователя для блокировки." : { + "comment" : "Message profile missing user id error" + }, "Не удалось открыть чат" : { "comment" : "Chat creation error title" }, @@ -1382,7 +1385,7 @@ }, "Не удалось подготовить данные запроса." : { - "comment" : "Blocked users delete encoding error\nProfile update encoding error" + "comment" : "Blocked users create encoding error\nBlocked users delete encoding error\nProfile update encoding error" }, "Не удалось подготовить изображение для загрузки." : { "comment" : "Avatar encoding error" @@ -2005,6 +2008,9 @@ }, "Подключение" : { + }, + "Подождите..." : { + "comment" : "Message profile block action pending subtitle" }, "Подтвердите пароль" : { @@ -2390,12 +2396,12 @@ }, "Разблокировать" : { - "comment" : "Message profile unblock alert title\nMessage profile unblock title\nUnblock confirmation action" + "comment" : "Message profile unblock title\nUnblock confirmation action" }, "Раздел скоро станет активным — собираем и индексируем вложения." : { "comment" : "Message profile media placeholder message" }, - "Разделы временно показывают заглушки — позже спрячем пустые категории." : { + "Разделы временно показывают заглушки." : { "comment" : "Message profile media footer new" }, "Разрешить пересылку сообщений" : { @@ -2645,9 +2651,6 @@ "Скоро появится мини-игра, где можно заработать очки для кастомизации профиля. Следите за обновлениями!" : { "comment" : "Concept tab placeholder description" }, - "Скоро появится разблокировка с подтверждением и синхронизацией." : { - "comment" : "Message profile unblock alert message" - }, "Скрыть" : { }, diff --git a/yobble/Views/Chat/MessageProfileView.swift b/yobble/Views/Chat/MessageProfileView.swift index 10bb413..9d7f4cb 100644 --- a/yobble/Views/Chat/MessageProfileView.swift +++ b/yobble/Views/Chat/MessageProfileView.swift @@ -10,10 +10,19 @@ struct MessageProfileView: View { let chat: PrivateChatListItem let currentUserId: String? private let avatarSize: CGFloat = 96 + private let blockedUsersService = BlockedUsersService() @State private var areNotificationsEnabled: Bool = true @State private var placeholderAlert: PlaceholderAlert? @State private var isBioExpanded: Bool = false + @State private var isBlockedByCurrentUserState: Bool + @State private var isProcessingBlockAction = false + + init(chat: PrivateChatListItem, currentUserId: String?) { + self.chat = chat + self.currentUserId = currentUserId + _isBlockedByCurrentUserState = State(initialValue: chat.chatData?.relationship?.isTargetUserBlockedByCurrentUser ?? false) + } var body: some View { ScrollView(showsIndicators: false) { @@ -270,10 +279,14 @@ struct MessageProfileView: View { title: isBlockedByCurrentUser ? NSLocalizedString("Разблокировать", comment: "Message profile unblock title") : NSLocalizedString("Заблокировать", comment: "Message profile block title"), + subtitle: isProcessingBlockAction + ? NSLocalizedString("Подождите...", comment: "Message profile block action pending subtitle") + : nil, tint: isBlockedByCurrentUser ? Color.green : Color.red ) { handleBlockToggleTap() } + .disabled(isProcessingBlockAction) } @@ -511,13 +524,48 @@ struct MessageProfileView: View { } private func handleBlockToggleTap() { - let title = isBlockedByCurrentUser - ? NSLocalizedString("Разблокировать", comment: "Message profile unblock alert title") - : NSLocalizedString("Заблокировать", comment: "Message profile block alert title") - let message = isBlockedByCurrentUser - ? NSLocalizedString("Скоро появится разблокировка с подтверждением и синхронизацией.", comment: "Message profile unblock alert message") - : NSLocalizedString("Блокировка чата пока в дизайне. Готовим отдельный экран со статусом и жалобой.", comment: "Message profile block alert message") - showPlaceholderAction(title: title, message: message) + guard !isProcessingBlockAction else { return } + + guard let userIdString = chat.chatData?.userId, + let userId = UUID(uuidString: userIdString) else { + placeholderAlert = PlaceholderAlert( + title: NSLocalizedString("Ошибка", comment: "Common error title"), + message: NSLocalizedString("Не удалось определить пользователя для блокировки.", comment: "Message profile missing user id error") + ) + return + } + + isProcessingBlockAction = true + let shouldBlock = !isBlockedByCurrentUser + Task { + await performBlockAction(shouldBlock: shouldBlock, userId: userId) + } + } + + private func performBlockAction(shouldBlock: Bool, userId: UUID) async { + do { + if shouldBlock { + _ = try await blockedUsersService.add(userId: userId) + } else { + _ = try await blockedUsersService.remove(userId: userId) + } + + await MainActor.run { + isBlockedByCurrentUserState = shouldBlock + isProcessingBlockAction = false + } + } catch { + if AppConfig.DEBUG { + print("[MessageProfileView] block action failed: \(error)") + } + await MainActor.run { + isProcessingBlockAction = false + placeholderAlert = PlaceholderAlert( + title: NSLocalizedString("Ошибка", comment: "Common error title"), + message: error.localizedDescription + ) + } + } } private var mediaCategories: [MediaCategory] { @@ -747,9 +795,7 @@ struct MessageProfileView: View { ] } - private var isBlockedByCurrentUser: Bool { - chat.chatData?.relationship?.isTargetUserBlockedByCurrentUser ?? false - } + private var isBlockedByCurrentUser: Bool { isBlockedByCurrentUserState } private var avatarUrl: URL? { guard let chatData = chat.chatData,