Compare commits
	
		
			2 Commits
		
	
	
		
			8acacdb8c1
			...
			43a5d8193d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 43a5d8193d | |||
| 6b81860960 | 
							
								
								
									
										172
									
								
								yobble/Network/BlockedUsersService.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								yobble/Network/BlockedUsersService.swift
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,172 @@
 | 
				
			|||||||
 | 
					import Foundation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum BlockedUsersServiceError: LocalizedError {
 | 
				
			||||||
 | 
					    case unexpectedStatus(String)
 | 
				
			||||||
 | 
					    case decoding(debugDescription: String)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    var errorDescription: String? {
 | 
				
			||||||
 | 
					        switch self {
 | 
				
			||||||
 | 
					        case .unexpectedStatus(let message):
 | 
				
			||||||
 | 
					            return message
 | 
				
			||||||
 | 
					        case .decoding(let debugDescription):
 | 
				
			||||||
 | 
					            return AppConfig.DEBUG
 | 
				
			||||||
 | 
					                ? debugDescription
 | 
				
			||||||
 | 
					                : NSLocalizedString("Не удалось загрузить список.", comment: "Blocked users service decoding error")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct BlockedUserPayload: Decodable {
 | 
				
			||||||
 | 
					    let userId: UUID
 | 
				
			||||||
 | 
					    let login: String
 | 
				
			||||||
 | 
					    let fullName: String?
 | 
				
			||||||
 | 
					    let customName: String?
 | 
				
			||||||
 | 
					    let createdAt: Date
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final class BlockedUsersService {
 | 
				
			||||||
 | 
					    private let client: NetworkClient
 | 
				
			||||||
 | 
					    private let decoder: JSONDecoder
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    init(client: NetworkClient = .shared) {
 | 
				
			||||||
 | 
					        self.client = client
 | 
				
			||||||
 | 
					        self.decoder = JSONDecoder()
 | 
				
			||||||
 | 
					        self.decoder.keyDecodingStrategy = .convertFromSnakeCase
 | 
				
			||||||
 | 
					        self.decoder.dateDecodingStrategy = .custom(Self.decodeDate)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    func fetchBlockedUsers(completion: @escaping (Result<[BlockedUserPayload], Error>) -> Void) {
 | 
				
			||||||
 | 
					        client.request(
 | 
				
			||||||
 | 
					            path: "/v1/user/blacklist/list",
 | 
				
			||||||
 | 
					            method: .get,
 | 
				
			||||||
 | 
					            requiresAuth: true
 | 
				
			||||||
 | 
					        ) { [decoder] result in
 | 
				
			||||||
 | 
					            switch result {
 | 
				
			||||||
 | 
					            case .success(let response):
 | 
				
			||||||
 | 
					                do {
 | 
				
			||||||
 | 
					                    let apiResponse = try decoder.decode(APIResponse<[BlockedUserPayload]>.self, from: response.data)
 | 
				
			||||||
 | 
					                    guard apiResponse.status == "fine" else {
 | 
				
			||||||
 | 
					                        let message = apiResponse.detail ?? NSLocalizedString("Не удалось загрузить список.", comment: "Blocked users service unexpected status")
 | 
				
			||||||
 | 
					                        completion(.failure(BlockedUsersServiceError.unexpectedStatus(message)))
 | 
				
			||||||
 | 
					                        return
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    completion(.success(apiResponse.data))
 | 
				
			||||||
 | 
					                } catch {
 | 
				
			||||||
 | 
					                    let debugMessage = Self.describeDecodingError(error: error, data: response.data)
 | 
				
			||||||
 | 
					                    if AppConfig.DEBUG {
 | 
				
			||||||
 | 
					                        print("[BlockedUsersService] decode blocked users 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 fetchBlockedUsers() async throws -> [BlockedUserPayload] {
 | 
				
			||||||
 | 
					        try await withCheckedThrowingContinuation { continuation in
 | 
				
			||||||
 | 
					            fetchBlockedUsers { result in
 | 
				
			||||||
 | 
					                continuation.resume(with: result)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func decodeDate(from decoder: Decoder) throws -> Date {
 | 
				
			||||||
 | 
					        let container = try decoder.singleValueContainer()
 | 
				
			||||||
 | 
					        let string = try container.decode(String.self)
 | 
				
			||||||
 | 
					        if let date = iso8601WithFractionalSeconds.date(from: string) {
 | 
				
			||||||
 | 
					            return date
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let date = iso8601Simple.date(from: string) {
 | 
				
			||||||
 | 
					            return date
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        throw DecodingError.dataCorruptedError(
 | 
				
			||||||
 | 
					            in: container,
 | 
				
			||||||
 | 
					            debugDescription: "Невозможно декодировать дату: \(string)"
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func describeDecodingError(error: Error, data: Data) -> String {
 | 
				
			||||||
 | 
					        var parts: [String] = []
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let decodingError = error as? DecodingError {
 | 
				
			||||||
 | 
					            parts.append(decodingDescription(from: decodingError))
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            parts.append(error.localizedDescription)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if let payload = truncatedPayload(from: data) {
 | 
				
			||||||
 | 
					            parts.append("payload=\(payload)")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return parts.joined(separator: "\n")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func decodingDescription(from error: DecodingError) -> String {
 | 
				
			||||||
 | 
					        switch error {
 | 
				
			||||||
 | 
					        case .typeMismatch(let type, let context):
 | 
				
			||||||
 | 
					            return "Type mismatch for \(type) at \(codingPath(from: context)): \(context.debugDescription)"
 | 
				
			||||||
 | 
					        case .valueNotFound(let type, let context):
 | 
				
			||||||
 | 
					            return "Value not found for \(type) at \(codingPath(from: context)): \(context.debugDescription)"
 | 
				
			||||||
 | 
					        case .keyNotFound(let key, let context):
 | 
				
			||||||
 | 
					            return "Missing key '\(key.stringValue)' at \(codingPath(from: context)): \(context.debugDescription)"
 | 
				
			||||||
 | 
					        case .dataCorrupted(let context):
 | 
				
			||||||
 | 
					            return "Corrupted data at \(codingPath(from: context)): \(context.debugDescription)"
 | 
				
			||||||
 | 
					        @unknown default:
 | 
				
			||||||
 | 
					            return error.localizedDescription
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func codingPath(from context: DecodingError.Context) -> String {
 | 
				
			||||||
 | 
					        let path = context.codingPath.map { $0.stringValue }.filter { !$0.isEmpty }
 | 
				
			||||||
 | 
					        return path.isEmpty ? "root" : path.joined(separator: ".")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func truncatedPayload(from data: Data, limit: Int = 512) -> String? {
 | 
				
			||||||
 | 
					        guard let string = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines),
 | 
				
			||||||
 | 
					              !string.isEmpty else {
 | 
				
			||||||
 | 
					            return nil
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if string.count <= limit {
 | 
				
			||||||
 | 
					            return string
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let index = string.index(string.startIndex, offsetBy: limit)
 | 
				
			||||||
 | 
					        return String(string[string.startIndex..<index]) + "…"
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static func errorMessage(from data: Data) -> String? {
 | 
				
			||||||
 | 
					        if let apiError = try? JSONDecoder().decode(ErrorResponse.self, from: data) {
 | 
				
			||||||
 | 
					            if let detail = apiError.detail, !detail.isEmpty {
 | 
				
			||||||
 | 
					                return detail
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            if let message = apiError.data?.message, !message.isEmpty {
 | 
				
			||||||
 | 
					                return message
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if let string = String(data: data, encoding: .utf8), !string.isEmpty {
 | 
				
			||||||
 | 
					            return string
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return nil
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static let iso8601WithFractionalSeconds: ISO8601DateFormatter = {
 | 
				
			||||||
 | 
					        let formatter = ISO8601DateFormatter()
 | 
				
			||||||
 | 
					        formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
 | 
				
			||||||
 | 
					        return formatter
 | 
				
			||||||
 | 
					    }()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static let iso8601Simple: ISO8601DateFormatter = {
 | 
				
			||||||
 | 
					        let formatter = ISO8601DateFormatter()
 | 
				
			||||||
 | 
					        formatter.formatOptions = [.withInternetDateTime]
 | 
				
			||||||
 | 
					        return formatter
 | 
				
			||||||
 | 
					    }()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -102,7 +102,7 @@
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "OK" : {
 | 
					    "OK" : {
 | 
				
			||||||
      "comment" : "Profile update alert button",
 | 
					      "comment" : "Common OK\nProfile update alert button",
 | 
				
			||||||
      "localizations" : {
 | 
					      "localizations" : {
 | 
				
			||||||
        "en" : {
 | 
					        "en" : {
 | 
				
			||||||
          "stringUnit" : {
 | 
					          "stringUnit" : {
 | 
				
			||||||
@ -386,6 +386,9 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "Добавление новых блокировок появится позже." : {
 | 
				
			||||||
 | 
					      "comment" : "Add blocked user placeholder message"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "Другое" : {
 | 
					    "Другое" : {
 | 
				
			||||||
      "localizations" : {
 | 
					      "localizations" : {
 | 
				
			||||||
        "en" : {
 | 
					        "en" : {
 | 
				
			||||||
@ -930,6 +933,9 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "Не удалось загрузить список." : {
 | 
				
			||||||
 | 
					      "comment" : "Blocked users service decoding error\nBlocked users service unexpected status"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "Не удалось загрузить текст правил." : {
 | 
					    "Не удалось загрузить текст правил." : {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -1265,6 +1271,9 @@
 | 
				
			|||||||
    },
 | 
					    },
 | 
				
			||||||
    "Открыть правила" : {
 | 
					    "Открыть правила" : {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "Отмена" : {
 | 
				
			||||||
 | 
					      "comment" : "Common cancel"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "Отображаемое имя" : {
 | 
					    "Отображаемое имя" : {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1606,6 +1615,9 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "Пользователь \"%1$@\" будет удалён из списка заблокированных." : {
 | 
				
			||||||
 | 
					      "comment" : "Unblock confirmation message"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "Пользователь Системы 1" : {
 | 
					    "Пользователь Системы 1" : {
 | 
				
			||||||
      "comment" : "Тестовая подмена офф аккаунта",
 | 
					      "comment" : "Тестовая подмена офф аккаунта",
 | 
				
			||||||
      "extractionState" : "manual",
 | 
					      "extractionState" : "manual",
 | 
				
			||||||
@ -1825,7 +1837,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "Разблокировать" : {
 | 
					    "Разблокировать" : {
 | 
				
			||||||
 | 
					      "comment" : "Unblock confirmation action"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "Разрешить пересылку сообщений" : {
 | 
					    "Разрешить пересылку сообщений" : {
 | 
				
			||||||
      "localizations" : {
 | 
					      "localizations" : {
 | 
				
			||||||
@ -2024,6 +2036,9 @@
 | 
				
			|||||||
    "Скопировать" : {
 | 
					    "Скопировать" : {
 | 
				
			||||||
      "comment" : "Search placeholder copy"
 | 
					      "comment" : "Search placeholder copy"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "Скоро" : {
 | 
				
			||||||
 | 
					      "comment" : "Add blocked user placeholder title"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "Скоро появится мини-игра, где можно заработать очки для кастомизации профиля. Следите за обновлениями!" : {
 | 
					    "Скоро появится мини-игра, где можно заработать очки для кастомизации профиля. Следите за обновлениями!" : {
 | 
				
			||||||
      "comment" : "Concept tab placeholder description"
 | 
					      "comment" : "Concept tab placeholder description"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -2203,6 +2218,9 @@
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "Удалить из заблокированных?" : {
 | 
				
			||||||
 | 
					      "comment" : "Unblock confirmation title"
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "Удалить чат (скоро)" : {
 | 
					    "Удалить чат (скоро)" : {
 | 
				
			||||||
      "localizations" : {
 | 
					      "localizations" : {
 | 
				
			||||||
        "en" : {
 | 
					        "en" : {
 | 
				
			||||||
 | 
				
			|||||||
@ -2,10 +2,21 @@ 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 loadError: String?
 | 
				
			||||||
 | 
					    @State private var showAddBlockedUserAlert = false
 | 
				
			||||||
 | 
					    @State private var pendingUnblock: BlockedUser?
 | 
				
			||||||
 | 
					    @State private var showUnblockConfirmation = false
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private let blockedUsersService = BlockedUsersService()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var body: some View {
 | 
					    var body: some View {
 | 
				
			||||||
        List {
 | 
					        List {
 | 
				
			||||||
            if blockedUsers.isEmpty {
 | 
					            if isLoading && blockedUsers.isEmpty {
 | 
				
			||||||
 | 
					                loadingState
 | 
				
			||||||
 | 
					            } else if let loadError, blockedUsers.isEmpty {
 | 
				
			||||||
 | 
					                errorState(loadError)
 | 
				
			||||||
 | 
					            } else if blockedUsers.isEmpty {
 | 
				
			||||||
                emptyState
 | 
					                emptyState
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                Section(header: Text(NSLocalizedString("Заблокированные", comment: ""))) {
 | 
					                Section(header: Text(NSLocalizedString("Заблокированные", comment: ""))) {
 | 
				
			||||||
@ -29,23 +40,57 @@ struct BlockedUsersView: View {
 | 
				
			|||||||
                                }
 | 
					                                }
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
                            Spacer()
 | 
					                            Spacer()
 | 
				
			||||||
                            Button(role: .destructive) {
 | 
					 | 
				
			||||||
                                unblock(user)
 | 
					 | 
				
			||||||
                            } label: {
 | 
					 | 
				
			||||||
                                Text(NSLocalizedString("Разблокировать", comment: ""))
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                            .buttonStyle(.borderless)
 | 
					 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        .padding(.vertical, 4)
 | 
					                        .padding(.vertical, 4)
 | 
				
			||||||
 | 
					                        .swipeActions(edge: .trailing) {
 | 
				
			||||||
 | 
					                            Button(role: .destructive) {
 | 
				
			||||||
 | 
					                                pendingUnblock = user
 | 
				
			||||||
 | 
					                                showUnblockConfirmation = true
 | 
				
			||||||
 | 
					                            } label: {
 | 
				
			||||||
 | 
					                                Label(NSLocalizedString("Разблокировать", comment: ""), systemImage: "person.crop.circle.badge.xmark")
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .navigationTitle(NSLocalizedString("Заблокированные", comment: ""))
 | 
					        .navigationTitle(NSLocalizedString("Заблокированные", comment: ""))
 | 
				
			||||||
        .navigationBarTitleDisplayMode(.inline)
 | 
					        .navigationBarTitleDisplayMode(.inline)
 | 
				
			||||||
 | 
					        .toolbar {
 | 
				
			||||||
 | 
					            ToolbarItem(placement: .navigationBarTrailing) {
 | 
				
			||||||
 | 
					                Button {
 | 
				
			||||||
 | 
					                    showAddBlockedUserAlert = true
 | 
				
			||||||
 | 
					                } label: {
 | 
				
			||||||
 | 
					                    Image(systemName: "plus")
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
        .task {
 | 
					        .task {
 | 
				
			||||||
            await loadBlockedUsers()
 | 
					            await loadBlockedUsers()
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        .refreshable {
 | 
				
			||||||
 | 
					            await loadBlockedUsers()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .alert(NSLocalizedString("Скоро", comment: "Add blocked user placeholder title"), isPresented: $showAddBlockedUserAlert) {
 | 
				
			||||||
 | 
					            Button(NSLocalizedString("OK", comment: "Common OK"), role: .cancel) {}
 | 
				
			||||||
 | 
					        } message: {
 | 
				
			||||||
 | 
					            Text(NSLocalizedString("Добавление новых блокировок появится позже.", comment: "Add blocked user placeholder message"))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .confirmationDialog(
 | 
				
			||||||
 | 
					            NSLocalizedString("Удалить из заблокированных?", comment: "Unblock confirmation title"),
 | 
				
			||||||
 | 
					            isPresented: $showUnblockConfirmation,
 | 
				
			||||||
 | 
					            presenting: pendingUnblock
 | 
				
			||||||
 | 
					        ) { user in
 | 
				
			||||||
 | 
					            Button(NSLocalizedString("Разблокировать", comment: "Unblock confirmation action"), role: .destructive) {
 | 
				
			||||||
 | 
					                unblock(user)
 | 
				
			||||||
 | 
					                pendingUnblock = nil
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Button(NSLocalizedString("Отмена", comment: "Common cancel"), role: .cancel) {
 | 
				
			||||||
 | 
					                pendingUnblock = nil
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } message: { user in
 | 
				
			||||||
 | 
					            Text(String(format: NSLocalizedString("Пользователь \"%1$@\" будет удалён из списка заблокированных.", comment: "Unblock confirmation message"), user.displayName))
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private var emptyState: some View {
 | 
					    private var emptyState: some View {
 | 
				
			||||||
@ -67,8 +112,41 @@ struct BlockedUsersView: View {
 | 
				
			|||||||
        .listRowSeparator(.hidden)
 | 
					        .listRowSeparator(.hidden)
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private var loadingState: 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 {
 | 
					    private func loadBlockedUsers() async {
 | 
				
			||||||
        // TODO: integrate with real data source once available
 | 
					        if isLoading {
 | 
				
			||||||
 | 
					            return
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        isLoading = true
 | 
				
			||||||
 | 
					        loadError = nil
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        do {
 | 
				
			||||||
 | 
					            let payloads = try await blockedUsersService.fetchBlockedUsers()
 | 
				
			||||||
 | 
					            blockedUsers = payloads.map(BlockedUser.init)
 | 
				
			||||||
 | 
					        } catch {
 | 
				
			||||||
 | 
					            loadError = error.localizedDescription
 | 
				
			||||||
 | 
					            if AppConfig.DEBUG {
 | 
				
			||||||
 | 
					                print("[BlockedUsersView] load blocked users failed: \(error)")
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        isLoading = false
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private func unblock(_ user: BlockedUser) {
 | 
					    private func unblock(_ user: BlockedUser) {
 | 
				
			||||||
@ -79,8 +157,13 @@ struct BlockedUsersView: View {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
private struct BlockedUser: Identifiable, Equatable {
 | 
					private struct BlockedUser: Identifiable, Equatable {
 | 
				
			||||||
    let id: UUID
 | 
					    let id: UUID
 | 
				
			||||||
    let displayName: String
 | 
					    let login: String
 | 
				
			||||||
    let handle: String?
 | 
					    let fullName: String?
 | 
				
			||||||
 | 
					    let customName: String?
 | 
				
			||||||
 | 
					    let createdAt: Date
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private(set) var displayName: String
 | 
				
			||||||
 | 
					    private(set) var handle: String?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    var initials: String {
 | 
					    var initials: String {
 | 
				
			||||||
        let components = displayName.split(separator: " ")
 | 
					        let components = displayName.split(separator: " ")
 | 
				
			||||||
@ -100,4 +183,26 @@ private struct BlockedUser: Identifiable, Equatable {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        return "??"
 | 
					        return "??"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    init(payload: BlockedUserPayload) {
 | 
				
			||||||
 | 
					        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
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user