Sfoglia il codice sorgente

feat: 补充 IM 的在线状态逻辑

陈文艺 3 mesi fa
parent
commit
437c635dc6

+ 1 - 0
Lanu.xcodeproj/project.pbxproj

@@ -74,6 +74,7 @@
 				Common/Views/LNAutoSizeTextView.swift,
 				Common/Views/LNBirthdayDatePickerPanel.swift,
 				Common/Views/LNCircleProgressView.swift,
+				Common/Views/LNOnlineView.swift,
 				Common/Views/LNPopupView.swift,
 				Common/Views/LNSortedEditView.swift,
 				Common/Views/Loading/LNLoadingView.swift,

+ 95 - 0
Lanu/Common/Views/LNOnlineView.swift

@@ -0,0 +1,95 @@
+//
+//  LNOnlineView.swift
+//  Lanu
+//
+//  Created by OneeChan on 2026/1/4.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNOnlineView: UIView {
+    private let borderColor: UIColor = .primary_3
+    private let borderWidth: CGFloat = 1
+    private let duration: Double = 1.2
+    private let scale = 1.2
+    private var animator: UIViewPropertyAnimator?
+    
+    private let borderLayer = CAShapeLayer()
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        layer.borderWidth = borderWidth
+        layer.borderColor = borderColor.cgColor
+        clipsToBounds = false
+        isUserInteractionEnabled = false
+        
+        borderLayer.borderWidth = borderWidth
+        borderLayer.borderColor = borderColor.cgColor
+        layer.addSublayer(borderLayer)
+        
+        startAnimate()
+    }
+    
+    private func startAnimate() {
+        let scaleAnim = CABasicAnimation(keyPath: "transform.scale")
+        scaleAnim.fromValue = 1.0
+        scaleAnim.toValue = scale
+        scaleAnim.duration = duration
+        
+        let opacityAnim = CABasicAnimation(keyPath: "opacity")
+        opacityAnim.fromValue = 1.0
+        opacityAnim.toValue = 0.0
+        opacityAnim.duration = duration
+        
+        let animGroup = CAAnimationGroup()
+        animGroup.animations = [scaleAnim, opacityAnim]
+        animGroup.duration = duration
+        animGroup.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
+        animGroup.isRemovedOnCompletion = false
+        animGroup.fillMode = .forwards
+        animGroup.repeatCount = .infinity
+        borderLayer.add(animGroup, forKey: "scaleAndFadeGroup")
+    }
+    
+    override func layoutSubviews() {
+        super.layoutSubviews()
+        
+        layer.cornerRadius = bounds.height * 0.5
+        borderLayer.frame = bounds
+        borderLayer.cornerRadius = bounds.height * 0.5
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+#if DEBUG
+
+import SwiftUI
+
+struct LNOnlineViewPreview: UIViewRepresentable {
+    func makeUIView(context: Context) -> some UIView {
+        let container = UIView()
+        container.backgroundColor = .lightGray
+        
+        let view = LNOnlineView()
+        container.addSubview(view)
+        view.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+            make.width.height.equalTo(50)
+        }
+        
+        return container
+    }
+    
+    func updateUIView(_ uiView: UIViewType, context: Context) { }
+}
+
+#Preview(body: {
+    LNOnlineViewPreview()
+})
+#endif

+ 24 - 40
Lanu/Manager/IM/LNIMManager.swift

@@ -10,17 +10,16 @@ import Foundation
 
 protocol LNIMManagerNotify {
     func onConversationListChanged()
-    func onIMUserStatusChanged(uid: String, status: V2TIMUserStatusType)
 }
 extension LNIMManagerNotify {
     func onConversationListChanged() {}
-    func onIMUserStatusChanged(uid: String, status: V2TIMUserStatusType) {}
 }
 
 
 extension String {
     var isImOfficialId: Bool {
-        (Int(self) ?? -1) <= (Int(LNIMManager.officialId) ?? 0)
+        guard let intValue = Int(self) else { return false }
+        return intValue <= LNIMManager.maxOfficialId
     }
 }
 
@@ -30,15 +29,20 @@ enum LNIMCustomErrorCode: Int {
     case userNotExist = 120002
 }
 
+enum LNIMOfficialIds: String {
+    case officialMessage = "10000"
+}
+
 
 class LNIMManager: NSObject {
     private static let appId: Int32 = 20030346
     
     static var shared = LNIMManager()
-    static let officialId = "10000"
+    
+    static let maxOfficialId = 10000
     
     private(set) var conversationList: [V2TIMConversation] = []
-    private(set) var userStatus: [String: V2TIMUserStatusType] = [:]
+    private(set) var userOnlineMap: [String: Bool] = [:]
     
     private override init() {
         super.init()
@@ -63,17 +67,16 @@ extension LNIMManager {
                 
                 if list.firstIndex(where: { $0.userID?.isImOfficialId == true }) == nil {
                     // 插入官方消息
-                    V2TIMManager.sharedInstance().setConversationDraft(conversationID: "c2c_" + Self.officialId, draftText: " ") {
-                        V2TIMManager.sharedInstance().setConversationDraft(conversationID: "c2c_" + Self.officialId, draftText: nil, succ: nil)
+                    let officialId = "c2c_" + LNIMOfficialIds.officialMessage.rawValue
+                    V2TIMManager.sharedInstance().setConversationDraft(conversationID: officialId, draftText: " ") {
+                        V2TIMManager.sharedInstance().setConversationDraft(conversationID: officialId, draftText: nil, succ: nil)
                     }
                 }
                 
                 conversationList = list
                 handler?(true)
                 notifyConversationListChanged()
-                
-                V2TIMManager.sharedInstance().subscribeUserStatus(userIDList: list.compactMap({ $0.userID }), succ: nil)
-                loadUsersOnlineStatus()
+                getUserOnlineState()
         } fail: { code, err in
             handler?(false)
         }
@@ -85,26 +88,20 @@ extension LNIMManager {
             conversationID: conversationId,
             cleanTimestamp: 0, cleanSequence: 0, succ: nil)
     }
-}
-
-extension LNIMManager {
-    func isUserOnline(uid: String) -> Bool {
-        userStatus[uid] == .USER_STATUS_ONLINE
-    }
     
-    private func loadUsersOnlineStatus() {
+    func getUserOnlineState() {
         let uids = conversationList.compactMap { $0.userID }
-        guard !uids.isEmpty else { return }
-        V2TIMManager.sharedInstance().getUserStatus(userIDList: uids)
-        { [weak self] userStatusList in
+        if uids.isEmpty { return }
+        LNProfileManager.shared.getUserOnlineState(uids: uids) { [weak self] map in
             guard let self else { return }
-            userStatusList?.forEach {
-                if let uid = $0.userID {
-                    self.userStatus[uid] = $0.statusType
-                    self.notifyUserStatusChanged(uid: uid, status: $0.statusType)
-                }
-            }
-        } fail: { _, _ in }
+            userOnlineMap = map
+            notifyConversationListChanged()
+        }
+    }
+    
+    func isUserOnline(uid: String?) -> Bool {
+        guard let uid else { return false }
+        return userOnlineMap[uid] == true
     }
 }
 
@@ -124,12 +121,6 @@ extension LNIMManager: V2TIMConversationListener {
 
 extension LNIMManager: V2TIMSDKListener {
     func onUserStatusChanged(userStatusList: [V2TIMUserStatus]!) {
-        userStatusList.forEach {
-            if let uid = $0.userID {
-                userStatus[uid] = $0.statusType
-                notifyUserStatusChanged(uid: uid, status: $0.statusType)
-            }
-        }
     }
 }
 
@@ -160,7 +151,6 @@ extension LNIMManager: LNAccountManagerNotify {
     
     func onUserLogout() {
         V2TIMManager.sharedInstance().logout(succ: nil)
-        userStatus.removeAll()
         
         Self.shared = LNIMManager()
     }
@@ -180,10 +170,4 @@ extension LNIMManager {
             ($0 as? LNIMManagerNotify)?.onConversationListChanged()
         }
     }
-    
-    private func notifyUserStatusChanged(uid: String, status: V2TIMUserStatusType) {
-        LNEventDeliver.notifyEvent {
-            ($0 as? LNIMManagerNotify)?.onIMUserStatusChanged(uid: uid, status: status)
-        }
-    }
 }

+ 10 - 0
Lanu/Manager/Profile/LNProfileManager.swift

@@ -137,6 +137,16 @@ extension LNProfileManager {
             }
         }
     }
+    
+    func getUserOnlineState(uids: [String], queue: DispatchQueue = .main, handler: @escaping ([String: Bool]) -> Void) {
+        LNHttpManager.shared.getUserOnlineState(uids: uids) { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res?.list.reduce(into: [String: Bool](), { partialResult, state in
+                    partialResult[state.userNo] = state.online
+                }) ?? [:])
+            }
+        }
+    }
 }
 
 extension LNProfileManager {

+ 9 - 0
Lanu/Manager/Profile/Network/LNHttpManager+Profile.swift

@@ -15,6 +15,8 @@ private let kNetPath_Profile_UsersInfo = "/user/get/infos"
 
 private let kNetPath_Profile_Random = "/user/info/random/profiles"
 
+private let kNetPath_Profile_Online = "/user/getUsersOnlineState"
+
 
 class LNProfileUpdateConfig {
     var avatar: String?
@@ -81,4 +83,11 @@ extension LNHttpManager {
     func getRandomProfile(completion: @escaping (LNRandomProfileResponse?, LNHttpError?) -> Void) {
         post(path: kNetPath_Profile_Random, completion: completion)
     }
+    
+    func getUserOnlineState(uids: [String], completion: @escaping (LNUserOnlineStateResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_Profile_Online, params: [
+            "userNos": uids
+        ], completion: completion)
+        
+    }
 }

+ 11 - 0
Lanu/Manager/Profile/Network/LNProfileResponse.swift

@@ -83,3 +83,14 @@ class LNRandomProfileResponse: Decodable {
     var male: LNProfileRandomInfoVO = LNProfileRandomInfoVO()
     var female: LNProfileRandomInfoVO = LNProfileRandomInfoVO()
 }
+
+@AutoCodable
+class LNUserOnlineStateVO: Decodable {
+    var userNo: String = ""
+    var online: Bool = false
+}
+
+@AutoCodable
+class LNUserOnlineStateResponse: Decodable {
+    var list: [LNUserOnlineStateVO] = []
+}

+ 7 - 0
Lanu/Views/Game/MateList/LNGameMateListCell.swift

@@ -12,6 +12,7 @@ import SnapKit
 
 class LNGameMateListCell: UITableViewCell {
     private let avatar = UIImageView()
+    private let onlineView = LNOnlineView()
     
     private let playButton = UIButton()
     private let voiceLabel = UILabel()
@@ -47,6 +48,7 @@ class LNGameMateListCell: UITableViewCell {
     
     func update(_ item: LNGameMateListItemVO) {
         avatar.sd_setImage(with: URL(string: item.avatar))
+        onlineView.isHidden = !item.online
         priceLabel.text = item.price.toDisplay
         unitLabel.text = "/\(item.unit)"
         
@@ -192,6 +194,11 @@ extension LNGameMateListCell {
             make.bottom.equalToSuperview()
         }
         
+        container.addSubview(onlineView)
+        onlineView.snp.makeConstraints { make in
+            make.directionalEdges.equalTo(avatar).inset(-2)
+        }
+        
         playButton.setBackgroundImage(.primary_7, for: .normal)
         playButton.layer.cornerRadius = 11
         playButton.clipsToBounds = true

+ 4 - 6
Lanu/Views/IM/Chat/LNIMChatTopMenuView.swift

@@ -61,8 +61,10 @@ extension LNIMChatTopMenuView {
     
     private func loadUserOnlineStatus() {
         guard let uid = viewModel?.userId else { return }
-        stateLabel.text = LNIMManager.shared.isUserOnline(uid: uid)
-        ? .init(key: "Online") : .init(key: "OffLine")
+        LNProfileManager.shared.getUserOnlineState(uids: [uid]) { [weak self] map in
+            guard let self else { return }
+            stateLabel.text = map[uid] == true ? .init(key: "Online") : .init(key: "OffLine")
+        }
     }
     
     private func loadRelation() {
@@ -72,10 +74,6 @@ extension LNIMChatTopMenuView {
 }
 
 extension LNIMChatTopMenuView: LNIMManagerNotify {
-    func onIMUserStatusChanged(uid: String, status: V2TIMUserStatusType) {
-        loadUserOnlineStatus()
-    }
-    
     func onConversationListChanged() {
         loadUnreadCount()
     }

+ 10 - 2
Lanu/Views/IM/ConversationList/LNIMConversationCell.swift

@@ -12,6 +12,7 @@ import SnapKit
 
 class LNIMConversationCell: UITableViewCell {
     private let avatar = UIImageView()
+    private let onlineView = LNOnlineView()
     private let titleLabel = UILabel()
     private let messageLabel = UILabel()
     private let timeLabel = UILabel()
@@ -25,9 +26,9 @@ class LNIMConversationCell: UITableViewCell {
         setupViews()
     }
     
-    func update(_ item: V2TIMConversation) {
+    func update(_ item: V2TIMConversation, isOnline: Bool) {
         if item.userID?.isImOfficialId == true {
-            if item.userID == LNIMManager.officialId {
+            if item.userID == LNIMOfficialIds.officialMessage.rawValue {
                 avatar.image = .init(named: "ic_im_official")
                 titleLabel.text = .init(key: "System Message")
             } else {
@@ -35,9 +36,11 @@ class LNIMConversationCell: UITableViewCell {
                 titleLabel.text = item.showName
             }
             titleLabel.textColor = .text_6
+            onlineView.isHidden = true
         } else {
             titleLabel.text = item.showName
             titleLabel.textColor = .text_5
+            onlineView.isHidden = !isOnline
         }
         messageLabel.attributedText = item.lastDisplayString
         if messageLabel.attributedText?.string.isEmpty != false,
@@ -88,6 +91,11 @@ extension LNIMConversationCell {
             make.width.height.equalTo(46)
         }
         
+        contentView.addSubview(onlineView)
+        onlineView.snp.makeConstraints { make in
+            make.directionalEdges.equalTo(avatar).inset(-2)
+        }
+        
         let textView = buildTextView()
         contentView.addSubview(textView)
         

+ 2 - 1
Lanu/Views/IM/ConversationList/LNIMConversationListController.swift

@@ -53,7 +53,7 @@ extension LNIMConversationListController: UITableViewDataSource, UITableViewDele
         let cell = tableView.dequeueReusableCell(withIdentifier: LNIMConversationCell.className, for: indexPath) as! LNIMConversationCell
         
         let item = LNIMManager.shared.conversationList[indexPath.row]
-        cell.update(item)
+        cell.update(item, isOnline: LNIMManager.shared.isUserOnline(uid: item.userID))
         
         return cell
     }
@@ -112,6 +112,7 @@ extension LNIMConversationListController {
            let icon = tabView.subviews.first(where: { $0 is UIImageView }) {
             redDotView.backgroundColor = .fill_6
             redDotView.layer.cornerRadius = 4
+            redDotView.isHidden = true
             tabView.addSubview(redDotView)
             redDotView.snp.makeConstraints { make in
                 make.top.trailing.equalTo(icon)

+ 2 - 2
Lanu/Views/IM/Notify/LNIMOfficialMessageViewController.swift

@@ -24,7 +24,7 @@ class LNIMOfficialMessageViewController: LNViewController {
     private let tableView = UITableView()
     
     init(uid: String) {
-        viewModel = LNIMChatViewModel(userId: LNIMManager.officialId)
+        viewModel = LNIMChatViewModel(userId: LNIMOfficialIds.officialMessage.rawValue)
         super.init(nibName: nil, bundle: nil)
     }
     
@@ -143,7 +143,7 @@ extension LNIMOfficialMessageViewController {
     private func setupViews() {
         view.backgroundColor = .primary_1
         
-        title = viewModel.userId == LNIMManager.officialId ? .init(key: "System Message") : .init(key: "Official Message")
+        title = viewModel.userId == LNIMOfficialIds.officialMessage.rawValue ? .init(key: "System Message") : .init(key: "Official Message")
         
         tableView.backgroundColor = .clear
         tableView.separatorStyle = .none

+ 7 - 0
Lanu/Views/Search/LNUserSearchItemCell.swift

@@ -11,6 +11,7 @@ import SnapKit
 
 class LNUserSearchItemCell: UITableViewCell {
     private let avatar = UIImageView()
+    private let onlineView = LNOnlineView()
     private let nameLabel = UILabel()
     private let genderView = LNGenderView()
     private let idLabel = UILabel()
@@ -29,6 +30,7 @@ class LNUserSearchItemCell: UITableViewCell {
     
     func update(_ item: LNGameMateSearchResultVO) {
         avatar.sd_setImage(with: URL(string: item.avatar))
+        onlineView.isHidden = !item.online
         nameLabel.text = item.nickname
         genderView.update(item.gender, item.age)
         idLabel.text = "ID \(item.userNo)"
@@ -81,6 +83,11 @@ extension LNUserSearchItemCell {
             make.directionalVerticalEdges.equalToSuperview().inset(10)
         }
         
+        contentView.addSubview(onlineView)
+        onlineView.snp.makeConstraints { make in
+            make.directionalEdges.equalTo(avatar).inset(-2)
+        }
+        
         let follow = buildFollow()
         contentView.addSubview(follow)
         follow.snp.makeConstraints { make in

+ 8 - 2
Lanu/Views/Search/LNUserSearchViewController.swift

@@ -64,9 +64,15 @@ extension LNUserSearchViewController {
                 }
                 return
             }
-            curList = list
+            if nextTag?.isEmpty != false {
+                curList = list
+            } else {
+                curList.append(contentsOf: list)
+            }
+            nextTag = next
+            
             tableView.reloadData()
-            if list.isEmpty {
+            if curList.isEmpty {
                 emptyView.showNoData(tips: .init(key: "暂无搜索结果"))
             } else {
                 emptyView.hide()