| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- //
- // 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)
- }
- }
- }
|