// // 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() 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 } }