525 lines
16 KiB
Swift
525 lines
16 KiB
Swift
//
|
||
// IMManager.swift
|
||
// Crush
|
||
//
|
||
// Created by Leon on 2025/8/18.
|
||
//
|
||
|
||
import NIMSDK
|
||
import Combine
|
||
import UserNotifications
|
||
|
||
class IMManager: NSObject {
|
||
public static let shared = IMManager()
|
||
|
||
var retryCount = 0
|
||
// appkey信息
|
||
var imInfo: IMConfigInfo?
|
||
|
||
// 给oc用的
|
||
var accountId: String {
|
||
imInfo?.accountId ?? ""
|
||
}
|
||
|
||
// session Cache
|
||
var cacheSessions = [String]()
|
||
|
||
// Config是否需要发送通知
|
||
var notNeedSendNotification = false
|
||
|
||
// UnreadCount 未读消息
|
||
var totalUnreadCount: Int {
|
||
return chatTabAllUnreadCount
|
||
}
|
||
|
||
/// 🔥 chat tab 的未读总数
|
||
@Published var chatTabAllUnreadCount: Int = 0
|
||
|
||
/// 🔥notice center 入口的未读数
|
||
@Published var noticeUnreadCount: Int = 0
|
||
@Published var noticeStat: MessageStat?
|
||
|
||
/// 🔥IM 消息未读数
|
||
@Published var sessionUnreadCount: Int = 0
|
||
|
||
private var cancellables = Set<AnyCancellable>()
|
||
|
||
override init() {
|
||
super.init()
|
||
setupDefaultConfig()
|
||
setupEvent()
|
||
}
|
||
|
||
|
||
func setupEvent() {
|
||
NotificationCenter.default.addObserver(self, selector: #selector(notiLoginSuccess), name: AppNotificationName.userLoginSuccess.notificationName, object: nil)
|
||
NotificationCenter.default.addObserver(self, selector: #selector(notiLogout), name: AppNotificationName.userLogout.notificationName, object: nil)
|
||
|
||
//
|
||
// UserCore.shared.isLogin()
|
||
|
||
Publishers.CombineLatest($noticeUnreadCount, $sessionUnreadCount)
|
||
.map { $0 + $1 }
|
||
.assign(to: \.chatTabAllUnreadCount, on: self)
|
||
.store(in: &cancellables)
|
||
|
||
$chatTabAllUnreadCount.sink { count in
|
||
UIApplication.shared.applicationIconBadgeNumber = count
|
||
}.store(in: &cancellables)
|
||
}
|
||
|
||
// sdk配置基础
|
||
func setupDefaultConfig() {
|
||
let config = NIMSDKConfig.shared()
|
||
|
||
// config.delegate = self
|
||
// 同步多端未读数
|
||
config.shouldSyncUnreadCount = true
|
||
// 链接失败重试次数
|
||
config.maxAutoLoginRetryTimes = 10
|
||
// 本地log存储天数
|
||
config.maximumLogDays = 30
|
||
// 是否将群通知计入未读数
|
||
config.shouldCountTeamNotification = true
|
||
// 是否支持动图缩略
|
||
config.animatedImageThumbnailEnabled = false
|
||
// 是否在收到消息后自动下载附件
|
||
config.fetchAttachmentAutomaticallyAfterReceiving = false
|
||
// 是否在收到聊天室消息后下载附件
|
||
config.fetchAttachmentAutomaticallyAfterReceivingInChatroom = true
|
||
// 是否开启异步获取最近会话
|
||
config.asyncLoadRecentSessionEnabled = true
|
||
// 禁止后台重连,退到后台即断开
|
||
config.reconnectInBackgroundStateDisabled = true
|
||
// 置顶会话同步开关。如果不使用置顶功能建议关闭(默认),有利于节省流量
|
||
config.shouldSyncStickTopSessionInfos = false
|
||
}
|
||
|
||
/// 🔥启动初始化SDK,主要入口
|
||
public func setupNIM() {
|
||
|
||
NIMSDK.shared().v2LoginService.add(self)
|
||
NIMSDK.shared().v2ConversationService.add(self)
|
||
NIMSDK.shared().v2SubscriptionService.add(self)
|
||
NIMSDK.shared().v2MessageService.add(self)
|
||
|
||
// 初始化SDK
|
||
v2InitailSDK()
|
||
|
||
refreshIMServerInfoConfig()
|
||
//setupNIMAppKey()
|
||
}
|
||
|
||
func v2InitailSDK(){
|
||
// guard UserCore.shared.isLogin() else {
|
||
// return
|
||
// }
|
||
|
||
let appkey = "2d6abc076f434fc52320c7118de5fead"
|
||
|
||
var option = NIMSDKOption.init(appKey: appkey)
|
||
#if DEBUG
|
||
option.apnsCername = "developerpush"
|
||
#else
|
||
option.apnsCername = "distributionpush"
|
||
#endif
|
||
let v2Option = V2NIMSDKOption()
|
||
v2Option.enableV2CloudConversation = true
|
||
|
||
NIMSDK.shared().register(withOptionV2: option, v2Option: v2Option)
|
||
|
||
if NIMSDK.shared().loginManager.isLogined(){
|
||
dlog("💬❌ NIMSDK 已登录")
|
||
}
|
||
|
||
// v2AutoLogin()
|
||
}
|
||
|
||
func v2AutoLogin(){
|
||
if imInfo == nil {
|
||
// 从本地取
|
||
if let info = AppCache.fetchCache(key: CacheKey.ImAppkeyInfo.rawValue, type: IMConfigInfo.self) {
|
||
imInfo = info
|
||
} else {
|
||
return
|
||
}
|
||
}
|
||
|
||
guard UserCore.shared.isLogin() else { return }
|
||
guard let info = imInfo else { return }
|
||
guard let account = info.accountId else { return }
|
||
guard let imToken = info.token else { return }
|
||
let option = V2NIMLoginOption()
|
||
option.retryCount = 3
|
||
NIMSDK.shared().v2LoginService.login(account, token: imToken, option: option) {[weak self] in
|
||
// dlog("☁️Log Success")
|
||
self?.regetConversationAllUnreadCount()
|
||
}
|
||
}
|
||
|
||
func logoutIM(){
|
||
NIMSDK.shared().v2LoginService.logout {
|
||
dlog("☁️Log out")
|
||
} failure: { error in
|
||
dlog("☁️Log error: \(error)")
|
||
}
|
||
|
||
}
|
||
|
||
/// 清理当前账号的IM登录信息
|
||
private func clearLogData() {
|
||
resetCount()
|
||
UserCore.shared.logout()
|
||
NotificationCenter.post(name: .presentSignInVc, object: nil, userInfo: nil)
|
||
}
|
||
|
||
func handleReceiveCreate(recentSession: V2NIMConversation) {
|
||
guard let message = recentSession.lastMessage else { return }
|
||
dealNotice(message: message)
|
||
}
|
||
|
||
/// 全局收到消息,针对性处理一些消息
|
||
func handleReveiceUpdate(recentSession: V2NIMConversation) {
|
||
guard let message = recentSession.lastMessage else { return }
|
||
if let remoteExt = message.serverExtension{
|
||
// if let type = remoteExt["type"] as? String{
|
||
// if type == MessageTypeStringCustom || type == MessageTypeStringNormal, let session = recentSession.session{
|
||
// // 打电话的消息,交给打电话处理
|
||
// dealPhoneCallMessage(message: message, session: session)
|
||
// }
|
||
// }
|
||
}else if message.messageType == .MESSAGE_TYPE_CUSTOM {
|
||
// 自定义消息,暂不处理
|
||
}
|
||
|
||
if message.messageType == .MESSAGE_TYPE_CUSTOM{
|
||
|
||
}else{
|
||
dealNotice(message: message)
|
||
}
|
||
}
|
||
|
||
func dealNotice(message: V2NIMLastMessage) {
|
||
// NIMMessage
|
||
guard UIApplication.shared.applicationState == .active else {
|
||
return
|
||
}
|
||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||
generator.impactOccurred()
|
||
|
||
}
|
||
}
|
||
|
||
// loadData
|
||
extension IMManager {
|
||
/// 刷新app 信息
|
||
func refreshIMServerInfoConfig() {
|
||
guard UserCore.shared.isLogin() else {
|
||
return
|
||
}
|
||
guard retryCount <= 5 else {
|
||
// 重试最多五次
|
||
return
|
||
}
|
||
|
||
IMProvider.request(.getIMAccount, modelType: IMConfigInfo.self) {[weak self] result in
|
||
switch result {
|
||
case let .success(model):
|
||
if model != nil {
|
||
self?.imInfo = model
|
||
AppCache.cache(key: CacheKey.ImAppkeyInfo.rawValue, value: model)
|
||
//self?.setupNIMAppKey()
|
||
self?.v2AutoLogin() //
|
||
self?.retryCount = 0
|
||
}
|
||
if String.realEmpty(str: model?.accountId) {
|
||
// 控制重试时间
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||
self?.refreshIMServerInfoConfig()
|
||
self?.retryCount += 1
|
||
}
|
||
}
|
||
case let .failure(error):
|
||
dlog(error)
|
||
// 控制重试时间
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||
self?.refreshIMServerInfoConfig()
|
||
self?.retryCount += 1
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
public func retryRefreshConfig() {
|
||
if NIMSDK.shared().loginManager.isLogined() {
|
||
return
|
||
}
|
||
// 重试登录
|
||
retryCount = 0
|
||
refreshIMServerInfoConfig()
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: 🔴Unread Count 未读数和红点处理
|
||
|
||
extension IMManager {
|
||
|
||
public func regetAllUnreadCount(){
|
||
regetNoticeUnread()
|
||
regetConversationAllUnreadCount()
|
||
}
|
||
|
||
/// 请求业务系统上的未读数
|
||
func regetNoticeUnread(completion:((Bool, MessageStat?)-> Void)? = nil){
|
||
guard UserCore.shared.isLogin() else{
|
||
completion?(false, nil)
|
||
return
|
||
}
|
||
IMProvider.request(.messageStat, modelType: MessageStat.self) {[weak self] result in
|
||
switch result {
|
||
case .success(let model):
|
||
self?.noticeStat = model
|
||
self?.noticeUnreadCount = model?.unRead ?? 0
|
||
completion?(true, model)
|
||
case .failure:
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
public func regetConversationAllUnreadCount(){
|
||
// 获取未读数
|
||
sessionUnreadCount = NIMSDK.shared().v2ConversationService.getTotalUnreadCount()
|
||
dlog("🚩IMManager reget unread count:\(sessionUnreadCount)")
|
||
}
|
||
|
||
func registerNIMCount(){
|
||
let filter = V2NIMConversationFilter()
|
||
filter.conversationTypes = [1]//V2NIMConversationType.CONVERSATION_TYPE_P2P
|
||
filter.ignoreMuted = true
|
||
NIMSDK.shared().v2ConversationService.subscribeUnreadCount(by: filter)
|
||
// Result can see in onUnreadCountChangedByFilter
|
||
}
|
||
|
||
/// 🔥conversationIds
|
||
func clearUnreadCountBy(ids:[String]){
|
||
guard ids.count > 0 else{ return }
|
||
NIMSDK.shared().v2ConversationService.clearUnreadCount(byIds: ids) {[weak self] resuls in
|
||
let getUnreadcount = NIMSDK.shared().v2ConversationService.getTotalUnreadCount()
|
||
// dlog("💬getTotalUnreadCount: \(getUnreadcount)")
|
||
self?.sessionUnreadCount = getUnreadcount
|
||
}
|
||
}
|
||
|
||
func clearAllUnread(){
|
||
NIMSDK.shared().v2ConversationService.clearTotalUnreadCount {
|
||
dlog("☁️Clear all unread message ok✅")
|
||
}
|
||
}
|
||
|
||
// 重置未读数
|
||
func resetCount() {
|
||
notNeedSendNotification = true
|
||
sessionUnreadCount = 0
|
||
noticeUnreadCount = 0
|
||
notNeedSendNotification = false
|
||
|
||
}
|
||
|
||
}
|
||
|
||
extension IMManager: V2NIMLoginListener{
|
||
func onLoginStatus(_ status: V2NIMLoginStatus) {
|
||
dlog("☁️login status:\(status)")
|
||
if status == .LOGIN_STATUS_LOGINED{
|
||
// 登录成功
|
||
dlog("☁️login success")
|
||
// 注册未读数
|
||
registerNIMCount()
|
||
|
||
regetConversationAllUnreadCount()
|
||
|
||
regetNoticeUnread()
|
||
}
|
||
}
|
||
|
||
|
||
|
||
func onKickedOffline(_ detail: V2NIMKickedOfflineDetail) {
|
||
// 判断是否登录
|
||
guard UserCore.shared.isLogin() else {
|
||
return
|
||
}
|
||
|
||
let reason = "Your account has signed in through another device"
|
||
|
||
|
||
let alert = Alert(title: "Session Interrupted", text: reason)
|
||
let action1 = AlertAction(title: "Got it", actionStyle: .confirm) {[weak self] in
|
||
self?.clearLogData()
|
||
}
|
||
alert.addAction(action1)
|
||
alert.show()
|
||
}
|
||
|
||
func onLoginFailed(_ error: V2NIMError) {
|
||
dlog("☁️onAutoLoginFailed__ \(error.description)")
|
||
if error.code == 417 {
|
||
guard let _ = imInfo?.accountId, let _ = imInfo?.token else { return }
|
||
v2AutoLogin()
|
||
}
|
||
}
|
||
}
|
||
|
||
// MARK: NIMConversationManagerDelegate
|
||
|
||
extension IMManager: V2NIMConversationListener{
|
||
func onSyncStarted() {
|
||
dlog("☁️onSyncStarted")
|
||
}
|
||
|
||
func onSyncFinished() {
|
||
dlog("☁️onSyncFinished")
|
||
}
|
||
|
||
func onSyncFailed(_ error: V2NIMError) {
|
||
dlog("☁️onSyncFailed:\(error)")
|
||
}
|
||
|
||
func onConversationCreated(_ conversation: V2NIMConversation) {
|
||
handleReveiceUpdate(recentSession: conversation)
|
||
}
|
||
|
||
func onConversationChanged(_ conversations: [V2NIMConversation]) {
|
||
for per in conversations{
|
||
handleReveiceUpdate(recentSession: per)
|
||
}
|
||
}
|
||
|
||
func onConversationDeleted(_ conversationIds: [String]) {
|
||
dlog("💬🗑️onConversationDeleted: \(conversationIds)")
|
||
}
|
||
|
||
func onTotalUnreadCountChanged(_ unreadCount: Int) {
|
||
dlog("💬onTotalUnreadCountChanged : \(unreadCount)")
|
||
sessionUnreadCount = unreadCount
|
||
}
|
||
|
||
// 账号多端登录会话已读时间戳标记通知回调
|
||
func onConversationReadTimeUpdated(_ conversationId: String, readTime: TimeInterval) {
|
||
|
||
}
|
||
|
||
func onUnreadCountChanged(by filter: V2NIMConversationFilter, unreadCount: Int) {
|
||
// dlog("☁️unreadCount:\(unreadCount)")
|
||
}
|
||
}
|
||
|
||
extension IMManager : V2NIMSubscribeListener{
|
||
/// 用户状态变化
|
||
func onUserStatusChanged(_ data: [V2NIMUserStatus]) {
|
||
// none
|
||
dlog("☁️onUserStatusChanged:\(data)")
|
||
}
|
||
}
|
||
|
||
extension IMManager : V2NIMMessageListener{
|
||
func onReceive(_ messages: [V2NIMMessage]) {
|
||
// ...
|
||
regetConversationAllUnreadCount()
|
||
|
||
}
|
||
}
|
||
|
||
// MARK: Notification
|
||
extension IMManager {
|
||
@objc func notiLoginSuccess() {
|
||
retryCount = 0
|
||
refreshIMServerInfoConfig()
|
||
|
||
|
||
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { granted, error in
|
||
print("Notification granted: \(granted), error: \(String(describing: error))")
|
||
}
|
||
}
|
||
|
||
@objc func notiLogout() {
|
||
imInfo = nil
|
||
resetCount()
|
||
removeAllCache()
|
||
NIMSDK.shared().v2LoginService.logout { [weak self] in
|
||
self?.resetCount()
|
||
}
|
||
}
|
||
}
|
||
|
||
// public cache
|
||
extension IMManager {
|
||
public func addCache(sessionID: String) {
|
||
if !cacheSessions.contains(sessionID) {
|
||
cacheSessions.append(sessionID)
|
||
}
|
||
}
|
||
|
||
public func deleteCache(sessionID: String) {
|
||
if cacheSessions.contains(sessionID) {
|
||
cacheSessions.removeObj(sessionID)
|
||
}
|
||
}
|
||
|
||
func findCache(sessionID: String) -> Bool{
|
||
return cacheSessions.contains(sessionID)
|
||
}
|
||
|
||
func removeAllCache() {
|
||
cacheSessions.removeAll()
|
||
}
|
||
}
|
||
|
||
// 消息处理
|
||
extension IMManager {
|
||
|
||
/// 发送消息, 注意这里传云信的account ID,不是用户userID
|
||
static func sendMessage(msgText: String, accID:String) {
|
||
|
||
if String.realEmpty(str: msgText.trimmed) {
|
||
// 为空,不做操作
|
||
return
|
||
}
|
||
// 创建消息
|
||
let message = IMMessageMaker.msgWithText(msgText)
|
||
// 判断消息是否可以发送
|
||
let canSend = IMManager.dealWillSendMessage(message: message)
|
||
if canSend {
|
||
// 创建会话
|
||
// let session = NIMSession(accID, type: .P2P)
|
||
// NIMSDK.shared().chatManager.send(message, to: session) { error in
|
||
// // 消息发送回调
|
||
// #warning("test action")
|
||
// }
|
||
} else {
|
||
// 这里需不需要处理成本地消息?
|
||
}
|
||
}
|
||
|
||
// 处理是否能发送消息
|
||
static public func dealWillSendMessage(message: V2NIMMessage) -> Bool {
|
||
// var dict = [String:String]()
|
||
|
||
// if message.messageType == .text {
|
||
// // 文本消息,检查敏感词
|
||
// let word = GameDataManager.shared.matchKeywordWith(sourceStr: message.text?.lowercased() ?? "")
|
||
// if word.count > 0 {
|
||
// dict["keyword"] = word
|
||
// dict["type"] = "NOTICE_KEYWORD"
|
||
// message.remoteExt = dict
|
||
// return false
|
||
// }
|
||
// }
|
||
|
||
return true
|
||
}
|
||
}
|