LNIMChatViewModel.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. //
  2. // LNIMChatViewModel.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/12/11.
  6. //
  7. import Foundation
  8. import Combine
  9. enum LNIMChatDataSourceChangeType {
  10. case insert
  11. case delete
  12. case reload
  13. }
  14. protocol LNIMChatViewModelNotify {
  15. func onIMMessageDataChanged(viewModel: LNIMChatViewModel, index: Int,
  16. type: LNIMChatDataSourceChangeType, toBottom: Bool)
  17. func onIMMessageDatasChanged(viewModel: LNIMChatViewModel, indexs: [IndexPath],
  18. type: LNIMChatDataSourceChangeType, toBottom: Bool)
  19. func onIMDidSendMessage(to uid: String)
  20. }
  21. extension LNIMChatViewModelNotify {
  22. func onIMMessageDataChanged(viewModel: LNIMChatViewModel, index: Int,
  23. type: LNIMChatDataSourceChangeType, toBottom: Bool) { }
  24. func onIMMessageDatasChanged(viewModel: LNIMChatViewModel, indexs: [IndexPath],
  25. type: LNIMChatDataSourceChangeType, toBottom: Bool) { }
  26. func onIMDidSendMessage(to uid: String) { }
  27. }
  28. class LNIMChatViewModel: NSObject {
  29. let userId: String
  30. @Published
  31. private(set) var userInfo: LNUserProfileVO?
  32. @Published
  33. private(set) var remark: String?
  34. // 消息
  35. private var loading = false
  36. private var topMessage: V2TIMMessage?
  37. private(set) var allMessage: [LNIMMessageData] = []
  38. private var lastData: Date? = nil
  39. private var orderMessageCache: [String: LNIMMessageData] = [:]
  40. private var callMessageCache: [String: LNIMMessageData] = [:]
  41. // 配置
  42. @Published
  43. private(set) var messageOpt: V2TIMReceiveMessageOpt = .RECEIVE_MESSAGE
  44. @Published
  45. private(set) var myOrders: [LNUnfinishedOrderVO] = []
  46. @Published
  47. private(set) var peerSkills: [LNGameMateSkillVO] = []
  48. var needMarkAutoReply = false
  49. init(userId: String) {
  50. self.userId = userId
  51. super.init()
  52. V2TIMManager.sharedInstance().addAdvancedMsgListener(listener: self)
  53. if !userId.isImOfficialId {
  54. loadUserInfo()
  55. getUserRemark()
  56. getUnfinishOrder()
  57. }
  58. LNEventDeliver.addObserver(self)
  59. V2TIMManager.sharedInstance().subscribeUserStatus(userIDList: [userId], succ: nil)
  60. }
  61. func cleanUnread() {
  62. LNIMManager.shared.cleanConversationUnread(uid: userId)
  63. }
  64. }
  65. extension LNIMChatViewModel {
  66. func getUnfinishOrder() {
  67. var nextTag: String = ""
  68. var orders: [LNUnfinishedOrderVO] = []
  69. func _loadOrder() {
  70. LNOrderManager.shared.getUnfinishedOrderWith(uid: userId, size: 30, next: nextTag)
  71. { [weak self] list, next in
  72. guard let self else { return }
  73. if let list, let next {
  74. orders.append(contentsOf: list.filter({
  75. $0.status == .created
  76. || $0.status == .waitingForAccept
  77. || $0.status == .accepted
  78. || $0.status == .servicing
  79. }))
  80. nextTag = next
  81. }
  82. if next?.isEmpty == false {
  83. _loadOrder()
  84. } else {
  85. myOrders = orders
  86. }
  87. }
  88. }
  89. _loadOrder()
  90. }
  91. private func loadUserInfo() {
  92. LNProfileManager.shared.getUserProfileDetail(uid: userId) { [weak self] info in
  93. guard let self else { return }
  94. guard let info else { return }
  95. userInfo = info
  96. if info.playmate {
  97. loadSkills()
  98. }
  99. }
  100. }
  101. private func loadSkills() {
  102. LNGameMateManager.shared.getUserSkills(uid: userId) { [weak self] skills in
  103. guard let self else { return }
  104. guard let skills else { return }
  105. peerSkills = skills
  106. }
  107. }
  108. private func getUserRemark() {
  109. LNIMManager.shared.getUsersRemark(uids: [userId]) { [weak self] remarks in
  110. guard let self else { return }
  111. guard let remarks, !remarks.isEmpty else { return }
  112. remark = remarks.first(where: { $0.userNo == self.userId })?.note
  113. }
  114. }
  115. }
  116. // MARK: 消息管理
  117. extension LNIMChatViewModel {
  118. func updateMessageOpt(opt: V2TIMReceiveMessageOpt) {
  119. V2TIMManager.sharedInstance().setC2CReceiveMessageOpt(userIDList: [userId], opt: opt) { [weak self] in
  120. guard let self else { return }
  121. messageOpt = opt
  122. }
  123. }
  124. func updateRemark(_ remark: String) {
  125. LNIMManager.shared.setUserRemark(uid: userId, remark: remark) { [weak self] success in
  126. guard let self else { return }
  127. guard success else { return }
  128. self.remark = remark
  129. }
  130. }
  131. private func getMessageOpt() {
  132. V2TIMManager.sharedInstance().getC2CReceiveMessageOpt(userIDList: [userId]) { [weak self] infos in
  133. guard let self else { return }
  134. guard let infos, !infos.isEmpty else { return }
  135. messageOpt = infos.first(where: { $0.userID == self.userId })?.receiveOpt ?? .RECEIVE_MESSAGE
  136. } fail: { _, _ in }
  137. }
  138. }
  139. // MARK: 消息发送
  140. extension LNIMChatViewModel {
  141. func sendTextMessage(text: String) {
  142. guard !text.isEmpty else { return }
  143. guard let message = V2TIMManager.sharedInstance().createTextMessage(text: text) else { return }
  144. sendMessage(message: message)
  145. }
  146. func sendVoiceMessage(voicePath: String, duration: Double) {
  147. guard !voicePath.isEmpty,
  148. FileManager.default.fileExists(atPath: voicePath) else {
  149. // MARK: 文件有效性判断
  150. return
  151. }
  152. guard duration > 1.0 else {
  153. // MARK: 最短限制
  154. return
  155. }
  156. guard let message = V2TIMManager.sharedInstance().createSoundMessage(
  157. audioFilePath: voicePath,
  158. duration: Int32(ceil(duration))) else { return }
  159. sendMessage(message: message)
  160. }
  161. func sendImageMessage(image: UIImage) {
  162. guard let imageData = image.jpegData(compressionQuality: 1.0) else { return }
  163. try? FileManager.default.createDirectory(
  164. at: URL.imageCacheFolder,
  165. withIntermediateDirectories: true,
  166. attributes: nil
  167. )
  168. let path = URL.imageCacheFolder.appendingPathComponent("\(curTimeInMicro).jpeg")
  169. do {
  170. try imageData.write(to: path, options: .atomic)
  171. guard let message = V2TIMManager.sharedInstance().createImageMessage(imagePath: path.path) else { return }
  172. sendMessage(message: message)
  173. } catch {
  174. return
  175. }
  176. }
  177. func resendMessage(message: LNIMMessageData) {
  178. guard message.imMessage.isSelf,
  179. message.imMessage.status == .MSG_STATUS_SEND_FAIL else { return }
  180. guard let index = allMessage.firstIndex(of: message) else { return }
  181. allMessage.remove(at: index)
  182. notifyMessageChanged(index: index, type: .delete, toBottom: false)
  183. sendMessage(message: message.imMessage)
  184. }
  185. private func sendMessage(message: V2TIMMessage) {
  186. if needMarkAutoReply {
  187. LNGameMateManager.shared.markUseAutoReplay(uid: userId)
  188. needMarkAutoReply = false
  189. }
  190. message.needReadReceipt = true
  191. let datas = transUIMsgFromIMMsg(messages: [message])
  192. guard !datas.isEmpty else { return }
  193. if allMessage.isEmpty {
  194. topMessage = message
  195. }
  196. allMessage.append(contentsOf: datas)
  197. notifyMessageChanged(index: allMessage.count - 1, type: .insert, toBottom: true)
  198. let pushInfo = V2TIMOfflinePushInfo()
  199. pushInfo.title = myUserInfo.nickname
  200. V2TIMManager.sharedInstance().sendMessage(
  201. message: message, receiver: userId,
  202. groupID: "", priority: .PRIORITY_NORMAL,
  203. onlineUserOnly: false, offlinePushInfo: pushInfo,
  204. progress: nil)
  205. { [weak self] in
  206. guard let self else { return }
  207. if let index = allMessage.firstIndex(of: datas.last!) {
  208. notifyMessageChanged(index: index, type: .reload, toBottom: false)
  209. }
  210. LNEventDeliver.notifyEvent { ($0 as? LNIMChatViewModelNotify)?.onIMDidSendMessage(to: self.userId) }
  211. if let last = allMessage.filter({ $0.isSelf && $0.type.isUserContent }).suffix(5).first,
  212. Int(curTime - (last.imMessage.timestamp?.timeIntervalSince1970 ?? 0)) <= 10 * 60 {
  213. let event = LNReportEvent()
  214. event.event = .deep_chat_hit
  215. event.scene = .playmate_deep_chat_report
  216. event.uid = userId
  217. LNReportManager.shared.reportEvent(event: event)
  218. }
  219. } fail: { [weak self] code, err in
  220. guard let self else { return }
  221. if let index = allMessage.firstIndex(of: datas.last!) {
  222. notifyMessageChanged(index: index, type: .reload, toBottom: false)
  223. }
  224. if code == LNIMCustomErrorCode.inBlackList.rawValue {
  225. showToast(.init(key: "A00087"))
  226. } else if code == LNIMCustomErrorCode.userNotExist.rawValue {
  227. showToast(.init(key: "A00088"))
  228. } else {
  229. showToast(err?.description)
  230. }
  231. }
  232. }
  233. }
  234. // MARK: 消息拉取
  235. extension LNIMChatViewModel {
  236. func loadNextPage(handler: @escaping (_ success: Bool, _ isFirst: Bool) -> Void) {
  237. guard !loading else {
  238. handler(false, false)
  239. return
  240. }
  241. loading = true
  242. V2TIMManager.sharedInstance().getC2CHistoryMessageList(
  243. userID: userId, count: 300,
  244. lastMsg: topMessage)
  245. { [weak self] list in
  246. guard let self else { return }
  247. let isFirst = allMessage.isEmpty
  248. guard let list, !list.isEmpty else {
  249. loading = false
  250. handler(false, isFirst)
  251. return
  252. }
  253. topMessage = list.last
  254. let messages = transUIMsgFromIMMsg(messages: list)
  255. allMessage.insert(contentsOf: messages, at: 0)
  256. loading = false
  257. handler(true, isFirst)
  258. getMessageReadReceipts(list: list)
  259. } fail: { [weak self] code, err in
  260. guard let self else { return }
  261. handler(false, false)
  262. loading = false
  263. }
  264. }
  265. private func getMessageReadReceipts(list: [V2TIMMessage]) {
  266. V2TIMManager.sharedInstance().getMessageReadReceipts(messageList: list) { [weak self] receipts in
  267. guard let self else { return }
  268. guard let receipts, !receipts.isEmpty else { return }
  269. var messageIndex: [IndexPath] = []
  270. receipts.forEach { item in
  271. if let index = self.allMessage.firstIndex(where: { $0.imMessage.msgID == item.msgID }) {
  272. self.allMessage[index].readReceipt = item
  273. messageIndex.append(.init(row: index, section: 0))
  274. }
  275. }
  276. notifyMessagesChanged(indexs: messageIndex, type: .reload, toBottom: false)
  277. } fail: { code, err in }
  278. }
  279. }
  280. // MARK: 消息处理
  281. extension LNIMChatViewModel {
  282. private func transUIMsgFromIMMsg(messages: [V2TIMMessage]) -> [LNIMMessageData] {
  283. var datas: [LNIMMessageData] = []
  284. for message in messages.reversed() {
  285. // 被标记不展示
  286. if message.isExcludedFromLastMessage || message.isExcludedFromUnreadCount {
  287. continue
  288. }
  289. let data = LNIMMessageData(imMessage: message)
  290. if case .order = data.type {
  291. guard let orderMessage: LNIMOrderMessage = data.decodeCustomMessage() else {
  292. continue // 解析失败,忽略
  293. }
  294. if let oldMessage = orderMessageCache[orderMessage.orderId] {
  295. // 存在旧的订单信息
  296. if (oldMessage.imMessage.timestamp?.timeIntervalSince1970 ?? 0)
  297. > (message.timestamp?.timeIntervalSince1970 ?? 0) {
  298. // 消息为旧的订单信息,忽略
  299. continue
  300. } else {
  301. // 订单的新消息
  302. removeOldMessage(oldMessage)
  303. if let index = datas.firstIndex(of: oldMessage) {
  304. datas.remove(at: index)
  305. }
  306. }
  307. }
  308. orderMessageCache[orderMessage.orderId] = data
  309. } else if case .call(let callId) = data.type {
  310. guard let callMessage: LNIMVoiceCallMessage = data.decodeCustomMessage() else {
  311. continue // 解析失败,忽略
  312. }
  313. // ->>>>> ⚠️ 挂断自己发起的通话,消息发送者会变成对方
  314. // ->>>>> ⚠️ 这里需要纠正发送者
  315. data.isSelf = callMessage.inviter.isMyUid
  316. // ->>>>> ⚠️
  317. if let oldMessage = callMessageCache[callId] {
  318. // 存在旧的订单信息
  319. if (oldMessage.imMessage.timestamp?.timeIntervalSince1970 ?? 0)
  320. > (message.timestamp?.timeIntervalSince1970 ?? 0) {
  321. // 消息为旧的订单信息,忽略
  322. continue
  323. } else {
  324. // 订单的新消息
  325. removeOldMessage(oldMessage)
  326. if let index = datas.firstIndex(of: oldMessage) {
  327. datas.remove(at: index)
  328. }
  329. }
  330. }
  331. callMessageCache[callId] = data
  332. }
  333. datas.append(data)
  334. }
  335. datas.forEach {
  336. guard let index = datas.firstIndex(of: $0) else { return }
  337. if let dateMessage = buildDateMessageIfNeed(message: $0.imMessage) {
  338. datas.insert(dateMessage, at: index)
  339. }
  340. }
  341. return datas
  342. }
  343. private func removeOldMessage(_ message: LNIMMessageData) {
  344. guard let index = allMessage.firstIndex(of: message) else { return }
  345. // 移除旧的订单消息
  346. var removeIndexs: [IndexPath] = []
  347. allMessage.remove(at: index)
  348. removeIndexs.append(.init(row: index, section: 0))
  349. if index - 1 >= 0 && index <= allMessage.count - 1,
  350. case .system = allMessage[index - 1].type,
  351. case .system = allMessage[index].type {
  352. // 出现连续的两个时间戳消息,需要将前一个移除
  353. allMessage.remove(at: index - 1)
  354. removeIndexs.append(.init(row: index - 1, section: 0))
  355. }
  356. notifyMessagesChanged(indexs: removeIndexs, type: .delete, toBottom: false)
  357. }
  358. private func buildDateMessageIfNeed(message: V2TIMMessage) -> LNIMMessageData? {
  359. guard let time = message.timestamp else { return nil }
  360. if let lastData,
  361. time.timeIntervalSince(lastData) < 5 * 60 {
  362. return nil
  363. }
  364. lastData = message.timestamp
  365. return LNIMMessageData.buildDateMessage(date: time)
  366. }
  367. }
  368. // MARK: 接收消息推送
  369. extension LNIMChatViewModel: V2TIMAdvancedMsgListener {
  370. func onRecvNewMessage(msg: V2TIMMessage!) {
  371. guard msg.userID == userId else { return }
  372. let list = transUIMsgFromIMMsg(messages: [msg])
  373. if list.isEmpty { return }
  374. allMessage.append(contentsOf: list)
  375. notifyMessageChanged(index: allMessage.count - 1, type: .insert, toBottom: true)
  376. var isOrderMessage = false
  377. for item in list {
  378. if case .order = item.type {
  379. isOrderMessage = true
  380. break
  381. }
  382. }
  383. if isOrderMessage {
  384. getUnfinishOrder()
  385. }
  386. }
  387. func onRecvMessageReadReceipts(receiptList: [V2TIMMessageReceipt]!) {
  388. var indexs: [Int] = []
  389. receiptList.forEach { item in
  390. if let index = allMessage.firstIndex(where: { $0.imMessage.msgID == item.msgID }) {
  391. allMessage[index].readReceipt = item
  392. indexs.append(index)
  393. }
  394. }
  395. notifyMessagesChanged(indexs: indexs.map({ .init(row: $0, section: 0) }), type: .reload, toBottom: false)
  396. }
  397. }
  398. // MARK: 对外通知
  399. extension LNIMChatViewModel {
  400. private func notifyMessageChanged(index: Int, type: LNIMChatDataSourceChangeType, toBottom: Bool) {
  401. LNEventDeliver.notifyEvent {
  402. ($0 as? LNIMChatViewModelNotify)?.onIMMessageDataChanged(viewModel: self, index: index, type: type, toBottom: toBottom)
  403. }
  404. }
  405. private func notifyMessagesChanged(indexs: [IndexPath], type: LNIMChatDataSourceChangeType, toBottom: Bool) {
  406. LNEventDeliver.notifyEvent {
  407. ($0 as? LNIMChatViewModelNotify)?.onIMMessageDatasChanged(viewModel: self, indexs: indexs, type: type, toBottom: toBottom)
  408. }
  409. }
  410. }