import Foundation @MainActor final class PrivateChatViewModel: ObservableObject { @Published private(set) var messages: [MessageItem] = [] @Published private(set) var isInitialLoading: Bool = false @Published private(set) var isLoadingMore: Bool = false @Published var errorMessage: String? @Published private(set) var isSending: Bool = false private let chatService: ChatService private let chatId: String private let currentUserId: String? private let pageSize: Int private let maxMessageLength: Int = 4096 private var hasMore: Bool = true private var didLoadInitially: Bool = false init(chatId: String, currentUserId: String?, chatService: ChatService = ChatService(), pageSize: Int = 30) { self.chatId = chatId self.currentUserId = currentUserId self.chatService = chatService self.pageSize = pageSize } func loadInitialHistory(force: Bool = false) { if !force && didLoadInitially { return } guard !isInitialLoading else { return } isInitialLoading = true errorMessage = nil hasMore = true chatService.fetchPrivateChatHistory(chatId: chatId, beforeMessageId: nil, limit: pageSize) { [weak self] result in guard let self else { return } switch result { case .success(let data): self.hasMore = data.hasMore self.messages = Self.merge(existing: [], newMessages: data.items) self.didLoadInitially = true case .failure(let error): self.errorMessage = self.message(for: error) } self.isInitialLoading = false } } func sendMessage(text: String, completion: @escaping (Bool) -> Void) { let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { completion(false) return } guard trimmed.count <= maxMessageLength else { errorMessage = NSLocalizedString("Сообщение слишком длинное.", comment: "") completion(false) return } guard !isSending else { completion(false) return } guard let currentUserId else { completion(false) return } isSending = true chatService.sendPrivateMessage(chatId: chatId, content: trimmed) { [weak self] result in guard let self else { return } switch result { case .success(let data): let newMessage = MessageItem( messageId: data.messageId, messageType: "text", chatId: data.chatId, senderId: currentUserId, senderData: nil, content: trimmed, mediaLink: nil, isViewed: true, createdAt: data.createdAt, updatedAt: data.createdAt, forwardMetadata: nil ) self.messages = Self.merge(existing: self.messages, newMessages: [newMessage]) self.errorMessage = nil completion(true) case .failure(let error): self.errorMessage = self.message(for: error) completion(false) } self.isSending = false } } func refresh() { didLoadInitially = false loadInitialHistory(force: true) } func loadMoreIfNeeded(for message: MessageItem) { guard didLoadInitially, !isInitialLoading, hasMore, !isLoadingMore else { return } guard let first = messages.first, first.id == message.id else { return } isLoadingMore = true chatService.fetchPrivateChatHistory(chatId: chatId, beforeMessageId: message.id, limit: pageSize) { [weak self] result in guard let self else { return } switch result { case .success(let data): self.hasMore = data.hasMore if !data.items.isEmpty { self.messages = Self.merge(existing: self.messages, newMessages: data.items) } case .failure(let error): self.errorMessage = self.message(for: error) } self.isLoadingMore = false } } private func message(for error: Error) -> String { if let chatError = error as? ChatServiceError { return chatError.errorDescription ?? NSLocalizedString("Не удалось загрузить историю чата.", comment: "") } if let networkError = error as? NetworkError { switch networkError { case .unauthorized: return NSLocalizedString("Сессия истекла. Войдите снова.", comment: "") case .invalidURL, .noResponse: return NSLocalizedString("Ошибка соединения с сервером.", comment: "") case .network(let underlying): return String(format: NSLocalizedString("Ошибка сети: %@", comment: ""), underlying.localizedDescription) case .server(let statusCode, _): return String(format: NSLocalizedString("Ошибка сервера (%@).", comment: ""), "\(statusCode)") } } return NSLocalizedString("Неизвестная ошибка. Попробуйте позже.", comment: "") } private static func merge(existing: [MessageItem], newMessages: [MessageItem]) -> [MessageItem] { var combined: [MessageItem] = [] combined.reserveCapacity(existing.count + newMessages.count) var seen: Set = [] for message in newMessages { if seen.insert(message.id).inserted { combined.append(message) } } for message in existing { if seen.insert(message.id).inserted { combined.append(message) } } combined.sort(by: compare) return combined } private static func compare(lhs: MessageItem, rhs: MessageItem) -> Bool { if let lhsDate = lhs.createdAt, let rhsDate = rhs.createdAt, lhsDate != rhsDate { return lhsDate < rhsDate } if let lhsId = Int(lhs.messageId), let rhsId = Int(rhs.messageId), lhsId != rhsId { return lhsId < rhsId } return lhs.messageId < rhs.messageId } }