LNIMChatViewModel.swift 16 KB

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