add block/unblock

This commit is contained in:
cheykrym 2025-12-11 03:00:18 +03:00
parent 12be44673a
commit 3c32ef6c70
3 changed files with 128 additions and 20 deletions

View File

@ -133,6 +133,60 @@ final class BlockedUsersService {
} }
} }
func add(userId: UUID, completion: @escaping (Result<BlockedUserInfo, Error>) -> 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<BlockedUserInfo>.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 { private static func decodeDate(from decoder: Decoder) throws -> Date {
let container = try decoder.singleValueContainer() let container = try decoder.singleValueContainer()
let string = try container.decode(String.self) let string = try container.decode(String.self)
@ -229,3 +283,8 @@ final class BlockedUsersService {
private struct BlockedUserDeleteRequest: Encodable { private struct BlockedUserDeleteRequest: Encodable {
let userId: UUID let userId: UUID
} }
private struct BlockedUserCreateRequest: Encodable {
let userId: UUID?
let login: String?
}

View File

@ -280,9 +280,6 @@
"Блокировка контакта \"%1$@\" появится позже." : { "Блокировка контакта \"%1$@\" появится позже." : {
"comment" : "Contacts block placeholder message" "comment" : "Contacts block placeholder message"
}, },
"Блокировка чата пока в дизайне. Готовим отдельный экран со статусом и жалобой." : {
"comment" : "Message profile block alert message"
},
"Бот" : { "Бот" : {
"comment" : "Тип сессии — бот" "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" "comment" : "Contacts context action block"
@ -1285,6 +1282,9 @@
"Не удалось выполнить поиск." : { "Не удалось выполнить поиск." : {
"comment" : "Search error fallback\nSearch service decoding error" "comment" : "Search error fallback\nSearch service decoding error"
}, },
"Не удалось заблокировать пользователя." : {
"comment" : "Blocked users create unexpected status"
},
"Не удалось завершить другие сессии." : { "Не удалось завершить другие сессии." : {
"comment" : "Sessions service revoke-all 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" "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" "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 placeholder message"
}, },
"Разделы временно показывают заглушки — позже спрячем пустые категории." : { "Разделы временно показывают заглушки." : {
"comment" : "Message profile media footer new" "comment" : "Message profile media footer new"
}, },
"Разрешить пересылку сообщений" : { "Разрешить пересылку сообщений" : {
@ -2645,9 +2651,6 @@
"Скоро появится мини-игра, где можно заработать очки для кастомизации профиля. Следите за обновлениями!" : { "Скоро появится мини-игра, где можно заработать очки для кастомизации профиля. Следите за обновлениями!" : {
"comment" : "Concept tab placeholder description" "comment" : "Concept tab placeholder description"
}, },
"Скоро появится разблокировка с подтверждением и синхронизацией." : {
"comment" : "Message profile unblock alert message"
},
"Скрыть" : { "Скрыть" : {
}, },

View File

@ -10,10 +10,19 @@ struct MessageProfileView: View {
let chat: PrivateChatListItem let chat: PrivateChatListItem
let currentUserId: String? let currentUserId: String?
private let avatarSize: CGFloat = 96 private let avatarSize: CGFloat = 96
private let blockedUsersService = BlockedUsersService()
@State private var areNotificationsEnabled: Bool = true @State private var areNotificationsEnabled: Bool = true
@State private var placeholderAlert: PlaceholderAlert? @State private var placeholderAlert: PlaceholderAlert?
@State private var isBioExpanded: Bool = false @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 { var body: some View {
ScrollView(showsIndicators: false) { ScrollView(showsIndicators: false) {
@ -270,10 +279,14 @@ struct MessageProfileView: View {
title: isBlockedByCurrentUser title: isBlockedByCurrentUser
? NSLocalizedString("Разблокировать", comment: "Message profile unblock title") ? NSLocalizedString("Разблокировать", comment: "Message profile unblock title")
: NSLocalizedString("Заблокировать", comment: "Message profile block title"), : NSLocalizedString("Заблокировать", comment: "Message profile block title"),
subtitle: isProcessingBlockAction
? NSLocalizedString("Подождите...", comment: "Message profile block action pending subtitle")
: nil,
tint: isBlockedByCurrentUser ? Color.green : Color.red tint: isBlockedByCurrentUser ? Color.green : Color.red
) { ) {
handleBlockToggleTap() handleBlockToggleTap()
} }
.disabled(isProcessingBlockAction)
} }
@ -511,13 +524,48 @@ struct MessageProfileView: View {
} }
private func handleBlockToggleTap() { private func handleBlockToggleTap() {
let title = isBlockedByCurrentUser guard !isProcessingBlockAction else { return }
? NSLocalizedString("Разблокировать", comment: "Message profile unblock alert title")
: NSLocalizedString("Заблокировать", comment: "Message profile block alert title") guard let userIdString = chat.chatData?.userId,
let message = isBlockedByCurrentUser let userId = UUID(uuidString: userIdString) else {
? NSLocalizedString("Скоро появится разблокировка с подтверждением и синхронизацией.", comment: "Message profile unblock alert message") placeholderAlert = PlaceholderAlert(
: NSLocalizedString("Блокировка чата пока в дизайне. Готовим отдельный экран со статусом и жалобой.", comment: "Message profile block alert message") title: NSLocalizedString("Ошибка", comment: "Common error title"),
showPlaceholderAction(title: title, message: message) 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] { private var mediaCategories: [MediaCategory] {
@ -747,9 +795,7 @@ struct MessageProfileView: View {
] ]
} }
private var isBlockedByCurrentUser: Bool { private var isBlockedByCurrentUser: Bool { isBlockedByCurrentUserState }
chat.chatData?.relationship?.isTargetUserBlockedByCurrentUser ?? false
}
private var avatarUrl: URL? { private var avatarUrl: URL? {
guard let chatData = chat.chatData, guard let chatData = chat.chatData,