import SwiftUI struct BlockedUsersView: View { @State private var blockedUsers: [BlockedUser] = [] @State private var isLoading = false @State private var hasMore = true @State private var offset = 0 @State private var loadError: String? @State private var pendingUnblock: BlockedUser? @State private var showUnblockConfirmation = false @State private var removingUserIds: Set = [] @State private var activeAlert: ActiveAlert? @State private var errorMessageDown: String? private let blockedUsersService = BlockedUsersService() private let limit = 20 var body: some View { List { if isLoading && blockedUsers.isEmpty { initialLoadingState } else if let loadError, blockedUsers.isEmpty { errorState(loadError) } else if blockedUsers.isEmpty { emptyState } else { usersSection if isLoading { Section { ProgressView() .frame(maxWidth: .infinity, alignment: .center) } } else if errorMessageDown != nil{ Text("error") } } } .navigationTitle(NSLocalizedString("Заблокированные", comment: "")) .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { activeAlert = .addPlaceholder } label: { Image(systemName: "plus") } } } .task { await loadBlockedUsers() } .alert(item: $activeAlert) { alert in switch alert { case .addPlaceholder: 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( NSLocalizedString("Удалить из заблокированных?", comment: "Unblock confirmation title"), isPresented: $showUnblockConfirmation, presenting: pendingUnblock ) { user in Button(NSLocalizedString("Разблокировать", comment: "Unblock confirmation action"), role: .destructive) { pendingUnblock = nil showUnblockConfirmation = false Task { await unblock(user) } } Button(NSLocalizedString("Отмена", comment: "Common cancel"), role: .cancel) { pendingUnblock = nil showUnblockConfirmation = false } } message: { user in Text(String(format: NSLocalizedString("Пользователь \"%1$@\" будет удалён из списка заблокированных.", comment: "Unblock confirmation message"), user.displayName)) } } 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 { VStack(spacing: 12) { Image(systemName: "hand.raised") .font(.system(size: 48)) .foregroundColor(.secondary) Text(NSLocalizedString("У вас нет заблокированных пользователей", comment: "")) .font(.headline) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, 32) .listRowInsets(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16)) .listRowSeparator(.hidden) } private var initialLoadingState: some View { Section { ProgressView() .frame(maxWidth: .infinity, alignment: .center) } } private func errorState(_ message: String) -> some View { Section { Text(message) .foregroundColor(.red) .frame(maxWidth: .infinity, alignment: .center) } } @MainActor private func loadBlockedUsers() async { errorMessageDown = nil guard !isLoading, hasMore else { return } isLoading = true defer { isLoading = false } if offset == 0 { loadError = nil } 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 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 } } catch { activeAlert = .error(message: error.localizedDescription) if AppConfig.DEBUG { print("[BlockedUsersView] unblock failed: \(error)") } } } } private struct BlockedUser: Identifiable, Equatable { let id: UUID let login: String let fullName: String? let customName: String? let createdAt: Date private(set) var displayName: String private(set) var handle: String? var initials: String { let components = displayName.split(separator: " ") let nameInitials = components.prefix(2).compactMap { $0.first } if !nameInitials.isEmpty { return nameInitials .map { String($0).uppercased() } .joined() } if let handle { let filtered = handle.filter { $0.isLetter }.prefix(2) if !filtered.isEmpty { return filtered.uppercased() } } return "??" } init(payload: BlockedUserInfo) { self.id = payload.userId self.login = payload.login self.fullName = payload.fullName self.customName = payload.customName self.createdAt = payload.createdAt if let customName = payload.customName, !customName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { self.displayName = customName } else if let fullName = payload.fullName, !fullName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { self.displayName = fullName } else { self.displayName = payload.login } if !payload.login.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { self.handle = "@\(payload.login)" } else { self.handle = nil } } } 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 } } }