186 lines
6.5 KiB
Swift
186 lines
6.5 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
|
||
|
||
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<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
|
||
}
|
||
}
|