Ver Fonte

Merge remote-tracking branch 'origin/feat/im_remark' into dev

* origin/feat/im_remark:
  feat: 补充 IM 备注功能
陈文艺 há 1 mês atrás
pai
commit
0d9363d296

+ 2 - 0
Lanu.xcodeproj/project.pbxproj

@@ -127,6 +127,7 @@
 				Manager/IM/LNIMManager.swift,
 				Manager/IM/LNIMMessageData.swift,
 				"Manager/IM/Network/LNHttpManager+IM.swift",
+				Manager/IM/Network/LNIMResponse.swift,
 				"Manager/IM/TUIUtils/String+TUILocalized.swift",
 				"Manager/IM/TUIUtils/V2TIMConversation+Extension.swift",
 				"Manager/IM/TUIUtils/V2TIMMessage+Extension.swift",
@@ -239,6 +240,7 @@
 				Views/IM/Chat/InputMenu/LNIMChatVoiceWaveView.swift,
 				Views/IM/Chat/LNIMChatViewController.swift,
 				Views/IM/Chat/UserMenu/LNIMChatUserMenuView.swift,
+				Views/IM/Chat/UserMenu/LNIMChatUserRemarkPanel.swift,
 				Views/IM/Chat/ViewModel/LNIMChatViewModel.swift,
 				Views/IM/ConversationList/LNIMConversationCell.swift,
 				Views/IM/ConversationList/LNIMConversationListController.swift,

+ 23 - 0
Lanu/Localizable.xcstrings

@@ -6625,6 +6625,29 @@
         }
       }
     },
+    "A00290" : {
+      "extractionState" : "manual",
+      "localizations" : {
+        "en" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Fill in the note name"
+          }
+        },
+        "id" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "Isi nama catatan"
+          }
+        },
+        "zh-Hans" : {
+          "stringUnit" : {
+            "state" : "translated",
+            "value" : "请填写备注名称"
+          }
+        }
+      }
+    },
     "B00001" : {
       "extractionState" : "manual",
       "localizations" : {

+ 35 - 4
Lanu/Manager/IM/LNIMManager.swift

@@ -60,6 +60,7 @@ class LNIMManager: NSObject {
     
     static let maxOfficialId = 10000
     static let maxMessageInput = 200
+    static let maxRemarkLength = 16
     
     private(set) var conversationList: [V2TIMConversation] = []
     private(set) var userStatus: [String: V2TIMUserStatusType] = [:]
@@ -97,10 +98,7 @@ extension LNIMManager {
             }
             
             for item in list {
-                if let old = conversationList.first(where: { $0.conversationID == item.conversationID }),
-                   let userInfo = old.userInfo {
-                    item.userInfo = userInfo
-                }
+                item.extraInfo = conversationList.first(where: { $0.conversationID == item.conversationID })?.extraInfo ?? LNIMConversationExtraInfo()
             }
             
             conversationList = list
@@ -109,6 +107,14 @@ extension LNIMManager {
             
             V2TIMManager.sharedInstance().subscribeUserStatus(userIDList: list.compactMap({ $0.userID }), succ: nil)
             loadUsersOnlineStatus()
+            getUsersRemark(uids: list.compactMap({ $0.userID })) { [weak self] remarks in
+                guard let remarks else { return }
+                guard let self else { return }
+                for item in remarks {
+                    list.first { $0.userID == item.userNo }?.extraInfo?.remark = item.note
+                }
+                notifyConversationListChanged()
+            }
         } fail: { code, err in
             handler?(false)
         }
@@ -201,6 +207,31 @@ extension LNIMManager: V2TIMSDKListener {
     }
 }
 
+extension LNIMManager {
+    func setUserRemark(uid: String, remark: String, queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) {
+        LNHttpManager.shared.setUserRemark(uid: uid, remark: remark) { [weak self] err in
+            queue.asyncIfNotGlobal {
+                handler(err == nil)
+            }
+            if let err {
+                showToast(err.errorDesc)
+            } else {
+                guard let self else { return }
+                conversationList.first { $0.userID == uid }?.extraInfo?.remark = remark
+                notifyConversationListChanged()
+            }
+        }
+    }
+    
+    func getUsersRemark(uids: [String], queue: DispatchQueue = .main, handler: @escaping ([LNIMUserRemarkVO]?) -> Void) {
+        LNHttpManager.shared.getUsersRemark(uids: uids) { res, err in
+            queue.asyncIfNotGlobal {
+                handler(res?.list)
+            }
+        }
+    }
+}
+
 extension LNIMManager: LNAccountManagerNotify {
     func onUserLogin() {
         // 初始化 SDK

+ 17 - 0
Lanu/Manager/IM/Network/LNHttpManager+IM.swift

@@ -9,6 +9,8 @@ import Foundation
 
 
 private let kNetPath_IM_Sign = "/im/userSign"
+private let kNetPath_IM_Remark = "/im/set/usernameNote"
+private let kNetPath_IM_Remark_Query = "/im/get/usernameNotes"
 
 
 extension LNHttpManager {
@@ -16,3 +18,18 @@ extension LNHttpManager {
         post(path: kNetPath_IM_Sign, completion: completion)
     }
 }
+
+extension LNHttpManager {
+    func setUserRemark(uid: String, remark: String, completion: @escaping (LNHttpError?) -> Void) {
+        post(path: kNetPath_IM_Remark, params: [
+            "userNo": uid,
+            "note": remark
+        ], completion: completion)
+    }
+    
+    func getUsersRemark(uids: [String], completion: @escaping (LNIMUsersRemarkResponse?, LNHttpError?) -> Void) {
+        post(path: kNetPath_IM_Remark_Query, params: [
+            "list": uids
+        ], completion: completion)
+    }
+}

+ 21 - 0
Lanu/Manager/IM/Network/LNIMResponse.swift

@@ -0,0 +1,21 @@
+//
+//  LNIMResponse.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/2/8.
+//
+
+import Foundation
+import AutoCodable
+
+
+@AutoCodable
+class LNIMUserRemarkVO: Decodable {
+    var userNo: String = ""
+    var note: String = ""
+}
+
+@AutoCodable
+class LNIMUsersRemarkResponse: Decodable {
+    var list: [LNIMUserRemarkVO] = []
+}

+ 16 - 4
Lanu/Manager/IM/TUIUtils/V2TIMConversation+Extension.swift

@@ -121,14 +121,26 @@ extension V2TIMConversation {
     }
 }
 
-private var userInfoKey: UInt8 = 0
+private var extraInfoKey: UInt8 = 0
+class LNIMConversationExtraInfo {
+    var userInfo: LNUserProfileVO?
+    var remark: String?
+}
 extension V2TIMConversation {
-    var userInfo: LNUserProfileVO? {
+    var extraInfo: LNIMConversationExtraInfo? {
         get {
-            objc_getAssociatedObject(self, &userInfoKey) as? LNUserProfileVO
+            objc_getAssociatedObject(self, &extraInfoKey) as? LNIMConversationExtraInfo
         }
         set {
-            objc_setAssociatedObject(self, &userInfoKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+            objc_setAssociatedObject(self, &extraInfoKey, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+        }
+    }
+    
+    var displayName: String? {
+        return if let remark = extraInfo?.remark, !remark.isEmpty {
+            remark
+        } else {
+            extraInfo?.userInfo?.nickname
         }
     }
 }

+ 7 - 1
Lanu/Views/IM/Chat/LNIMChatViewController.swift

@@ -109,12 +109,18 @@ extension LNIMChatViewController {
         loadRelation()
         loadUnreadCount()
         loadUserOnlineStatus()
+        
         viewModel.$userInfo.sink { [weak self] newInfo in
             guard let self else { return }
             guard let newInfo else { return }
-            nameLabel.text = newInfo.nickname
+            nameLabel.text = viewModel.remark?.isEmpty == false ? viewModel.remark : newInfo.nickname
             avatar.sd_setImage(with: URL(string: newInfo.avatar))
         }.store(in: &bag)
+        viewModel.$remark.sink { [weak self] newValue in
+            guard let self else { return }
+            guard let newValue else { return }
+            nameLabel.text = !newValue.isEmpty ? newValue : viewModel.userInfo?.nickname
+        }.store(in: &bag)
     }
     
     private func loadUnreadCount() {

+ 62 - 113
Lanu/Views/IM/Chat/UserMenu/LNIMChatUserMenuView.swift

@@ -51,8 +51,8 @@ extension LNIMChatUserMenuView {
         let menus = [
             buildMute(),
             
-//            buildLine(),
-//            buildRemark(),
+            buildLine(),
+            buildRemark(),
             
             buildLine(),
             buildBlack(),
@@ -65,110 +65,40 @@ extension LNIMChatUserMenuView {
     }
     
     private func buildMute() -> UIView {
-        let container = UIView()
-        container.snp.makeConstraints { make in
-            make.height.equalTo(52)
-        }
-        
-        let ic = UIImageView()
-        ic.image = .icImChatMenuMute
-        container.addSubview(ic)
-        ic.snp.makeConstraints { make in
-            make.leading.equalToSuperview().offset(16)
-            make.centerY.equalToSuperview()
-        }
-        
-        let titleLabel = UILabel()
-        titleLabel.text = .init(key: "A00085")
-        titleLabel.font = .body_m
-        titleLabel.textColor = .text_5
-        container.addSubview(titleLabel)
-        titleLabel.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.leading.equalTo(ic.snp.trailing).offset(10)
-        }
-
+        let scaleX: CGFloat = 40.0 / 51.0
+        let scaleY: CGFloat = 24.5 / 31.0
+        muteSwitch.onTintColor = .primary_5
+        muteSwitch.transform = CGAffineTransform(scaleX: scaleX, y: scaleY)
         muteSwitch.addAction(UIAction(handler: { [weak self] _ in
             guard let self else { return }
             viewModel?.updateMessageOpt(opt: muteSwitch.isOn ? .RECEIVE_NOT_NOTIFY_MESSAGE : .RECEIVE_MESSAGE)
         }), for: .valueChanged)
-        container.addSubview(muteSwitch)
-        muteSwitch.snp.makeConstraints { make in
-            make.trailing.equalToSuperview().offset(-16)
-            make.centerY.equalToSuperview()
-        }
-        
-        return container
+        return buildMenuItem(icon: .icImChatMenuMute, title: .init(key: "A00085"), contentView: muteSwitch)
     }
     
     private func buildRemark() -> UIView {
-        let container = UIView()
-        container.snp.makeConstraints { make in
-            make.height.equalTo(52)
-        }
-        
-        let ic = UIImageView()
-        ic.image = .icImChatMenuRemark
-        container.addSubview(ic)
-        ic.snp.makeConstraints { make in
-            make.leading.equalToSuperview().offset(16)
-            make.centerY.equalToSuperview()
-        }
-        
-        let titleLabel = UILabel()
-        titleLabel.text = .init(key: "A00086")
-        titleLabel.font = .body_m
-        titleLabel.textColor = .text_5
-        container.addSubview(titleLabel)
-        titleLabel.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.leading.equalTo(ic.snp.trailing).offset(10)
-        }
-        
-        let arrow = UIImageView()
-        arrow.image = .init(systemName: "chevron.forward")
-        arrow.tintColor = .text_4
-        container.addSubview(arrow)
-        arrow.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.trailing.equalToSuperview().offset(-16)
+        let menu = buildMenuItem(icon: .icImChatMenuRemark, title: .init(key: "A00086"), contentView: nil)
+        menu.onTap { [weak self] in
+            guard let self else { return }
+            let panel = LNIMChatUserRemarkPanel()
+            panel.update(viewModel?.remark ?? "")
+            panel.handler = { [weak self] remark in
+                guard let self else { return }
+                dismiss()
+                viewModel?.updateRemark(remark)
+            }
+            panel.popup()
         }
         
-        return container
+        return menu
     }
     
     private func buildBlack() -> UIView {
-        let container = UIView()
-        container.snp.makeConstraints { make in
-            make.height.equalTo(52)
-        }
-        
-        let ic = UIImageView()
-        ic.image = .icImChatMenuBlack
-        container.addSubview(ic)
-        ic.snp.makeConstraints { make in
-            make.leading.equalToSuperview().offset(16)
-            make.centerY.equalToSuperview()
-        }
-        
         blackLabel.font = .body_m
         blackLabel.textColor = .text_5
-        container.addSubview(blackLabel)
-        blackLabel.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.leading.equalTo(ic.snp.trailing).offset(10)
-        }
-        
-        let arrow = UIImageView()
-        arrow.image = .init(systemName: "chevron.forward")
-        arrow.tintColor = .text_4
-        container.addSubview(arrow)
-        arrow.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.trailing.equalToSuperview().offset(-16)
-        }
+        let menu = buildMenuItem(icon: .icImChatMenuBlack, titleView: blackLabel, contentView: nil)
         
-        container.onTap { [weak self] in
+        menu.onTap { [weak self] in
             guard let self else { return }
             guard let uid = viewModel?.userId else { return }
             dismiss()
@@ -179,47 +109,66 @@ extension LNIMChatUserMenuView {
             }
         }
         
-        return container
+        return menu
     }
     
     private func buildReport() -> UIView {
+        let menu = buildMenuItem(icon: .icImChatMenuRemark, title: .init(key: "A00043"), contentView: nil)
+        
+        menu.onTap { [weak self] in
+            guard let self else { return }
+            guard let uid = viewModel?.userId else { return }
+            dismiss()
+            pushToReport(uid: uid)
+        }
+        
+        return menu
+    }
+    
+    private func buildMenuItem(icon: UIImage, title: String, contentView: UIView?) -> UIView {
+        let titleLabel = UILabel()
+        titleLabel.text = title
+        titleLabel.font = .body_m
+        titleLabel.textColor = .text_5
+        
+        return buildMenuItem(icon: icon, titleView: titleLabel, contentView: contentView)
+    }
+    
+    private func buildMenuItem(icon: UIImage, titleView: UIView, contentView: UIView?) -> UIView {
         let container = UIView()
         container.snp.makeConstraints { make in
             make.height.equalTo(52)
         }
         
         let ic = UIImageView()
-        ic.image = .icImChatMenuRemark
+        ic.image = icon
         container.addSubview(ic)
         ic.snp.makeConstraints { make in
             make.leading.equalToSuperview().offset(16)
             make.centerY.equalToSuperview()
         }
         
-        let titleLabel = UILabel()
-        titleLabel.text = .init(key: "A00043")
-        titleLabel.font = .body_m
-        titleLabel.textColor = .text_5
-        container.addSubview(titleLabel)
-        titleLabel.snp.makeConstraints { make in
+        container.addSubview(titleView)
+        titleView.snp.makeConstraints { make in
             make.centerY.equalToSuperview()
             make.leading.equalTo(ic.snp.trailing).offset(10)
         }
         
-        let arrow = UIImageView()
-        arrow.image = .init(systemName: "chevron.forward")
-        arrow.tintColor = .text_4
-        container.addSubview(arrow)
-        arrow.snp.makeConstraints { make in
-            make.centerY.equalToSuperview()
-            make.trailing.equalToSuperview().offset(-16)
-        }
-        
-        container.onTap { [weak self] in
-            guard let self else { return }
-            guard let uid = viewModel?.userId else { return }
-            dismiss()
-            pushToReport(uid: uid)
+        if let contentView {
+            container.addSubview(contentView)
+            contentView.snp.makeConstraints { make in
+                make.centerY.equalToSuperview()
+                make.trailing.equalToSuperview().offset(-16)
+            }
+        } else {
+            let arrow = UIImageView()
+            arrow.image = .init(systemName: "chevron.forward")
+            arrow.tintColor = .text_4
+            container.addSubview(arrow)
+            arrow.snp.makeConstraints { make in
+                make.centerY.equalToSuperview()
+                make.trailing.equalToSuperview().offset(-16)
+            }
         }
         
         return container

+ 155 - 0
Lanu/Views/IM/Chat/UserMenu/LNIMChatUserRemarkPanel.swift

@@ -0,0 +1,155 @@
+//
+//  LNIMChatUserRemarkPanel.swift
+//  Gami
+//
+//  Created by OneeChan on 2026/2/8.
+//
+
+import Foundation
+import UIKit
+import SnapKit
+
+
+class LNIMChatUserRemarkPanel: LNPopupView {
+    private let countLabel = UILabel()
+    private let inputField = UITextField()
+    private let confirmButton = UIButton()
+    
+    var handler: ((String) -> Void)?
+    
+    override init(frame: CGRect) {
+        super.init(frame: frame)
+        
+        setupViews()
+    }
+    
+    func update(_ remark: String) {
+        inputField.text = remark
+        updateCount()
+    }
+    
+    required init?(coder: NSCoder) {
+        fatalError("init(coder:) has not been implemented")
+    }
+}
+
+extension LNIMChatUserRemarkPanel: UITextFieldDelegate {
+    func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
+        let currentText = textField.text ?? ""
+                
+        guard let range = Range(range, in: currentText) else { return false }
+        let newText = currentText.replacingCharacters(in: range, with: string)
+        if newText.count < currentText.count {
+            return true
+        }
+        
+        return newText.count <= LNIMManager.maxRemarkLength
+    }
+    
+    func textFieldShouldReturn(_ textField: UITextField) -> Bool {
+        textField.resignFirstResponder()
+        
+        return true
+    }
+}
+
+extension LNIMChatUserRemarkPanel {
+    private func updateCount() {
+        let count = inputField.text?.count ?? 0
+        countLabel.text = "\(count)/\(LNIMManager.maxRemarkLength)"
+    }
+    
+    private func setupViews() {
+        let header = buildHeader()
+        container.addSubview(header)
+        header.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview()
+            make.top.equalToSuperview()
+        }
+        
+        let tipsLabel = UILabel()
+        tipsLabel.font = .heading_h4
+        tipsLabel.text = .init(key: "A00290")
+        tipsLabel.textColor = .text_5
+        container.addSubview(tipsLabel)
+        tipsLabel.snp.makeConstraints { make in
+            make.leading.equalToSuperview().offset(16)
+            make.top.equalTo(header.snp.bottom)
+        }
+        
+        updateCount()
+        countLabel.font = .body_m
+        countLabel.textColor = .text_5
+        container.addSubview(countLabel)
+        countLabel.snp.makeConstraints { make in
+            make.trailing.equalToSuperview().offset(-16)
+            make.centerY.equalTo(tipsLabel)
+        }
+        
+        let inputHolder = UIView()
+        inputHolder.backgroundColor = .fill_1
+        inputHolder.layer.cornerRadius = 8
+        container.addSubview(inputHolder)
+        inputHolder.snp.makeConstraints { make in
+            make.top.equalTo(tipsLabel.snp.bottom).offset(8)
+            make.horizontalEdges.equalToSuperview().inset(16)
+            make.height.equalTo(46)
+        }
+        
+        inputField.font = .body_m
+        inputField.textColor = .text_5
+        inputField.clearButtonMode = .whileEditing
+        inputField.delegate = self
+        inputField.returnKeyType = .done
+        inputField.visibleView = inputHolder
+        inputField.placeholder = .init(key: "A00006")
+        inputField.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            updateCount()
+        }), for: .editingChanged)
+        inputHolder.addSubview(inputField)
+        inputField.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(12)
+            make.centerY.equalToSuperview()
+        }
+        
+        confirmButton.setTitle(.init(key: "A00185"), for: .normal)
+        confirmButton.setTitleColor(.text_1, for: .normal)
+        confirmButton.titleLabel?.font = .heading_h3
+        confirmButton.setBackgroundImage(.primary_8, for: .normal)
+        confirmButton.layer.cornerRadius = 23.5
+        confirmButton.clipsToBounds = true
+        confirmButton.backgroundColor = .fill_4
+        confirmButton.addAction(UIAction(handler: { [weak self] _ in
+            guard let self else { return }
+            guard let text = inputField.text else { return }
+            dismiss()
+            handler?(text)
+        }), for: .touchUpInside)
+        container.addSubview(confirmButton)
+        confirmButton.snp.makeConstraints { make in
+            make.horizontalEdges.equalToSuperview().inset(12)
+            make.top.equalTo(inputHolder.snp.bottom).offset(66)
+            make.bottom.equalToSuperview().offset(commonBottomInset)
+            make.height.equalTo(47)
+        }
+    }
+    
+    private func buildHeader() -> UIView {
+        let container = UIView()
+        container.snp.makeConstraints { make in
+            make.height.equalTo(50)
+        }
+        
+        let titleLabel = UILabel()
+        titleLabel.font = .heading_h3
+        titleLabel.textColor = .text_5
+        titleLabel.text = .init(key: "A00086")
+        container.addSubview(titleLabel)
+        titleLabel.snp.makeConstraints { make in
+            make.center.equalToSuperview()
+        }
+        
+        return container
+    }
+}

+ 19 - 0
Lanu/Views/IM/Chat/ViewModel/LNIMChatViewModel.swift

@@ -26,6 +26,8 @@ class LNIMChatViewModel: NSObject {
     let userId: String
     @Published
     private(set) var userInfo: LNUserProfileVO?
+    @Published
+    private(set) var remark: String?
     
     // 消息
     private var loading = false
@@ -49,6 +51,7 @@ class LNIMChatViewModel: NSObject {
         
         if !userId.isImOfficialId {
             loadUserInfo()
+            getUserRemark()
             getUnfinishOrder()
         }
         LNEventDeliver.addObserver(self)
@@ -108,6 +111,14 @@ extension LNIMChatViewModel {
             peerSkills = skills
         }
     }
+    
+    private func getUserRemark() {
+        LNIMManager.shared.getUsersRemark(uids: [userId]) { [weak self] remarks in
+            guard let self else { return }
+            guard let remarks, !remarks.isEmpty else { return }
+            remark = remarks.first(where: { $0.userNo == self.userId })?.note
+        }
+    }
 }
 
 // MARK: 消息管理
@@ -119,6 +130,14 @@ extension LNIMChatViewModel {
         }
     }
     
+    func updateRemark(_ remark: String) {
+        LNIMManager.shared.setUserRemark(uid: userId, remark: remark) { [weak self] success in
+            guard let self else { return }
+            guard success else { return }
+            self.remark = remark
+        }
+    }
+    
     private func getMessageOpt() {
         V2TIMManager.sharedInstance().getC2CReceiveMessageOpt(userIDList: [userId]) { [weak self] infos in
             guard let self else { return }

+ 5 - 4
Lanu/Views/IM/ConversationList/LNIMConversationCell.swift

@@ -62,19 +62,20 @@ class LNIMConversationCell: UITableViewCell {
         }
         curItem = item
         
-        if let userInfo = item.userInfo {
+        titleLabel.text = item.displayName
+        
+        if let userInfo = item.extraInfo?.userInfo {
             avatar.sd_setImage(with: URL(string: userInfo.avatar))
-            titleLabel.text = userInfo.nickname
         } else if let userId = item.userID, !userId.isImOfficialId {
             LNProfileManager.shared.getUserProfile(uid: userId) { [weak self] info in
                 guard let self else { return }
                 guard let info else { return }
-                item.userInfo = info
+                item.extraInfo?.userInfo = info
                 
                 guard info.userNo == curItem?.userID else { return }
                 
                 avatar.sd_setImage(with: URL(string: info.avatar))
-                titleLabel.text = info.nickname
+                titleLabel.text = item.displayName
             }
         }
     }