283 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			283 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
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<UUID> = []
 | 
						||
    @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
 | 
						||
            }
 | 
						||
        }
 | 
						||
        .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
 | 
						||
                userRow(user)
 | 
						||
            }
 | 
						||
            if isLoading {
 | 
						||
                Text("Идет загрузка...")
 | 
						||
                    .frame(maxWidth: .infinity, alignment: .center)
 | 
						||
                    .listRowBackground(Color.clear)
 | 
						||
                    .listRowSeparator(.hidden)
 | 
						||
            } else if let errorMessage = errorMessageDown {
 | 
						||
                Text(errorMessage)
 | 
						||
                    .foregroundColor(.red)
 | 
						||
                    .frame(maxWidth: .infinity, alignment: .center)
 | 
						||
                    .listRowBackground(Color.clear)
 | 
						||
                    .listRowSeparator(.hidden)
 | 
						||
            }
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    private func userRow(_ user: BlockedUser) -> some View {
 | 
						||
        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
 | 
						||
        }
 | 
						||
    }
 | 
						||
}
 |