add delete user from blacklist

This commit is contained in:
cheykrym 2025-10-23 22:37:23 +03:00
parent 43a5d8193d
commit 2eabbd59c3
3 changed files with 120 additions and 15 deletions

View File

@ -3,6 +3,7 @@ import Foundation
enum BlockedUsersServiceError: LocalizedError { enum BlockedUsersServiceError: LocalizedError {
case unexpectedStatus(String) case unexpectedStatus(String)
case decoding(debugDescription: String) case decoding(debugDescription: String)
case encoding(String)
var errorDescription: String? { var errorDescription: String? {
switch self { switch self {
@ -12,6 +13,8 @@ enum BlockedUsersServiceError: LocalizedError {
return AppConfig.DEBUG return AppConfig.DEBUG
? debugDescription ? debugDescription
: NSLocalizedString("Не удалось загрузить список.", comment: "Blocked users service decoding error") : NSLocalizedString("Не удалось загрузить список.", comment: "Blocked users service decoding error")
case .encoding(let message):
return message
} }
} }
} }
@ -78,6 +81,60 @@ final class BlockedUsersService {
} }
} }
func remove(userId: UUID, completion: @escaping (Result<String, Error>) -> Void) {
let request = BlockedUserDeleteRequest(userId: userId)
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
guard let body = try? encoder.encode(request) else {
let message = NSLocalizedString("Не удалось подготовить данные запроса.", comment: "Blocked users delete encoding error")
completion(.failure(BlockedUsersServiceError.encoding(message)))
return
}
client.request(
path: "/v1/user/blacklist/remove",
method: .delete,
body: body,
requiresAuth: true
) { [decoder] result in
switch result {
case .success(let response):
do {
let apiResponse = try decoder.decode(APIResponse<MessagePayload>.self, from: response.data)
guard apiResponse.status == "fine" else {
let message = apiResponse.detail ?? NSLocalizedString("Не удалось удалить пользователя из списка.", comment: "Blocked users delete unexpected status")
completion(.failure(BlockedUsersServiceError.unexpectedStatus(message)))
return
}
completion(.success(apiResponse.data.message))
} catch {
let debugMessage = Self.describeDecodingError(error: error, data: response.data)
if AppConfig.DEBUG {
print("[BlockedUsersService] decode delete 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 remove(userId: UUID) async throws -> String {
try await withCheckedThrowingContinuation { continuation in
remove(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)
@ -170,3 +227,7 @@ final class BlockedUsersService {
return formatter return formatter
}() }()
} }
private struct BlockedUserDeleteRequest: Encodable {
let userId: UUID
}

View File

@ -993,7 +993,7 @@
}, },
"Не удалось подготовить данные запроса." : { "Не удалось подготовить данные запроса." : {
"comment" : "Profile update encoding error" "comment" : "Blocked users delete encoding error\nProfile update encoding error"
}, },
"Не удалось сериализовать данные запроса." : { "Не удалось сериализовать данные запроса." : {
"localizations" : { "localizations" : {
@ -1011,6 +1011,9 @@
"Не удалось сохранить изменения профиля." : { "Не удалось сохранить изменения профиля." : {
"comment" : "Profile update unexpected status" "comment" : "Profile update unexpected status"
}, },
"Не удалось удалить пользователя из списка." : {
"comment" : "Blocked users delete unexpected status"
},
"Неверный запрос (400)." : { "Неверный запрос (400)." : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@ -1323,7 +1326,7 @@
} }
}, },
"Ошибка" : { "Ошибка" : {
"comment" : "Profile update error title", "comment" : "Common error title\nProfile update error title",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {

View File

@ -4,9 +4,10 @@ struct BlockedUsersView: View {
@State private var blockedUsers: [BlockedUser] = [] @State private var blockedUsers: [BlockedUser] = []
@State private var isLoading = false @State private var isLoading = false
@State private var loadError: String? @State private var loadError: String?
@State private var showAddBlockedUserAlert = false
@State private var pendingUnblock: BlockedUser? @State private var pendingUnblock: BlockedUser?
@State private var showUnblockConfirmation = false @State private var showUnblockConfirmation = false
@State private var removingUserIds: Set<UUID> = []
@State private var activeAlert: ActiveAlert?
private let blockedUsersService = BlockedUsersService() private let blockedUsersService = BlockedUsersService()
@ -49,6 +50,7 @@ struct BlockedUsersView: View {
} label: { } label: {
Label(NSLocalizedString("Разблокировать", comment: ""), systemImage: "person.crop.circle.badge.xmark") Label(NSLocalizedString("Разблокировать", comment: ""), systemImage: "person.crop.circle.badge.xmark")
} }
.disabled(removingUserIds.contains(user.id))
} }
} }
} }
@ -59,7 +61,7 @@ struct BlockedUsersView: View {
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button { Button {
showAddBlockedUserAlert = true activeAlert = .addPlaceholder
} label: { } label: {
Image(systemName: "plus") Image(systemName: "plus")
} }
@ -71,10 +73,21 @@ struct BlockedUsersView: View {
.refreshable { .refreshable {
await loadBlockedUsers() await loadBlockedUsers()
} }
.alert(NSLocalizedString("Скоро", comment: "Add blocked user placeholder title"), isPresented: $showAddBlockedUserAlert) { .alert(item: $activeAlert) { alert in
Button(NSLocalizedString("OK", comment: "Common OK"), role: .cancel) {} switch alert {
} message: { case .addPlaceholder:
Text(NSLocalizedString("Добавление новых блокировок появится позже.", comment: "Add blocked user placeholder message")) return Alert(
title: Text(NSLocalizedString("Скоро", comment: "Add blocked user placeholder title")),
message: Text(NSLocalizedString("Добавление новых блокировок появится позже.", comment: "Add blocked user placeholder message")),
dismissButton: .default(Text(NSLocalizedString("OK", comment: "Common OK")))
)
case .error(_, let message):
return Alert(
title: Text(NSLocalizedString("Ошибка", comment: "Common error title")),
message: Text(message),
dismissButton: .default(Text(NSLocalizedString("OK", comment: "Common OK")))
)
}
} }
.confirmationDialog( .confirmationDialog(
NSLocalizedString("Удалить из заблокированных?", comment: "Unblock confirmation title"), NSLocalizedString("Удалить из заблокированных?", comment: "Unblock confirmation title"),
@ -82,11 +95,15 @@ struct BlockedUsersView: View {
presenting: pendingUnblock presenting: pendingUnblock
) { user in ) { user in
Button(NSLocalizedString("Разблокировать", comment: "Unblock confirmation action"), role: .destructive) { Button(NSLocalizedString("Разблокировать", comment: "Unblock confirmation action"), role: .destructive) {
unblock(user)
pendingUnblock = nil pendingUnblock = nil
showUnblockConfirmation = false
Task {
await unblock(user)
}
} }
Button(NSLocalizedString("Отмена", comment: "Common cancel"), role: .cancel) { Button(NSLocalizedString("Отмена", comment: "Common cancel"), role: .cancel) {
pendingUnblock = nil pendingUnblock = nil
showUnblockConfirmation = false
} }
} message: { user in } message: { user in
Text(String(format: NSLocalizedString("Пользователь \"%1$@\" будет удалён из списка заблокированных.", comment: "Unblock confirmation message"), user.displayName)) Text(String(format: NSLocalizedString("Пользователь \"%1$@\" будет удалён из списка заблокированных.", comment: "Unblock confirmation message"), user.displayName))
@ -141,17 +158,27 @@ struct BlockedUsersView: View {
blockedUsers = payloads.map(BlockedUser.init) blockedUsers = payloads.map(BlockedUser.init)
} catch { } catch {
loadError = error.localizedDescription loadError = error.localizedDescription
if AppConfig.DEBUG { activeAlert = .error(message: error.localizedDescription)
print("[BlockedUsersView] load blocked users failed: \(error)") if AppConfig.DEBUG { print("[BlockedUsersView] load blocked users failed: \(error)") }
}
} }
isLoading = false isLoading = false
} }
private func unblock(_ user: BlockedUser) { @MainActor
// TODO: implement unblock logic when backend is ready private func unblock(_ user: BlockedUser) async {
guard !removingUserIds.contains(user.id) else { return }
removingUserIds.insert(user.id)
defer { removingUserIds.remove(user.id) }
do {
_ = try await blockedUsersService.remove(userId: user.id)
blockedUsers.removeAll { $0.id == user.id } blockedUsers.removeAll { $0.id == user.id }
} catch {
activeAlert = .error(message: error.localizedDescription)
if AppConfig.DEBUG { print("[BlockedUsersView] unblock failed: \(error)") }
}
} }
} }
@ -206,3 +233,17 @@ private struct BlockedUser: Identifiable, Equatable {
} }
} }
} }
private enum ActiveAlert: Identifiable {
case addPlaceholder
case error(id: UUID = UUID(), message: String)
var id: String {
switch self {
case .addPlaceholder:
return "addPlaceholder"
case .error(let id, _):
return id.uuidString
}
}
}