Compare commits

...

3 Commits

Author SHA1 Message Date
9f6beecb49 patch 2025-10-26 02:34:28 +03:00
3e9d6696b0 patch 2025-10-26 01:53:21 +03:00
3f0543aa3a add new model to blacklist 2025-10-26 01:46:31 +03:00
4 changed files with 107 additions and 67 deletions

View File

@ -29,3 +29,16 @@ struct ErrorResponse: Decodable {
struct MessagePayload: Decodable { struct MessagePayload: Decodable {
let message: String let message: String
} }
struct BlockedUserInfo: Decodable {
let userId: UUID
let login: String
let fullName: String?
let customName: String?
let createdAt: Date
}
struct BlockedUsersPayload: Decodable {
let hasMore: Bool
let items: [BlockedUserInfo]
}

View File

@ -19,14 +19,6 @@ enum BlockedUsersServiceError: LocalizedError {
} }
} }
struct BlockedUserPayload: Decodable {
let userId: UUID
let login: String
let fullName: String?
let customName: String?
let createdAt: Date
}
final class BlockedUsersService { final class BlockedUsersService {
private let client: NetworkClient private let client: NetworkClient
private let decoder: JSONDecoder private let decoder: JSONDecoder
@ -38,16 +30,22 @@ final class BlockedUsersService {
self.decoder.dateDecodingStrategy = .custom(Self.decodeDate) self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
} }
func fetchBlockedUsers(completion: @escaping (Result<[BlockedUserPayload], Error>) -> Void) { func fetchBlockedUsers(limit: Int, offset: Int, completion: @escaping (Result<BlockedUsersPayload, Error>) -> Void) {
let query = [
"limit": String(limit),
"offset": String(offset)
]
client.request( client.request(
path: "/v1/user/blacklist/list", path: "/v1/user/blacklist/list",
method: .get, method: .get,
query: query,
requiresAuth: true requiresAuth: true
) { [decoder] result in ) { [decoder] result in
switch result { switch result {
case .success(let response): case .success(let response):
do { do {
let apiResponse = try decoder.decode(APIResponse<[BlockedUserPayload]>.self, from: response.data) let apiResponse = try decoder.decode(APIResponse<BlockedUsersPayload>.self, from: response.data)
guard apiResponse.status == "fine" else { guard apiResponse.status == "fine" else {
let message = apiResponse.detail ?? NSLocalizedString("Не удалось загрузить список.", comment: "Blocked users service unexpected status") let message = apiResponse.detail ?? NSLocalizedString("Не удалось загрузить список.", comment: "Blocked users service unexpected status")
completion(.failure(BlockedUsersServiceError.unexpectedStatus(message))) completion(.failure(BlockedUsersServiceError.unexpectedStatus(message)))
@ -73,9 +71,9 @@ final class BlockedUsersService {
} }
} }
func fetchBlockedUsers() async throws -> [BlockedUserPayload] { func fetchBlockedUsers(limit: Int, offset: Int) async throws -> BlockedUsersPayload {
try await withCheckedThrowingContinuation { continuation in try await withCheckedThrowingContinuation { continuation in
fetchBlockedUsers { result in fetchBlockedUsers(limit: limit, offset: offset) { result in
continuation.resume(with: result) continuation.resume(with: result)
} }
} }

View File

@ -94,6 +94,9 @@
}, },
"Email не подтверждён. Подтвердите, чтобы активировать дополнительные проверки." : { "Email не подтверждён. Подтвердите, чтобы активировать дополнительные проверки." : {
"comment" : "Описание необходимости подтверждения email" "comment" : "Описание необходимости подтверждения email"
},
"error" : {
}, },
"Fun Fest" : { "Fun Fest" : {
"comment" : "Fun Fest", "comment" : "Fun Fest",
@ -118,6 +121,9 @@
}, },
"Home" : { "Home" : {
},
"Login must not end with 'bot' for non-bot accounts" : {
}, },
"OK" : { "OK" : {
"comment" : "Common OK\nProfile update alert button\nОбщий текст кнопки OK", "comment" : "Common OK\nProfile update alert button\nОбщий текст кнопки OK",
@ -509,9 +515,6 @@
}, },
"Заблокировать контакт" : { "Заблокировать контакт" : {
"comment" : "Contacts context action block" "comment" : "Contacts context action block"
},
"Заблокируйте аккаунт, чтобы скрыть его сообщения и взаимодействия" : {
}, },
"Завершить" : { "Завершить" : {
"comment" : "Кнопка завершения конкретной сессии\nПодтверждение завершения других сессий\nПодтверждение завершения конкретной сессии" "comment" : "Кнопка завершения конкретной сессии\nПодтверждение завершения других сессий\nПодтверждение завершения конкретной сессии"

View File

@ -3,56 +3,35 @@ import SwiftUI
struct BlockedUsersView: View { 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 hasMore = true
@State private var offset = 0
@State private var loadError: String? @State private var loadError: String?
@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 removingUserIds: Set<UUID> = []
@State private var activeAlert: ActiveAlert? @State private var activeAlert: ActiveAlert?
@State private var errorMessageDown: String?
private let blockedUsersService = BlockedUsersService() private let blockedUsersService = BlockedUsersService()
private let limit = 20
var body: some View { var body: some View {
List { List {
if isLoading && blockedUsers.isEmpty { if isLoading && blockedUsers.isEmpty {
loadingState initialLoadingState
} else if let loadError, blockedUsers.isEmpty { } else if let loadError, blockedUsers.isEmpty {
errorState(loadError) errorState(loadError)
} else if blockedUsers.isEmpty { } else if blockedUsers.isEmpty {
emptyState emptyState
} else { } else {
Section(header: Text(NSLocalizedString("Заблокированные", comment: ""))) { usersSection
ForEach(blockedUsers) { user in if isLoading {
HStack(spacing: 12) { Section {
Circle() ProgressView()
.fill(Color.accentColor.opacity(0.15)) .frame(maxWidth: .infinity, alignment: .center)
.frame(width: 44, height: 44)
.overlay(
Text(user.initials)
.font(.headline)
.foregroundColor(.accentColor)
)
VStack(alignment: .leading, spacing: 4) {
Text(user.displayName)
.font(.body)
if let handle = user.handle {
Text(handle)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
}
.padding(.vertical, 4)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
pendingUnblock = user
showUnblockConfirmation = true
} label: {
Label(NSLocalizedString("Разблокировать", comment: ""), systemImage: "person.crop.circle.badge.xmark")
}
.disabled(removingUserIds.contains(user.id))
}
} }
} else if errorMessageDown != nil{
Text("error")
} }
} }
} }
@ -70,9 +49,6 @@ struct BlockedUsersView: View {
.task { .task {
await loadBlockedUsers() await loadBlockedUsers()
} }
// .refreshable {
// await loadBlockedUsers()
// }
.alert(item: $activeAlert) { alert in .alert(item: $activeAlert) { alert in
switch alert { switch alert {
case .addPlaceholder: case .addPlaceholder:
@ -110,6 +86,51 @@ struct BlockedUsersView: View {
} }
} }
private var usersSection: some View {
Section(header: Text(NSLocalizedString("Заблокированные", comment: ""))) {
ForEach(blockedUsers) {
user in
HStack(spacing: 12) {
Circle()
.fill(Color.accentColor.opacity(0.15))
.frame(width: 44, height: 44)
.overlay(
Text(user.initials)
.font(.headline)
.foregroundColor(.accentColor)
)
VStack(alignment: .leading, spacing: 4) {
Text(user.displayName)
.font(.body)
if let handle = user.handle {
Text(handle)
.font(.caption)
.foregroundColor(.secondary)
}
}
Spacer()
}
.padding(.vertical, 0)
.swipeActions(edge: .trailing) {
Button(role: .destructive) {
pendingUnblock = user
showUnblockConfirmation = true
} label: {
Label(NSLocalizedString("Разблокировать", comment: ""), systemImage: "person.crop.circle.badge.xmark")
}
.disabled(removingUserIds.contains(user.id))
}
.onAppear {
if user.id == blockedUsers.last?.id {
Task {
await loadBlockedUsers()
}
}
}
}
}
}
private var emptyState: some View { private var emptyState: some View {
VStack(spacing: 12) { VStack(spacing: 12) {
Image(systemName: "hand.raised") Image(systemName: "hand.raised")
@ -118,10 +139,6 @@ struct BlockedUsersView: View {
Text(NSLocalizedString("У вас нет заблокированных пользователей", comment: "")) Text(NSLocalizedString("У вас нет заблокированных пользователей", comment: ""))
.font(.headline) .font(.headline)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
// Text(NSLocalizedString("Заблокируйте аккаунт, чтобы скрыть его сообщения и взаимодействия", comment: ""))
// .font(.subheadline)
// .foregroundColor(.secondary)
// .multilineTextAlignment(.center)
} }
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
.padding(.vertical, 32) .padding(.vertical, 32)
@ -129,7 +146,7 @@ struct BlockedUsersView: View {
.listRowSeparator(.hidden) .listRowSeparator(.hidden)
} }
private var loadingState: some View { private var initialLoadingState: some View {
Section { Section {
ProgressView() ProgressView()
.frame(maxWidth: .infinity, alignment: .center) .frame(maxWidth: .infinity, alignment: .center)
@ -146,23 +163,32 @@ struct BlockedUsersView: View {
@MainActor @MainActor
private func loadBlockedUsers() async { private func loadBlockedUsers() async {
if isLoading { errorMessageDown = nil
guard !isLoading, hasMore else {
return return
} }
isLoading = true isLoading = true
loadError = nil defer { isLoading = false }
do { if offset == 0 {
let payloads = try await blockedUsersService.fetchBlockedUsers() loadError = nil
blockedUsers = payloads.map(BlockedUser.init)
} catch {
loadError = error.localizedDescription
activeAlert = .error(message: error.localizedDescription)
if AppConfig.DEBUG { print("[BlockedUsersView] load blocked users failed: \(error)") }
} }
isLoading = false do {
let payload = try await blockedUsersService.fetchBlockedUsers(limit: limit, offset: offset)
blockedUsers.append(contentsOf: payload.items.map(BlockedUser.init))
offset += payload.items.count
hasMore = payload.hasMore
} catch {
let message = error.localizedDescription
if offset == 0 {
loadError = message
}
activeAlert = .error(message: message)
errorMessageDown = message
if AppConfig.DEBUG { print("[BlockedUsersView] load blocked users failed: \(error)") }
}
} }
@MainActor @MainActor
@ -211,7 +237,7 @@ private struct BlockedUser: Identifiable, Equatable {
return "??" return "??"
} }
init(payload: BlockedUserPayload) { init(payload: BlockedUserInfo) {
self.id = payload.userId self.id = payload.userId
self.login = payload.login self.login = payload.login
self.fullName = payload.fullName self.fullName = payload.fullName