666 lines
19 KiB
Swift
666 lines
19 KiB
Swift
//
|
||
// 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 onRecvMessages(_ messages: [NIMMessage])
|
||
func willSend(_ message: NIMMessage)
|
||
}
|
||
|
||
|
||
public class ChatViewModel: NSObject,
|
||
NIMConversationManagerDelegate, NIMSystemNotificationManagerDelegate {
|
||
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)
|
||
NIMSDK.shared().conversationManager.add(self)
|
||
NIMSDK.shared().systemNotificationManager.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
|
||
|
||
}
|
||
}
|
||
|
||
|
||
@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")
|
||
}
|
||
}
|
||
|
||
|
||
extension ChatViewModel: NIMChatManagerDelegate {
|
||
// MARK: NIMChatManagerDelegate
|
||
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?.onRecvMessages(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)
|
||
}
|
||
|
||
}
|