// // ChatViewModel.swift // yinmeng-ios // // Created by MaiMang on 2024/2/27. // import Foundation import NIMSDK public typealias ChatProviCompletion = ((Error?, [NIMMessage]?) -> ()) public enum LoadMessageDirection: Int { case old = 1 case new } public protocol ChatViewModelDelegate: NSObjectProtocol { func didAppend(_ message: NIMMessage) func onDeleteMessage(_ message: NIMMessage, atIndexs: [IndexPath]) func send(_ message: NIMMessage, didCompleteWithError error: Error?) func send(_ message: NIMMessage, progress: Float) func onRecvMessages1(_ messages: [NIMMessage]) func willSend(_ message: NIMMessage) } public class ChatViewModel: NSObject, NIMChatManagerDelegate { private var userInfo = [String: NIMUser]() public let messagPageNum: UInt = 100 public var session: NIMSession ///消息的条数 public var messageObjects: [ChatSessionProtocol] = .init() // 下拉时间戳 private var oldMsg: NIMMessage? // 上拉时间戳 private var newMsg: NIMMessage? // 可信时间戳 public var credibleTimestamp: TimeInterval = 0 public var anchor: NIMMessage? internal var isHistoryAnchorChat = false public weak var delegate: ChatViewModelDelegate? private func addMessageListener() { NIMSDK.shared().chatManager.add(self) } init(session: NIMSession) { self.session = session super.init() addMessageListener() } /// 发送消息 /// - Parameters: /// - message: 消息对象 /// - completion: 发送完成后的回调,这里的回调完成只表示当前这个函数调用完成,需要后续的回调才能判断消息是否已经发送至服务器 public func sendMessage(message: NIMMessage, _ completion: @escaping (Error?) -> Void) { NIMSDK.shared().chatManager.send(message, to: session, completion: completion) } ////发送语音消息 public func sendAudioMessage(filePath: String, _ completion: @escaping (Error?) -> Void) { let audioObject = NIMAudioObject(sourcePath: filePath) let audioMessage = NIMMessage() audioMessage.messageObject = audioObject audioMessage.apnsContent = "发来了一段语音" let setting = NIMMessageSetting() setting.teamReceiptEnabled = false audioMessage.setting = setting sendMessage(message: audioMessage, completion) } ///发送图片 public func sendImageMessage(image: UIImage, _ completion: @escaping (Error?) -> Void) { let imageMessage = NIMMessage() let imageOpt = NIMImageOption() imageOpt.compressQuality = 0.8 let imageObject = NIMImageObject(image: image) imageObject.option = imageOpt imageMessage.messageObject = imageObject imageMessage.apnsContent = "发送了一张图片" sendMessage(message: imageMessage, completion) } public func sendTextMessage(text: String, _ completion: @escaping (Error?) -> Void) { if text.count <= 0 { return } let textMessage = NIMMessage() textMessage.text = text sendMessage(message: textMessage, completion) } // 查询远端历史消息 public func reloadRemoteHistoryMessage(direction: LoadMessageDirection, updateCredible: Bool, option: NIMHistoryMessageSearchOption, _ completion: @escaping (Error?, NSInteger, [ChatSessionProtocol]?) -> Void) { weak var weakSelf = self NIMSDK.shared().conversationManager.fetchMessageHistory(session, option: option) { error, messages in if error == nil { if let messageArray = messages, messageArray.count > 0 { if direction == .old { weakSelf?.oldMsg = messageArray.last } else { weakSelf?.newMsg = messageArray.first } for msg in messageArray { if let model = weakSelf?.modelTransformMessage(message: msg) { weakSelf?.addTimeMessage(msg) weakSelf?.messageObjects.insert(model, at: 0) } } if let updateMessage = messageArray.first, updateCredible { // 更新可信时间戳 weakSelf?.credibleTimestamp = updateMessage.timestamp } completion(error, messageArray.count, weakSelf?.messageObjects) } else { completion(error, 0, weakSelf?.messageObjects) } } else { completion(error, 0, nil) } } } // 下拉获取历史消息 public func dropDownRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [ChatSessionProtocol]?) -> Void) { let isCredible = isMessageCredible(message: oldMsg ?? NIMMessage()) if isCredible { // 继续拉去本地消息 getMoreMessageHistory(completion) } else { let option = NIMHistoryMessageSearchOption() option.startTime = 0 option.endTime = oldMsg?.timestamp ?? 0 option.limit = messagPageNum option.sync = true // 不可信拉去远端消息 reloadRemoteHistoryMessage( direction: .old, updateCredible: false, option: option, completion ) } } public func queryRoamMsgHasMoreTime_v2(_ completion: @escaping (Error?, NSInteger, NSInteger, [ChatSessionProtocol]?, Int) -> Void) { weak var weakSelf = self NIMSDK.shared().conversationManager.incompleteSessionInfo(by: session) { error, sessionInfos in if error == nil { let sessionInfo = sessionInfos?.first // 记录可信时间戳 weakSelf?.credibleTimestamp = sessionInfo?.timestamp ?? 0 weakSelf?.getMessageHistory(self.newMsg) { error, value, models in completion(error, value, 0, models, 0) } } } } // 查询本地历史消息 public func getMessageHistory(_ message: NIMMessage?, _ completion: @escaping (Error?, NSInteger, [ChatSessionProtocol]?) -> Void) { NIMSDK.shared().conversationManager.messages(in: session, message: message, limit: Int(messagPageNum), completion: result(completion)) func result(_ completion: @escaping (Error?, NSInteger, [ChatSessionProtocol]?) -> ()) -> ChatProviCompletion { return { [weak self] error, messages in if let messageArray = messages, messageArray.count > 0 { self?.oldMsg = messageArray.first for msg in messageArray { if let model = self?.modelTransformMessage(message: msg) { self?.addTimeMessage(msg) self?.messageObjects.append(model) } } completion(error, messageArray.count, self?.messageObjects) self?.markRead(messages: messageArray) { error in } } else { completion(error, 0, self?.messageObjects) } } } } // 查询更多本地历史消息 public func getMoreMessageHistory(_ completion: @escaping (Error?, NSInteger, [ChatSessionProtocol]?) -> Void) { let messageParam = oldMsg ?? newMsg weak var weakSelf = self NIMSDK.shared().conversationManager.messages(in: session, message: messageParam, limit: Int(messagPageNum)) { error, messages in if let messageArray = messages, messageArray.count > 0 { weakSelf?.oldMsg = messageArray.first // 如果可信就使用本次请求数据,如果不可信就去远端拉去数据,并更新可信时间戳 let isCredible = weakSelf? .isMessageCredible(message: messageArray.first ?? NIMMessage()) if let isTrust = isCredible, isTrust { for msg in messageArray.reversed() { if let model = weakSelf?.modelTransformMessage(message: msg) { weakSelf?.addTimeMessage(msg) weakSelf?.messageObjects.insert(model, at: 0) } } completion(error, messageArray.count, weakSelf?.messageObjects) } else { let option = NIMHistoryMessageSearchOption() option.startTime = 0 option.endTime = weakSelf?.oldMsg?.timestamp ?? 0 option.limit = weakSelf?.messagPageNum ?? 100 option.sync = true weakSelf?.reloadRemoteHistoryMessage( direction: .old, updateCredible: true, option: option, completion ) } weakSelf?.markRead(messages: messageArray) { error in } } else { if let messageArray = messages, messageArray.isEmpty, weakSelf?.credibleTimestamp ?? 0 > 0 { // 如果远端拉倒了信息 就去更新可信时间戳,拉不到就不更新。 let option = NIMHistoryMessageSearchOption() option.startTime = 0 option.endTime = weakSelf?.oldMsg?.timestamp ?? 0 option.limit = weakSelf?.messagPageNum ?? 100 weakSelf?.reloadRemoteHistoryMessage( direction: .old, updateCredible: true, option: option, completion ) } else { completion(error, 0, weakSelf?.messageObjects) } } } } // 搜索历史记录查询的本地消息 public func searchMessageHistory(direction: LoadMessageDirection, startTime: TimeInterval, endTime: TimeInterval, _ completion: @escaping (Error?, NSInteger, [ChatSessionProtocol]?) -> Void) { let option = NIMMessageSearchOption() option.startTime = startTime option.endTime = endTime option.order = .asc option.limit = messagPageNum NIMSDK.shared().conversationManager.searchMessages(session, option: option) { [weak self] error, messages in if error == nil { if let messageArray = messages, messageArray.count > 0 { var newMessages = [NIMMessage]() for msg in messageArray { newMessages.append(msg) if let model = self?.modelTransformMessage(message: msg) { self?.addTimeMessage(msg) self?.messageObjects.append(model) } } if direction == .old { self?.oldMsg = newMessages.first } else { self?.newMsg = newMessages.last } completion(error, newMessages.count, self?.messageObjects) } else { completion(error, 0, self?.messageObjects) } } else { completion(error, 0, nil) } } } // 判断消息是否可信 public func isMessageCredible(message: NIMMessage) -> Bool { return credibleTimestamp <= 0 || message.timestamp >= credibleTimestamp } public func markRead(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { NIMSDK.shared().conversationManager.markAllMessagesRead(in: session) { error in } } public func send(_ message: NIMMessage, didCompleteWithError error: Error?) { for (i, msg) in messageObjects.enumerated() { if message.messageId == msg.msg?.messageId { messageObjects[i].msg = message break } } delegate?.send(message, didCompleteWithError: error) } // 收到消息 public func onRecvMessages(_ messages: [NIMMessage]) { for msg in messages { if msg.session?.sessionId == session.sessionId { if let model = modelTransformMessage(message: msg) { newMsg = msg addTimeMessage(msg) self.messageObjects.append(model) } } } delegate?.onRecvMessages1(messages) } public func willSend(_ message: NIMMessage) { if message.session?.sessionId != session.sessionId { return } guard let model = modelTransformMessage(message: message) else { return } if newMsg == nil { newMsg = message } var isResend = false for (i, msg) in messageObjects.enumerated() { if message.messageId == msg.msg?.messageId { messageObjects[i].msg = message isResend = true break } } if !isResend { addTimeMessage(message) messageObjects.append(model) } delegate?.didAppend(message) } public func onReceive(_ notification: NIMCustomSystemNotification) { } public func send(_ message: NIMMessage, progress: Float) { delegate?.send(message, progress: progress) } @discardableResult public func resendMessage(message: NIMMessage) -> Error? { var e: Error? = nil do { try NIMSDK.shared().chatManager.resend(message) } catch { e = error } return e } public func getUserInfo(userId: String) -> NIMUser? { return NIMSDK.shared().userManager.userInfo(userId) } public func getTeamMember(userId: String, teamId: String) -> NIMTeamMember? { // return repo.getTeamMemberList(userId: userId, teamId: teamId) return nil } public func deleteMessage(message: NIMMessage) { NIMSDK.shared().conversationManager.delete(message) deleteMessageUpdateUI(message) } // MARK: collection func addColletion(_ message: NIMMessage, completion: @escaping (NSError?, NIMCollectInfo?) -> Void) { let param = NIMAddCollectParams() var string: String? if message.messageType == .text { string = message.text param.type = 1024 } else { switch message.messageType { case .audio: if let obj = message.messageObject as? NIMAudioObject { string = obj.url } param.type = message.messageType.rawValue case .image: if let obj = message.messageObject as? NIMImageObject { string = obj.url } param.type = message.messageType.rawValue case .video: if let obj = message.messageObject as? NIMVideoObject { string = obj.url } param.type = message.messageType.rawValue default: param.type = 0 } param.data = string ?? "" } param.uniqueId = message.serverID // repo.collectMessage(param, completion) } // MARK: revoke // history message insert message at first of messages, send message add last of messages private func addTimeMessage(_ message: NIMMessage) { let lastTs = messageObjects.last?.msg?.timestamp ?? 0.0 let curTs = message.timestamp let dur = curTs - lastTs print("curTs:\(curTs) lastTs:\(lastTs)") if (dur / 60) > 5 { messageObjects.append(timeModel(message)) } } private func ddTimeForHistoryMessage(_ message: NIMMessage) { let firstTs = messageObjects.first?.msg?.timestamp ?? 0.0 let curTs = message.timestamp let dur = firstTs - curTs print("HistorycurTs:\(curTs) firstTs:\(firstTs)") if (dur / 60) > 5 { messageObjects.insert(timeModel(message), at: 0) } } private func timeModel(_ message: NIMMessage) -> ChatSessionProtocol { let timeMsg = NIMMessage() timeMsg.timestamp = message.timestamp let model = ChatTimeObject(msg: timeMsg) return model } private func modelTransformMessage(message: NIMMessage) -> ChatSessionProtocol? { var model: ChatSessionProtocol switch message.messageType { case .text: model = ChatTextObject(msg: message) case .audio: model = ChatVoiceObject(msg: message) case .image: model = ChatImageObject(msg: message) default: return nil } if let uid = message.from { model.userID = uid let user = getUserInfo(userId: uid) var fullName = uid if let nickName = user?.userInfo?.nickName { fullName = nickName } model.avatar = user?.userInfo?.avatarUrl if session.sessionType == .team { // team let teamMember = getTeamMember(userId: uid, teamId: session.sessionId) if let teamNickname = teamMember?.nickname { fullName = teamNickname } } if let alias = user?.alias { fullName = alias } model.name = fullName } return model } func deleteMessageUpdateUI(_ message: NIMMessage) { var index = -1 for (i, model) in messageObjects.enumerated() { if model.msg?.serverID == message.serverID { index = i break } } var indexs = [IndexPath]() if index >= 0 { // remove time tip let last = index - 1 // if last >= 0, let timeModel = messages[last] as? MAIMessageTipsModel, // timeModel.type == .time { // messageObjects.removeSubrange(last ... index) // indexs.append(IndexPath(row: last, section: 0)) // indexs.append(IndexPath(row: index, section: 0)) // } else { messageObjects.remove(at: index) indexs.append(IndexPath(row: index, section: 0)) // } } delegate?.onDeleteMessage(message, atIndexs: indexs) } private func getUserInfo(_ userId: String, _ completion: @escaping (NIMUser?, NSError?) -> Void) { if let user = userInfo[userId] { completion(user, nil) } if let user = getUserInfo(userId: userId) { userInfo[userId] = user completion(user, nil) } } // 获取展示的用户名字,p2p: 备注》昵称>ID team: 备注〉群昵称》 昵称〉 ID private func getShowName(userId: String, teamId: String?) -> String { let user = getUserInfo(userId: userId) var fullName = userId if let nickName = user?.userInfo?.nickName { fullName = nickName } // model.avatar = user?.userInfo?.thumbAvatarUrl if let tID = teamId, session.sessionType == .team { // team let teamMember = getTeamMember(userId: userId, teamId: tID) if let teamNickname = teamMember?.nickname { fullName = teamNickname } } if let alias = user?.alias { fullName = alias } return fullName } public func fetchMessageAttachment(_ message: NIMMessage, _ completion: @escaping (Error?) -> Void) { do { try NIMSDK.shared().chatManager.fetchMessageAttachment(message) } catch let error { completion(error) } } public func downLoad(_ urlString: String, _ filePath: String, _ progress: NIMHttpProgressBlock?, _ completion: NIMDownloadCompleteBlock?) { NIMSDK.shared().resourceManager.download(urlString, filepath: filePath, progress: progress, completion: completion) } public func getUrls() -> [String] { var urls = [String]() messageObjects.forEach { model in if model.type == .image, let message = model.msg?.messageObject as? NIMImageObject { if let url = message.url { urls.append(url) } else { if let path = message.path, FileManager.default.fileExists(atPath: path) { urls.append(path) } } } } return urls } public func sendInputTypingState() { if session.sessionType == .P2P { setTypingCustom(1) } } public func sendInputTypingEndState() { if session.sessionType == .P2P { setTypingCustom(0) } } func setTypingCustom(_ typing: Int) { let message = NIMMessage() if message.setting == nil { message.setting = NIMMessageSetting() } message.setting?.apnsEnabled = false message.setting?.shouldBeCounted = false let noti = NIMCustomSystemNotification(content: getJSONStringFromDictionary(["typing": typing])) NIMSDK.shared().systemNotificationManager.sendCustomNotification(noti, to: session) } public func getHandSetEnable() -> Bool { return false // return repo.getHandsetMode() } public func getMessageRead() -> Bool { return NIMSDK.shared().conversationManager.allUnreadCount() > 0 } // MARK: NIMConversationManagerDelegate private func getJSONStringFromDictionary(_ dictionary: [String: Any]) -> String { if !JSONSerialization.isValidJSONObject(dictionary) { print("not parse to json string") return "" } if let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []), let JSONString = String(data: data, encoding: .utf8) { return JSONString } return "" } private func getDictionaryFromJSONString(_ jsonString: String) -> NSDictionary? { if let jsonData = jsonString.data(using: .utf8), let dict = try? JSONSerialization.jsonObject( with: jsonData, options: .mutableContainers ) as? NSDictionary { return dict } return nil } deinit { print("deinit") } }