LNIMManager.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. //
  2. // LNIMManager.swift
  3. // Lanu
  4. //
  5. // Created by OneeChan on 2025/11/12.
  6. //
  7. import Foundation
  8. import TUICallEngine
  9. protocol LNIMManagerNotify {
  10. func onConversationListChanged()
  11. func onIMUserStatusChanged(uid: String, status: V2TIMUserStatusType)
  12. func onVoiceCallBegin()
  13. func onVoiceCallEnd()
  14. func onVoiceCallInfoChanged()
  15. }
  16. extension LNIMManagerNotify {
  17. func onConversationListChanged() {}
  18. func onIMUserStatusChanged(uid: String, status: V2TIMUserStatusType) {}
  19. func onVoiceCallBegin() { }
  20. func onVoiceCallEnd() { }
  21. func onVoiceCallInfoChanged() { }
  22. }
  23. extension String {
  24. var isImOfficialId: Bool {
  25. guard let intValue = Int(self) else { return false }
  26. return intValue <= LNIMManager.maxOfficialId
  27. }
  28. }
  29. enum LNIMCustomErrorCode: Int {
  30. case inBlackList = 120001
  31. case userNotExist = 120002
  32. }
  33. enum LNIMOfficialIds: String {
  34. case officialMessage = "9998"
  35. }
  36. class LNIMVoiceCallInfo {
  37. let uid: String
  38. var isInCome = false
  39. var beginTime: TimeInterval = 0
  40. var isMute = false
  41. var isSpeaker = false
  42. init(uid: String) {
  43. self.uid = uid
  44. }
  45. }
  46. class LNIMManager: NSObject {
  47. private static var appId: Int32 {
  48. if LNAppConfig.shared.curEnv == .test {
  49. 20030346
  50. } else {
  51. 80000456
  52. }
  53. }
  54. static var shared = LNIMManager()
  55. static let maxOfficialId = 10000
  56. static let maxMessageInput = 200
  57. private(set) var conversationList: [V2TIMConversation] = []
  58. private(set) var userStatus: [String: V2TIMUserStatusType] = [:]
  59. private(set) var voiceCallAvailable = false
  60. private(set) var curCallInfo: LNIMVoiceCallInfo?
  61. private override init() {
  62. super.init()
  63. _ = LNIMEmojiManager.shared
  64. LNEventDeliver.addObserver(self)
  65. V2TIMManager.sharedInstance().addConversationListener(listener: self)
  66. V2TIMManager.sharedInstance().addIMSDKListener(listener: self)
  67. }
  68. }
  69. extension LNIMManager {
  70. func reloadConversationList(handler: ((Bool) -> Void)? = nil) {
  71. let filter = V2TIMConversationListFilter()
  72. filter.type = .C2C
  73. V2TIMManager.sharedInstance().getConversationListByFilter(
  74. filter: filter,
  75. nextSeq: 0, count: 1000)
  76. { [weak self] list, nextTag, isFinish in
  77. guard let self else { return }
  78. guard var list else {
  79. handler?(false)
  80. return
  81. }
  82. // 按照时间排序
  83. list.sort { $0.userID?.isImOfficialId == true || $0.orderKey > $1.orderKey }
  84. if list.firstIndex(where: { $0.userID?.isImOfficialId == true }) == nil {
  85. // 插入官方消息
  86. let officialId = "c2c_" + LNIMOfficialIds.officialMessage.rawValue
  87. saveDraftFor(officialId, draft: " ")
  88. saveDraftFor(officialId, draft: nil)
  89. }
  90. for item in list {
  91. if let old = conversationList.first(where: { $0.conversationID == item.conversationID }),
  92. let userInfo = old.userInfo {
  93. item.userInfo = userInfo
  94. }
  95. }
  96. conversationList = list
  97. handler?(true)
  98. notifyConversationListChanged()
  99. V2TIMManager.sharedInstance().subscribeUserStatus(userIDList: list.compactMap({ $0.userID }), succ: nil)
  100. loadUsersOnlineStatus()
  101. } fail: { code, err in
  102. handler?(false)
  103. }
  104. }
  105. func cleanConversationUnread(uid: String) {
  106. let conversationId = "c2c_" + uid
  107. V2TIMManager.sharedInstance().cleanConversationUnreadMessageCount(
  108. conversationID: conversationId,
  109. cleanTimestamp: 0, cleanSequence: 0, succ: nil)
  110. }
  111. func saveDraftFor(_ conversationId: String?, draft: String?) {
  112. guard let conversationId else { return }
  113. V2TIMManager.sharedInstance().setConversationDraft(conversationID: conversationId, draftText: draft, succ: nil)
  114. }
  115. }
  116. extension LNIMManager {
  117. func isUserOnline(uid: String) -> Bool {
  118. userStatus[uid] == .USER_STATUS_ONLINE
  119. }
  120. func getUserOnlineState(uid: String, handler: @escaping (Bool) -> Void) {
  121. V2TIMManager.sharedInstance().getUserStatus(userIDList: [uid])
  122. { [weak self] userStatusList in
  123. guard let self else { return }
  124. userStatusList?.forEach {
  125. if let uid = $0.userID {
  126. self.userStatus[uid] = $0.statusType
  127. self.notifyUserStatusChanged(uid: uid, status: $0.statusType)
  128. }
  129. }
  130. handler(userStatusList?.first(where: { $0.userID == uid })?.statusType == .USER_STATUS_ONLINE)
  131. } fail: { _, _ in }
  132. }
  133. private func loadUsersOnlineStatus() {
  134. let uids = conversationList.compactMap { $0.userID }
  135. guard !uids.isEmpty else { return }
  136. V2TIMManager.sharedInstance().getUserStatus(userIDList: uids)
  137. { [weak self] userStatusList in
  138. guard let self else { return }
  139. userStatusList?.forEach {
  140. if let uid = $0.userID {
  141. self.userStatus[uid] = $0.statusType
  142. self.notifyUserStatusChanged(uid: uid, status: $0.statusType)
  143. }
  144. }
  145. } fail: { _, _ in }
  146. }
  147. }
  148. extension LNIMManager {
  149. func makeVoiceCall(uid: String) {
  150. guard curCallInfo == nil else { return }
  151. curCallInfo = .init(uid: uid)
  152. curCallInfo?.isInCome = false
  153. let panel = LNVoiceCallPanel()
  154. panel.toCallOut(uid: uid)
  155. panel.popup()
  156. let param = TUICallParams()
  157. TUICallEngine.createInstance().call(userId: uid, callMediaType: .audio, params: param) {
  158. } fail: { _, err in
  159. showToast(err)
  160. }
  161. }
  162. func rejectVoiceCall() {
  163. TUICallEngine.createInstance().reject { }
  164. fail: { _, err in
  165. showToast(err)
  166. }
  167. }
  168. func acceptVoiceCall() {
  169. TUICallEngine.createInstance().accept { }
  170. fail: { _, err in
  171. showToast(err)
  172. }
  173. }
  174. func hangupVoiceCall() {
  175. TUICallEngine.createInstance().hangup { }
  176. fail: { _, err in
  177. showToast(err)
  178. }
  179. }
  180. func switchVoiceCallMicrophone() {
  181. if curCallInfo?.isMute == true {
  182. TUICallEngine.createInstance().openMicrophone { [weak self] in
  183. guard let self else { return }
  184. curCallInfo?.isMute = false
  185. LNEventDeliver.notifyEvent {
  186. ($0 as? LNIMManagerNotify)?.onVoiceCallInfoChanged()
  187. }
  188. }
  189. fail: { _, err in
  190. showToast(err)
  191. }
  192. } else {
  193. TUICallEngine.createInstance().closeMicrophone()
  194. curCallInfo?.isMute = true
  195. LNEventDeliver.notifyEvent {
  196. ($0 as? LNIMManagerNotify)?.onVoiceCallInfoChanged()
  197. }
  198. }
  199. }
  200. func switchVoiceCallSpeakerType() {
  201. let isSpeaker = curCallInfo?.isSpeaker == true
  202. TUICallEngine.createInstance().selectAudioPlaybackDevice(isSpeaker ? .earpiece : .speakerphone)
  203. curCallInfo?.isSpeaker = !isSpeaker
  204. LNEventDeliver.notifyEvent {
  205. ($0 as? LNIMManagerNotify)?.onVoiceCallInfoChanged()
  206. }
  207. }
  208. }
  209. extension LNIMManager: V2TIMConversationListener {
  210. func onNewConversation(conversationList: [V2TIMConversation]!) {
  211. reloadConversationList()
  212. loadUsersOnlineStatus()
  213. }
  214. func onConversationChanged(conversationList: [V2TIMConversation]!) {
  215. var isChanged = false
  216. for item in conversationList {
  217. let old = self.conversationList.first(where: { $0.conversationID == item.conversationID })
  218. if let old, old.unreadCount != item.unreadCount
  219. || old.lastDisplayDate != item.lastDisplayDate
  220. || old.lastMessage?.msgID != item.lastMessage?.msgID {
  221. isChanged = true
  222. break
  223. }
  224. }
  225. if isChanged {
  226. reloadConversationList()
  227. }
  228. }
  229. func onConversationDeleted(conversationIDList: [String]!) {
  230. reloadConversationList()
  231. loadUsersOnlineStatus()
  232. }
  233. }
  234. extension LNIMManager: V2TIMSDKListener {
  235. func onUserStatusChanged(userStatusList: [V2TIMUserStatus]!) {
  236. userStatusList.forEach {
  237. if let uid = $0.userID {
  238. userStatus[uid] = $0.statusType
  239. notifyUserStatusChanged(uid: uid, status: $0.statusType)
  240. }
  241. }
  242. }
  243. }
  244. extension LNIMManager: TUICallObserver {
  245. func onCallReceived(callerId: String, calleeIdList: [String],
  246. groupId: String?, callMediaType: TUICallMediaType,
  247. userData: String?)
  248. {
  249. guard curCallInfo == nil else {
  250. return
  251. }
  252. curCallInfo = .init(uid: callerId)
  253. curCallInfo?.isInCome = true
  254. let panel = LNVoiceCallPanel()
  255. panel.onCallIn(uid: callerId)
  256. panel.popup()
  257. }
  258. func onCallCancelled(callerId: String) {
  259. curCallInfo = nil
  260. LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
  261. }
  262. func onCallBegin(roomId: TUIRoomId, callMediaType: TUICallMediaType, callRole: TUICallRole) {
  263. curCallInfo?.beginTime = curTime
  264. LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallBegin() }
  265. }
  266. func onCallEnd(roomId: TUIRoomId, callMediaType: TUICallMediaType, callRole: TUICallRole, totalTime: Float) {
  267. curCallInfo = nil
  268. LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
  269. }
  270. func onUserReject(userId: String) {
  271. // 会同步回调 onCallCancelled
  272. // curCallInfo = nil
  273. // LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
  274. }
  275. func onUserNoResponse(userId: String) {
  276. // 会同步回调 onCallCancelled
  277. // curCallInfo = nil
  278. // LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
  279. }
  280. func onUserLineBusy(userId: String) {
  281. curCallInfo = nil
  282. LNEventDeliver.notifyEvent { ($0 as? LNIMManagerNotify)?.onVoiceCallEnd() }
  283. }
  284. }
  285. extension LNIMManager: LNAccountManagerNotify {
  286. func onUserLogin() {
  287. // 初始化 SDK
  288. let config = V2TIMSDKConfig()
  289. config.logLevel = .LOG_INFO
  290. guard V2TIMManager.sharedInstance().initSDK(Self.appId, config: config) else {
  291. return
  292. }
  293. // 登录
  294. let loginSuccessBlock = { [weak self] in
  295. guard let self else { return }
  296. reloadConversationList()
  297. }
  298. getIMSignToken { token in
  299. guard let token else { return }
  300. if V2TIMManager.sharedInstance().getLoginUser()?.isMyUid == true {
  301. loginSuccessBlock()
  302. } else {
  303. V2TIMManager.sharedInstance().login(userID: myUid, userSig: token, succ: loginSuccessBlock)
  304. }
  305. TUICallEngine.createInstance().`init`(Self.appId, userId: myUid, userSig: token) { [weak self] in
  306. guard let self else { return }
  307. voiceCallAvailable = true
  308. TUICallEngine.createInstance().addObserver(self)
  309. } fail: { _, _ in
  310. }
  311. }
  312. }
  313. func onUserLogout() {
  314. V2TIMManager.sharedInstance().logout(succ: nil)
  315. TUICallEngine.destroyInstance()
  316. Self.shared = LNIMManager()
  317. }
  318. }
  319. extension LNIMManager {
  320. private func getIMSignToken(handler: @escaping (String?) -> Void) {
  321. LNHttpManager.shared.getIMSign { token, err in
  322. handler(token)
  323. }
  324. }
  325. }
  326. extension LNIMManager {
  327. private func notifyConversationListChanged() {
  328. LNEventDeliver.notifyEvent {
  329. ($0 as? LNIMManagerNotify)?.onConversationListChanged()
  330. }
  331. }
  332. private func notifyUserStatusChanged(uid: String, status: V2TIMUserStatusType) {
  333. LNEventDeliver.notifyEvent {
  334. ($0 as? LNIMManagerNotify)?.onIMUserStatusChanged(uid: uid, status: status)
  335. }
  336. }
  337. }