206 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
			
		
		
	
	
			206 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Swift
		
	
	
	
	
	
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
 | 
						||
    private var messageObserver: NSObjectProtocol?
 | 
						||
 | 
						||
    init(chatId: String, currentUserId: String?, chatService: ChatService = ChatService(), pageSize: Int = 30) {
 | 
						||
        self.chatId = chatId
 | 
						||
        self.currentUserId = currentUserId
 | 
						||
        self.chatService = chatService
 | 
						||
        self.pageSize = pageSize
 | 
						||
 | 
						||
        messageObserver = NotificationCenter.default.addObserver(
 | 
						||
            forName: .socketDidReceivePrivateMessage,
 | 
						||
            object: nil,
 | 
						||
            queue: .main
 | 
						||
        ) { [weak self] notification in
 | 
						||
            guard
 | 
						||
                let self,
 | 
						||
                let message = notification.object as? MessageItem,
 | 
						||
                message.chatId == self.chatId
 | 
						||
            else { return }
 | 
						||
            self.messages = Self.merge(existing: self.messages, newMessages: [message])
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    deinit {
 | 
						||
        if let observer = messageObserver {
 | 
						||
            NotificationCenter.default.removeObserver(observer)
 | 
						||
        }
 | 
						||
    }
 | 
						||
 | 
						||
    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<String> = []
 | 
						||
 | 
						||
        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
 | 
						||
    }
 | 
						||
}
 |