// // LNIMChatViewModel.swift // Lanu // // Created by OneeChan on 2025/12/11. // import Foundation import Combine enum LNIMChatDataSourceChangeType { case insert case delete case reload } protocol LNIMChatViewModelNotify { func onIMMessageDataChanged(viewModel: LNIMChatViewModel, index: Int, type: LNIMChatDataSourceChangeType, toBottom: Bool) func onIMMessageDatasChanged(viewModel: LNIMChatViewModel, indexs: [IndexPath], type: LNIMChatDataSourceChangeType, toBottom: Bool) func onIMDidSendMessage(to uid: String) } extension LNIMChatViewModelNotify { func onIMMessageDataChanged(viewModel: LNIMChatViewModel, index: Int, type: LNIMChatDataSourceChangeType, toBottom: Bool) { } func onIMMessageDatasChanged(viewModel: LNIMChatViewModel, indexs: [IndexPath], type: LNIMChatDataSourceChangeType, toBottom: Bool) { } func onIMDidSendMessage(to uid: String) { } } class LNIMChatViewModel: NSObject { let userId: String @Published private(set) var userInfo: LNUserProfileVO? @Published private(set) var remark: String? // 消息 private var loading = false private var topMessage: V2TIMMessage? private(set) var allMessage: [LNIMMessageData] = [] private var lastData: Date? = nil private var orderMessageCache: [String: LNIMMessageData] = [:] private var callMessageCache: [String: LNIMMessageData] = [:] // 配置 @Published private(set) var messageOpt: V2TIMReceiveMessageOpt = .RECEIVE_MESSAGE @Published private(set) var myOrders: [LNUnfinishedOrderVO] = [] @Published private(set) var peerSkills: [LNGameMateSkillVO] = [] var needMarkAutoReply = false init(userId: String) { self.userId = userId super.init() V2TIMManager.sharedInstance().addAdvancedMsgListener(listener: self) if !userId.isImOfficialId { loadUserInfo() getUserRemark() getUnfinishOrder() } LNEventDeliver.addObserver(self) V2TIMManager.sharedInstance().subscribeUserStatus(userIDList: [userId], succ: nil) } func cleanUnread() { LNIMManager.shared.cleanConversationUnread(uid: userId) } } extension LNIMChatViewModel { func getUnfinishOrder() { var nextTag: String = "" var orders: [LNUnfinishedOrderVO] = [] func _loadOrder() { LNOrderManager.shared.getUnfinishedOrderWith(uid: userId, size: 30, next: nextTag) { [weak self] list, next in guard let self else { return } if let list, let next { orders.append(contentsOf: list.filter({ $0.status == .created || $0.status == .waitingForAccept || $0.status == .accepted || $0.status == .servicing })) nextTag = next } if next?.isEmpty == false { _loadOrder() } else { myOrders = orders } } } _loadOrder() } private func loadUserInfo() { LNProfileManager.shared.getUserProfileDetail(uid: userId) { [weak self] info in guard let self else { return } guard let info else { return } userInfo = info if info.playmate { loadSkills() } } } private func loadSkills() { LNGameMateManager.shared.getUserSkills(uid: userId) { [weak self] skills in guard let self else { return } guard let skills else { return } peerSkills = skills } } private func getUserRemark() { LNIMManager.shared.getUsersRemark(uids: [userId]) { [weak self] remarks in guard let self else { return } guard let remarks, !remarks.isEmpty else { return } remark = remarks.first(where: { $0.userNo == self.userId })?.note } } } // MARK: 消息管理 extension LNIMChatViewModel { func updateMessageOpt(opt: V2TIMReceiveMessageOpt) { V2TIMManager.sharedInstance().setC2CReceiveMessageOpt(userIDList: [userId], opt: opt) { [weak self] in guard let self else { return } messageOpt = opt } } func updateRemark(_ remark: String) { LNIMManager.shared.setUserRemark(uid: userId, remark: remark) { [weak self] success in guard let self else { return } guard success else { return } self.remark = remark } } private func getMessageOpt() { V2TIMManager.sharedInstance().getC2CReceiveMessageOpt(userIDList: [userId]) { [weak self] infos in guard let self else { return } guard let infos, !infos.isEmpty else { return } messageOpt = infos.first(where: { $0.userID == self.userId })?.receiveOpt ?? .RECEIVE_MESSAGE } fail: { _, _ in } } } // MARK: 消息发送 extension LNIMChatViewModel { func sendTextMessage(text: String) { guard !text.isEmpty else { return } guard let message = V2TIMManager.sharedInstance().createTextMessage(text: text) else { return } sendMessage(message: message) } func sendVoiceMessage(voicePath: String, duration: Double) { guard !voicePath.isEmpty, FileManager.default.fileExists(atPath: voicePath) else { // MARK: 文件有效性判断 return } guard duration > 1.0 else { // MARK: 最短限制 return } guard let message = V2TIMManager.sharedInstance().createSoundMessage( audioFilePath: voicePath, duration: Int32(ceil(duration))) else { return } sendMessage(message: message) } func sendImageMessage(image: UIImage) { guard let imageData = image.jpegData(compressionQuality: 1.0) else { return } try? FileManager.default.createDirectory( at: URL.imageCacheFolder, withIntermediateDirectories: true, attributes: nil ) let path = URL.imageCacheFolder.appendingPathComponent("\(curTimeInMicro).jpeg") do { try imageData.write(to: path, options: .atomic) guard let message = V2TIMManager.sharedInstance().createImageMessage(imagePath: path.path) else { return } sendMessage(message: message) } catch { return } } func resendMessage(message: LNIMMessageData) { guard message.imMessage.isSelf, message.imMessage.status == .MSG_STATUS_SEND_FAIL else { return } guard let index = allMessage.firstIndex(of: message) else { return } allMessage.remove(at: index) notifyMessageChanged(index: index, type: .delete, toBottom: false) sendMessage(message: message.imMessage) } private func sendMessage(message: V2TIMMessage) { if needMarkAutoReply { LNGameMateManager.shared.markUseAutoReplay(uid: userId) needMarkAutoReply = false } message.needReadReceipt = true let datas = transUIMsgFromIMMsg(messages: [message]) guard !datas.isEmpty else { return } if allMessage.isEmpty { topMessage = message } allMessage.append(contentsOf: datas) notifyMessageChanged(index: allMessage.count - 1, type: .insert, toBottom: true) let pushInfo = V2TIMOfflinePushInfo() pushInfo.title = myUserInfo.nickname V2TIMManager.sharedInstance().sendMessage( message: message, receiver: userId, groupID: "", priority: .PRIORITY_NORMAL, onlineUserOnly: false, offlinePushInfo: pushInfo, progress: nil) { [weak self] in guard let self else { return } if let index = allMessage.firstIndex(of: datas.last!) { notifyMessageChanged(index: index, type: .reload, toBottom: false) } LNEventDeliver.notifyEvent { ($0 as? LNIMChatViewModelNotify)?.onIMDidSendMessage(to: self.userId) } if let last = allMessage.filter({ $0.isSelf && $0.type.isUserContent }).suffix(5).first, Int(curTime - (last.imMessage.timestamp?.timeIntervalSince1970 ?? 0)) <= 10 * 60 { let event = LNReportEvent() event.event = .deep_chat_hit event.scene = .playmate_deep_chat_report event.uid = userId LNReportManager.shared.reportEvent(event: event) } } fail: { [weak self] code, err in guard let self else { return } if let index = allMessage.firstIndex(of: datas.last!) { notifyMessageChanged(index: index, type: .reload, toBottom: false) } if code == LNIMCustomErrorCode.inBlackList.rawValue { showToast(.init(key: "A00087")) } else if code == LNIMCustomErrorCode.userNotExist.rawValue { showToast(.init(key: "A00088")) } else { showToast(err?.description) } } } } // MARK: 消息拉取 extension LNIMChatViewModel { func loadNextPage(handler: @escaping (_ success: Bool, _ isFirst: Bool) -> Void) { guard !loading else { handler(false, false) return } loading = true V2TIMManager.sharedInstance().getC2CHistoryMessageList( userID: userId, count: 300, lastMsg: topMessage) { [weak self] list in guard let self else { return } let isFirst = allMessage.isEmpty guard let list, !list.isEmpty else { loading = false handler(false, isFirst) return } topMessage = list.last let messages = transUIMsgFromIMMsg(messages: list) allMessage.insert(contentsOf: messages, at: 0) loading = false handler(true, isFirst) getMessageReadReceipts(list: list) } fail: { [weak self] code, err in guard let self else { return } handler(false, false) loading = false } } private func getMessageReadReceipts(list: [V2TIMMessage]) { V2TIMManager.sharedInstance().getMessageReadReceipts(messageList: list) { [weak self] receipts in guard let self else { return } guard let receipts, !receipts.isEmpty else { return } var messageIndex: [IndexPath] = [] receipts.forEach { item in if let index = self.allMessage.firstIndex(where: { $0.imMessage.msgID == item.msgID }) { self.allMessage[index].readReceipt = item messageIndex.append(.init(row: index, section: 0)) } } notifyMessagesChanged(indexs: messageIndex, type: .reload, toBottom: false) } fail: { code, err in } } } // MARK: 消息处理 extension LNIMChatViewModel { private func transUIMsgFromIMMsg(messages: [V2TIMMessage]) -> [LNIMMessageData] { var datas: [LNIMMessageData] = [] for message in messages.reversed() { // 被标记不展示 if message.isExcludedFromLastMessage || message.isExcludedFromUnreadCount { continue } let data = LNIMMessageData(imMessage: message) if case .order = data.type { guard let orderMessage: LNIMOrderMessage = data.decodeCustomMessage() else { continue // 解析失败,忽略 } if let oldMessage = orderMessageCache[orderMessage.orderId] { // 存在旧的订单信息 if (oldMessage.imMessage.timestamp?.timeIntervalSince1970 ?? 0) > (message.timestamp?.timeIntervalSince1970 ?? 0) { // 消息为旧的订单信息,忽略 continue } else { // 订单的新消息 removeOldMessage(oldMessage) if let index = datas.firstIndex(of: oldMessage) { datas.remove(at: index) } } } orderMessageCache[orderMessage.orderId] = data } else if case .call(let callId) = data.type { guard let callMessage: LNIMVoiceCallMessage = data.decodeCustomMessage() else { continue // 解析失败,忽略 } // ->>>>> ⚠️ 挂断自己发起的通话,消息发送者会变成对方 // ->>>>> ⚠️ 这里需要纠正发送者 data.isSelf = callMessage.inviter.isMyUid // ->>>>> ⚠️ if let oldMessage = callMessageCache[callId] { // 存在旧的订单信息 if (oldMessage.imMessage.timestamp?.timeIntervalSince1970 ?? 0) > (message.timestamp?.timeIntervalSince1970 ?? 0) { // 消息为旧的订单信息,忽略 continue } else { // 订单的新消息 removeOldMessage(oldMessage) if let index = datas.firstIndex(of: oldMessage) { datas.remove(at: index) } } } callMessageCache[callId] = data } datas.append(data) } datas.forEach { guard let index = datas.firstIndex(of: $0) else { return } if let dateMessage = buildDateMessageIfNeed(message: $0.imMessage) { datas.insert(dateMessage, at: index) } } return datas } private func removeOldMessage(_ message: LNIMMessageData) { guard let index = allMessage.firstIndex(of: message) else { return } // 移除旧的订单消息 var removeIndexs: [IndexPath] = [] allMessage.remove(at: index) removeIndexs.append(.init(row: index, section: 0)) if index - 1 >= 0 && index <= allMessage.count - 1, case .system = allMessage[index - 1].type, case .system = allMessage[index].type { // 出现连续的两个时间戳消息,需要将前一个移除 allMessage.remove(at: index - 1) removeIndexs.append(.init(row: index - 1, section: 0)) } notifyMessagesChanged(indexs: removeIndexs, type: .delete, toBottom: false) } private func buildDateMessageIfNeed(message: V2TIMMessage) -> LNIMMessageData? { guard let time = message.timestamp else { return nil } if let lastData, time.timeIntervalSince(lastData) < 5 * 60 { return nil } lastData = message.timestamp return LNIMMessageData.buildDateMessage(date: time) } } // MARK: 接收消息推送 extension LNIMChatViewModel: V2TIMAdvancedMsgListener { func onRecvNewMessage(msg: V2TIMMessage!) { guard msg.userID == userId else { return } let list = transUIMsgFromIMMsg(messages: [msg]) if list.isEmpty { return } allMessage.append(contentsOf: list) notifyMessageChanged(index: allMessage.count - 1, type: .insert, toBottom: true) var isOrderMessage = false for item in list { if case .order = item.type { isOrderMessage = true break } } if isOrderMessage { getUnfinishOrder() } } func onRecvMessageReadReceipts(receiptList: [V2TIMMessageReceipt]!) { var indexs: [Int] = [] receiptList.forEach { item in if let index = allMessage.firstIndex(where: { $0.imMessage.msgID == item.msgID }) { allMessage[index].readReceipt = item indexs.append(index) } } notifyMessagesChanged(indexs: indexs.map({ .init(row: $0, section: 0) }), type: .reload, toBottom: false) } } // MARK: 对外通知 extension LNIMChatViewModel { private func notifyMessageChanged(index: Int, type: LNIMChatDataSourceChangeType, toBottom: Bool) { LNEventDeliver.notifyEvent { ($0 as? LNIMChatViewModelNotify)?.onIMMessageDataChanged(viewModel: self, index: index, type: type, toBottom: toBottom) } } private func notifyMessagesChanged(indexs: [IndexPath], type: LNIMChatDataSourceChangeType, toBottom: Bool) { LNEventDeliver.notifyEvent { ($0 as? LNIMChatViewModelNotify)?.onIMMessageDatasChanged(viewModel: self, indexs: indexs, type: type, toBottom: toBottom) } } }