Compare commits
3 Commits
052ff5fe4f
...
9f6beecb49
| Author | SHA1 | Date | |
|---|---|---|---|
| 9f6beecb49 | |||
| 3e9d6696b0 | |||
| 3f0543aa3a |
@ -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]
|
||||||
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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Подтверждение завершения конкретной сессии"
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user