From a655252a21bab9ca2facde78d8797711e1deca4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=AF=97=E6=96=87?= Date: Mon, 22 Apr 2024 10:18:52 +0800 Subject: [PATCH 1/2] v10.0.0-beta --- Podfile | 28 +-- app.xcodeproj/project.pbxproj | 8 + app/Custom/CustomAttachment.swift | 3 +- app/Custom/CustomChatCell.swift | 3 +- app/Custom/CustomContactTableViewCell.swift | 16 +- app/Custom/CustomContactsViewController.swift | 12 +- app/Custom/CustomConversationController.swift | 21 +- app/Custom/CustomConversationListCell.swift | 20 +- app/Custom/CustomFunChatViewController.swift | 43 ++++ .../CustomNormalChatViewController.swift | 42 ++++ app/Custom/CustomP2PChatViewController.swift | 68 +++--- app/Custom/CustomView.swift | 22 +- app/Main/AppDelegate.swift | 26 +-- app/Main/AppKey.swift | 2 + app/Main/NETabBarController.swift | 116 +++++++---- .../Controller/ConfigTestViewController.swift | 28 +-- .../InputPersonInfoController.swift | 15 +- .../IntroduceBrandViewController.swift | 88 ++++---- app/Mine/Controller/MeViewController.swift | 118 ++++++----- .../MessageRemindViewController.swift | 31 +-- .../MineSettingViewController.swift | 55 ++--- .../Controller/NEAboutWebViewController.swift | 2 +- .../Controller/NELoginViewController.swift | 193 ++++++++++-------- .../Controller/NENodeViewController.swift | 33 +-- .../Controller/PersonInfoViewController.swift | 149 +++++++------- .../StyleSelectionViewController.swift | 32 +-- app/Mine/View/BirthdayDatePickerView.swift | 40 ++-- app/Mine/View/MineTableViewCell.swift | 97 ++++----- app/Mine/View/NodeSelectCell.swift | 24 +-- app/Mine/View/StyleSelectionCell.swift | 19 +- .../Theme/CustomTeamArrowSettingCell.swift | 6 +- .../Theme/CustomTeamSettingHeaderCell.swift | 8 +- .../CustomTeamSettingRightCustomCell.swift | 10 +- .../Theme/CustomTeamSettingSubtitleCell.swift | 10 +- app/Mine/View/VersionCell.swift | 91 +++++---- app/Mine/ViewModel/IntroduceViewModel.swift | 8 +- .../ViewModel/MessageRemindViewModel.swift | 55 ++--- app/Mine/ViewModel/MineSettingViewModel.swift | 12 +- app/Mine/ViewModel/NodeViewModel.swift | 6 +- app/Mine/ViewModel/PersonInfoViewModel.swift | 158 +++++++------- 40 files changed, 954 insertions(+), 764 deletions(-) create mode 100644 app/Custom/CustomFunChatViewController.swift create mode 100644 app/Custom/CustomNormalChatViewController.swift diff --git a/Podfile b/Podfile index 30148591..d8988906 100644 --- a/Podfile +++ b/Podfile @@ -10,28 +10,28 @@ target 'app' do pod 'YXLogin', '1.0.0' #可选UI库 - pod 'NEContactUIKit', '9.7.0' - pod 'NEConversationUIKit', '9.7.0' - pod 'NEChatUIKit', '9.7.0' - pod 'NETeamUIKit', '9.7.0' + pod 'NEContactUIKit', '10.0.0-beta' + pod 'NEConversationUIKit', '10.0.0-beta' + pod 'NEChatUIKit', '10.0.0-beta' + pod 'NETeamUIKit', '10.0.0-beta' #可选Kit库(和UIKit对应) - pod 'NEChatKit', '9.7.0' + pod 'NEChatKit', '10.0.0-beta' #基础kit库 - pod 'NECommonUIKit', '9.6.6' - pod 'NECommonKit', '9.6.6' - pod 'NECoreIMKit', '9.6.7' - pod 'NECoreKit', '9.6.6' + pod 'NECommonUIKit', '9.6.7' + pod 'NECommonKit', '9.6.7' + pod 'NECoreIM2Kit', '1.0.0-beta' + pod 'NECoreKit', '9.6.8' #扩展库 -# pod 'NEMapKit', '9.7.0' + pod 'NEMapKit', '10.0.0-beta' #呼叫组件,音视频通话能力,需要开通 音视频2.0,可选,聊天一面会根据依赖初始化自动显示音视频通话入口 - pod 'NIMSDK_LITE','9.14.2' - pod 'NERtcCallKit/NOS_Special', '2.2.0' - pod 'NERtcCallUIKit/NOS_Special', '2.2.0' - pod 'NERtcSDK', '5.5.2' + pod 'NIMSDK_LITE','10.2.5-beta' + pod 'NERtcCallKit/NOS_Special', '2.4.0' + pod 'NERtcCallUIKit/NOS_Special', '2.4.0' + pod 'NERtcSDK', '5.5.33' # # 如果需要查看UI部分源码请注释掉以上在线依赖,打开下面的本地依赖 diff --git a/app.xcodeproj/project.pbxproj b/app.xcodeproj/project.pbxproj index b10bcfa1..c0aa9ca6 100644 --- a/app.xcodeproj/project.pbxproj +++ b/app.xcodeproj/project.pbxproj @@ -41,6 +41,8 @@ 181EE6012B234E540043817F /* CustomTeamSettingSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5E72B234E540043817F /* CustomTeamSettingSwitchCell.swift */; }; 181EE6022B234E540043817F /* NodeSelectCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5E82B234E540043817F /* NodeSelectCell.swift */; }; 181EE6032B234E540043817F /* VersionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5E92B234E540043817F /* VersionCell.swift */; }; + 18B05B342BD5FD0300666AD1 /* CustomFunChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B05B322BD5FD0300666AD1 /* CustomFunChatViewController.swift */; }; + 18B05B352BD5FD0300666AD1 /* CustomNormalChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B05B332BD5FD0300666AD1 /* CustomNormalChatViewController.swift */; }; 39E9E27728D87E9800A11820 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 39E9E27528D87E9800A11820 /* Localizable.strings */; }; 4B3B9BE6277AFEE50091A74E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B3B9BE4277AFEE50091A74E /* Main.storyboard */; }; 4B3B9BEB277AFEE70091A74E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B3B9BE9277AFEE70091A74E /* LaunchScreen.storyboard */; }; @@ -108,6 +110,8 @@ 181EE5E72B234E540043817F /* CustomTeamSettingSwitchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomTeamSettingSwitchCell.swift; sourceTree = ""; }; 181EE5E82B234E540043817F /* NodeSelectCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeSelectCell.swift; sourceTree = ""; }; 181EE5E92B234E540043817F /* VersionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionCell.swift; sourceTree = ""; }; + 18B05B322BD5FD0300666AD1 /* CustomFunChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunChatViewController.swift; sourceTree = ""; }; + 18B05B332BD5FD0300666AD1 /* CustomNormalChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomNormalChatViewController.swift; sourceTree = ""; }; 39E9E27628D87E9800A11820 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 39E9E27828D87EA000A11820 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 4B3B9BDB277AFEE50091A74E /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -141,6 +145,8 @@ 181EE57D2B234C510043817F /* Custom */ = { isa = PBXGroup; children = ( + 18B05B322BD5FD0300666AD1 /* CustomFunChatViewController.swift */, + 18B05B332BD5FD0300666AD1 /* CustomNormalChatViewController.swift */, 181EE57E2B234C510043817F /* CustomView.swift */, 181EE57F2B234C510043817F /* CustomP2PChatViewController.swift */, 181EE5812B234C510043817F /* CustomContactsViewController.swift */, @@ -457,10 +463,12 @@ DD141E042A56ABFE0091318F /* NETabBarController.swift in Sources */, DD141DFC2A56ABFD0091318F /* Constant.swift in Sources */, 181EE6012B234E540043817F /* CustomTeamSettingSwitchCell.swift in Sources */, + 18B05B342BD5FD0300666AD1 /* CustomFunChatViewController.swift in Sources */, 181EE5F52B234E540043817F /* MessageRemindViewController.swift in Sources */, 181EE5FD2B234E540043817F /* CustomTeamSettingRightCustomCell.swift in Sources */, 181EE5FA2B234E540043817F /* MineTableViewCell.swift in Sources */, 181EE5EC2B234E540043817F /* PersonInfoViewModel.swift in Sources */, + 18B05B352BD5FD0300666AD1 /* CustomNormalChatViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/app/Custom/CustomAttachment.swift b/app/Custom/CustomAttachment.swift index 58cdac24..15fc4920 100644 --- a/app/Custom/CustomAttachment.swift +++ b/app/Custom/CustomAttachment.swift @@ -5,6 +5,7 @@ import NEChatUIKit import NIMSDK import UIKit + public class CustomAttachment: NECustomAttachment { public var goodsName = "name" @@ -13,7 +14,7 @@ public class CustomAttachment: NECustomAttachment { override public func encode() -> String { // 自定义序列化方法之前必须调用父类的序列化方法 let neContent = super.encode() - var info: [String: Any] = getDictionaryFromJSONString(neContent) as? [String: Any] ?? [:] + var info: [String: Any] = CustomAttachment.getDictionaryFromJSONString(neContent) ?? [:] info["goodsName"] = goodsName info["goodsURL"] = goodsURL diff --git a/app/Custom/CustomChatCell.swift b/app/Custom/CustomChatCell.swift index 0625c66f..01bb2931 100644 --- a/app/Custom/CustomChatCell.swift +++ b/app/Custom/CustomChatCell.swift @@ -4,6 +4,7 @@ import NEChatUIKit import UIKit + class CustomChatCell: NEChatBaseCell { public var testLabel = UILabel() @@ -19,7 +20,7 @@ class CustomChatCell: NEChatBaseCell { } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { diff --git a/app/Custom/CustomContactTableViewCell.swift b/app/Custom/CustomContactTableViewCell.swift index e93c4a63..8b56d6e0 100644 --- a/app/Custom/CustomContactTableViewCell.swift +++ b/app/Custom/CustomContactTableViewCell.swift @@ -7,26 +7,26 @@ import NEContactUIKit public class CustomContactTableViewCell: ContactTableViewCell { private lazy var onlineView: UIImageView = { - let notify = UIImageView() - notify.translatesAutoresizingMaskIntoConstraints = false - notify.image = UIImage(named: "about_yunxin") - notify.isHidden = true - return notify + let notifyView = UIImageView() + notifyView.translatesAutoresizingMaskIntoConstraints = false + notifyView.image = UIImage(named: "about_yunxin") + notifyView.isHidden = true + return notifyView }() override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.addSubview(onlineView) NSLayoutConstraint.activate([ - onlineView.rightAnchor.constraint(equalTo: avatarImage.rightAnchor), - onlineView.bottomAnchor.constraint(equalTo: avatarImage.bottomAnchor), + onlineView.rightAnchor.constraint(equalTo: avatarImageView.rightAnchor), + onlineView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), onlineView.widthAnchor.constraint(equalToConstant: 12), onlineView.heightAnchor.constraint(equalToConstant: 12), ]) } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } // 根据数据模型设置 cell 内容 diff --git a/app/Custom/CustomContactsViewController.swift b/app/Custom/CustomContactsViewController.swift index 2d0dbad6..2d6eabcb 100644 --- a/app/Custom/CustomContactsViewController.swift +++ b/app/Custom/CustomContactsViewController.swift @@ -21,7 +21,7 @@ public class CustomContactsViewController: ContactsViewController, NEBaseContact } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override public func viewDidLoad() { @@ -107,13 +107,13 @@ public class CustomContactsViewController: ContactsViewController, NEBaseContact viewController.navigationView.backgroundColor = .gray // 顶部bodyTopView中添加自定义view(需要设置bodyTopView的高度) - self.customTopView.btn.setTitle("通过配置项添加", for: .normal) + self.customTopView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyTopView.backgroundColor = .purple viewController.bodyTopView.addSubview(self.customTopView) viewController.bodyTopViewHeight = 80 // 底部bodyBottomView中添加自定义view(需要设置bodyBottomView的高度) - self.customBottomView.btn.setTitle("通过配置项添加", for: .normal) + self.customBottomView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyBottomView.backgroundColor = .purple viewController.bodyBottomView.addSubview(self.customBottomView) viewController.bodyBottomViewHeight = 60 @@ -122,12 +122,12 @@ public class CustomContactsViewController: ContactsViewController, NEBaseContact func customByOverread() { // 顶部bodyTopView中添加自定义view(需要设置bodyTopView的高度) - customTopView.btn.setTitle("通过重写方式添加", for: .normal) + customTopView.button.setTitle("通过重写方式添加", for: .normal) bodyTopView.addSubview(customTopView) bodyTopViewHeight = 80 // 底部bodyBottomView中添加自定义view(需要设置bodyBottomView的高度) - customBottomView.btn.setTitle("通过重写方式添加", for: .normal) + customBottomView.button.setTitle("通过重写方式添加", for: .normal) bodyBottomView.addSubview(customBottomView) bodyBottomViewHeight = 60 } @@ -146,7 +146,7 @@ public class CustomContactsViewController: ContactsViewController, NEBaseContact // 父类加载完数据后会调用此方法,可在此对数据进行二次处理 public func onDataLoaded() { - viewModel.contacts[1].contacts.forEach { info in + for info in viewModel.contacts[1].contacts { info.contactCellType = ContactCellType.ContactCutom.rawValue } } diff --git a/app/Custom/CustomConversationController.swift b/app/Custom/CustomConversationController.swift index 8a302134..e021d168 100644 --- a/app/Custom/CustomConversationController.swift +++ b/app/Custom/CustomConversationController.swift @@ -15,7 +15,7 @@ open class CustomConversationController: ConversationController, NEBaseConversat } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override public func viewDidLoad() { @@ -117,9 +117,9 @@ open class CustomConversationController: ConversationController, NEBaseConversat } /// 会话列表点击事件 -// NEKitConversationConfig.shared.ui.itemClick = { model, indexPath in -// self.showToast((model?.userInfo?.showName(true) ?? model?.teamInfo?.getShowName()) ?? "会话列表点击事件") -// } + NEKitConversationConfig.shared.ui.itemClick = { model, indexPath in + // self.showToast((model?.userInfo?.showName(true) ?? model?.teamInfo?.getShowName()) ?? "会话列表点击事件") + } /* 布局自定义 @@ -130,13 +130,13 @@ open class CustomConversationController: ConversationController, NEBaseConversat viewController.navigationView.backgroundColor = .gray // 顶部bodyTopView中添加自定义view(需要设置bodyTopView的高度) - self.customTopView.btn.setTitle("通过配置项添加", for: .normal) + self.customTopView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyTopView.backgroundColor = .purple viewController.bodyTopView.addSubview(self.customTopView) viewController.bodyTopViewHeight = 80 // 底部bodyBottomView中添加自定义view(需要设置bodyBottomView的高度) - self.customBottomView.btn.setTitle("通过配置项添加", for: .normal) + self.customBottomView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyBottomView.backgroundColor = .purple viewController.bodyBottomView.addSubview(self.customBottomView) viewController.bodyBottomViewHeight = 60 @@ -157,12 +157,12 @@ open class CustomConversationController: ConversationController, NEBaseConversat navigationView.addBtn.setImage(UIImage.ne_imageNamed(name: "noNeed_notify"), for: .normal) // 顶部bodyTopView中添加自定义view(需要设置bodyTopView的高度) - customTopView.btn.setTitle("通过重写方式添加", for: .normal) + customTopView.button.setTitle("通过重写方式添加", for: .normal) bodyTopView.addSubview(customTopView) bodyTopViewHeight = 80 // 底部bodyBottomView中添加自定义view(需要设置bodyBottomView的高度) - customBottomView.btn.setTitle("通过重写方式添加", for: .normal) + customBottomView.button.setTitle("通过重写方式添加", for: .normal) bodyBottomView.addSubview(customBottomView) bodyBottomViewHeight = 60 @@ -195,9 +195,10 @@ open class CustomConversationController: ConversationController, NEBaseConversat // 可自行处理数据 public func onDataLoaded() { - guard let conversationList = viewModel.conversationListArray else { return + for model in viewModel.conversationListData { + model.customType = 1 } - conversationList.forEach { model in + for model in viewModel.stickTopConversations { model.customType = 1 } tableView.reloadData() diff --git a/app/Custom/CustomConversationListCell.swift b/app/Custom/CustomConversationListCell.swift index de8d11e9..9e85ba51 100644 --- a/app/Custom/CustomConversationListCell.swift +++ b/app/Custom/CustomConversationListCell.swift @@ -9,10 +9,10 @@ import UIKit open class CustomConversationListCell: ConversationListCell { // 新增 UI 元素,用于展示在线状态 private lazy var onlineView: UIImageView = { - let notify = UIImageView() - notify.translatesAutoresizingMaskIntoConstraints = false - notify.image = UIImage(named: "about_yunxin") - return notify + let notifyView = UIImageView() + notifyView.translatesAutoresizingMaskIntoConstraints = false + notifyView.image = UIImage(named: "about_yunxin") + return notifyView }() override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -21,20 +21,20 @@ open class CustomConversationListCell: ConversationListCell { // 头像右下角 contentView.addSubview(onlineView) NSLayoutConstraint.activate([ - onlineView.rightAnchor.constraint(equalTo: headImge.rightAnchor), - onlineView.bottomAnchor.constraint(equalTo: headImge.bottomAnchor), + onlineView.rightAnchor.constraint(equalTo: headImageView.rightAnchor), + onlineView.bottomAnchor.constraint(equalTo: headImageView.bottomAnchor), onlineView.widthAnchor.constraint(equalToConstant: 12), onlineView.heightAnchor.constraint(equalToConstant: 12), ]) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } // 此方法用于数据和 UI 的绑定,可在此处在数据展示前对数据进行处理 - override open func configData(sessionModel: ConversationListModel?) { - super.configData(sessionModel: sessionModel) -// subTitle.text = "[自定义类型文案]" + override open func configureData(_ sessionModel: NEConversationListModel?) { + super.configureData(sessionModel) + // subTitle.text = "[自定义类型文案]" } } diff --git a/app/Custom/CustomFunChatViewController.swift b/app/Custom/CustomFunChatViewController.swift new file mode 100644 index 00000000..3ccbecb3 --- /dev/null +++ b/app/Custom/CustomFunChatViewController.swift @@ -0,0 +1,43 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatUIKit +import NERtcCallKit +import NIMSDK +import UIKit + +class CustomFunChatViewController: FunP2PChatViewController, NERecordProvider { + /// 话单拦截 + func onRecordSend(_ config: NERecordConfig) { + NEALog.infoLog(className(), desc: "call status : \(NECallEngine.sharedInstance().callStatus)") + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + if NECallEngine.sharedInstance().callStatus == .calling { + return + } + } + + let message = V2NIMMessageCreator.createCallMessage("", type: Int(config.callType.rawValue), channelId: "", status: Int(config.callState.rawValue), durations: []) + if let cid = V2NIMConversationIdUtil.p2pConversationId(config.accId) { + viewModel.chatRepo.sendMessage(message: message, conversationId: cid) { [weak self] result, error, ret in + NEALog.infoLog(self?.className() ?? "", desc: "CustomNormalChatViewController result: \(error?.localizedDescription ?? "")") + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + NECallEngine.sharedInstance().setCall(self) + // Do any additional setup after loading the view. + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ +} diff --git a/app/Custom/CustomNormalChatViewController.swift b/app/Custom/CustomNormalChatViewController.swift new file mode 100644 index 00000000..bef24e59 --- /dev/null +++ b/app/Custom/CustomNormalChatViewController.swift @@ -0,0 +1,42 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatUIKit +import NERtcCallKit +import NIMSDK +import UIKit + +class CustomNormalChatViewController: P2PChatViewController, NERecordProvider { + /// 话单拦截 + func onRecordSend(_ config: NERecordConfig) { + NEALog.infoLog(className(), desc: "call status : \(NECallEngine.sharedInstance().callStatus)") + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + if NECallEngine.sharedInstance().callStatus == .calling { + return + } + } + let message = V2NIMMessageCreator.createCallMessage("", type: Int(config.callType.rawValue), channelId: "", status: Int(config.callState.rawValue), durations: []) + if let cid = V2NIMConversationIdUtil.p2pConversationId(config.accId) { + viewModel.chatRepo.sendMessage(message: message, conversationId: cid) { [weak self] result, error, ret in + NEALog.infoLog(self?.className() ?? "", desc: "CustomNormalChatViewController result: \(error?.localizedDescription ?? "")") + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + NECallEngine.sharedInstance().setCall(self) + // Do any additional setup after loading the view. + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ +} diff --git a/app/Custom/CustomP2PChatViewController.swift b/app/Custom/CustomP2PChatViewController.swift index 91302645..2a6db30e 100644 --- a/app/Custom/CustomP2PChatViewController.swift +++ b/app/Custom/CustomP2PChatViewController.swift @@ -5,6 +5,7 @@ import NEChatUIKit import NIMSDK import UIKit + class CustomP2PChatViewController: P2PChatViewController { let customMessageType = 20 override func viewDidLoad() { @@ -131,13 +132,13 @@ class CustomP2PChatViewController: P2PChatViewController { viewController.navigationView.backgroundColor = .gray // 顶部bodyTopView中添加自定义view(需要设置bodyTopView的高度) - self.customTopView.btn.setTitle("通过配置项添加", for: .normal) + self.customTopView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyTopView.backgroundColor = .purple viewController.bodyTopView.addSubview(self.customTopView) viewController.bodyTopViewHeight = 80 // 底部bodyBottomView中添加自定义view(需要设置bodyBottomView的高度) - self.customBottomView.btn.setTitle("通过配置项添加", for: .normal) + self.customBottomView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyBottomView.backgroundColor = .purple viewController.bodyBottomView.addSubview(self.customBottomView) viewController.bodyBottomViewHeight = 60 @@ -147,13 +148,13 @@ class CustomP2PChatViewController: P2PChatViewController { /// 通过重写实现自定义 func customByOverread() { // 聊天页顶部导航栏下方扩展视图示例 - customTopView.btn.setTitle("通过重写方式添加", for: .normal) + customTopView.button.setTitle("通过重写方式添加", for: .normal) bodyTopView.addSubview(customTopView) bodyTopView.backgroundColor = .yellow bodyTopViewHeight = 80 // 输入框上区域扩展视图示例 - customBottomView.btn.setTitle("通过重写方式添加", for: .normal) + customBottomView.button.setTitle("通过重写方式添加", for: .normal) bodyBottomView.addSubview(customBottomView) bodyBottomView.backgroundColor = .yellow bodyBottomViewHeight = 60 @@ -206,31 +207,32 @@ class CustomP2PChatViewController: P2PChatViewController { NIMCustomObject.registerCustomDecoder(CustomAttachmentDecoder()) // 测试自定义消息发送按钮 - let testBtn = UIButton() - testBtn.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(testBtn) + let testButton = UIButton() + testButton.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(testButton) NSLayoutConstraint.activate([ - testBtn.widthAnchor.constraint(equalToConstant: 100), - testBtn.heightAnchor.constraint(equalToConstant: 40), - testBtn.centerXAnchor.constraint(equalTo: view.centerXAnchor), - testBtn.centerYAnchor.constraint(equalTo: view.centerYAnchor), + testButton.widthAnchor.constraint(equalToConstant: 100), + testButton.heightAnchor.constraint(equalToConstant: 40), + testButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + testButton.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) - testBtn.backgroundColor = UIColor.red - testBtn.addTarget(self, action: #selector(sendCustomButton), for: .touchUpInside) + testButton.backgroundColor = UIColor.red + testButton.addTarget(self, action: #selector(sendCustomButton), for: .touchUpInside) } // 自定义标题 - // override func getSessionInfo(session: NIMSession) { - // super.getSessionInfo(session: session) - // title = "小易助手" - // } +// override func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { +// super.getSessionInfo(sessionId: sessionId) { +// self.title = "小易助手" +// } +// } // 长按消息功能弹窗列表自定义(可针对不同 type 消息自定义长按功能项) - // override func setOperationItems(items: inout [OperationItem], model: MessageContentModel?) { - // if model?.type == .rtcCallRecord { - // items.append(OperationItem.deleteItem()) - // } - // } +// override func setOperationItems(items: inout [OperationItem], model: MessageContentModel?) { +// if model?.type == .rtcCallRecord { +// items.append(OperationItem.deleteItem()) +// } +// } @objc func customClick() { showToast("自定义点击事件") @@ -238,7 +240,7 @@ class CustomP2PChatViewController: P2PChatViewController { func customBottomBar() { let subviews = chatInputView.stackView.subviews - subviews.forEach { view in + for view in subviews { view.removeFromSuperview() chatInputView.stackView.removeArrangedSubview(view) } @@ -258,15 +260,15 @@ class CustomP2PChatViewController: P2PChatViewController { @objc func buttonEvent(_ btn: UIButton) { if btn.tag == 0 { // 表情 - layoutInputView(offset: bottomExanpndHeight) + layoutInputView(offset: bottomExanpndHeight, true) chatInputView.addEmojiView() } else if btn.tag == 1 { // 语音 - layoutInputView(offset: bottomExanpndHeight) + layoutInputView(offset: bottomExanpndHeight, true) chatInputView.addRecordView() } else if btn.tag == 2 { // 照片 goPhotoAlbumWithVideo(self) } else if btn.tag == 3 { // 更多 - layoutInputView(offset: bottomExanpndHeight) + layoutInputView(offset: bottomExanpndHeight, true) chatInputView.addMoreActionView() } } @@ -274,13 +276,15 @@ class CustomP2PChatViewController: P2PChatViewController { @objc func sendCustomButton() { let data = ["type": customMessageType] let attachment = CustomAttachment(customType: customMessageType, cellHeight: 50, data: data) - let message = NIMMessage() - let object = NIMCustomObject() - object.attachment = attachment - message.messageObject = object + let message = V2NIMMessage() + let object = V2NIMMessageAttachment() + message.attachment = object + + NIMSDK.shared().v2MessageService.send(message, conversationId: V2NIMConversationIdUtil.conversationTargetId(viewModel.conversationId) ?? "", params: nil) { _ in - NIMSDK.shared().chatManager.send(message, to: viewmodel.session) { error in - print("send custom message error : ", error?.localizedDescription as Any) + } failure: { error in + print("send custom message error : ", error.nserror.localizedDescription) + } progress: { _ in } } diff --git a/app/Custom/CustomView.swift b/app/Custom/CustomView.swift index 71cd2b0b..c9bb29a1 100644 --- a/app/Custom/CustomView.swift +++ b/app/Custom/CustomView.swift @@ -5,25 +5,25 @@ import UIKit public class CustomView: UIView { - public let btn = UIButton() + public let button = UIButton() override public init(frame: CGRect) { super.init(frame: frame) - btn.translatesAutoresizingMaskIntoConstraints = false - btn.addTarget(self, action: #selector(tapView), for: .touchUpInside) - btn.setTitle("按钮", for: .normal) - btn.backgroundColor = .red - addSubview(btn) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(tapView), for: .touchUpInside) + button.setTitle("按钮", for: .normal) + button.backgroundColor = .red + addSubview(button) NSLayoutConstraint.activate([ - btn.topAnchor.constraint(equalTo: topAnchor), - btn.bottomAnchor.constraint(equalTo: bottomAnchor), - btn.widthAnchor.constraint(equalToConstant: 200), - btn.centerXAnchor.constraint(equalTo: centerXAnchor), + button.topAnchor.constraint(equalTo: topAnchor), + button.bottomAnchor.constraint(equalTo: bottomAnchor), + button.widthAnchor.constraint(equalToConstant: 200), + button.centerXAnchor.constraint(equalTo: centerXAnchor), ]) } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } @objc func tapView() { diff --git a/app/Main/AppDelegate.swift b/app/Main/AppDelegate.swift index 8668a71b..d80ff9df 100644 --- a/app/Main/AppDelegate.swift +++ b/app/Main/AppDelegate.swift @@ -8,7 +8,7 @@ import NEContactUIKit import YXLogin import NECoreKit import NIMSDK -import NECoreIMKit +import NECoreIM2Kit import NEConversationUIKit import NETeamUIKit import NEChatUIKit @@ -35,9 +35,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // 初始化NIMSDK let option = NIMSDKOption() + option.v2 = true option.appKey = AppKey.appKey option.apnsCername = AppKey.pushCerName - IMKitClient.instance.setupCoreKitIM(option) + IMKitClient.instance.setupIM(option) // 登录IM之前先初始化 @ 消息监听mananger NEAtMessageManager.setupInstance() @@ -46,7 +47,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let token = "<#token#>" weak var weakSelf = self - IMKitClient.instance.loginIM(account, token) { error in + IMKitClient.instance.login(account, token, nil) { error in if let err = error { print("login error in app : ", err.localizedDescription) }else { @@ -96,11 +97,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - NELog.infoLog("app delegate : ", desc: error.localizedDescription) + NEALog.infoLog("app delegate : ", desc: error.localizedDescription) } func initializePage() { - self.window?.rootViewController = NETabBarController() + self.window?.rootViewController = NETabBarController(true) loadService() } @@ -122,7 +123,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD customVerification() //地图map初始化 - NEMapClient.shared().setupMapClient(withAppkey: AppKey.gaodeMapAppkey) + NEMapClient.shared().setupMapClient(withAppkey: AppKey.gaodeMapAppkey, withServerKey: AppKey.gaodeMapServerAppkey) /* 聊天面板外部扩展示例 // 新增未知类型 @@ -172,11 +173,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Router.shared.register(PushP2pChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let p2pChatVC = P2PChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let p2pChatVC = CustomNormalChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { @@ -185,7 +186,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return } } - if let remove = param["removeUserVC"] as? Bool, remove { nav?.viewControllers.removeLast() } @@ -196,11 +196,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Router.shared.register(PushP2pChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let p2pChatVC = FunP2PChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let p2pChatVC = CustomFunChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { diff --git a/app/Main/AppKey.swift b/app/Main/AppKey.swift index 234281af..00e6e459 100644 --- a/app/Main/AppKey.swift +++ b/app/Main/AppKey.swift @@ -8,9 +8,11 @@ public struct AppKey { public static let pushCerName = "<#请输入推送证书#>" public static let appKey = "<#请输入appkey#>" public static let gaodeMapAppkey = "<#输入高德地图key#>" + public static let gaodeMapServerAppkey = "<#输入高德地图key#>" #else public static let pushCerName = "<#请输入推送证书#>" public static let appKey = "<#请输入appkey#>" public static let gaodeMapAppkey = "<#输入高德地图key#>" + public static let gaodeMapServerAppkey = "<#输入高德地图key#>" #endif } diff --git a/app/Main/NETabBarController.swift b/app/Main/NETabBarController.swift index bb20aca2..2ddc8b75 100644 --- a/app/Main/NETabBarController.swift +++ b/app/Main/NETabBarController.swift @@ -1,39 +1,58 @@ - // Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import UIKit -import NIMSDK -import NECoreKit -import NECoreIMKit -import NEConversationUIKit -import NETeamUIKit -import NEChatUIKit +import NEChatKit import NEContactUIKit +import NEConversationUIKit +import NIMSDK +import UIKit -class NETabBarController: UITabBarController { +class NETabBarController: UITabBarController, NEConversationListener, NEContactListener { private var sessionUnreadCount = 0 private var contactUnreadCount = 0 + /// 是通过切换UI风格触发,需要重置会话是否同步完成标志位,因为不是首次登录,已经同步过,同步完成回调不会再触发,正常单皮肤可忽略此逻辑 + public var isChangeUIType = false { + didSet {} + } + + public init(_ isChangeUI: Bool) { + isChangeUIType = isChangeUI + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + override func viewDidLoad() { super.viewDidLoad() + ContactRepo.shared.addContactListener(self) setUpControllers() setUpSessionBadgeValue() setUpContactBadgeValue() - NIMSDK.shared().conversationManager.add(self) - NIMSDK.shared().systemNotificationManager.add(self) + ConversationRepo.shared.addListener(self) + + NotificationCenter.default.addObserver(self, selector: #selector(clearValidationUnreadCount), name: NENotificationName.clearValidationUnreadCount, object: nil) + } + + deinit { + ConversationRepo.shared.removeListener(self) + ContactRepo.shared.removeContactListener(self) } func setUpControllers() { if NEStyleManager.instance.isNormalStyle() { // chat let chat = ConversationController() + chat.viewModel.syncFinished = isChangeUIType chat.tabBarItem = UITabBarItem( title: NSLocalizedString("message", comment: ""), image: UIImage(named: "chat"), selectedImage: UIImage(named: "chatSelect")?.withRenderingMode(.alwaysOriginal) ) + chat.tabBarItem.accessibilityIdentifier = "id.conversation" let chatNav = NENavigationController(rootViewController: chat) // Contacts @@ -43,36 +62,48 @@ class NETabBarController: UITabBarController { image: UIImage(named: "contact"), selectedImage: UIImage(named: "contactSelect")?.withRenderingMode(.alwaysOriginal) ) + contactVC.tabBarItem.accessibilityIdentifier = "id.contact" let contactsNav = NENavigationController(rootViewController: contactVC) // Me let meVC = MeViewController() - meVC.view.backgroundColor = UIColor.white meVC.tabBarItem = UITabBarItem( title: NSLocalizedString("mine", comment: ""), image: UIImage(named: "person"), selectedImage: UIImage(named: "personSelect")?.withRenderingMode(.alwaysOriginal) ) + meVC.tabBarItem.accessibilityIdentifier = "id.mine" + meVC.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#999999")], for: .normal) + meVC.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#337EFF")], for: .selected) let meNav = NENavigationController(rootViewController: meVC) - tabBar.backgroundColor = .white + tabBar.backgroundColor = UIColor(hexString: "#F6F8FA") viewControllers = [chatNav, contactsNav, meNav] selectedIndex = 0 if #available(iOS 13.0, *) { let appearance = UITabBarAppearance() + appearance.stackedLayoutAppearance.normal.iconColor = UIColor(hexString: "#C5C9D2") + appearance.stackedLayoutAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor(hexString: "#999999")] appearance.stackedLayoutAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor(hexString: "#337EFF")] tabBar.standardAppearance = appearance + } else { + tabBar.unselectedItemTintColor = UIColor(hexString: "#C5C9D2") + viewControllers?.forEach { vc in + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#999999")], for: .normal) + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#337EFF")], for: .selected) + } } } else { // chat let chat = FunConversationController() + chat.viewModel.syncFinished = isChangeUIType chat.tabBarItem = UITabBarItem( title: NSLocalizedString("message", comment: ""), image: UIImage(named: "funChat"), selectedImage: UIImage(named: "funChatSelect")?.withRenderingMode(.alwaysOriginal) ) - setFunStyleColor(chat.tabBarItem) + chat.tabBarItem.accessibilityIdentifier = "id.conversation" let chatNav = NENavigationController(rootViewController: chat) // Contacts @@ -82,7 +113,7 @@ class NETabBarController: UITabBarController { image: UIImage(named: "funContact"), selectedImage: UIImage(named: "funContactSelect")?.withRenderingMode(.alwaysOriginal) ) - setFunStyleColor(contactVC.tabBarItem) + contactVC.tabBarItem.accessibilityIdentifier = "id.contact" let contactsNav = NENavigationController(rootViewController: contactVC) // Me @@ -92,23 +123,36 @@ class NETabBarController: UITabBarController { image: UIImage(named: "funPerson"), selectedImage: UIImage(named: "funPersonSelect")?.withRenderingMode(.alwaysOriginal) ) - setFunStyleColor(meVC.tabBarItem) + meVC.tabBarItem.accessibilityIdentifier = "id.mine" let meNav = NENavigationController(rootViewController: meVC) - tabBar.backgroundColor = .white + tabBar.backgroundColor = UIColor(hexString: "#F6F6F6") + tabBar.unselectedItemTintColor = UIColor(hexString: "#C5C9D2") viewControllers = [chatNav, contactsNav, meNav] + viewControllers?.forEach { vc in + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#999999")], for: .normal) + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#58BE6B")], for: .selected) + } selectedIndex = 0 if #available(iOS 13.0, *) { let appearance = UITabBarAppearance() + appearance.stackedLayoutAppearance.normal.iconColor = UIColor(hexString: "#C5C9D2") + appearance.stackedLayoutAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor(hexString: "#999999")] appearance.stackedLayoutAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor(hexString: "#58BE6B")] tabBar.standardAppearance = appearance + } else { + tabBar.unselectedItemTintColor = UIColor(hexString: "#C5C9D2") + viewControllers?.forEach { vc in + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#999999")], for: .normal) + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#58BE6B")], for: .selected) + } } } } func setUpSessionBadgeValue() { - sessionUnreadCount = ConversationProvider.shared.allUnreadCount(notify: true) + sessionUnreadCount = ConversationRepo.shared.getMsgUnreadCount() if sessionUnreadCount > 0 { tabBar.showBadgOn(index: 0, tabbarItemNums: 3) } else { @@ -117,11 +161,13 @@ class NETabBarController: UITabBarController { } func setUpContactBadgeValue() { - contactUnreadCount = NIMSDK.shared().systemNotificationManager.allUnreadCount() - if contactUnreadCount > 0 { - tabBar.showBadgOn(index: 1, tabbarItemNums: 3) - } else { - tabBar.hideBadg(on: 1) + ContactRepo.shared.getUnreadApplicationCount { [self] unreadCount, error in + contactUnreadCount = unreadCount + if unreadCount > 0 { + tabBar.showBadgOn(index: 1, tabbarItemNums: 3) + } else { + tabBar.hideBadg(on: 1) + } } } @@ -129,33 +175,27 @@ class NETabBarController: UITabBarController { setUpSessionBadgeValue() } - private func setFunStyleColor(_ item: UITabBarItem) { - item.setTitleTextAttributes([.foregroundColor: UIColor(hexString: "#58BE6B")], for: .selected) - } - - deinit { - NIMSDK.shared().systemNotificationManager.remove(self) - NIMSDK.shared().conversationManager.remove(self) + @objc public func clearValidationUnreadCount() { + setUpContactBadgeValue() } } -extension NETabBarController: NIMConversationManagerDelegate { - func didAdd(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { +// MARK: - V2NIMConversationListener + +extension NETabBarController { + func onConversationChanged(_ conversations: [V2NIMConversation]) { refreshSessionBadge() } - func didUpdate(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { + func onConversationCreated(_ conversation: V2NIMConversation) { refreshSessionBadge() } - func didRemove(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { + func onConversationDeleted(_ conversationIds: [String]) { refreshSessionBadge() } -} -extension NETabBarController: NIMSystemNotificationManagerDelegate { - func onSystemNotificationCountChanged(_ unreadCount: Int) { - contactUnreadCount = unreadCount + func onFriendAddApplication(_ application: V2NIMFriendAddApplication) { setUpContactBadgeValue() } } diff --git a/app/Mine/Controller/ConfigTestViewController.swift b/app/Mine/Controller/ConfigTestViewController.swift index 3300c4bf..3dc0ea3d 100644 --- a/app/Mine/Controller/ConfigTestViewController.swift +++ b/app/Mine/Controller/ConfigTestViewController.swift @@ -87,24 +87,24 @@ class ConfigTestViewController: NEBaseViewController, UITableViewDelegate, tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in + for (key, value) in cellClassDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - return table + return tableView }() @objc func saveConfig() { @@ -156,8 +156,8 @@ class ConfigTestViewController: NEBaseViewController, UITableViewDelegate, } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView } } diff --git a/app/Mine/Controller/InputPersonInfoController.swift b/app/Mine/Controller/InputPersonInfoController.swift index 1871cd0b..6dfc1cd0 100644 --- a/app/Mine/Controller/InputPersonInfoController.swift +++ b/app/Mine/Controller/InputPersonInfoController.swift @@ -34,10 +34,12 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { })) } + /// 初始化UI(内容区域) func setupSubviews() { view.addSubview(textfieldBgView) textfieldBgView.addSubview(textField) + /// 文本框白色背景 if NEStyleManager.instance.isNormalStyle() { NSLayoutConstraint.activate([ textfieldBgView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20.0), @@ -55,6 +57,8 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { ]) textfieldBgView.layer.cornerRadius = 0 } + + /// 文本框 NSLayoutConstraint.activate([ textField.leftAnchor.constraint(equalTo: textfieldBgView.leftAnchor, constant: 16), textField.rightAnchor.constraint(equalTo: textfieldBgView.rightAnchor, constant: -12), @@ -62,6 +66,7 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { ]) } + /// 初始化UI(导航栏) func initialConfig() { addRightAction(NSLocalizedString("save", comment: ""), #selector(saveName), self) @@ -81,19 +86,21 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { } } + /// 保存昵称 @objc func saveName() { - weak var weakSelf = self if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { - weakSelf?.showToast(commonLocalizable("network_error")) + showToast(commonLocalizable("network_error")) return } if let block = callBack { block(textField.text ?? "") -// weakSelf?.navigationController?.popViewController(animated: true) +// navigationController?.popViewController(animated: true) } } + /// 配置标题类型 + /// - Parameter editType: 标题类型 func configTitle(editType: EditType) { switch editType { case .nickName: @@ -143,7 +150,7 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { func textFieldChange() { guard let _ = textField.markedTextRange else { if let text = textField.text, - text.count > limitNumberCount { + text.utf16.count > limitNumberCount { textField.text = String(text.prefix(limitNumberCount)) showToast(String(format: NSLocalizedString("text_count_limit", comment: ""), limitNumberCount)) } diff --git a/app/Mine/Controller/IntroduceBrandViewController.swift b/app/Mine/Controller/IntroduceBrandViewController.swift index 31c3c438..d9ea27ef 100644 --- a/app/Mine/Controller/IntroduceBrandViewController.swift +++ b/app/Mine/Controller/IntroduceBrandViewController.swift @@ -11,75 +11,77 @@ class IntroduceBrandViewController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource { private var viewModel = IntroduceViewModel() + /// 网易云信IM Logo + private lazy var headImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "yunxin_logo")) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.accessibilityIdentifier = "id.aboutLogo" + return imageView + }() + + /// 网易云信 文本 + private lazy var headLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = NSLocalizedString("brand_des", comment: "") + label.font = UIFont.systemFont(ofSize: 20.0) + label.textColor = UIColor(hexString: "333333") + label.accessibilityIdentifier = "id.aboutApp" + return label + }() + + /// 内容列表控件 + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .white + tableView.dataSource = self + tableView.delegate = self + tableView.separatorStyle = .none + tableView.register(VersionCell.self, forCellReuseIdentifier: "VersionCell") + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + override func viewDidLoad() { super.viewDidLoad() viewModel.getData() setupSubviews() } + /// UI 初始化 func setupSubviews() { - view.addSubview(headImage) + view.addSubview(headImageView) view.addSubview(headLabel) view.addSubview(tableView) navigationController?.navigationBar.backgroundColor = .white navigationView.backgroundColor = .white NSLayoutConstraint.activate([ - headImage.centerXAnchor.constraint(equalTo: view.centerXAnchor), - headImage.topAnchor.constraint( + headImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + headImageView.topAnchor.constraint( equalTo: view.topAnchor, constant: topConstant + 20 ), - headImage.widthAnchor.constraint(equalToConstant: 72), - headImage.heightAnchor.constraint(equalToConstant: 53), + headImageView.widthAnchor.constraint(equalToConstant: 72), + headImageView.heightAnchor.constraint(equalToConstant: 53), ]) NSLayoutConstraint.activate([ - headLabel.centerXAnchor.constraint(equalTo: headImage.centerXAnchor), - headLabel.topAnchor.constraint(equalTo: headImage.bottomAnchor, constant: 10), + headLabel.centerXAnchor.constraint(equalTo: headImageView.centerXAnchor), + headLabel.topAnchor.constraint(equalTo: headImageView.bottomAnchor, constant: 10), ]) NSLayoutConstraint.activate([ tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), - tableView.topAnchor.constraint(equalTo: headImage.bottomAnchor, constant: 45), + tableView.topAnchor.constraint(equalTo: headImageView.bottomAnchor, constant: 45), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } - // MARK: lazy method - - private lazy var headImage: UIImageView = { - let image = UIImageView(image: UIImage(named: "yunxin_logo")) - image.translatesAutoresizingMaskIntoConstraints = false - image.accessibilityIdentifier = "id.aboutLogo" - return image - }() - - private lazy var headLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = NSLocalizedString("brand_des", comment: "") - label.font = UIFont.systemFont(ofSize: 20.0) - label.textColor = UIColor(hexString: "333333") - label.accessibilityIdentifier = "id.aboutApp" - return label - }() - - lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .white - table.dataSource = self - table.delegate = self - table.separatorStyle = .none - table.register(VersionCell.self, forCellReuseIdentifier: "VersionCell") - if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 - } - return table - }() - // MARK: UITableViewDelegate, UITableViewDataSource func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -93,7 +95,7 @@ class IntroduceBrandViewController: NEBaseViewController, UITableViewDelegate, for: indexPath ) as? VersionCell { cell.configData(model: model) - if indexPath.row == 0 { + if indexPath.row == 0 || indexPath.row == 1 { cell.cellType = .version } else { cell.cellType = .productIntroduce @@ -104,7 +106,7 @@ class IntroduceBrandViewController: NEBaseViewController, UITableViewDelegate, } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if indexPath.row == 1 { + if indexPath.row == 2 { let ctrl = NEAboutWebViewController(url: "https://netease.im/m/") navigationController?.pushViewController(ctrl, animated: true) } diff --git a/app/Mine/Controller/MeViewController.swift b/app/Mine/Controller/MeViewController.swift index 24e36faa..5b45d72e 100644 --- a/app/Mine/Controller/MeViewController.swift +++ b/app/Mine/Controller/MeViewController.swift @@ -3,8 +3,9 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -16,7 +17,38 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { [NSLocalizedString("about_yunxin", comment: ""): "about_yunxin"], ] - private let userProvider = UserInfoProvider.shared + private lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + tableView.register( + MineTableViewCell.self, + forCellReuseIdentifier: "\(NSStringFromClass(MineTableViewCell.self))" + ) + tableView.rowHeight = 52 + return tableView + }() + + private lazy var arrowImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "arrow_right")) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.accessibilityIdentifier = "id.rightArrow" + return imageView + }() + + private lazy var personInfoButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(personInfoButtonClick), for: .touchUpInside) + return button + }() + + @objc func personInfoButtonClick(sender: UIButton) { + let personInfo = PersonInfoViewController() + navigationController?.pushViewController(personInfo, animated: true) + } lazy var headerView: UIView = { let view = UIView(frame: .zero) @@ -33,12 +65,12 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { }() lazy var nameLabel: UILabel = { - let name = UILabel() - name.textColor = .ne_darkText - name.font = UIFont.systemFont(ofSize: 22.0) - name.translatesAutoresizingMaskIntoConstraints = false - name.accessibilityIdentifier = "id.name" - return name + let nameLabel = UILabel() + nameLabel.textColor = .ne_darkText + nameLabel.font = UIFont.systemFont(ofSize: 22.0) + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.accessibilityIdentifier = "id.name" + return nameLabel }() lazy var idLabel: UILabel = { @@ -70,6 +102,7 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { } func setupSubviews() { + // 顶部视图 view.addSubview(header) if #available(iOS 11.0, *) { NSLayoutConstraint.activate([ @@ -126,8 +159,8 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { ]) view.addSubview(tableView) - view.addSubview(arrow) - view.addSubview(personInfoBtn) + view.addSubview(arrowImageView) + view.addSubview(personInfoButton) tableView.backgroundColor = NEStyleManager.instance.isNormalStyle() ? UIColor.white : UIColor.clear NSLayoutConstraint.activate([ @@ -138,15 +171,15 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: header.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + arrowImageView.centerYAnchor.constraint(equalTo: header.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), ]) NSLayoutConstraint.activate([ - personInfoBtn.topAnchor.constraint(equalTo: header.topAnchor), - personInfoBtn.leftAnchor.constraint(equalTo: view.leftAnchor), - personInfoBtn.rightAnchor.constraint(equalTo: view.rightAnchor), - personInfoBtn.bottomAnchor.constraint(equalTo: divider.topAnchor), + personInfoButton.topAnchor.constraint(equalTo: header.topAnchor), + personInfoButton.leftAnchor.constraint(equalTo: view.leftAnchor), + personInfoButton.rightAnchor.constraint(equalTo: view.rightAnchor), + personInfoButton.bottomAnchor.constraint(equalTo: divider.topAnchor), ]) view.insertSubview(headerView, belowSubview: header) @@ -158,47 +191,22 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { ]) } - func updateUserInfo() { - let user = userProvider.getUserInfo(userId: IMKitClient.instance.imAccid()) - idLabel.text = "\(NSLocalizedString("account", comment: "")):\(user?.userId ?? "")" - nameLabel.text = user?.showName(false) - header.configHeadData(headUrl: user?.userInfo?.avatarUrl, - name: user?.showName(false) ?? "", - uid: user?.userId ?? "") + func setupUserInfo(_ userFriend: NEUserWithFriend?) { + idLabel.text = "\(NSLocalizedString("account", comment: "")):\(userFriend?.user?.accountId ?? "")" + nameLabel.text = userFriend?.showName(false) + header.configHeadData(headUrl: userFriend?.user?.avatar, + name: userFriend?.showName(false) ?? "", + uid: userFriend?.user?.accountId ?? "") } - // MAKR: lazy method - private lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .plain) - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.register( - MineTableViewCell.self, - forCellReuseIdentifier: "\(NSStringFromClass(MineTableViewCell.self))" - ) - tableView.rowHeight = 52 - return tableView - }() - - private lazy var arrow: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "arrow_right")) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.accessibilityIdentifier = "id.rightArrow" - return imageView - }() - - private lazy var personInfoBtn: UIButton = { - let btn = UIButton() - btn.translatesAutoresizingMaskIntoConstraints = false - btn.addTarget(self, action: #selector(personInfoBtnClick), for: .touchUpInside) - return btn - }() - - @objc func personInfoBtnClick(sender: UIButton) { - let personInfo = PersonInfoViewController() - navigationController?.pushViewController(personInfo, animated: true) + func updateUserInfo() { + if let userFriend = NEFriendUserCache.shared.getFriendInfo(IMKitClient.instance.account()) { + setupUserInfo(userFriend) + } else { + ContactRepo.shared.getMyUserInfo { [weak self] userFriend in + self?.setupUserInfo(userFriend) + } + } } } diff --git a/app/Mine/Controller/MessageRemindViewController.swift b/app/Mine/Controller/MessageRemindViewController.swift index c0bad2dd..de7bc94a 100644 --- a/app/Mine/Controller/MessageRemindViewController.swift +++ b/app/Mine/Controller/MessageRemindViewController.swift @@ -21,6 +21,7 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, initialConfig() } + /// 导航栏配置 func initialConfig() { title = NSLocalizedString("message_remind", comment: "") if NEStyleManager.instance.isNormalStyle() { @@ -32,6 +33,7 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, } } + /// 页面主题元素初始化以及布局 func setupSubviews() { view.addSubview(tableView) if NEStyleManager.instance.isNormalStyle() { @@ -44,24 +46,24 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in + for (key, value) in cellClassDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - return table + return tableView }() // MARK: UITableViewDelegate, UITableViewDataSource @@ -93,6 +95,7 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { // let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] // if let block = model.cellClick { + // block() // } } @@ -116,8 +119,8 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView } } diff --git a/app/Mine/Controller/MineSettingViewController.swift b/app/Mine/Controller/MineSettingViewController.swift index 05b8ddb9..93512a1b 100644 --- a/app/Mine/Controller/MineSettingViewController.swift +++ b/app/Mine/Controller/MineSettingViewController.swift @@ -3,6 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NECoreIM2Kit import NECoreKit import NETeamUIKit import NIMSDK @@ -50,30 +51,30 @@ class MineSettingViewController: NEBaseViewController, UITableViewDataSource, UI tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in + for (key, value) in cellClassDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.tableFooterView = getFooterView() + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.tableFooterView = getFooterView() if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - return table + return tableView }() func getFooterView() -> UIView? { - let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) + let footerView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) let button = UIButton() - footer.addSubview(button) + footerView.addSubview(button) button.backgroundColor = .white button.clipsToBounds = true button.setTitleColor(UIColor(hexString: "0xE6605C"), for: .normal) @@ -88,14 +89,14 @@ class MineSettingViewController: NEBaseViewController, UITableViewDataSource, UI } else { button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 0), - button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: 0), - button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), + button.leftAnchor.constraint(equalTo: footerView.leftAnchor, constant: 0), + button.rightAnchor.constraint(equalTo: footerView.rightAnchor, constant: 0), + button.topAnchor.constraint(equalTo: footerView.topAnchor, constant: 12), button.heightAnchor.constraint(equalToConstant: 40), ]) } - return footer + return footerView } @objc func loginOutAction() { @@ -104,28 +105,32 @@ class MineSettingViewController: NEBaseViewController, UITableViewDataSource, UI withConfirm: NSLocalizedString("want_to_logout", comment: ""), withCompletion: { [weak self] user, error in if error != nil { + NEALog.infoLog(self?.className() ?? "", desc: "logout author manager error : \(error?.localizedDescription ?? "")") self?.view.makeToast(error?.localizedDescription) - NELog.errorLog( + NEALog.errorLog( self?.tag ?? "", desc: "CALLBACK logout failed,error = \(error!)" ) } else { - IMKitClient.instance.logout { error in + IMKitClient.instance.logoutIM { error in if error != nil { + NEALog.infoLog(self?.className() ?? "", desc: "logout im error : \(error?.localizedDescription ?? "")") self?.view.makeToast(error?.localizedDescription) - NELog.errorLog( + NEALog.errorLog( self?.tag ?? "", - desc: "CALLBACK logout failed,error = \(error!)" + desc: "CALLBACK logout SUCCESS = \(error!)" ) } else { + NEALog.infoLog(self?.className() ?? "", desc: "logout im success ") NotificationCenter.default.post( name: Notification.Name("logout"), object: nil ) - NELog.infoLog( + NEALog.infoLog( self?.tag ?? "", desc: "CALLBACK logout SUCCESS" ) + NEFriendUserCache.shared.removeAllFriendInfo() } } } @@ -185,9 +190,9 @@ class MineSettingViewController: NEBaseViewController, UITableViewDataSource, UI } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .clear - return header + let headerView = UIView() + headerView.backgroundColor = .clear + return headerView } func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { diff --git a/app/Mine/Controller/NEAboutWebViewController.swift b/app/Mine/Controller/NEAboutWebViewController.swift index 402e6f0a..41065b61 100644 --- a/app/Mine/Controller/NEAboutWebViewController.swift +++ b/app/Mine/Controller/NEAboutWebViewController.swift @@ -22,7 +22,7 @@ class NEAboutWebViewController: NEBaseViewController { } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setUpSubViews() { diff --git a/app/Mine/Controller/NELoginViewController.swift b/app/Mine/Controller/NELoginViewController.swift index 4b298a55..fba8e4de 100644 --- a/app/Mine/Controller/NELoginViewController.swift +++ b/app/Mine/Controller/NELoginViewController.swift @@ -5,7 +5,8 @@ import NEChatUIKit import NECommonKit -import NECoreIMKit +import NECoreIM2Kit +import NIMSDK import UIKit import YXLogin @@ -15,6 +16,66 @@ public class NELoginViewController: UIViewController { public var successLogin: LoginBlock? + lazy var launchIconView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = UIImage(named: "launchIcon") + return imageView + }() + + lazy var launchIconLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = NSLocalizedString("appName", comment: "") + label.font = UIFont.systemFont(ofSize: 24.0) + label.textColor = UIColor(hexString: "333333") + label.accessibilityIdentifier = "id.appYunxin" + return label + }() + + lazy var loginButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.layer.cornerRadius = 8 + button.backgroundColor = UIColor.ne_blueText + button.setTitleColor(UIColor.white, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 15.0) + button.setTitle(NSLocalizedString("register_login", comment: ""), for: .normal) + button.addTarget(self, action: #selector(loginBtnClick), for: .touchUpInside) + button.accessibilityIdentifier = "id.loginButton" + return button + }() + + lazy var emailLoginButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitleColor(UIColor.ne_lightText, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 12.0) + button.setTitle(NSLocalizedString("email_login", comment: ""), for: .normal) + button.addTarget(self, action: #selector(emailLoginBtnClick), for: .touchUpInside) + button.accessibilityIdentifier = "id.emailLogin" + return button + }() + + lazy var dividerLineView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.ne_lightText + return view + }() + + /// 节点按钮 + lazy var nodeButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitleColor(UIColor.ne_lightText, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 12.0) + button.setTitle(NSLocalizedString("node_select", comment: ""), for: .normal) + button.addTarget(self, action: #selector(nodeBtnClick), for: .touchUpInside) + button.accessibilityIdentifier = "id.serverConfig" + return button + }() + override public func viewDidLoad() { super.viewDidLoad() setupUI() @@ -29,61 +90,77 @@ public class NELoginViewController: UIViewController { } func setupUI() { - view.addSubview(launchIcon) + view.addSubview(launchIconView) view.addSubview(launchIconLabel) - view.addSubview(loginBtn) - view.addSubview(emailLoginBtn) - view.addSubview(divideView) - view.addSubview(nodeBtn) + view.addSubview(loginButton) + view.addSubview(emailLoginButton) + view.addSubview(dividerLineView) + view.addSubview(nodeButton) if #available(iOS 11.0, *) { NSLayoutConstraint.activate([ - launchIcon.centerXAnchor.constraint(equalTo: view.centerXAnchor), - launchIcon.topAnchor.constraint( + launchIconView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + launchIconView.topAnchor.constraint( equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 145.0 ), ]) } else { NSLayoutConstraint.activate([ - launchIcon.centerXAnchor.constraint(equalTo: view.centerXAnchor), - launchIcon.topAnchor.constraint(equalTo: view.topAnchor, constant: 145.0), + launchIconView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + launchIconView.topAnchor.constraint(equalTo: view.topAnchor, constant: 145.0), ]) } NSLayoutConstraint.activate([ launchIconLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - launchIconLabel.topAnchor.constraint(equalTo: launchIcon.bottomAnchor, constant: -12.0), + launchIconLabel.topAnchor.constraint(equalTo: launchIconView.bottomAnchor, constant: -12.0), ]) NSLayoutConstraint.activate([ - loginBtn.centerXAnchor.constraint(equalTo: view.centerXAnchor), - loginBtn.topAnchor.constraint(equalTo: launchIconLabel.bottomAnchor, constant: 20), - loginBtn.widthAnchor.constraint(equalToConstant: NEConstant.screenWidth - 80), - loginBtn.heightAnchor.constraint(equalToConstant: 44), + loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + loginButton.topAnchor.constraint(equalTo: launchIconLabel.bottomAnchor, constant: 20), + loginButton.widthAnchor.constraint(equalToConstant: NEConstant.screenWidth - 80), + loginButton.heightAnchor.constraint(equalToConstant: 44), ]) NSLayoutConstraint.activate([ - divideView.bottomAnchor.constraint( + dividerLineView.bottomAnchor.constraint( equalTo: view.bottomAnchor, constant: -10 - NEConstant.statusBarHeight ), - divideView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - divideView.widthAnchor.constraint(equalToConstant: 1), - divideView.heightAnchor.constraint(equalToConstant: 10), + dividerLineView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + dividerLineView.widthAnchor.constraint(equalToConstant: 1), + dividerLineView.heightAnchor.constraint(equalToConstant: 10), ]) NSLayoutConstraint.activate([ - emailLoginBtn.centerYAnchor.constraint(equalTo: divideView.centerYAnchor), - emailLoginBtn.rightAnchor.constraint(equalTo: divideView.leftAnchor, constant: -8), + emailLoginButton.centerYAnchor.constraint(equalTo: dividerLineView.centerYAnchor), + emailLoginButton.rightAnchor.constraint(equalTo: dividerLineView.leftAnchor, constant: -8), ]) NSLayoutConstraint.activate([ - nodeBtn.centerYAnchor.constraint(equalTo: divideView.centerYAnchor), - nodeBtn.leftAnchor.constraint(equalTo: divideView.rightAnchor, constant: 8), + nodeButton.centerYAnchor.constraint(equalTo: dividerLineView.centerYAnchor), + nodeButton.leftAnchor.constraint(equalTo: dividerLineView.rightAnchor, constant: 8), ]) } @objc func loginBtnClick(sender: UIButton) { + // login to business server + let config = YXConfig() + config.appKey = AppKey.appKey + config.parentScope = NSNumber(integerLiteral: 2) + config.scope = NSNumber(integerLiteral: 7) + config.supportInternationalize = true + config.type = .phone + #if DEBUG + config.isOnline = false + print("debug") + #else + config.isOnline = true + print("release") + #endif + AuthorManager.shareInstance()?.initAuthor(with: config) + weak var weakSelf = self AuthorManager.shareInstance()?.startLogin(completion: { user, error in if let err = error { @@ -110,6 +187,7 @@ public class NELoginViewController: UIViewController { print("release") #endif AuthorManager.shareInstance()?.initAuthor(with: config) + weak var weakSelf = self AuthorManager.shareInstance()?.startLogin(completion: { user, error in if let err = error { @@ -128,11 +206,16 @@ public class NELoginViewController: UIViewController { private func setupSuccessLogic(_ user: YXUserInfo?) { if let token = user?.imToken, let account = user?.imAccid { weak var weakSelf = self - IMKitClient.instance.loginIM(account, token) { error in + print("login accid : ", account) + print("login token : ", token) + + let option = V2NIMLoginOption() + IMKitClient.instance.login(account, token, option) { error in if let err = error { - print("loginIM error : ", err) + NEALog.infoLog(weakSelf?.className() ?? "", desc: "login IM error : \(err.localizedDescription)") UIApplication.shared.keyWindow?.makeToast(err.localizedDescription) } else { + NEALog.infoLog(weakSelf?.className() ?? "", desc: "login IM Success") ChatRouter.setupInit() if let block = weakSelf?.successLogin { block() @@ -141,64 +224,4 @@ public class NELoginViewController: UIViewController { } } } - - // lazy method - lazy var launchIcon: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.image = UIImage(named: "launchIcon") - return imageView - }() - - lazy var launchIconLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = NSLocalizedString("appName", comment: "") - label.font = UIFont.systemFont(ofSize: 24.0) - label.textColor = UIColor(hexString: "333333") - label.accessibilityIdentifier = "id.appYunxin" - return label - }() - - lazy var loginBtn: UIButton = { - let btn = UIButton() - btn.translatesAutoresizingMaskIntoConstraints = false - btn.layer.cornerRadius = 8 - btn.backgroundColor = UIColor.ne_blueText - btn.setTitleColor(UIColor.white, for: .normal) - btn.titleLabel?.font = UIFont.systemFont(ofSize: 15.0) - btn.setTitle(NSLocalizedString("register_login", comment: ""), for: .normal) - btn.addTarget(self, action: #selector(loginBtnClick), for: .touchUpInside) - btn.accessibilityIdentifier = "id.loginButton" - return btn - }() - - lazy var emailLoginBtn: UIButton = { - let btn = UIButton() - btn.translatesAutoresizingMaskIntoConstraints = false - btn.setTitleColor(UIColor.ne_lightText, for: .normal) - btn.titleLabel?.font = UIFont.systemFont(ofSize: 12.0) - btn.setTitle(NSLocalizedString("email_login", comment: ""), for: .normal) - btn.addTarget(self, action: #selector(emailLoginBtnClick), for: .touchUpInside) - btn.accessibilityIdentifier = "id.emailLogin" - return btn - }() - - lazy var divideView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.ne_lightText - return view - }() - - lazy var nodeBtn: UIButton = { - let btn = UIButton() - btn.translatesAutoresizingMaskIntoConstraints = false - btn.setTitleColor(UIColor.ne_lightText, for: .normal) - btn.titleLabel?.font = UIFont.systemFont(ofSize: 12.0) - btn.setTitle(NSLocalizedString("node_select", comment: ""), for: .normal) - btn.addTarget(self, action: #selector(nodeBtnClick), for: .touchUpInside) - btn.accessibilityIdentifier = "id.serverConfig" - return btn - }() } diff --git a/app/Mine/Controller/NENodeViewController.swift b/app/Mine/Controller/NENodeViewController.swift index 22539d8c..b0d902a8 100644 --- a/app/Mine/Controller/NENodeViewController.swift +++ b/app/Mine/Controller/NENodeViewController.swift @@ -20,11 +20,13 @@ class NENodeViewController: NEBaseViewController, UITableViewDataSource, UITable func setupUI() { title = NSLocalizedString("node_select", comment: "") + navigationView.backgroundColor = .ne_lightBackgroundColor + view.addSubview(tableView) NSLayoutConstraint.activate([ tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), - tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) @@ -46,7 +48,8 @@ class NENodeViewController: NEBaseViewController, UITableViewDataSource, UITable alertController.addAction(cancelAction) let sureAction = UIAlertAction(title: NSLocalizedString("restart", comment: ""), style: .default) { action in // 设置节点 - IMKitClient.instance.getSettingRepo().setNodeValue(isDomestic) + // TODO: - 未实现 +// SettingRepo.shared.setNodeValue(isDomestic) exit(0) } alertController.addAction(sureAction) @@ -54,18 +57,18 @@ class NENodeViewController: NEBaseViewController, UITableViewDataSource, UITable } lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .ne_lightBackgroundColor - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .ne_lightBackgroundColor + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - return table + return tableView }() // MARK: UITableViewDataSource, UITableViewDelegate @@ -116,8 +119,8 @@ class NENodeViewController: NEBaseViewController, UITableViewDataSource, UITable } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView } } diff --git a/app/Mine/Controller/PersonInfoViewController.swift b/app/Mine/Controller/PersonInfoViewController.swift index 62cb08d9..0f38d5d6 100644 --- a/app/Mine/Controller/PersonInfoViewController.swift +++ b/app/Mine/Controller/PersonInfoViewController.swift @@ -9,9 +9,9 @@ import NIMSDK import UIKit @objcMembers -class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, +class PersonInfoViewController: NEBaseViewController, UINavigationControllerDelegate, PersonInfoViewModelDelegate, UITableViewDelegate, - UITableViewDataSource { + UITableViewDataSource, NEContactListener { public var cellClassDic = [ SettingCellType.SettingSubtitleCell.rawValue: CustomTeamSettingSubtitleCell.self, SettingCellType.SettingHeaderCell.rawValue: CustomTeamSettingHeaderCell.self, @@ -22,6 +22,7 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, override func viewDidLoad() { super.viewDidLoad() + ContactRepo.shared.addContactListener(self) viewModel.getData() setupSubviews() initialConfig() @@ -39,7 +40,6 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, view.backgroundColor = .funChatBackgroundColor } viewModel.delegate = self - NIMSDK.shared().userManager.add(self) } func setupSubviews() { @@ -54,7 +54,7 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in + for (key, value) in cellClassDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } @@ -104,16 +104,14 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, func showDatePicker() { view.addSubview(pickerView) - - weak var weakSelf = self - pickerView.timeCallBack = { time in + pickerView.timeCallBack = { [weak self] time in if let t = time { - weakSelf?.viewModel.updateBirthday(birthDay: t) { error in - if error != nil { - if error?.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) + self?.viewModel.updateSelfBirthday(t) { error in + if let err = error { + if err.code == protocolSendFailed { + self?.showToast(commonLocalizable("network_error")) } else { - weakSelf?.showToast(NSLocalizedString("setting_birthday_failure", comment: "")) + self?.showToast(NSLocalizedString("setting_birthday_failure", comment: "")) } } } @@ -128,17 +126,17 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, } lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - return table + return tableView }() private lazy var pickerView: BirthdayDatePickerView = { @@ -147,16 +145,17 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, return picker }() - deinit { - NIMSDK.shared().userManager.remove(self) - } - - // MARK: NIMUserManagerDelegate - - func onUserInfoChanged(_ user: NIMUser) { - if user.userId == IMKitClient.instance.imAccid() { - viewModel.getData() - tableView.reloadData() + /// 用户信息变更回调 + /// - Parameter users: 用户列表 + func onUserProfileChanged(_ users: [V2NIMUser]) { + for user in users { + if user.accountId == IMKitClient.instance.account() { + print("change self user profile : ", user.yx_modelToJSONString() as Any) + viewModel.userInfo = NEUserWithFriend(user: user) + viewModel.refreshData() + tableView.reloadData() + break + } } } @@ -178,29 +177,27 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, } view.makeToastActivity(.center) - if let imageData = image.jpegData(compressionQuality: 0.6) as NSData? { - let filePath = NSHomeDirectory().appending("/Documents/") - .appending(IMKitClient.instance.imAccid()) + if let imageData = image.jpegData(compressionQuality: 0.6) as NSData?, + var filePath = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit/image/") { + filePath += "\(IMKitClient.instance.account())_avatar.jpg" let succcess = imageData.write(toFile: filePath, atomically: true) if succcess { - NIMSDK.shared().resourceManager - .upload(filePath, scene: NIMNOSSceneTypeAvatar, - progress: nil) { urlString, error in - if error == nil { - weakSelf?.viewModel.updateAvatar(avatar: urlString ?? "") { error in - if error != nil { - weakSelf?.showToast(NSLocalizedString("setting_head_failure", comment: "")) - } + let fileTask = ResourceRepo.shared.createUploadFileTask(filePath) + ResourceRepo.shared.upload(fileTask, nil) { [weak self] urlString, error in + if error == nil { + self?.viewModel.updateSelfAvatar(urlString ?? "") { [weak self] error in + if error != nil { + self?.showToast(NSLocalizedString("setting_head_failure", comment: "")) } - - } else { - NELog.errorLog( - weakSelf?.className ?? "", - desc: "❌CALLBACK upload image failed,error = \(error!)" - ) } - self.view.hideToastActivity() + } else { + NEALog.errorLog( + weakSelf?.className ?? "", + desc: "❌CALLBACK upload image failed,error = \(error!)" + ) } + self?.view.hideToastActivity() + } } } } @@ -221,11 +218,11 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, ctrl.contentText = name weak var weakSelf = self ctrl.callBack = { editText in - weakSelf?.viewModel.updateNickName(name: editText) { error in + weakSelf?.viewModel.updateSelfNickName(editText) { [weak self] error in if error != nil { - weakSelf?.showToastInWindow(NSLocalizedString("setting_nickname_failure", comment: "")) + self?.showToastInWindow(NSLocalizedString("setting_nickname_failure", comment: "")) } else { - weakSelf?.navigationController?.popViewController(animated: true) + self?.navigationController?.popViewController(animated: true) } } } @@ -233,17 +230,18 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, } func didClickGender() { - var sex = NIMUserGender.unknown + var gender = V2NIMGender.GENDER_UNKNOWN weak var weakSelf = self let block: ((_ value: NSInteger) -> Void) = { value in - sex = value == 0 ? .male : .female - weakSelf?.viewModel.updateSex(sex: sex) { error in + gender = value == 0 ? .GENDER_MALE : .GENDER_FEMALE + + weakSelf?.viewModel.updateSelfSex(gender) { [weak self] error in if error != nil { - if error?.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) + if error?.code == protocolSendFailed { + self?.showToast(commonLocalizable("network_error")) } else { - weakSelf?.showToast(NSLocalizedString("change_gender_failure", comment: "")) + self?.showToast(NSLocalizedString("change_gender_failure", comment: "")) } } } @@ -274,28 +272,39 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, ctrl.contentText = mobile weak var weakSelf = self ctrl.callBack = { editText in - weakSelf?.viewModel.updateMobile(mobile: editText) { error in + weakSelf?.viewModel.updateSelfMobile(editText) { [weak self] error in if error != nil { - weakSelf?.showToastInWindow(NSLocalizedString("change_phone_failure", comment: "")) + self?.showToastInWindow(NSLocalizedString("change_phone_failure", comment: "")) } else { - weakSelf?.navigationController?.popViewController(animated: true) + self?.navigationController?.popViewController(animated: true) } } } navigationController?.pushViewController(ctrl, animated: true) } + func isValidEmail(_ email: String) -> Bool { + let emailRegex = #"^\w+@\w+\.[a-zA-Z]{2,}"# + return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: email) + } + func didClickEmail(email: String) { let ctrl = InputPersonInfoController() ctrl.configTitle(editType: .email) ctrl.contentText = email weak var weakSelf = self ctrl.callBack = { editText in - weakSelf?.viewModel.updateEmail(email: editText) { error in + + if weakSelf?.isValidEmail(editText) == false { + weakSelf?.showToastInWindow(NSLocalizedString("change_email_failure", comment: "")) + return + } + + weakSelf?.viewModel.updateSelfEmail(editText) { [weak self] error in if error != nil { - weakSelf?.showToastInWindow(NSLocalizedString("change_email_failure", comment: "")) + self?.showToastInWindow(NSLocalizedString("change_email_failure", comment: "")) } else { - weakSelf?.navigationController?.popViewController(animated: true) + self?.navigationController?.popViewController(animated: true) } } } @@ -308,11 +317,11 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, ctrl.contentText = sign weak var weakSelf = self ctrl.callBack = { editText in - weakSelf?.viewModel.updateSign(sign: editText) { error in + weakSelf?.viewModel.updateSelfSign(editText) { [weak self] error in if error != nil { - weakSelf?.showToastInWindow(NSLocalizedString("change_sign_failure", comment: "")) + self?.showToastInWindow(NSLocalizedString("change_sign_failure", comment: "")) } else { - weakSelf?.navigationController?.popViewController(animated: true) + self?.navigationController?.popViewController(animated: true) } } } @@ -376,8 +385,8 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView } } diff --git a/app/Mine/Controller/StyleSelectionViewController.swift b/app/Mine/Controller/StyleSelectionViewController.swift index 21124fd5..a9f333bd 100644 --- a/app/Mine/Controller/StyleSelectionViewController.swift +++ b/app/Mine/Controller/StyleSelectionViewController.swift @@ -39,16 +39,16 @@ open class StyleSelectionViewController: NEBaseViewController, UICollectionViewD layout.minimumLineSpacing = 0 layout.minimumInteritemSpacing = 0 layout.sectionInset = UIEdgeInsets(top: topMargin, left: 0, bottom: topMargin, right: 0) - let collect = UICollectionView(frame: .zero, collectionViewLayout: layout) - collect.translatesAutoresizingMaskIntoConstraints = false - collect.dataSource = self - collect.delegate = self - collect.isUserInteractionEnabled = true - collect.backgroundColor = .white - collect.contentMode = .center - collect.register(StyleSelectionCell.self, forCellWithReuseIdentifier: "\(StyleSelectionCell.self)") - - return collect + let collectView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectView.translatesAutoresizingMaskIntoConstraints = false + collectView.dataSource = self + collectView.delegate = self + collectView.isUserInteractionEnabled = true + collectView.backgroundColor = .white + collectView.contentMode = .center + collectView.register(StyleSelectionCell.self, forCellWithReuseIdentifier: "\(StyleSelectionCell.self)") + + return collectView }() override open func viewDidLoad() { @@ -119,7 +119,6 @@ open class StyleSelectionViewController: NEBaseViewController, UICollectionViewD public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.row == 0 { if NEStyleManager.instance.isNormalStyle() == false { - removeConversationDelegate() NEStyleManager.instance.setNormalStyle() NotificationCenter.default.post( name: Notification.Name(CHANGE_UI), @@ -128,7 +127,6 @@ open class StyleSelectionViewController: NEBaseViewController, UICollectionViewD } } else if indexPath.row == 1 { if NEStyleManager.instance.isNormalStyle() == true { - removeConversationDelegate() NEStyleManager.instance.setFunStyle() NotificationCenter.default.post( name: Notification.Name(CHANGE_UI), @@ -137,14 +135,4 @@ open class StyleSelectionViewController: NEBaseViewController, UICollectionViewD } } } - - public func removeConversationDelegate() { - if let tabcontroller = UIApplication.shared.keyWindow?.rootViewController as? NETabBarController { - tabcontroller.viewControllers?.forEach { controller in - if let nav = controller as? NENavigationController, let conversationController = nav.topViewController as? NEBaseConversationController { - NIMSDK.shared().chatManager.remove(conversationController) - } - } - } - } } diff --git a/app/Mine/View/BirthdayDatePickerView.swift b/app/Mine/View/BirthdayDatePickerView.swift index 907717d5..ed76adf4 100644 --- a/app/Mine/View/BirthdayDatePickerView.swift +++ b/app/Mine/View/BirthdayDatePickerView.swift @@ -11,7 +11,7 @@ public class BirthdayDatePickerView: UIView { public typealias SelectTimeCallBack = (String?) -> Void public var timeCallBack: SelectTimeCallBack? - lazy var cancelBtn: UIButton = { + lazy var cancelButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle(NSLocalizedString("cancel", comment: ""), for: .normal) @@ -21,7 +21,7 @@ public class BirthdayDatePickerView: UIView { return button }() - lazy var sureBtn: UIButton = { + lazy var sureButton: UIButton = { let button = UIButton(type: .custom) button.translatesAutoresizingMaskIntoConstraints = false button.setTitle(NSLocalizedString("confirm", comment: ""), for: .normal) @@ -56,6 +56,14 @@ public class BirthdayDatePickerView: UIView { return datePicker }() + /// 日期选择器背景视图 + public lazy var pickerBackView: UIView = { + let pickerBackView = UIView() + pickerBackView.translatesAutoresizingMaskIntoConstraints = false + pickerBackView.backgroundColor = .white + return pickerBackView + }() + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = UIColor(white: 0, alpha: 0.25) @@ -65,13 +73,11 @@ public class BirthdayDatePickerView: UIView { } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } + /// UI 初始化 func setupSubviews() { - let pickerBackView = UIView() - pickerBackView.translatesAutoresizingMaskIntoConstraints = false - pickerBackView.backgroundColor = .white addSubview(pickerBackView) NSLayoutConstraint.activate([ @@ -81,29 +87,29 @@ public class BirthdayDatePickerView: UIView { pickerBackView.heightAnchor.constraint(equalToConstant: 229), ]) - pickerBackView.addSubview(cancelBtn) - pickerBackView.addSubview(sureBtn) + pickerBackView.addSubview(cancelButton) + pickerBackView.addSubview(sureButton) pickerBackView.addSubview(bottomLine) pickerBackView.addSubview(picker) NSLayoutConstraint.activate([ - cancelBtn.leftAnchor.constraint(equalTo: pickerBackView.leftAnchor, constant: 15), - cancelBtn.topAnchor.constraint(equalTo: pickerBackView.topAnchor, constant: 8), - cancelBtn.widthAnchor.constraint(equalToConstant: 45), - cancelBtn.heightAnchor.constraint(equalToConstant: 20), + cancelButton.leftAnchor.constraint(equalTo: pickerBackView.leftAnchor, constant: 15), + cancelButton.topAnchor.constraint(equalTo: pickerBackView.topAnchor, constant: 8), + cancelButton.widthAnchor.constraint(equalToConstant: 45), + cancelButton.heightAnchor.constraint(equalToConstant: 20), ]) NSLayoutConstraint.activate([ - sureBtn.rightAnchor.constraint(equalTo: pickerBackView.rightAnchor, constant: -15), - sureBtn.topAnchor.constraint(equalTo: pickerBackView.topAnchor, constant: 8), - sureBtn.widthAnchor.constraint(equalToConstant: 45), - sureBtn.heightAnchor.constraint(equalToConstant: 20), + sureButton.rightAnchor.constraint(equalTo: pickerBackView.rightAnchor, constant: -15), + sureButton.topAnchor.constraint(equalTo: pickerBackView.topAnchor, constant: 8), + sureButton.widthAnchor.constraint(equalToConstant: 45), + sureButton.heightAnchor.constraint(equalToConstant: 20), ]) NSLayoutConstraint.activate([ bottomLine.leftAnchor.constraint(equalTo: pickerBackView.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: pickerBackView.rightAnchor), - bottomLine.topAnchor.constraint(equalTo: cancelBtn.bottomAnchor, constant: 8), + bottomLine.topAnchor.constraint(equalTo: cancelButton.bottomAnchor, constant: 8), bottomLine.heightAnchor.constraint(equalToConstant: 0.5), ]) diff --git a/app/Mine/View/MineTableViewCell.swift b/app/Mine/View/MineTableViewCell.swift index b3d712b7..076a997c 100644 --- a/app/Mine/View/MineTableViewCell.swift +++ b/app/Mine/View/MineTableViewCell.swift @@ -7,19 +7,41 @@ import NECommonUIKit import UIKit public class MineTableViewCell: UITableViewCell { -// override func awakeFromNib() { -// super.awakeFromNib() -// // Initialization code -// } -// -// override func setSelected(_ selected: Bool, animated: Bool) { -// super.setSelected(selected, animated: animated) -// -// // Configure the view for the selected state -// } + /// 头像 + public lazy var avatarImageView: UIImageView = { + let avatarView = UIImageView() + avatarView.translatesAutoresizingMaskIntoConstraints = false + return avatarView + }() + + /// 昵称 + public lazy var titleLabel: UILabel = { + let nameLabel = UILabel() + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.textColor = UIColor.ne_darkText + nameLabel.font = UIFont.systemFont(ofSize: 16.0) + nameLabel.text = NSLocalizedString("setting", comment: "") + nameLabel.accessibilityIdentifier = "id.titleLabel" + return nameLabel + }() + + /// 分割线 + private lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor(hexString: "0xDBE0E8") + return view + }() + + /// 箭头图片 + public lazy var arrowImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "arrow_right")) + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -29,21 +51,21 @@ public class MineTableViewCell: UITableViewCell { func setUpSubViews() { selectionStyle = .none - contentView.addSubview(avatarImage) + contentView.addSubview(avatarImageView) contentView.addSubview(titleLabel) contentView.addSubview(bottomLine) - contentView.addSubview(arrow) + contentView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - avatarImage.widthAnchor.constraint(equalToConstant: 20), - avatarImage.heightAnchor.constraint(equalToConstant: 20), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: 20), + avatarImageView.heightAnchor.constraint(equalToConstant: 20), ]) NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 14), - titleLabel.centerYAnchor.constraint(equalTo: avatarImage.centerYAnchor), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 14), + titleLabel.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor), ]) NSLayoutConstraint.activate([ @@ -54,47 +76,16 @@ public class MineTableViewCell: UITableViewCell { ]) NSLayoutConstraint.activate([ - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -25), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -25), // arrow.widthAnchor.constraint(equalToConstant: 15), - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } func configCell(data: [String: String]) { titleLabel.text = data.keys.first if let imageName = data.values.first { - avatarImage.image = UIImage(named: imageName) + avatarImageView.image = UIImage(named: imageName) } } - - // MARK: lazy Method - - public lazy var avatarImage: UIImageView = { - let avatar = UIImageView() - avatar.translatesAutoresizingMaskIntoConstraints = false - return avatar - }() - - public lazy var titleLabel: UILabel = { - let name = UILabel() - name.translatesAutoresizingMaskIntoConstraints = false - name.textColor = UIColor.ne_darkText - name.font = UIFont.systemFont(ofSize: 16.0) - name.text = NSLocalizedString("setting", comment: "") - name.accessibilityIdentifier = "id.titleLabel" - return name - }() - - private lazy var bottomLine: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor(hexString: "0xDBE0E8") - return view - }() - - public lazy var arrow: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "arrow_right")) - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() } diff --git a/app/Mine/View/NodeSelectCell.swift b/app/Mine/View/NodeSelectCell.swift index 62bc3b96..e2f71c44 100644 --- a/app/Mine/View/NodeSelectCell.swift +++ b/app/Mine/View/NodeSelectCell.swift @@ -25,27 +25,27 @@ class NodeSelectCell: CornerCell { } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } public func configure(_ cellModel: SettingCellModel) { cornerType = cellModel.cornerType - stateImg.isHighlighted = cellModel.switchOpen ? true : false + stateImageView.isHighlighted = cellModel.switchOpen ? true : false titleLabel.text = cellModel.subTitle } func setupUI() { contentView.addSubview(titleLabel) - contentView.addSubview(stateImg) + contentView.addSubview(stateImageView) NSLayoutConstraint.activate([ - stateImg.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - stateImg.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 30), + stateImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + stateImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 30), ]) NSLayoutConstraint.activate([ titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - titleLabel.leftAnchor.constraint(equalTo: stateImg.rightAnchor, constant: 10), + titleLabel.leftAnchor.constraint(equalTo: stateImageView.rightAnchor, constant: 10), ]) } @@ -57,11 +57,11 @@ class NodeSelectCell: CornerCell { return label }() - lazy var stateImg: UIImageView = { - let img = UIImageView() - img.image = UIImage(named: "unselect") - img.highlightedImage = UIImage(named: "select") - img.translatesAutoresizingMaskIntoConstraints = false - return img + lazy var stateImageView: UIImageView = { + let imgView = UIImageView() + imgView.image = UIImage(named: "unselect") + imgView.highlightedImage = UIImage(named: "select") + imgView.translatesAutoresizingMaskIntoConstraints = false + return imgView }() } diff --git a/app/Mine/View/StyleSelectionCell.swift b/app/Mine/View/StyleSelectionCell.swift index d14baed1..0b9dc8db 100644 --- a/app/Mine/View/StyleSelectionCell.swift +++ b/app/Mine/View/StyleSelectionCell.swift @@ -7,7 +7,7 @@ import UIKit open class StyleSelectionCell: UICollectionViewCell { var styleName = "default" var stylePreview = UIImageView() - var styleTitle = UILabel() + var styleTitleLabel = UILabel() var selectButton = UIButton() override public init(frame: CGRect) { @@ -17,7 +17,7 @@ open class StyleSelectionCell: UICollectionViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setupSubviews() { @@ -25,8 +25,9 @@ open class StyleSelectionCell: UICollectionViewCell { stylePreview.layer.cornerRadius = 8 addSubview(stylePreview) - styleTitle.translatesAutoresizingMaskIntoConstraints = false - addSubview(styleTitle) + styleTitleLabel.translatesAutoresizingMaskIntoConstraints = false + styleTitleLabel.accessibilityIdentifier = "id.styleTitle" + addSubview(styleTitleLabel) selectButton.translatesAutoresizingMaskIntoConstraints = false selectButton.setImage(UIImage(named: "unclicked"), for: .normal) @@ -42,13 +43,13 @@ open class StyleSelectionCell: UICollectionViewCell { ]) NSLayoutConstraint.activate([ - styleTitle.topAnchor.constraint(equalTo: stylePreview.bottomAnchor, constant: 16), - styleTitle.centerXAnchor.constraint(equalTo: centerXAnchor), - styleTitle.heightAnchor.constraint(equalToConstant: 18), + styleTitleLabel.topAnchor.constraint(equalTo: stylePreview.bottomAnchor, constant: 16), + styleTitleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + styleTitleLabel.heightAnchor.constraint(equalToConstant: 18), ]) NSLayoutConstraint.activate([ - selectButton.topAnchor.constraint(equalTo: styleTitle.bottomAnchor, constant: 16), + selectButton.topAnchor.constraint(equalTo: styleTitleLabel.bottomAnchor, constant: 16), selectButton.centerXAnchor.constraint(equalTo: centerXAnchor), selectButton.widthAnchor.constraint(equalToConstant: 22), selectButton.heightAnchor.constraint(equalToConstant: 22), @@ -58,7 +59,7 @@ open class StyleSelectionCell: UICollectionViewCell { func configData(model: StyleCellModel) { styleName = model.styleName stylePreview.image = UIImage(named: model.styleImageName) - styleTitle.text = model.styleTitle + styleTitleLabel.text = model.styleTitle selectButton.setImage(UIImage(named: model.selectedImageName), for: .selected) selectButton.isSelected = model.selected } diff --git a/app/Mine/View/Theme/CustomTeamArrowSettingCell.swift b/app/Mine/View/Theme/CustomTeamArrowSettingCell.swift index 064b08c6..e0002f02 100644 --- a/app/Mine/View/Theme/CustomTeamArrowSettingCell.swift +++ b/app/Mine/View/Theme/CustomTeamArrowSettingCell.swift @@ -28,10 +28,10 @@ class CustomTeamArrowSettingCell: TeamArrowSettingCell { titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -68), ]) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), ]) dividerLineLeftMargin?.constant = 20 dividerLineRightMargin?.constant = 0 diff --git a/app/Mine/View/Theme/CustomTeamSettingHeaderCell.swift b/app/Mine/View/Theme/CustomTeamSettingHeaderCell.swift index 1e716cd6..a68eb3c8 100644 --- a/app/Mine/View/Theme/CustomTeamSettingHeaderCell.swift +++ b/app/Mine/View/Theme/CustomTeamSettingHeaderCell.swift @@ -28,15 +28,15 @@ class CustomTeamSettingHeaderCell: TeamSettingHeaderCell { titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -68), ]) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), ]) contentView.addSubview(headerView) NSLayoutConstraint.activate([ - headerView.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), + headerView.centerYAnchor.constraint(equalTo: arrowView.centerYAnchor), headerView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -48.0), headerView.widthAnchor.constraint(equalToConstant: 42.0), headerView.heightAnchor.constraint(equalToConstant: 42.0), diff --git a/app/Mine/View/Theme/CustomTeamSettingRightCustomCell.swift b/app/Mine/View/Theme/CustomTeamSettingRightCustomCell.swift index 051aba0b..faa6a857 100644 --- a/app/Mine/View/Theme/CustomTeamSettingRightCustomCell.swift +++ b/app/Mine/View/Theme/CustomTeamSettingRightCustomCell.swift @@ -23,7 +23,7 @@ class CustomTeamSettingRightCustomCell: TeamSettingRightCustomCell { contentView.addSubview(titleLabel) contentView.addSubview(subTitleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) NSLayoutConstraint.activate([ titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), @@ -34,14 +34,14 @@ class CustomTeamSettingRightCustomCell: TeamSettingRightCustomCell { titleWidthAnchor?.isActive = true NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - arrow.widthAnchor.constraint(equalToConstant: 7), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowView.widthAnchor.constraint(equalToConstant: 7), ]) NSLayoutConstraint.activate([ subTitleLabel.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: 10), - subTitleLabel.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -10), + subTitleLabel.rightAnchor.constraint(equalTo: arrowView.leftAnchor, constant: -10), subTitleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) diff --git a/app/Mine/View/Theme/CustomTeamSettingSubtitleCell.swift b/app/Mine/View/Theme/CustomTeamSettingSubtitleCell.swift index 98e3cfb1..0b2a22b1 100644 --- a/app/Mine/View/Theme/CustomTeamSettingSubtitleCell.swift +++ b/app/Mine/View/Theme/CustomTeamSettingSubtitleCell.swift @@ -23,7 +23,7 @@ class CustomTeamSettingSubtitleCell: TeamSettingSubtitleCell { contentView.addSubview(titleLabel) contentView.addSubview(subTitleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) NSLayoutConstraint.activate([ titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), @@ -34,14 +34,14 @@ class CustomTeamSettingSubtitleCell: TeamSettingSubtitleCell { titleWidthAnchor?.isActive = true NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - arrow.widthAnchor.constraint(equalToConstant: 7), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowView.widthAnchor.constraint(equalToConstant: 7), ]) NSLayoutConstraint.activate([ subTitleLabel.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: 10), - subTitleLabel.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -10), + subTitleLabel.rightAnchor.constraint(equalTo: arrowView.leftAnchor, constant: -10), subTitleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) diff --git a/app/Mine/View/VersionCell.swift b/app/Mine/View/VersionCell.swift index 9a799576..c62d5a6c 100644 --- a/app/Mine/View/VersionCell.swift +++ b/app/Mine/View/VersionCell.swift @@ -12,14 +12,49 @@ enum IntroduceCellType: Int { } class VersionCell: UITableViewCell { + /// 标题 + lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor(hexString: "0x333333") + label.font = UIFont.systemFont(ofSize: 14) + return label + }() + + /// 子标题 + lazy var subTitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor(hexString: "0x333333") + label.font = UIFont.systemFont(ofSize: 14) + label.isHidden = true + label.accessibilityIdentifier = "id.version" + return label + }() + + /// 箭头图片 + public lazy var arrowImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "arrow_right")) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.isHidden = true + return imageView + }() + + private lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor(hexString: "0xDBE0E8") + return view + }() + public var cellType: IntroduceCellType? { didSet { if cellType == .version { - subTitle.isHidden = false - arrow.isHidden = true + subTitleLabel.isHidden = false + arrowImageView.isHidden = true } else { - subTitle.isHidden = true - arrow.isHidden = false + subTitleLabel.isHidden = true + arrowImageView.isHidden = false } } } @@ -43,8 +78,8 @@ class VersionCell: UITableViewCell { func setupSubviews() { contentView.addSubview(titleLabel) - contentView.addSubview(subTitle) - contentView.addSubview(arrow) + contentView.addSubview(subTitleLabel) + contentView.addSubview(arrowImageView) contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ @@ -53,13 +88,13 @@ class VersionCell: UITableViewCell { ]) NSLayoutConstraint.activate([ - subTitle.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - subTitle.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + subTitleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + subTitleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) NSLayoutConstraint.activate([ - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) NSLayoutConstraint.activate([ @@ -71,43 +106,11 @@ class VersionCell: UITableViewCell { } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func configData(model: SettingCellModel) { titleLabel.text = model.cellName - subTitle.text = model.subTitle + subTitleLabel.text = model.subTitle } - - lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = UIColor(hexString: "0x333333") - label.font = UIFont.systemFont(ofSize: 14) - return label - }() - - lazy var subTitle: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = UIColor(hexString: "0x333333") - label.font = UIFont.systemFont(ofSize: 14) - label.isHidden = true - label.accessibilityIdentifier = "id.version" - return label - }() - - public lazy var arrow: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "arrow_right")) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.isHidden = true - return imageView - }() - - private lazy var bottomLine: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor(hexString: "0xDBE0E8") - return view - }() } diff --git a/app/Mine/ViewModel/IntroduceViewModel.swift b/app/Mine/ViewModel/IntroduceViewModel.swift index c4109b15..afdccda6 100644 --- a/app/Mine/ViewModel/IntroduceViewModel.swift +++ b/app/Mine/ViewModel/IntroduceViewModel.swift @@ -5,6 +5,7 @@ import Foundation import NETeamUIKit +import NIMSDK @objcMembers public class IntroduceViewModel: NSObject { @@ -18,8 +19,13 @@ public class IntroduceViewModel: NSObject { versionItem.subTitle = "V\(version)" } + let imVersionItem = SettingCellModel() + imVersionItem.cellName = "IM 版本" + imVersionItem.subTitle = "\(NIMSDK.shared().sdkVersion())" + imVersionItem.type = SettingCellType.SettingSubtitleCell.rawValue + let introduceItem = SettingCellModel() introduceItem.cellName = NSLocalizedString("product_intro", comment: "") - sectionData.append(contentsOf: [versionItem, introduceItem]) + sectionData.append(contentsOf: [versionItem, imVersionItem, introduceItem]) } } diff --git a/app/Mine/ViewModel/MessageRemindViewModel.swift b/app/Mine/ViewModel/MessageRemindViewModel.swift index 169f66dc..e9d0ea9b 100644 --- a/app/Mine/ViewModel/MessageRemindViewModel.swift +++ b/app/Mine/ViewModel/MessageRemindViewModel.swift @@ -5,16 +5,16 @@ import Foundation import NETeamUIKit +import NIMSDK @objcMembers public class MessageRemindViewModel: NSObject { var sectionData = [SettingSectionModel]() - let repo = SettingRepo.shared + let settingRepo = SettingRepo.shared func getData() { sectionData.append(getFirstSection()) -// sectionData.append(getSecondSection()) sectionData.append(getThirdSection()) } @@ -26,9 +26,13 @@ public class MessageRemindViewModel: NSObject { let messageNotify = SettingCellModel() messageNotify.cellName = NSLocalizedString("new_message_remind", comment: "") messageNotify.type = SettingCellType.SettingSwitchCell.rawValue - messageNotify.switchOpen = repo.getPushEnable() + // TODO: 换V2 + messageNotify.switchOpen = settingRepo.getPushEnable() messageNotify.swichChange = { isOpen in - weakSelf?.repo.setPushEnable(isOpen) +// let config = V2NIMDndConfig() +// config.dndOn = isOpen +// weakSelf?.repo.setDndConfig(config: config) + weakSelf?.settingRepo.setPushEnable(isOpen) } model.cellModels.append(contentsOf: [ messageNotify, // 新消息通知 @@ -37,49 +41,22 @@ public class MessageRemindViewModel: NSObject { return model } - private func getSecondSection() -> SettingSectionModel { - let model = SettingSectionModel() - weak var weakSelf = self - let ringBellItem = SettingCellModel() - ringBellItem.cellName = NSLocalizedString("ring_mode", comment: "") - ringBellItem.type = SettingCellType.SettingSwitchCell.rawValue - ringBellItem.switchOpen = repo.getRingMode() - ringBellItem.swichChange = { isOpen in - weakSelf?.repo.setRingMode(isOpen) - } - - let vibrationItem = SettingCellModel() - vibrationItem.cellName = NSLocalizedString("vibration_mode", comment: "") - vibrationItem.type = SettingCellType.SettingSwitchCell.rawValue - vibrationItem.switchOpen = repo.getVibrateMode() - vibrationItem.swichChange = { isOpen in - weakSelf?.repo.setVibrateMode(isOpen) - } - model.cellModels.append(contentsOf: [ - ringBellItem, - vibrationItem, - ]) - model.setCornerType() - return model - } - private func getThirdSection() -> SettingSectionModel { let model = SettingSectionModel() weak var weakSelf = self -// let receiveItem = SettingCellModel() -// receiveItem.cellName = NSLocalizedString("syn_receive_push", comment: "") -// receiveItem.type = SettingCellType.SettingSwitchCell.rawValue -// receiveItem.switchOpen = repo.getPcWebPushEnable() -// receiveItem.swichChange = { isOpen in -// weakSelf?.repo.updatePcWebPushEnable(isOpen) -// } let messageDetailItem = SettingCellModel() messageDetailItem.cellName = NSLocalizedString("display_message_detail", comment: "") messageDetailItem.type = SettingCellType.SettingSwitchCell.rawValue - messageDetailItem.switchOpen = repo.getPushShowDetail() + // TODO: 换V2 + messageDetailItem.switchOpen = settingRepo.getPushDetailEnable() messageDetailItem.swichChange = { isOpen in - weakSelf?.repo.setPushShowDetail(isOpen) + weakSelf?.settingRepo.setPushShowDetail(isOpen) { error in + if let err = error { + print("设置失败: \(err)") + messageDetailItem.switchOpen = !isOpen + } + } } model.cellModels.append(contentsOf: [messageDetailItem]) diff --git a/app/Mine/ViewModel/MineSettingViewModel.swift b/app/Mine/ViewModel/MineSettingViewModel.swift index 9b38538d..aecde795 100644 --- a/app/Mine/ViewModel/MineSettingViewModel.swift +++ b/app/Mine/ViewModel/MineSettingViewModel.swift @@ -74,10 +74,10 @@ public class MineSettingViewModel: NSObject { receiverModel.cellName = NSLocalizedString("receiver_mode", comment: "") receiverModel.type = SettingCellType.SettingSwitchCell.rawValue // receiverModel.switchOpen = CoreKitEngine.instance.repo.getHandSetMode() - receiverModel.switchOpen = IMKitClient.instance.getSettingRepo().getHandsetMode() + receiverModel.switchOpen = SettingRepo.shared.getHandsetMode() receiverModel.swichChange = { isOpen in - IMKitClient.instance.getSettingRepo().setHandsetMode(isOpen) + SettingRepo.shared.setHandsetMode(isOpen) } // //过滤通知 // let filterNotify = SettingCellModel() @@ -93,10 +93,10 @@ public class MineSettingViewModel: NSObject { // let deleteFriend = SettingCellModel() // deleteFriend.cellName = NSLocalizedString("delete_friend", comment: "") // deleteFriend.type = SettingCellType.SettingSwitchCell.rawValue -// deleteFriend.switchOpen = IMKitClient.instance.getSettingRepo().getDeleteFriendAlias() +// deleteFriend.switchOpen = SettingRepo.shared.getDeleteFriendAlias() // // deleteFriend.swichChange = { isOpen in -// IMKitClient.instance.getSettingRepo().setDeleteFriendAlias(isOpen) +// SettingRepo.shared.setDeleteFriendAlias(isOpen) // } // 消息已读未读功能 @@ -104,9 +104,9 @@ public class MineSettingViewModel: NSObject { hasRead.cellName = NSLocalizedString("message_read_function", comment: "") hasRead.type = SettingCellType.SettingSwitchCell.rawValue // hasRead.switchOpen = true - hasRead.switchOpen = IMKitClient.instance.getSettingRepo().getShowReadStatus() + hasRead.switchOpen = SettingRepo.shared.getShowReadStatus() hasRead.swichChange = { isOpen in - IMKitClient.instance.getSettingRepo().setShowReadStatus(isOpen) + SettingRepo.shared.setShowReadStatus(isOpen) } model.cellModels.append(contentsOf: [ receiverModel, // 听筒模式 diff --git a/app/Mine/ViewModel/NodeViewModel.swift b/app/Mine/ViewModel/NodeViewModel.swift index e8989c75..6871245f 100644 --- a/app/Mine/ViewModel/NodeViewModel.swift +++ b/app/Mine/ViewModel/NodeViewModel.swift @@ -19,12 +19,14 @@ class NodeViewModel: NSObject { let home = SettingCellModel() home.subTitle = NSLocalizedString("domestic_node", comment: "") home.rowHeight = 44.0 - home.switchOpen = IMKitClient.instance.getSettingRepo().getNodeValue() == true ? true : false + // TODO: - 未实现 +// home.switchOpen = SettingRepo.shared.getNodeValue() == true ? true : false // 海外节点配置 let overseas = SettingCellModel() overseas.subTitle = NSLocalizedString("overseas_node", comment: "") - overseas.switchOpen = IMKitClient.instance.getSettingRepo().getNodeValue() == true ? false : true + // TODO: - 未实现 +// overseas.switchOpen = SettingRepo.shared.getNodeValue() == true ? false : true overseas.rowHeight = 44.0 model.cellModels.append(contentsOf: [ diff --git a/app/Mine/ViewModel/PersonInfoViewModel.swift b/app/Mine/ViewModel/PersonInfoViewModel.swift index 8a7e54c9..b5075158 100644 --- a/app/Mine/ViewModel/PersonInfoViewModel.swift +++ b/app/Mine/ViewModel/PersonInfoViewModel.swift @@ -21,15 +21,21 @@ protocol PersonInfoViewModelDelegate: AnyObject { @objcMembers public class PersonInfoViewModel: NSObject { var sectionData = [SettingSectionModel]() - public let friendProvider = FriendProvider.shared - public let userProvider = UserInfoProvider.shared - private var userInfo: NEKitUser? + let contactRepo = ContactRepo.shared + + var userInfo: NEUserWithFriend? weak var delegate: PersonInfoViewModelDelegate? func getData() { sectionData.removeAll() - userInfo = userProvider.getUserInfo(userId: IMKitClient.instance.imAccid()) + userInfo = NEFriendUserCache.shared.getFriendInfo(IMKitClient.instance.account()) + sectionData.append(getFirstSection()) + sectionData.append(getSecondSection()) + } + + func refreshData() { + sectionData.removeAll() sectionData.append(getFirstSection()) sectionData.append(getSecondSection()) } @@ -45,8 +51,9 @@ public class PersonInfoViewModel: NSObject { let headImageItem = SettingCellModel() headImageItem.type = SettingCellType.SettingHeaderCell.rawValue headImageItem.cellName = NSLocalizedString("headImage", comment: "") - headImageItem.headerUrl = userInfo?.userInfo?.avatarUrl + headImageItem.headerUrl = userInfo?.user?.avatar headImageItem.defaultHeadData = userInfo?.showName() + headImageItem.subTitle = userInfo?.user?.accountId headImageItem.rowHeight = 64.0 headImageItem.cellClick = { weakSelf?.delegate?.didClickHeadImage() @@ -66,11 +73,11 @@ public class PersonInfoViewModel: NSObject { let accountItem = SettingCellModel() accountItem.type = SettingCellType.SettingSubtitleCustomCell.rawValue accountItem.cellName = NSLocalizedString("account", comment: "") - accountItem.subTitle = mineInfo.userId + accountItem.subTitle = mineInfo.user?.accountId accountItem.rowHeight = 46.0 accountItem.rightCustomViewIcon = "copy_icon" accountItem.customViewClick = { - weakSelf?.delegate?.didCopyAccount(account: mineInfo.userId ?? "") + weakSelf?.delegate?.didCopyAccount(account: mineInfo.user?.accountId ?? "") } // 性别 @@ -78,10 +85,10 @@ public class PersonInfoViewModel: NSObject { sexItem.type = SettingCellType.SettingSubtitleCell.rawValue sexItem.cellName = NSLocalizedString("gender", comment: "") var sex = NSLocalizedString("unknown", comment: "") - switch mineInfo.userInfo?.gender { - case .male: + switch mineInfo.user?.gender { + case 1: sex = NSLocalizedString("male", comment: "") - case .female: + case 2: sex = NSLocalizedString("female", comment: "") default: sex = NSLocalizedString("unknown", comment: "") @@ -96,16 +103,17 @@ public class PersonInfoViewModel: NSObject { let birthdayItem = SettingCellModel() birthdayItem.type = SettingCellType.SettingSubtitleCell.rawValue birthdayItem.cellName = NSLocalizedString("birthday", comment: "") - birthdayItem.subTitle = mineInfo.userInfo?.birth + birthdayItem.subTitle = mineInfo.user?.birthday birthdayItem.rowHeight = 46.0 birthdayItem.cellClick = { - weakSelf?.delegate?.didClickBirthday(birth: mineInfo.userInfo?.birth ?? "") + weakSelf?.delegate?.didClickBirthday(birth: mineInfo.user?.birthday ?? "") } + // 手机 let telephoneItem = SettingCellModel() telephoneItem.type = SettingCellType.SettingSubtitleCell.rawValue telephoneItem.cellName = NSLocalizedString("phone", comment: "") - telephoneItem.subTitle = mineInfo.userInfo?.mobile + telephoneItem.subTitle = mineInfo.user?.mobile telephoneItem.rowHeight = 46.0 telephoneItem.cellClick = { weakSelf?.delegate?.didClickMobile(mobile: telephoneItem.subTitle ?? "") @@ -115,11 +123,12 @@ public class PersonInfoViewModel: NSObject { let emailItem = SettingCellModel() emailItem.type = SettingCellType.SettingSubtitleCell.rawValue emailItem.cellName = NSLocalizedString("email", comment: "") - emailItem.subTitle = mineInfo.userInfo?.email + emailItem.subTitle = mineInfo.user?.email emailItem.rowHeight = 46.0 emailItem.cellClick = { weakSelf?.delegate?.didClickEmail(email: emailItem.subTitle ?? "") } + model.cellModels.append(contentsOf: [ headImageItem, nickNameItem, @@ -142,7 +151,7 @@ public class PersonInfoViewModel: NSObject { let signItem = SettingCellModel() signItem.type = SettingCellType.SettingSubtitleCell.rawValue signItem.cellName = NSLocalizedString("individuality_sign", comment: "") - signItem.subTitle = mineInfo.userInfo?.sign + signItem.subTitle = mineInfo.user?.sign signItem.rowHeight = 46.0 signItem.titleWidth = 64 weak var weakSelf = self @@ -154,81 +163,86 @@ public class PersonInfoViewModel: NSObject { return model } - func updateAvatar(avatar: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.avatar.rawValue): avatar] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户头像 + /// - Parameter avatar: 头像地址 + /// - Parameter completion: 更新结果回调 + func updateSelfAvatar(_ avatar: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.avatar = avatar + contactRepo.updateSelfUserInfo(parameter) { error in + completion(error) } } - func updateSex(sex: NIMUserGender, _ completion: @escaping (NSError?) -> Void) { - let changeValue = - [NSNumber(value: NIMUserInfoUpdateTag.gender.rawValue): NSNumber(value: sex.rawValue)] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户性别 + /// - Parameter gender: 用户性别 + /// - Parameter completion: 更新结果回调 + func updateSelfSex(_ gender: V2NIMGender, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.gender = gender + contactRepo.updateSelfUserInfo(parameter) { error in + completion(error) } } - func updateBirthday(birthDay: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.birth.rawValue): birthDay] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户生日 + /// - Parameter birthDay: 生日 + /// - Parameter completion: 更新结果回调 + func updateSelfBirthday(_ birthDay: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.birthday = birthDay + contactRepo.updateSelfUserInfo(parameter) { error in + completion(error) } } - func updateNickName(name: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.nick.rawValue): name] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户昵称 + /// - Parameter nickName: 昵称 + /// - Parameter completion: 更新结果回调 + func updateSelfNickName(_ nickName: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.name = nickName + + // 如果昵称为空(不设置昵称),则使用账号作为昵称 + if nickName.isEmpty { + parameter.name = IMKitClient.instance.account() + } + + contactRepo.updateSelfUserInfo(parameter) { error in + completion(error) } } - func updateMobile(mobile: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.mobile.rawValue): mobile] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户电话号码 + /// - Parameter mobile: 电话号码 + /// - Parameter completion: 更新结果回调 + func updateSelfMobile(_ mobile: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.mobile = mobile + contactRepo.updateSelfUserInfo(parameter) { error in + completion(error) } } - func updateEmail(email: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.email.rawValue): email] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户的邮箱 + /// - Parameter email: 邮箱 + /// - Parameter completion: 完成回调 + func updateSelfEmail(_ email: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.email = email + contactRepo.updateSelfUserInfo(parameter) { error in + completion(error) } } - func updateSign(sign: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.sign.rawValue): sign] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户的签名 + /// - Parameter sign: 签名 + /// - Parameter completion: 完成回调 + func updateSelfSign(_ sign: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.sign = sign + contactRepo.updateSelfUserInfo(parameter) { error in + completion(error) } } } From d2ff081af877bf244b67545a0f067f9d370f88c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=AF=97=E6=96=87?= Date: Mon, 22 Apr 2024 10:48:58 +0800 Subject: [PATCH 2/2] source code of v10.0.0-beta --- NEChatUIKit/NEChatUIKit.podspec | 2 +- .../chat_map_default.imageset/Contents.json | 22 + .../location_default 1.png | Bin 0 -> 35621 bytes .../location_default.png | Bin 0 -> 35621 bytes .../Contents.json | 22 + .../map_placeholder_image@2x.png | Bin 0 -> 4334 bytes .../map_placeholder_image@3x.png | Bin 0 -> 6737 bytes .../merge_message_send@2x.png | Bin 1295 -> 1485 bytes .../merge_message_send@3x.png | Bin 1949 -> 2304 bytes .../Assets/en.lproj/Localizable.strings | 8 +- .../Assets/zh-Hans.lproj/Localizable.strings | 4 +- .../Base/BaseView/ChatCenterTextCell.swift | 2 +- .../Base/BaseView/ChatCornerCell.swift | 1 + .../Base/BaseView/ChatHeaderView.swift | 8 - .../Base/BaseView/ChatImageTextCell.swift | 72 +- .../Base/BaseView/ChatSectionView.swift | 2 +- .../Classes/Base/BaseView/ChatStateCell.swift | 29 +- .../Base/BaseView/ChatTextArrowCell.swift | 2 +- .../Classes/Base/BaseView/ChatTextCell.swift | 5 +- .../Base/BaseView/ChatUnfoldCell.swift | 20 +- .../Base/BaseView/NEChatBaseCell.swift | 4 +- .../Chat/Controller/ChatViewController.swift | 1618 ++++++----- .../MultiForwardViewController.swift | 183 +- .../NEBaseForwardAlertViewController.swift | 258 +- .../NEBasePinMessageViewController.swift | 527 ++-- .../Controller/NEBaseReadViewController.swift | 80 +- .../NEBaseSelectUserViewController.swift | 76 +- .../NEBaseUserSettingViewController.swift | 169 +- .../Chat/Controller/TextViewController.swift | 20 +- .../Classes/Chat/Emoji/EmojiPageView.swift | 4 +- .../Emoji/InputEmoticonContainerView.swift | 18 +- .../Chat/Emoji/InputEmoticonTabView.swift | 23 +- .../Chat/Emoji/NIMInputEmoticonButton.swift | 37 +- .../Chat/Emoji/NIMInputEmoticonManager.swift | 13 +- .../Chat/Helper/ChatDeduplicationHelper.swift | 30 +- .../Chat/Helper/ChatMessageHelper.swift | 361 ++- .../Classes/Chat/Helper/ChatTeamCache.swift | 159 + .../Classes/Chat/Helper/ChatUserCache.swift | 57 + .../Classes/Chat/Helper/MessageUtils.swift | 145 +- .../Helper/NotificationMessageUtils.swift | 337 +-- .../Chat/Helper/ReplyMessageUtil.swift | 2 +- .../Chat/Model/MessageAudioModel.swift | 8 +- .../Chat/Model/MessageCallRecordModel.swift | 44 +- .../Chat/Model/MessageContentModel.swift | 54 +- .../Chat/Model/MessageCustomModel.swift | 14 +- .../Classes/Chat/Model/MessageFileModel.swift | 18 +- .../Chat/Model/MessageImageModel.swift | 20 +- .../Chat/Model/MessageLocationModel.swift | 8 +- .../Classes/Chat/Model/MessageModel.swift | 48 +- .../Chat/Model/MessageRichTextModel.swift | 11 +- .../Classes/Chat/Model/MessageTextModel.swift | 17 +- .../Classes/Chat/Model/MessageTipsModel.swift | 18 +- .../Chat/Model/MessageVideoModel.swift | 15 +- .../Chat/Model/NEPinMessageModel.swift | 93 + .../Chat/Model/PinMessageFileModel.swift | 2 +- .../Classes/Chat/Model/PinMessageModel.swift | 104 - .../View/Cell/NEBaseChatMessageCell.swift | 178 +- .../View/Cell/NEBaseChatMessageTipCell.swift | 10 +- .../View/Cell/NEBaseChatTeamMemberCell.swift | 8 +- .../View/Cell/NEBaseUserSettingCell.swift | 2 +- .../Cell/NEBaseUserSettingSelectCell.swift | 2 +- .../Chat/View/Cell/OperationCell.swift | 2 +- .../PinCell/NEBasePinMessageAudioCell.swift | 2 +- .../Cell/PinCell/NEBasePinMessageCell.swift | 48 +- .../PinCell/NEBasePinMessageDefaultCell.swift | 2 +- .../PinCell/NEBasePinMessageFileCell.swift | 97 +- .../PinCell/NEBasePinMessageImageCell.swift | 4 +- .../NEBasePinMessageLocationCell.swift | 114 +- .../NEBasePinMessageMultiForwardCell.swift | 6 +- .../NEBasePinMessageRichTextCell.swift | 4 +- .../PinCell/NEBasePinMessageTextCell.swift | 5 +- .../PinCell/NEBasePinMessageVideoCell.swift | 38 +- .../View/Cell/UserBaseTableViewCell.swift | 58 +- .../ChatView/ChatActivityIndicatorView.swift | 56 +- .../View/ChatView/ChatBrokenNetworkView.swift | 12 +- .../Chat/View/ChatView/ChatRecordView.swift | 3 +- .../View/ChatView/CirleProgressView.swift | 2 +- .../View/ChatView/MessageOperationView.swift | 3 +- .../View/ChatView/NEBaseChatInputView.swift | 210 +- .../View/ChatView/NEChatMoreActionView.swift | 2 +- .../Chat/View/ChatView/NEInputMoreCell.swift | 32 +- .../ChatView/NEMutilSelectBottomView.swift | 4 +- .../Chat/View/ChatView/ReplyView.swift | 2 +- .../Chat/View/MapView/NEMapAddressCell.swift | 118 - .../View/MapView/NEMapGuideBottomView.swift | 84 - .../Chat/ViewModel/ChatViewModel.swift | 2551 ++++++++--------- .../ViewModel/MultiForwardViewModel.swift | 36 +- .../Chat/ViewModel/P2PChatViewModel.swift | 190 ++ .../Chat/ViewModel/PinMessageViewModel.swift | 243 +- .../Chat/ViewModel/TeamChatViewModel.swift | 233 +- .../Chat/ViewModel/TeamMemberSelectVM.swift | 10 +- .../Chat/ViewModel/UserSettingViewModel.swift | 142 +- .../Classes/ChatRouter/NEBaseChatRouter.swift | 2 +- .../Common/ChatCellConstantValue.swift | 2 +- .../Classes/Common/ChatConstant.swift | 2 +- .../Classes/Common/NEChatUIKitClient.swift | 22 +- .../Classes/Extension/AlertVCExtention.swift | 1 + .../Extension/ChatImageExtension.swift | 65 + .../Extension/ChatStringExtension.swift | 20 +- .../Classes/Extension/NEErrorExtension.swift | 1 + .../FunUI/Cell/FunChatMessageAudioCell.swift | 28 +- .../FunUI/Cell/FunChatMessageBaseCell.swift | 5 +- .../FunUI/Cell/FunChatMessageCallCell.swift | 6 +- .../FunUI/Cell/FunChatMessageFileCell.swift | 119 +- .../FunUI/Cell/FunChatMessageImageCell.swift | 14 +- .../Cell/FunChatMessageLocationCell.swift | 150 +- .../Cell/FunChatMessageMultiForwardCell.swift | 22 +- .../FunUI/Cell/FunChatMessageReplyCell.swift | 23 +- .../FunUI/Cell/FunChatMessageRevokeCell.swift | 17 +- .../Cell/FunChatMessageRichTextCell.swift | 23 +- .../FunUI/Cell/FunChatMessageTextCell.swift | 11 +- .../FunUI/Cell/FunChatMessageVideoCell.swift | 40 +- .../FunUI/Cell/FunUserSettingSelectCell.swift | 4 +- .../FunUI/Cell/FunUserTableViewCell.swift | 16 +- .../Controller/FunChatViewController.swift | 116 +- .../FunForwardAlertViewController.swift | 10 +- .../FunMultiForwardViewController.swift | 18 +- .../Controller/FunP2PChatViewController.swift | 107 +- .../FunPinMessageViewController.swift | 14 +- .../Controller/FunReadViewController.swift | 2 +- .../FunSelectUserViewController.swift | 4 +- ....swift => FunTeamChatViewController.swift} | 115 +- .../FunUserSettingViewController.swift | 98 +- .../Classes/FunUI/FunChatRouter.swift | 16 +- .../Classes/FunUI/View/FunChatInputView.swift | 25 +- .../FunUI/View/FunRecordAudioView.swift | 22 +- .../NormalUI/Cell/ChatMessageAudioCell.swift | 24 +- .../NormalUI/Cell/ChatMessageCallCell.swift | 4 +- .../NormalUI/Cell/ChatMessageFileCell.swift | 105 +- .../NormalUI/Cell/ChatMessageImageCell.swift | 12 +- .../Cell/ChatMessageLocationCell.swift | 145 +- .../Cell/ChatMessageMultiForwardCell.swift | 22 +- .../NormalUI/Cell/ChatMessageReplyCell.swift | 19 +- .../NormalUI/Cell/ChatMessageRevokeCell.swift | 13 +- .../Cell/ChatMessageRichTextCell.swift | 28 +- .../NormalUI/Cell/ChatMessageTextCell.swift | 7 +- .../NormalUI/Cell/ChatMessageVideoCell.swift | 40 +- .../NormalUI/Cell/UserSettingSelectCell.swift | 4 +- .../NormalUI/Cell/UserTableViewCell.swift | 14 +- .../ForwardAlertViewController.swift | 6 +- .../Controller/NormalChatViewController.swift | 13 +- .../NormalMultiForwardViewController.swift | 2 +- .../Controller/P2PChatViewController.swift | 111 +- .../Controller/ReadViewController.swift | 8 +- .../Controller/SelectUserViewController.swift | 4 +- ...ler.swift => TeamChatViewController.swift} | 119 +- .../UserSettingViewController.swift | 8 +- .../Classes/NormalUI/NormalChatRouter.swift | 38 +- NEContactUIKit/NEContactUIKit.podspec | 4 +- .../Assets/en.lproj/Localizable.strings | 4 +- .../Assets/zh-Hans.lproj/Localizable.strings | 4 +- .../Classes/Base/NEBaseContactViewCell.swift | 46 +- .../BlackList/Cell/NEBaseBlackListCell.swift | 35 +- .../NEBaseBlackListViewController.swift | 104 +- .../ViewModel/BlackListViewModel.swift | 135 +- .../Classes/Common/NEBaseContactRouter.swift | 2 +- .../ContactConfig/ContactUIConfig.swift | 1 + .../Extension/ContactImageExtension.swift | 1 + .../Classes/FunUI/Cell/FunBlackListCell.swift | 10 +- .../FunUI/Cell/FunContactSelectedCell.swift | 20 +- .../FunUI/Cell/FunContactTableViewCell.swift | 18 +- .../FunUI/Cell/FunContactUnCheckCell.swift | 10 +- .../Cell/FunSystemNotificationCell.swift | 16 +- .../FunUI/Cell/FunTeamTableViewCell.swift | 10 +- .../Classes/FunUI/FunContactRouter.swift | 6 +- .../FunUI/View/FunUserInfoHeaderView.swift | 18 +- .../FunBlackListViewController.swift | 13 +- .../FunContactRemakNameViewController.swift | 2 +- .../FunContactUserViewController.swift | 8 +- .../FunContactsSelectedViewController.swift | 8 +- .../FunContactsViewController.swift | 11 +- .../FunFindFriendViewController.swift | 54 +- .../FunValidationMessageViewController.swift | 5 +- .../Classes/Model/ContactInfo.swift | 4 +- .../Classes/Model/ContactSection.swift | 2 +- .../Classes/NormalUI/Cell/BlackListCell.swift | 18 +- .../NormalUI/Cell/ContactSelectedCell.swift | 13 +- .../NormalUI/Cell/ContactTableViewCell.swift | 8 +- .../NormalUI/Cell/ContactUnCheckCell.swift | 10 +- .../Cell/SystemNotificationCell.swift | 10 +- .../NormalUI/Cell/TeamTableViewCell.swift | 4 +- .../NormalUI/NromalContactRouter.swift | 6 +- .../NormalUI/View/UserInfoHeaderView.swift | 18 +- .../BlackListViewController.swift | 13 +- .../ContactRemakNameViewController.swift | 2 +- .../ContactUserViewController.swift | 6 +- .../ContactsSelectedViewController.swift | 8 +- .../ContactsViewController.swift | 6 +- .../FindFriendViewController.swift | 2 +- .../ValidationMessageViewController.swift | 5 +- .../Team/Cell/NEBaseTeamTableViewCell.swift | 42 +- .../NEBaseTeamListViewController.swift | 15 +- .../Team/ViewModel/TeamListViewModel.swift | 63 +- .../UserInfo/Views/CenterTextCell.swift | 3 +- .../UserInfo/Views/ContactBaseTextCell.swift | 3 +- .../Views/TextWithDetailTextCell.swift | 3 +- .../Views/TextWithRightArrowCell.swift | 19 +- .../UserInfo/Views/TextWithSwitchCell.swift | 2 +- ...EBaseValidationMessageViewController.swift | 237 +- .../ValidationMessageViewModel.swift | 261 +- .../Views/NEBaseSystemNotificationCell.swift | 203 +- .../Views/NEBaseValidationCell.swift | 95 +- .../ViewModel/ContactUserViewModel.swift | 53 +- .../Classes/ViewModel/ContactViewModel.swift | 344 ++- .../ViewModel/FindFriendViewModel.swift | 16 +- .../Cell/NEBaseContactSelectedCell.swift | 19 +- .../Cell/NEBaseContactTableViewCell.swift | 55 +- .../Views/Cell/NEBaseContactUnCheckCell.swift | 10 +- .../Classes/Views/ContactSectionView.swift | 2 +- ...NEBaseContactRemakNameViewController.swift | 15 +- .../NEBaseContactUserViewController.swift | 227 +- ...NEBaseContactsSelectedViewController.swift | 154 +- .../Views/NEBaseContactsViewController.swift | 254 +- .../NEBaseFindFriendViewController.swift | 105 +- .../Views/NEBaseUserInfoHeaderView.swift | 62 +- .../NEConversationUIKit.podspec | 3 +- .../Assets/en.lproj/Localizable.strings | 1 - .../Assets/zh-Hans.lproj/Localizable.strings | 1 - .../ConversationDeduplicationHelper.swift | 14 +- .../Classes/Common/ConversationUI.swift | 2 +- .../Cell/NEBaseConversationListCell.swift | 159 +- .../Cell/NEBaseConversationSearchCell.swift | 38 +- .../NEBaseConversationController.swift | 721 ++--- .../NEBaseConversationSearchController.swift | 203 +- .../Controller/NEBasePopListView.swift | 7 +- .../ConversationSearchViewModel.swift | 193 +- .../ViewModel/ConversationViewModel.swift | 821 +++--- .../ConversationUIConfig.swift | 6 +- .../FunUI/Cell/FunConversationListCell.swift | 63 +- .../Cell/FunConversationSearchCell.swift | 20 +- .../FunConversationController.swift | 27 +- .../FunConversationSearchController.swift | 25 +- .../Classes/Manager/NEAtMessageManager.swift | 350 ++- .../NormalUI/Cell/ConversationListCell.swift | 45 +- .../Controller/ConversationController.swift | 31 +- .../ConversationSearchController.swift | 8 +- .../Classes/Util/NEMessageUtil.swift | 70 +- NEMapKit/NEMapKit.podspec | 2 +- .../NEMapKit.xcassets/Map/Contents.json | 6 + .../chat_loacaiton_img.imageset/Contents.json | 22 + .../chat_loacaiton_img@2x.png | Bin 0 -> 1069 bytes .../chat_loacaiton_img@3x.png | Bin 0 -> 1603 bytes .../Map/chat_map_back.imageset/Contents.json | 22 + .../chat_map_back@2x.png | Bin 0 -> 668 bytes .../chat_map_back@3x.png | Bin 0 -> 966 bytes .../Map/chat_map_empty.imageset/Contents.json | 22 + .../chat_map_empty.imageset/map_empty@2x.png | Bin 0 -> 7683 bytes .../chat_map_empty.imageset/map_empty@3x.png | Bin 0 -> 11406 bytes .../Map/chat_map_path.imageset/Contents.json | 22 + .../chat_map_path@2x.png | Bin 0 -> 1701 bytes .../chat_map_path@3x.png | Bin 0 -> 2558 bytes .../chat_map_select.imageset/Contents.json | 22 + .../chat_map_select@2x.png | Bin 0 -> 433 bytes .../chat_map_select@3x.png | Bin 0 -> 565 bytes .../location_point.imageset}/Contents.json | 0 .../location_point@2x.png | Bin .../location_point@3x.png | Bin .../map_reset_normal.imageset/Contents.json | 22 + .../map_select_normal@2x.png | Bin 0 -> 18976 bytes .../map_select_normal@3x.png | Bin 0 -> 40017 bytes .../map_reset_select.imageset/Contents.json | 22 + .../map_reset_select@2x.png | Bin 0 -> 18912 bytes .../map_reset_select@3x.png | Bin 0 -> 39874 bytes .../Assets/en.lproj/Localizable.strings | 9 + .../Assets/zh-Hans.lproj/Localizable.strings | 10 + .../Controller/NELocationViewController.swift | 551 ++-- NEMapKit/NEMapKit/Classes/NELocaitonModel.h | 21 + NEMapKit/NEMapKit/Classes/NELocaitonModel.m | 9 + NEMapKit/NEMapKit/Classes/NEMapClient.h | 58 +- NEMapKit/NEMapKit/Classes/NEMapClient.m | 71 +- NEMapKit/NEMapKit/Classes/NEMapConstant.swift | 37 + .../Classes/View/NELocationAddressCell.swift | 123 + .../View/NELocationGuideBottomView.swift | 86 + NERtcCallUIKit/NERtcCallUIKit.podspec | 6 +- .../Assets/avchat_connecting_en.mp3 | Bin 0 -> 13968 bytes .../Assets/avchat_no_response_en.mp3 | Bin 0 -> 33840 bytes .../Assets/avchat_peer_busy_en.mp3 | Bin 0 -> 25920 bytes .../Assets/avchat_peer_reject_en.mp3 | Bin 0 -> 27216 bytes .../NERtcCallUIKit/Assets/avchat_ring_en.mp3 | Bin 0 -> 28569 bytes .../Assets/en.lproj/Localizable.strings | 40 +- .../Assets/zh-Hans.lproj/Localizable.strings | 18 +- .../Controller/NECallUIStateController.h | 2 - .../Controller/NECallUIStateController.m | 39 +- .../Classes/Controller/NECallViewController.m | 120 +- .../Controller/NECalledViewController.m | 3 +- .../NERtcCallUIKit/Classes/Model/NERingFile.h | 5 +- .../NERtcCallUIKit/Classes/Model/NERingFile.m | 46 +- .../Classes/Model/NEUICallParam.h | 3 - .../NERtcCallUIKit/Classes/NECallKitUtil.h | 7 +- .../NERtcCallUIKit/Classes/NECallKitUtil.m | 30 + .../Classes/NECallUIKitConfig.h | 13 + .../NERtcCallUIKit/Classes/NERtcCallUIKit.m | 21 +- NETeamUIKit/NETeamUIKit.podspec | 2 +- .../Assets/en.lproj/Localizable.strings | 1 - .../Assets/zh-Hans.lproj/Localizable.strings | 1 - .../FunUI/Cell/FunHistoryMessageCell.swift | 35 +- .../FunUI/Cell/FunTeamArrowSettingCell.swift | 4 +- .../FunUI/Cell/FunTeamDefaultIconCell.swift | 16 +- .../FunUI/Cell/FunTeamManagerMemberCell.swift | 9 - .../FunUI/Cell/FunTeamSettingHeaderCell.swift | 6 +- .../Cell/FunTeamSettingLabelArrowCell.swift | 4 +- .../FunUI/Cell/FunTeamSettingSelectCell.swift | 6 +- .../Classes/FunUI/Cell/FunTeamUserCell.swift | 12 +- .../FunTeamAvatarViewController.swift | 49 +- .../FunTeamHistoryMessageController.swift | 8 +- .../FunTeamInfoViewController.swift | 16 +- ...r.swift => FunTeamManagerController.swift} | 31 +- .../FunTeamManagerListController.swift | 7 +- .../FunTeamMemberSelectController.swift | 2 +- .../Controller/FunTeamMembersController.swift | 22 +- .../FunTeamNameViewController.swift | 16 +- .../FunTeamSettingViewController.swift | 277 +- .../Classes/FunUI/FunTeamRouter.swift | 10 +- .../Classes/NEBaseTeamRouter.swift | 51 +- .../NormalUI/Cell/HistoryMessageCell.swift | 23 +- .../NormalUI/Cell/TeamArrowSettingCell.swift | 4 +- .../NormalUI/Cell/TeamDefaultIconCell.swift | 16 +- .../NormalUI/Cell/TeamSettingHeaderCell.swift | 6 +- .../Cell/TeamSettingLabelArrowCell.swift | 4 +- .../NormalUI/Cell/TeamSettingSelectCell.swift | 6 +- .../Classes/NormalUI/Cell/TeamUserCell.swift | 12 +- .../Controller/TeamAvatarViewController.swift | 48 +- .../TeamHistoryMessageController.swift | 8 +- .../Controller/TeamInfoViewController.swift | 16 +- .../Controller/TeamManageController.swift | 55 - .../Controller/TeamManagerController.swift | 34 + .../TeamManagerListController.swift | 4 +- .../TeamMemberSelectController.swift | 2 +- .../Controller/TeamMembersController.swift | 14 +- .../Controller/TeamNameViewController.swift | 8 +- .../TeamSettingViewController.swift | 282 +- .../Classes/NormalUI/NormalTeamRouter.swift | 10 +- .../Setting/Common/NETeamMemberCache.swift | 221 ++ .../Setting/Model/NESelectTeamMember.swift | 2 +- .../Setting/Model/SettingSectionModel.swift | 2 +- .../NEBaseTeamAvatarViewController.swift | 263 ++ .../NEBaseTeamHistoryMessageController.swift | 132 +- .../NEBaseTeamInfoViewController.swift | 96 + .../NEBaseTeamIntroduceViewController.swift | 98 +- .../Setting/NEBaseTeamManageController.swift | 386 --- .../Setting/NEBaseTeamManagerController.swift | 411 +++ .../NEBaseTeamManagerListController.swift | 76 +- .../NEBaseTeamMemberSelectController.swift | 187 +- .../Setting/NEBaseTeamMembersController.swift | 233 +- .../NEBaseTeamNameViewController.swift | 153 +- .../NEBaseTeamSettingViewController.swift | 422 +-- .../View/NEBaseHistoryMessageCell.swift | 114 +- .../View/NEBaseTeamArrowSettingCell.swift | 2 +- .../View/NEBaseTeamAvatarViewController.swift | 237 -- .../View/NEBaseTeamDefaultIconCell.swift | 42 +- .../View/NEBaseTeamInfoViewController.swift | 84 - .../Setting/View/NEBaseTeamMemberCell.swift | 39 +- .../View/NEBaseTeamMemberSelectCell.swift | 22 +- .../Setting/View/NEBaseTeamSettingCell.swift | 3 +- .../View/NEBaseTeamSettingHeaderCell.swift | 4 +- .../View/NEBaseTeamSettingSelectCell.swift | 6 +- .../Setting/View/NEBaseTeamUserCell.swift | 48 +- .../View/TeamSettingRightCustomCell.swift | 12 +- .../View/TeamSettingSubtitleCell.swift | 33 +- .../ViewModel/TeamAvatarViewModel.swift | 49 +- .../TeamHistoryMessageViewModel.swift | 224 ++ .../Setting/ViewModel/TeamInfoViewModel.swift | 43 +- .../ViewModel/TeamIntroduceViewModel.swift | 28 +- .../ViewModel/TeamManageViewModel.swift | 230 -- .../ViewModel/TeamManagerListViewModel.swift | 284 +- .../ViewModel/TeamManagerViewModel.swift | 358 +++ .../ViewModel/TeamMemberSelectViewModel.swift | 58 +- .../ViewModel/TeamMembersViewModel.swift | 331 ++- .../Setting/ViewModel/TeamNameViewModel.swift | 15 +- .../ViewModel/TeamSettingViewModel.swift | 643 +++-- .../NETeamUIKit/Classes/TeamConstant.swift | 3 +- 371 files changed, 14546 insertions(+), 10618 deletions(-) create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/location_default 1.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/location_default.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/map_placeholder_image@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/map_placeholder_image@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatTeamCache.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatUserCache.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEPinMessageModel.swift delete mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift delete mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapAddressCell.swift delete mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapGuideBottomView.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/P2PChatViewModel.swift rename NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/{FunGroupChatViewController.swift => FunTeamChatViewController.swift} (62%) rename NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/{GroupChatViewController.swift => TeamChatViewController.swift} (61%) create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/Contents.json create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/Contents.json create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/chat_loacaiton_img@2x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/chat_loacaiton_img@3x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/Contents.json create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/chat_map_back@2x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/chat_map_back@3x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/Contents.json create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/map_empty@2x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/map_empty@3x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/Contents.json create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@2x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@3x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/Contents.json create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/chat_map_select@2x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/chat_map_select@3x.png rename NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/{ne_location.imageset => Map/location_point.imageset}/Contents.json (100%) rename NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/{ne_location.imageset => Map/location_point.imageset}/location_point@2x.png (100%) rename NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/{ne_location.imageset => Map/location_point.imageset}/location_point@3x.png (100%) create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/Contents.json create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/map_select_normal@2x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/map_select_normal@3x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/Contents.json create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@2x.png create mode 100644 NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@3x.png create mode 100644 NEMapKit/NEMapKit/Assets/en.lproj/Localizable.strings create mode 100644 NEMapKit/NEMapKit/Assets/zh-Hans.lproj/Localizable.strings rename NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift => NEMapKit/NEMapKit/Classes/Controller/NELocationViewController.swift (59%) create mode 100644 NEMapKit/NEMapKit/Classes/NELocaitonModel.h create mode 100644 NEMapKit/NEMapKit/Classes/NELocaitonModel.m create mode 100644 NEMapKit/NEMapKit/Classes/NEMapConstant.swift create mode 100644 NEMapKit/NEMapKit/Classes/View/NELocationAddressCell.swift create mode 100644 NEMapKit/NEMapKit/Classes/View/NELocationGuideBottomView.swift create mode 100644 NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_connecting_en.mp3 create mode 100644 NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_no_response_en.mp3 create mode 100644 NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_busy_en.mp3 create mode 100644 NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_reject_en.mp3 create mode 100644 NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_ring_en.mp3 rename NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/{FunTeamManageController.swift => FunTeamManagerController.swift} (51%) delete mode 100644 NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManageController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/Common/NETeamMemberCache.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamAvatarViewController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamInfoViewController.swift rename NETeamUIKit/NETeamUIKit/Classes/Setting/{View => }/NEBaseTeamIntroduceViewController.swift (58%) delete mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManageController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerController.swift rename NETeamUIKit/NETeamUIKit/Classes/Setting/{View => }/NEBaseTeamNameViewController.swift (58%) rename NETeamUIKit/NETeamUIKit/Classes/Setting/{View => }/NEBaseTeamSettingViewController.swift (58%) delete mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamAvatarViewController.swift delete mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamInfoViewController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamHistoryMessageViewModel.swift delete mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManageViewModel.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerViewModel.swift diff --git a/NEChatUIKit/NEChatUIKit.podspec b/NEChatUIKit/NEChatUIKit.podspec index 34d6e4d9..99d70e98 100644 --- a/NEChatUIKit/NEChatUIKit.podspec +++ b/NEChatUIKit/NEChatUIKit.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = 'NEChatUIKit' - s.version = '9.7.0' + s.version = '10.0.0-beta' s.summary = 'Chat Module of IM.' # This description is used to generate tags and improve search results. diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/Contents.json new file mode 100644 index 00000000..6cca0ae0 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_default 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_default.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/location_default 1.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/location_default 1.png new file mode 100644 index 0000000000000000000000000000000000000000..3a8d7cb7c9df0960b96f73a2523dcce1108c6127 GIT binary patch literal 35621 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

;AY$ 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

;AY$ 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 literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/map_placeholder_image@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/map_placeholder_image@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..2543b28afad20b2eb2b70ec598d05429bfd8c038 GIT binary patch literal 6737 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 literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@2x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@2x.png index a1be3fd4bcaa3e6fafe3aa5f89775e42751c704f..5dddc8a01ec462a191f48cf6ce1510074425084d 100644 GIT binary patch literal 1485 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 X(=Ba&?};Qse*ySFVnDP1*}Q)Nkd;wh diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@3x.png index 7b4b6a0c99b12eefb9e9bb6f2ec970066824c461..d877bc03e0ef3cdcf165215716a658aeec8da362 100644 GIT binary patch literal 2304 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 Ac>n+a diff --git a/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings b/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings index fcfad433..23d06c40 100644 --- a/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings +++ b/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings @@ -17,7 +17,6 @@ "record_too_short"="Time is too short"; "no_microphone_permission"="no micro permission"; "choose"="select"; -"take_photo"="camera"; "select_from_album"="album"; "select_from_icloud"="icloud"; "editing"="typing"; @@ -51,9 +50,10 @@ "team_join_mode" = "join mode"; "team_be_invited_mode" = "invite mode"; "team_be_invited_permission" = "Invite Permission"; -"team_be_invited_author" = "是否需要被邀请者同意权限"; -"team_update_info_permission" = "update info permission"; +"team_be_invited_author" = "BeInvite Permission"; +"team_update_info_permission" = "Update info permission"; "team_update_client_custom"="Update Permission"; +"team_at_permission"="@All Permission update to "; "team_custom_info" = "custom info"; "not_mute" = "unmute"; @@ -169,6 +169,7 @@ "failed_operation"="Failed Operation"; "team_not_exist"="team not exist"; "unpin_failed"="Unpin failed"; +"pin_limit_exceeded"="The number of pin has reached the upper limit"; "fun_hold_to_talk"="Hold to talk"; @@ -194,6 +195,7 @@ "multiForward_open_failed"="Failed to obtain information"; "file_check_failed"="File verification failed"; "per_item_forward_limit"="Forward one by one to limit %d messages"; +"selete_messages_limit"="Delete limits %d messages"; "multiForward_forward_limit"="Combined forwarding limits %d messages"; "exception_description"="Exception description"; diff --git a/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings index 0d699799..d5f6c183 100644 --- a/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -17,7 +17,6 @@ "record_too_short"="录音时间太短"; "no_microphone_permission"="没有麦克风权限"; "choose"="请选择"; -"take_photo"="拍照"; "select_from_album"="从相册选择"; "select_from_icloud"="从iCloud选择"; "editing"="对方正在输入中..."; @@ -54,6 +53,7 @@ "team_be_invited_author" = "是否需要被邀请者同意权限"; "team_update_info_permission" = "群资料修改权限"; "team_update_client_custom"="更新客户端自定义字段的权限"; +"team_at_permission"="@所有人权限更新为"; "team_custom_info" = "自定义扩展字段"; "not_mute" = "解除禁言"; @@ -167,6 +167,7 @@ "failed_operation"="操作失败"; "team_not_exist"="群组不存在"; "unpin_failed"="取消标记失败"; +"pin_limit_exceeded"="已超出 pin 数量上限"; "fun_hold_to_talk"="按住 说话"; @@ -192,6 +193,7 @@ "multiForward_open_failed"="信息获取失败"; "file_check_failed"="文件校验失败"; "per_item_forward_limit"="逐条转发限制%d条消息"; +"selete_messages_limit"="批量删除限制%d条消息"; "multiForward_forward_limit"="合并转发限制%d条消息"; "exception_description"="异常说明"; diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCenterTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCenterTextCell.swift index f6b587af..6da85cc3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCenterTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCenterTextCell.swift @@ -36,6 +36,6 @@ open class ChatCenterTextCell: ChatCornerCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCornerCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCornerCell.swift index f238c1c0..9b29aa67 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCornerCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCornerCell.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import NECommonUIKit + // this cell has rounding corner style import UIKit diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatHeaderView.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatHeaderView.swift index 4f52d551..10b02a91 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatHeaderView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatHeaderView.swift @@ -15,14 +15,6 @@ open class ChatHeaderView: UIView { return label }() - /* - // Only override draw() if you perform custom drawing. - // An empty implementation adversely affects performance during animation. - override func draw(_ rect: CGRect) { - // Drawing code - } - */ - override init(frame: CGRect) { super.init(frame: frame) setupUI() diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift index 2ce65364..f146518b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift @@ -7,10 +7,44 @@ import UIKit @objcMembers open class ChatImageTextCell: ChatStateCell { + public lazy var avatarImageView: UIImageView = { + let avatarView = UIImageView() + avatarView.translatesAutoresizingMaskIntoConstraints = false + avatarView.clipsToBounds = true + avatarView.backgroundColor = .ne_defautAvatarColor + return avatarView + }() + + public lazy var shortNameLabel: UILabel = { + let nameLabel = UILabel() + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.textColor = .white + nameLabel.textAlignment = .center + nameLabel.font = UIFont.systemFont(ofSize: 14.0) + return nameLabel + }() + + public lazy var nameLabel: UILabel = { + let label = UILabel() + label.textAlignment = .left + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 14.0) + label.textColor = .ne_darkText + return label + }() + var circleView = UIImageView() + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - // circle view + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupUI() { + super.setupUI() circleView.translatesAutoresizingMaskIntoConstraints = false circleView.layer.cornerRadius = 16 circleView.clipsToBounds = true @@ -22,7 +56,7 @@ open class ChatImageTextCell: ChatStateCell { circleView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 40), circleView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) -// short name label + // short name label contentView.addSubview(shortNameLabel) NSLayoutConstraint.activate([ shortNameLabel.widthAnchor.constraint(equalTo: circleView.widthAnchor), @@ -30,14 +64,14 @@ open class ChatImageTextCell: ChatStateCell { shortNameLabel.leftAnchor.constraint(equalTo: circleView.leftAnchor), shortNameLabel.topAnchor.constraint(equalTo: circleView.topAnchor), ]) -// name label + // name label contentView.addSubview(nameLabel) NSLayoutConstraint.activate([ nameLabel.leftAnchor.constraint(equalTo: circleView.rightAnchor, constant: 12), nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor), nameLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) -// line + // line let line = UIView() line.backgroundColor = .ne_greyLine line.translatesAutoresizingMaskIntoConstraints = false @@ -50,36 +84,6 @@ open class ChatImageTextCell: ChatStateCell { ]) } - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public lazy var avatarImage: UIImageView = { - let avatar = UIImageView() - avatar.translatesAutoresizingMaskIntoConstraints = false - avatar.clipsToBounds = true - avatar.backgroundColor = .ne_defautAvatarColor - return avatar - }() - - public lazy var shortNameLabel: UILabel = { - let name = UILabel() - name.translatesAutoresizingMaskIntoConstraints = false - name.textColor = .white - name.textAlignment = .center - name.font = UIFont.systemFont(ofSize: 14.0) - return name - }() - - public lazy var nameLabel: UILabel = { - let label = UILabel() - label.textAlignment = .left - label.translatesAutoresizingMaskIntoConstraints = false - label.font = UIFont.systemFont(ofSize: 14.0) - label.textColor = .ne_darkText - return label - }() - open func setup(accid: String?, nickName: String?) { let name = nickName?.count ?? 0 > 0 ? nickName : accid nameLabel.text = name diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift index e0998732..5ac4eac6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift @@ -14,7 +14,7 @@ open class ChatSectionView: UITableViewHeaderFooterView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift index 84f90b78..60ae2245 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift @@ -15,7 +15,7 @@ public enum RightStyle: Int { @objcMembers open class ChatStateCell: ChatCornerCell { private var style: RightStyle = .none - public var rightImage = UIImageView() + public var rightImageView = UIImageView() var rightImageMargin: NSLayoutConstraint? public var rightStyle: RightStyle { get { @@ -25,33 +25,38 @@ open class ChatStateCell: ChatCornerCell { style = newValue switch style { case .none: - rightImage.image = nil + rightImageView.image = nil case .indicate: - rightImage.image = UIImage.ne_imageNamed(name: "arrowRight") + rightImageView.image = UIImage.ne_imageNamed(name: "arrowRight") case .delete: - rightImage.image = UIImage.ne_imageNamed(name: "delete") + rightImageView.image = UIImage.ne_imageNamed(name: "delete") } } } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - rightImage.contentMode = .center - rightImage.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(rightImage) - rightImageMargin = rightImage.rightAnchor.constraint( + setupUI() + } + + /// UI 初始化 + open func setupUI() { + rightImageView.contentMode = .center + rightImageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(rightImageView) + rightImageMargin = rightImageView.rightAnchor.constraint( equalTo: contentView.rightAnchor, constant: -36 ) rightImageMargin?.isActive = true NSLayoutConstraint.activate([ - rightImage.widthAnchor.constraint(equalToConstant: 20), - rightImage.heightAnchor.constraint(equalToConstant: 20), - rightImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + rightImageView.widthAnchor.constraint(equalToConstant: 20), + rightImageView.heightAnchor.constraint(equalToConstant: 20), + rightImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift index 1acc67a0..d76003c2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift @@ -13,6 +13,6 @@ open class ChatTextArrowCell: ChatTextCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift index d8fa170d..f602321b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift @@ -17,7 +17,10 @@ open class ChatTextCell: ChatStateCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) + } + override open func setupUI() { + super.setupUI() titleLabel.font = UIFont.systemFont(ofSize: 16) titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.textColor = .ne_darkText @@ -64,6 +67,6 @@ open class ChatTextCell: ChatStateCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift index e9b21bc2..39dfcac2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift @@ -7,11 +7,11 @@ import UIKit @objcMembers open class ChatUnfoldCell: ChatCornerCell { - lazy var arrowImage: UIImageView = { - let arrow = UIImageView() - arrow.translatesAutoresizingMaskIntoConstraints = false - arrow.image = UIImage.ne_imageNamed(name: "arrowDown") - return arrow + lazy var arrowImageView: UIImageView = { + let arrowImageView = UIImageView() + arrowImageView.translatesAutoresizingMaskIntoConstraints = false + arrowImageView.image = UIImage.ne_imageNamed(name: "arrowDown") + return arrowImageView }() lazy var contentLabel: UILabel = { @@ -38,18 +38,18 @@ open class ChatUnfoldCell: ChatCornerCell { contentLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), ]) - contentView.addSubview(arrowImage) + contentView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - arrowImage.leftAnchor.constraint(equalTo: contentLabel.rightAnchor, constant: 5), - arrowImage.centerYAnchor.constraint(equalTo: contentLabel.centerYAnchor), + arrowImageView.leftAnchor.constraint(equalTo: contentLabel.rightAnchor, constant: 5), + arrowImageView.centerYAnchor.constraint(equalTo: contentLabel.centerYAnchor), ]) } func changeToArrowUp() { - arrowImage.image = UIImage.ne_imageNamed(name: "arrowUp") + arrowImageView.image = UIImage.ne_imageNamed(name: "arrowUp") } func changeToArrowDown() { - arrowImage.image = UIImage.ne_imageNamed(name: "arrowDown") + arrowImageView.image = UIImage.ne_imageNamed(name: "arrowDown") } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift index 42f8e49d..6ae1366d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift @@ -14,10 +14,10 @@ open class NEChatBaseCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } - open func uploadProgress(byRight: Bool, _ progress: Float) { + open func uploadProgress(byRight: Bool, _ progress: UInt) { fatalError("override in sub class") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift index bc51c77a..396bb3e6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift @@ -8,7 +8,7 @@ import MJRefresh import NEChatKit import NECommonKit import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import Photos @@ -16,16 +16,12 @@ import UIKit import WebKit @objcMembers -open class ChatViewController: ChatBaseViewController, UINavigationControllerDelegate, - ChatInputViewDelegate, ChatViewModelDelegate, NIMMediaManagerDelegate, - MessageOperationViewDelegate, UITableViewDataSource, - UITableViewDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate, CLLocationManagerDelegate, UITextViewDelegate, ChatInputMultilineDelegate { - private let tag = "ChatViewController" +open class ChatViewController: ChatBaseViewController, UINavigationControllerDelegate, UITableViewDataSource, UITableViewDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate, NIMMediaManagerDelegate, CLLocationManagerDelegate, UITextViewDelegate, ChatInputViewDelegate, ChatInputMultilineDelegate, ChatViewModelDelegate, MessageOperationViewDelegate, NEContactListener { private let kCallKitDismissNoti = "kCallKitDismissNoti" private let kCallKitShowNoti = "kCallKitShowNoti" public var titleContent = "" - public var viewmodel: ChatViewModel + public var viewModel: ChatViewModel = .init() let interactionController = UIDocumentInteractionController() private lazy var manager = CLLocationManager() private var playingCell: ChatAudioCellProtocol? @@ -35,10 +31,12 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel public var isCurrentPage = true public var isMute = false // 是否禁言 private var isMutilSelect = false // 是否多选模式 + private var isUploadingData = false // 是否正在加载数据(上拉) + private var uploadHasNoMore = false // 上拉无更多数据 public var operationCellFilter: [OperationType]? // 消息长按菜单全局过滤列表 public var cellRegisterDic = [String: UITableViewCell.Type]() - private var needMarkReadMsgs = [NIMMessage]() + private var needMarkReadMsgs = [V2NIMMessage]() private var atUsers = [NSRange]() var replyView = ReplyView() @@ -76,17 +74,28 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel public var bottomViewTopAnchor: NSLayoutConstraint? public var bottomViewHeightAnchor: NSLayoutConstraint? - public init(session: NIMSession) { - viewmodel = ChatViewModel(session: session, anchor: nil) + public init(conversationId: String) { super.init(nibName: nil, bundle: nil) NEKeyboardManager.shared.enable = false NEKeyboardManager.shared.enableAutoToolbar = false NIMSDK.shared().mediaManager.add(self) + ContactRepo.shared.addContactListener(self) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) + } + + deinit { + NEALog.infoLog(className(), desc: "deinit") + viewModel.clearUnreadCount() + cleanDelegate() + } + + func cleanDelegate() { + NIMSDK.shared().mediaManager.remove(self) + viewModel.delegate = nil } override open func viewWillAppear(_ animated: Bool) { @@ -95,7 +104,6 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel NEKeyboardManager.shared.shouldResignOnTouchOutside = false isCurrentPage = true markNeedReadMsg() - getSessionInfo(session: viewmodel.session) clearAtRemind() NEChatDetectNetworkTool.shareInstance.netWorkReachability { [weak self] status in @@ -111,13 +119,20 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel override open func viewDidLoad() { super.viewDidLoad() - viewmodel.delegate = self + viewModel.delegate = self commonUI() addObseve() weak var weakSelf = self - viewmodel.fetchPinMessage { + getSessionInfo(sessionId: viewModel.sessionId) { weakSelf?.loadData() } + Router.shared.register(NERouterUrl.LocationSearchResult) { result in + if let model = ChatLocaitonModel.yx_model(with: result) { + weakSelf?.viewModel.sendLocationMessage(model: model) { error in + weakSelf?.showErrorToast(error) + } + } + } } override open func viewWillDisappear(_ animated: Bool) { @@ -151,7 +166,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func commonUI() { - title = viewmodel.session.sessionId + title = viewModel.sessionId navigationView.titleBarBottomLine.isHidden = false setMoreButton() setMutilSelectBottomView() @@ -205,11 +220,11 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel forCellReuseIdentifier: "\(NEBaseChatMessageCell.self)" ) - NEChatUIKitClient.instance.getRegisterCustomCell().forEach { (key: String, value: UITableViewCell.Type) in + for (key, value) in NEChatUIKitClient.instance.getRegisterCustomCell() { cellRegisterDic[key] = value } - cellRegisterDic.forEach { (key: String, value: UITableViewCell.Type) in + for (key, value) in cellRegisterDic { tableView.register(value, forCellReuseIdentifier: key) } @@ -222,7 +237,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // MARK: 子类可重写方法 - public func onTeamMemberChange(team: NIMTeam) {} + public func onTeamMemberChange(team: V2NIMTeam) {} override open func backEvent() { super.backEvent() @@ -230,12 +245,20 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } // load data的时候会调用 - open func getSessionInfo(session: NIMSession) {} + open func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { + if viewModel.getShowName(IMKitClient.instance.account()).user == nil { + ContactRepo.shared.getMyUserInfo { _ in + completion() + } + } else { + completion() + } + } /// 点击头像回调 /// - Parameter model: cell模型 open func didTapHeadPortrait(model: MessageContentModel?) { - if let isOut = model?.message?.isOutgoingMsg, isOut { + if let isOut = model?.message?.isSelf, isOut { Router.shared.use( MeSettingRouter, parameters: ["nav": navigationController as Any], @@ -243,7 +266,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ) return } - if let uid = model?.message?.from { + if let uid = model?.message?.senderId { Router.shared.use( ContactUserInfoPageRouter, parameters: ["nav": navigationController as Any, "uid": uid], @@ -278,7 +301,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel operationView?.removeFromSuperview() // get operations - guard let items = viewmodel.avalibleOperationsForMessage(model) else { + guard let items = viewModel.avalibleOperationsForMessage(model) else { return } @@ -300,7 +323,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // 供用户自定义 items setOperationItems(items: &filterItems, model: model) - viewmodel.operationModel = model + viewModel.operationModel = model guard let index = tableView.indexPath(for: cell) else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: DispatchWorkItem(block: { [self] in // size @@ -319,7 +342,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } var frameX = 0.0 if let msg = model?.message, - msg.isOutgoingMsg { + msg.isSelf { frameX = kScreenWidth - w } var frame = CGRect(x: frameX, y: operationY, width: w, height: h) @@ -378,19 +401,19 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel }() public lazy var contentView: UIView = { - let content = UIView() - content.translatesAutoresizingMaskIntoConstraints = false - content.backgroundColor = UIColor.clear - content.addSubview(tableView) + let contentView = UIView() + contentView.translatesAutoresizingMaskIntoConstraints = false + contentView.backgroundColor = UIColor.clear + contentView.addSubview(tableView) NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: content.topAnchor), - tableView.leftAnchor.constraint(equalTo: content.leftAnchor), - tableView.rightAnchor.constraint(equalTo: content.rightAnchor), - tableView.bottomAnchor.constraint(equalTo: content.bottomAnchor), + tableView.topAnchor.constraint(equalTo: contentView.topAnchor), + tableView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + tableView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + tableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) - return content + return contentView }() public lazy var tableView: UITableView = { @@ -513,20 +536,10 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ) } - deinit { - NELog.infoLog(className(), desc: "deinit") - cleanDelegate() - } - - func cleanDelegate() { - NIMSDK.shared().mediaManager.remove(self) - viewmodel.delegate = nil - } - // MARK: objc 方法 func getUserSettingViewController() -> NEBaseUserSettingViewController { - UserSettingViewController(userId: viewmodel.session.sessionId) + UserSettingViewController(userId: viewModel.sessionId) } /// 设置按钮点击事件 @@ -542,14 +555,14 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return } - if viewmodel.session.sessionType == .team { + if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) == .CONVERSATION_TYPE_TEAM { Router.shared.use( TeamSettingViewRouter, parameters: ["nav": navigationController as Any, - "teamid": viewmodel.session.sessionId], + "teamid": viewModel.sessionId as Any], closure: nil ) - } else if viewmodel.session.sessionType == .P2P { + } else if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) == .CONVERSATION_TYPE_P2P { let userSetting = getUserSettingViewController() navigationController?.pushViewController(userSetting, animated: true) } @@ -569,7 +582,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel chatInputView.textView.resignFirstResponder() chatInputView.titleField.resignFirstResponder() } else { - layoutInputView(offset: 0) + if tap.location(in: view).y < kScreenHeight - bottomExanpndHeight { + layoutInputView(offset: 0) + } } } } @@ -579,30 +594,31 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel func loadData() { weak var weakSelf = self - viewmodel.queryRoamMsgHasMoreTime_v2 { error, historyEnd, newEnd, index in - NELog.infoLog( - ModuleName + " " + self.tag, - desc: #function + "CALLBACK queryRoamMsgHasMoreTime_v2 " + (error?.localizedDescription ?? "no error") + // 多端登录清空未读数 + viewModel.clearUnreadCount() + + viewModel.loadData { error, historyEnd, newEnd, index in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK loadData " + (error?.localizedDescription ?? "no error") ) - if let ms = weakSelf?.viewmodel.messages, ms.count > 0 { - weakSelf?.didRefreshTable() - if weakSelf?.viewmodel.isHistoryChat == true { + if let ms = weakSelf?.viewModel.messages, ms.count > 0 { + weakSelf?.tableViewReload() + if weakSelf?.viewModel.isHistoryChat == true, + let num = weakSelf?.tableView.numberOfRows(inSection: 0), + index < num, index >= 0 { let indexPath = IndexPath(row: index, section: 0) weakSelf?.tableView.scrollToRow(at: indexPath, at: .middle, animated: false) if newEnd > 0 { weakSelf?.addBottomLoadMore() } } else { - if let tempArray = weakSelf?.viewmodel.messages, tempArray.count > 0 { - weakSelf?.tableView.scrollToRow( - at: IndexPath(row: tempArray.count - 1, section: 0), - at: .bottom, - animated: false - ) + if let last = weakSelf?.tableView.numberOfRows(inSection: 0) { + let indexPath = IndexPath(row: last - 1, section: 0) + weakSelf?.tableView.scrollToRow(at: indexPath, at: .bottom, animated: false) } } - } else if let err = error { weakSelf?.showErrorToast(err) } @@ -612,14 +628,14 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel func loadMoreData() { weak var weakSelf = self - viewmodel.dropDownRemoteRefresh { error, count, messages in - NELog.infoLog( - ModuleName + " " + self.tag, + viewModel.dropDownRemoteRefresh { error, count, messages in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK dropDownRemoteRefresh " + (error?.localizedDescription ?? "no error") ) weakSelf?.tableView.reloadData() - if count > 0 { + if count > 0, let num = weakSelf?.tableView.numberOfRows(inSection: 0), count <= num { weakSelf?.tableView.scrollToRow( at: IndexPath(row: count - 1, section: 0), at: .top, @@ -634,38 +650,29 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel func loadCloserToNowData() { weak var weakSelf = self - viewmodel.pullRemoteRefresh { error, count, datas in - NELog.infoLog( - ModuleName + " " + self.tag, + viewModel.pullRemoteRefresh { error, count, datas in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK pullRemoteRefresh " + (error?.localizedDescription ?? "no error") ) if count <= 0 { weakSelf?.removeBottomLoadMore() } else { weakSelf?.tableView.mj_footer?.endRefreshing() - weakSelf?.didRefreshTable() + weakSelf?.tableViewReload() } } } func addObseve() { - NotificationCenter.default.addObserver(self, selector: #selector(didRefreshTable), name: NENotificationName.updateFriendInfo, object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(keyBoardWillShow(_:)), - name: UIResponder.keyboardWillShowNotification, - object: nil) - - NotificationCenter.default.addObserver(self, - selector: #selector(keyBoardWillHide(_:)), - name: UIResponder.keyboardWillHideNotification, - object: nil) - - NotificationCenter.default.addObserver(self, selector: #selector(didShowCallView), name: Notification.Name(kCallKitShowNoti), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(appEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(appEnterForegournd), name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didShowCallView), name: Notification.Name(kCallKitShowNoti), object: nil) + let tap = UITapGestureRecognizer(target: self, action: #selector(viewTap)) tap.delegate = self tap.cancelsTouchesInView = false @@ -677,27 +684,29 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func addBottomLoadMore() { - tableView.mj_footer = MJRefreshBackNormalFooter( + let footer = MJRefreshAutoFooter( refreshingTarget: self, refreshingAction: #selector(loadCloserToNowData) ) + footer.triggerAutomaticallyRefreshPercent = -10 + tableView.mj_footer = footer } open func removeBottomLoadMore() { tableView.mj_footer?.endRefreshingWithNoMoreData() tableView.mj_footer = nil - viewmodel.isHistoryChat = false // 转为普通聊天页面 + viewModel.isHistoryChat = false // 转为普通聊天页面 } func markNeedReadMsg() { if isCurrentPage, needMarkReadMsgs.count > 0 { - viewmodel.markRead(messages: needMarkReadMsgs) { error in - NELog.infoLog( - ModuleName + " " + self.tag, + viewModel.markRead(messages: needMarkReadMsgs) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK markRead " + (error?.localizedDescription ?? "no error") ) } - needMarkReadMsgs = [NIMMessage]() + needMarkReadMsgs = [V2NIMMessage]() } } @@ -735,18 +744,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel animationDuration = keyboardAnimationDuration } - layoutInputViewWithAnimation(offset: keyboardRect.size.height, animationDuration) - weak var weakSelf = self - UIView.animate(withDuration: 0.25, animations: { - weakSelf?.view.layoutIfNeeded() - }) - - // 键盘已经弹出 - if oldKeyboardRect == keyboardRect { - return - } - - scrollTableViewToBottom() + // oldKeyboardRect == keyboardRect 说明键盘已经弹出,无需重复滚动 + layoutInputViewWithAnimation(offset: keyboardRect.size.height, animationDuration, oldKeyboardRect != keyboardRect) } open func keyBoardWillHide(_ notification: Notification) { @@ -764,25 +763,21 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } private func scrollTableViewToBottom() { - NELog.infoLog(className(), desc: "self.viewmodel.messages.count\(viewmodel.messages.count)") - NELog.infoLog(className(), desc: "self.tableView.numberOfRows(inSection: 0)\(tableView.numberOfRows(inSection: 0))") - if viewmodel.messages.count > 0 { - weak var weakSelf = self - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: DispatchWorkItem(block: { - if let row = weakSelf?.tableView.numberOfRows(inSection: 0) { - let indexPath = IndexPath(row: row - 1, section: 0) - weakSelf?.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true) - } - })) + NEALog.infoLog(className(), desc: "self.viewModel.messages.count\(viewModel.messages.count)") + NEALog.infoLog(className(), desc: "self.tableView.numberOfRows(inSection: 0)\(tableView.numberOfRows(inSection: 0))") + let row = tableView.numberOfRows(inSection: 0) + if row > 0 { + let indexPath = IndexPath(row: row - 1, section: 0) + tableView.scrollToRow(at: indexPath, at: .bottom, animated: true) } } - open func layoutInputView(offset: CGFloat) { - layoutInputViewWithAnimation(offset: offset) + open func layoutInputView(offset: CGFloat, _ scrollToBottom: Bool = false) { + layoutInputViewWithAnimation(offset: offset, 0.1, scrollToBottom) } - open func layoutInputViewWithAnimation(offset: CGFloat, _ animation: CGFloat = 0.1) { - NELog.infoLog(className(), desc: "normal height : \(normalInputHeight) normal offset: \(normalOffset) offset : \(offset)") + open func layoutInputViewWithAnimation(offset: CGFloat, _ animation: CGFloat = 0.1, _ scrollToBottom: Bool = false) { + NEALog.infoLog(className(), desc: "normal height : \(normalInputHeight) normal offset: \(normalOffset) offset : \(offset)") weak var weakSelf = self var topValue = normalInputHeight if chatInputView.chatInpuMode != .multipleReturn { @@ -792,9 +787,14 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel chatInputView.contentSubView?.isHidden = true chatInputView.currentButton?.isSelected = false } - UIView.animate(withDuration: animation, animations: { + + UIView.animate(withDuration: animation) { weakSelf?.bottomViewTopAnchor?.constant = -topValue - offset - }) + if scrollToBottom { + weakSelf?.view.layoutIfNeeded() + weakSelf?.scrollTableViewToBottom() + } + } } // MARK: ChatInputViewDelegate @@ -802,7 +802,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func sendText(text: String?, attribute: NSAttributedString?) { if let title = chatInputView.titleField.text, title.trimmingCharacters(in: .whitespaces).isEmpty == false { // 换行消息 - NELog.infoLog(className(), desc: "换行消息: \(title)") + NEALog.infoLog(className(), desc: "换行消息: \(title)") var dataDic = [String: Any]() dataDic["title"] = title if let t = text?.trimmingCharacters(in: .whitespacesAndNewlines), !t.isEmpty { @@ -813,33 +813,29 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel attachDic["type"] = customRichTextType attachDic["data"] = dataDic - let attachment = NECustomAttachment(customType: customRichTextType, data: attachDic) - let remoteExt = chatInputView.getRemoteExtension(attribute) + let rawAttachment = getJSONStringFromDictionary(attachDic) + let customMessage = MessageUtils.customMessage(text: title, rawAttachment: rawAttachment) + if let remoteExt = chatInputView.getRemoteExtension(attribute) { + customMessage.serverExtension = getJSONStringFromDictionary(remoteExt) + } - weak var weakSelf = self - if viewmodel.isReplying, let msg = viewmodel.operationModel?.message { - viewmodel.replyMessageWithoutThread(message: - MessageUtils.customMessage(attachment: attachment, - remoteExt: remoteExt, - apnsContent: title), - target: msg) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), - desc: #function + "CALLBACK replyMessage " + (error?.localizedDescription ?? "no error") - ) - if error != nil { - weakSelf?.showErrorToast(error) - } else { - weakSelf?.closeReply(button: nil) - } - self?.chatInputView.titleField.text = nil - self?.chatInputView.textView.text = nil - self?.didSendFinishAndCheckoutInput() + if viewModel.isReplying, let msg = viewModel.operationModel?.message { + viewModel.replyMessageWithoutThread(message: customMessage, + target: msg) { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK replyMessage " + (error?.localizedDescription ?? "no error") + ) + if error != nil { + self?.showErrorToast(error) } + self?.chatInputView.titleField.text = nil + self?.chatInputView.textView.text = nil + self?.didSendFinishAndCheckoutInput() + } + closeReply(button: nil) } else { - viewmodel.sendCustomMessage(attachment: attachment, - remoteExt: remoteExt, - apnsConstent: title) { [weak self] error in + viewModel.sendMessage(message: customMessage) { [weak self] error in self?.showErrorToast(error) self?.chatInputView.titleField.text = nil self?.chatInputView.textView.text = nil @@ -855,7 +851,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel func sendContentText(text: String?, attribute: NSAttributedString?) { guard let removeSpace = text?.trimmingCharacters(in: .whitespaces), removeSpace.count > 0 else { chatInputView.titleField.text = nil - showToast(chatLocalizable("null_message_not_support")) + view.makeToast(chatLocalizable("null_message_not_support"), position: .center) return } guard let content = text, content.count > 0 else { @@ -863,28 +859,26 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } let remoteExt = chatInputView.getRemoteExtension(attribute) chatInputView.cleartAtCache() - weak var weakSelf = self - if viewmodel.isReplying, let msg = viewmodel.operationModel?.message { - viewmodel.replyMessageWithoutThread(message: MessageUtils.textMessage(text: content, remoteExt: remoteExt), target: msg) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), + + if viewModel.isReplying, let msg = viewModel.operationModel?.message { + viewModel.replyMessageWithoutThread(message: MessageUtils.textMessage(text: content, remoteExt: remoteExt), target: msg) { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK replyMessage " + (error?.localizedDescription ?? "no error") ) if error != nil { - weakSelf?.showErrorToast(error) - } else { - weakSelf?.closeReply(button: nil) + self?.showErrorToast(error) } - weakSelf?.didSendFinishAndCheckoutInput() + self?.didSendFinishAndCheckoutInput() } - + closeReply(button: nil) } else { - viewmodel.sendTextMessage(text: content, remoteExt: remoteExt) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), + viewModel.sendTextMessage(text: content, remoteExt: remoteExt) { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendTextMessage " + (error?.localizedDescription ?? "no error") ) - weakSelf?.showErrorToast(error) + self?.showErrorToast(error) self?.chatInputView.titleField.text = nil self?.chatInputView.textView.text = nil self?.didSendFinishAndCheckoutInput() @@ -944,44 +938,45 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func showRtcCallAction() { - var param = [String: AnyObject]() - param["remoteUserAccid"] = viewmodel.session.sessionId as AnyObject - param["currentUserAccid"] = NIMSDK.shared().loginManager.currentAccount() as AnyObject - param["remoteShowName"] = titleContent as AnyObject - if let user = viewmodel.repo.getUserInfo(userId: viewmodel.session.sessionId), let avatar = user.userInfo?.avatarUrl { - param["remoteAvatar"] = avatar as AnyObject + let sessionId = viewModel.sessionId + + var param = [String: Any]() + param["remoteUserAccid"] = sessionId + param["currentUserAccid"] = IMKitClient.instance.account() + param["remoteShowName"] = titleContent + + if let user = viewModel.getShowName(sessionId).user { + param["remoteAvatar"] = user.user?.avatar } let videoCallAction = UIAlertAction(title: chatLocalizable("video_call"), style: .default) { _ in - param["type"] = NSNumber(integerLiteral: 2) as AnyObject + param["type"] = NSNumber(integerLiteral: 2) Router.shared.use(CallViewRouter, parameters: param) } + let audioCallAction = UIAlertAction(title: chatLocalizable("audio_call"), style: .default) { _ in - param["type"] = NSNumber(integerLiteral: 1) as AnyObject + param["type"] = NSNumber(integerLiteral: 1) Router.shared.use(CallViewRouter, parameters: param) } + let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), style: .cancel) { action in } + showActionSheet([videoCallAction, audioCallAction, cancelAction]) } func didToSearchLocationView() { - let ctrl = NEDetailMapController(type: .search) - navigationController?.pushViewController(ctrl, animated: true) - weak var weakSelf = self - ctrl.completion = { model in - NELog.infoLog(self.className(), desc: "position : \(model.yx_modelToJSONString() ?? "")") - weakSelf?.viewmodel.sendLocationMessage(model) { error in - weakSelf?.showErrorToast(error) - } - } + var params = [String: Any]() + params["type"] = NEMapType.search.rawValue + params["nav"] = navigationController + Router.shared.use(NERouterUrl.LocationVCRouter, parameters: params) } open func textChanged(text: String) -> Bool { if text == "@" { // 做p2p类型判断 - if viewmodel.session.sessionType == .P2P { + if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) == .CONVERSATION_TYPE_P2P { return true } else { DispatchQueue.main.async { @@ -1048,7 +1043,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func textFieldDidEndEditing(_ text: String?) { - viewmodel.sendInputTypingEndState() + checkAndSendTypingState(endEdit: true) } open func textFieldDidBeginEditing(_ text: String?) { @@ -1059,31 +1054,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel checkAndSendTypingState() } - func checkAndSendTypingState() { - if chatInputView.chatInpuMode == .normal { - if let content = chatInputView.textView.text, content.count > 0 { - viewmodel.sendInputTypingState() - } else { - viewmodel.sendInputTypingEndState() - } - } else { - var title = "" - var content = "" - - if let titleText = chatInputView.titleField.text { - title = titleText - } - - if let contentText = chatInputView.textView.text { - content = contentText - } - if title.count <= 0, content.count <= 0 { - viewmodel.sendInputTypingEndState() - } else { - viewmodel.sendInputTypingState() - } - } - } + /// 检查并发送正在输入状态 + /// - Parameter endEdit: 是否停止输入 + open func checkAndSendTypingState(endEdit: Bool = false) {} open func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { textView.typingAttributes = [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)] @@ -1095,26 +1068,23 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel if index == 2 || button?.isSelected == true { if index == 0 { // 语音 - layoutInputView(offset: bottomExanpndHeight) - scrollTableViewToBottom() + layoutInputView(offset: bottomExanpndHeight, true) } else if index == 1 { // emoji - layoutInputView(offset: bottomExanpndHeight) - scrollTableViewToBottom() + layoutInputView(offset: bottomExanpndHeight, true) } else if index == 2 { // 相册 isFile = false goPhotoAlbumWithVideo(self) { [weak self] in if NIMSDK.shared().mediaManager.isPlaying() { NIMSDK.shared().mediaManager.stopPlay() - self?.playingCell?.stopAnimation(byRight: self?.playingModel?.message?.isOutgoingMsg ?? true) + self?.playingCell?.stopAnimation(byRight: self?.playingModel?.message?.isSelf ?? true) self?.playingModel?.isPlaying = false } } } else if index == 3 { // 更多 - layoutInputView(offset: bottomExanpndHeight) - scrollTableViewToBottom() + layoutInputView(offset: bottomExanpndHeight, true) } } else { layoutInputView(offset: 0) @@ -1128,7 +1098,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel preferredStyle: .actionSheet ) alert.modalPresentationStyle = .popover - let camera = UIAlertAction(title: chatLocalizable("take_photo"), style: .default) { action in + let camera = UIAlertAction(title: commonLocalizable("take_picture"), style: .default) { action in self.takePhoto() } let photo = UIAlertAction(title: chatLocalizable("select_from_album"), style: .default) { action in @@ -1154,7 +1124,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel imagePickerVC.delegate = self imagePickerVC.allowsEditing = false imagePickerVC.sourceType = .photoLibrary - present(imagePickerVC, animated: true) {} + present(imagePickerVC, animated: true) } open func takePhoto() { @@ -1162,18 +1132,22 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel imagePickerVC.delegate = self imagePickerVC.allowsEditing = false imagePickerVC.sourceType = .camera - present(imagePickerVC, animated: true) {} + present(imagePickerVC, animated: true) } open func clearAtRemind() { - let sessionId = viewmodel.session.sessionId - let param = ["sessionId": sessionId] + let param = ["sessionId": viewModel.conversationId] Router.shared.use("ClearAtMessageRemind", parameters: param, closure: nil) } open func sendMediaMessage(didFinishPickingMediaWithInfo info: [UIImagePickerController .InfoKey: Any]) { var imageName = "IMG_0001" + var imageWidth: Int32 = 0 + var imageHeight: Int32 = 0 + var videoDuration: Int32 = 0 + + // 获取展示名称 if isFile == true, let imgUrl = info[.referenceURL] as? URL { let fetchRes = PHAsset.fetchAssets(withALAssetURLs: [imgUrl], options: nil) @@ -1183,16 +1157,40 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - if let url = info[.mediaURL] as? URL { - // video - print("image picker video : url", url) + // 获取图片宽高、视频时长 + // phAsset 不一定有 + if #available(iOS 11.0, *) { + if let phAsset = info[.phAsset] as? PHAsset { + imageWidth = Int32(phAsset.pixelWidth) + imageHeight = Int32(phAsset.pixelHeight) + videoDuration = Int32(phAsset.duration * 1000) + } + } + + // video + if let videoUrl = info[.mediaURL] as? URL { + print("image picker video : url", videoUrl) + + // 获取视频宽高、时长 + let asset = AVURLAsset(url: videoUrl) + videoDuration = Int32(asset.duration.seconds * 1000) + + let track = asset.tracks(withMediaType: .video).first + if let track = track { + let size = track.naturalSize + let transform = track.preferredTransform + let correctedSize = size.applying(transform) + imageWidth = Int32(abs(correctedSize.width)) + imageHeight = Int32(abs(correctedSize.height)) + } + weak var weakSelf = self if isFile == true { - copyFileToSend(url: url, displayName: imageName) + copyFileToSend(url: videoUrl, displayName: imageName) } else { - viewmodel.sendVideoMessage(url: url) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ChatViewController"), + viewModel.sendVideoMessage(url: videoUrl, name: imageName, width: imageWidth, height: imageHeight, duration: videoDuration) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendVideoMessage " + (error?.localizedDescription ?? "no error") ) weakSelf?.showErrorToast(error) @@ -1201,79 +1199,104 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return } - guard let image = info[.originalImage] as? UIImage else { - showToast(chatLocalizable("image_is_nil")) - return - } + if #available(iOS 11.0, *) { + var imageUrl = info[.imageURL] as? URL + var image = info[.originalImage] as? UIImage + image = image?.fixOrientation() - if isFile == true, - let imgData = image.pngData() { - let imgSize_MB = Double(imgData.count) / 1e6 - NELog.infoLog(ModuleName + " " + tag, desc: #function + "imgSize_MB: \(imgSize_MB) MB") - if imgSize_MB > NEKitChatConfig.shared.ui.fileSizeLimit { - showToast(String(format: chatLocalizable("fileSize_over_limit"), "\(NEKitChatConfig.shared.ui.fileSizeLimit)")) - } else { - viewmodel.sendFileMessage(data: imgData, displayName: imageName) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), - desc: #function + "CALLBACK sendFileMessage" + (error?.localizedDescription ?? "no error") - ) - if error != nil { - self?.view.makeToast(error!.localizedDescription) + // 获取图片宽度 + if let width = image?.size.width { + imageWidth = Int32(width) + } + + // 获取图片高度度 + if let height = image?.size.height { + imageHeight = Int32(height) + } + + let pngImage = image?.pngData() + var needDelete = false + + // 无url则临时保存到本地,发送成功后删除临时文件 + if imageUrl == nil { + if let data = pngImage, let path = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit/image/") { + let url = URL(fileURLWithPath: path + "\(imageName).png") + do { + try data.write(to: url) + imageUrl = url + needDelete = true + } catch { + showToast(chatLocalizable("image_is_nil")) } } } - } else { - if let url = info[.referenceURL] as? URL { - if url.absoluteString.hasSuffix("ext=GIF") == true { - // GIF 需要特殊处理 - let imageAsset: PHAsset? - if #available(iOS 11.0, *) { - imageAsset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset - } else { - imageAsset = PHAsset.fetchAssets(withALAssetURLs: [url], options: nil).firstObject - } - let options = PHImageRequestOptions() - options.version = .current - guard let asset = imageAsset else { - return + + guard let imageUrl = imageUrl else { + showToast(chatLocalizable("image_is_nil")) + return + } + + if isFile == true { + let imgSize_MB = Double(pngImage?.count ?? 0) / 1e6 + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "imgSize_MB: \(imgSize_MB) MB") + if imgSize_MB > NEKitChatConfig.shared.ui.fileSizeLimit { + showToast(String(format: chatLocalizable("fileSize_over_limit"), "\(NEKitChatConfig.shared.ui.fileSizeLimit)")) + } else { + viewModel.sendFileMessage(filePath: imageUrl.relativePath, displayName: imageName) { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK sendFileMessage" + (error?.localizedDescription ?? "no error") + ) + self?.showErrorToast(error) } - weak var weakSelf = self - PHImageManager.default().requestImageData(for: asset, options: options) { imageData, dataUTI, orientation, info in - if let data = imageData { - let tempDirectoryURL = FileManager.default.temporaryDirectory - let uniqueString = UUID().uuidString - let temUrl = tempDirectoryURL.appendingPathComponent(uniqueString + ".gif") - print("tem url path : ", temUrl.path) - do { - try data.write(to: temUrl) - DispatchQueue.main.async { - weakSelf?.viewmodel.sendImageMessage(path: temUrl.path) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ChatViewController"), - desc: #function + "CALLBACK sendImageMessage " + (error?.localizedDescription ?? "no error") - ) - if error != nil { - weakSelf?.view.makeToast(error?.localizedDescription) + } + } else { + if let url = info[.referenceURL] as? URL { + if url.absoluteString.hasSuffix("ext=GIF") == true { + // GIF 需要特殊处理 + let imageAsset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset + let options = PHImageRequestOptions() + options.version = .current + guard let asset = imageAsset else { + return + } + weak var weakSelf = self + PHImageManager.default().requestImageData(for: asset, options: options) { imageData, dataUTI, orientation, info in + if let data = imageData { + let tempDirectoryURL = FileManager.default.temporaryDirectory + let uniqueString = UUID().uuidString + let temUrl = tempDirectoryURL.appendingPathComponent(uniqueString + ".gif") + print("tem url path : ", temUrl.path) + do { + try data.write(to: temUrl) + DispatchQueue.main.async { + weakSelf?.viewModel.sendImageMessage(path: temUrl.path, name: imageName, width: imageWidth, height: imageHeight) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK sendImageMessage " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.showErrorToast(error) } } + } catch { + NEALog.infoLog(ModuleName, desc: #function + "write tem gif data error : \(error.localizedDescription)") } - } catch { - NELog.infoLog(ModuleName, desc: #function + "write tem gif data error : \(error.localizedDescription)") } } + return } - return } - } - viewmodel.sendImageMessage(image: image) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), - desc: #function + "CALLBACK sendImageMessage " + (error?.localizedDescription ?? "no error") - ) - if error != nil { - self?.view.makeToast(error?.localizedDescription) + viewModel.sendImageMessage(path: imageUrl.relativePath, name: imageName, width: imageWidth, height: imageHeight) { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK sendImageMessage " + (error?.localizedDescription ?? "no error") + ) + self?.showErrorToast(error) + // 删除临时保存的图片 + if needDelete { + try? FileManager.default.removeItem(at: imageUrl) + } } } } @@ -1296,19 +1319,23 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // MARK: UIDocumentPickerDelegate + /// 拷贝文件到沙盒,用于发送 + /// - Parameters: + /// - url: 原始路径 + /// - displayName: 显示名称 func copyFileToSend(url: URL, displayName: String) { let desPath = NSTemporaryDirectory() + "\(url.lastPathComponent)" let dirUrl = URL(fileURLWithPath: desPath) if !FileManager.default.fileExists(atPath: desPath) { - NELog.infoLog(ModuleName + " " + tag, desc: #function + "file not exist") + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "file not exist") do { try FileManager.default.copyItem(at: url, to: dirUrl) } catch { - NELog.errorLog(ModuleName + " " + tag, desc: #function + "copyItem [\(desPath)] ERROR: \(error)") + NEALog.errorLog(ModuleName + " " + ChatViewController.className(), desc: #function + "copyItem [\(desPath)] ERROR: \(error)") } } if FileManager.default.fileExists(atPath: desPath) { - NELog.infoLog(ModuleName + " " + tag, desc: #function + "fileExists") + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "fileExists") do { let fileAttributes = try FileManager.default.attributesOfItem(atPath: desPath) if let size_B = fileAttributes[FileAttributeKey.size] as? Double { @@ -1317,19 +1344,17 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel showToast(String(format: chatLocalizable("fileSize_over_limit"), "\(NEKitChatConfig.shared.ui.fileSizeLimit)")) try? FileManager.default.removeItem(atPath: desPath) } else { - viewmodel.sendFileMessage(filePath: desPath, displayName: displayName) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), + viewModel.sendFileMessage(filePath: desPath, displayName: displayName) { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendFileMessage " + (error?.localizedDescription ?? "no error") ) - if error != nil { - self?.view.makeToast(error!.localizedDescription) - } + self?.showErrorToast(error) } } } } catch { - NELog.errorLog(ModuleName + " " + tag, desc: #function + "get file size error: \(error)") + NEALog.errorLog(ModuleName + " " + ChatViewController.className(), desc: #function + "get file size error: \(error)") } } } @@ -1348,7 +1373,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // 停止安全访问权限 url.stopAccessingSecurityScopedResource() } else { - NELog.errorLog(ModuleName + " " + tag, desc: #function + "fileUrlAuthozied FAILED") + NEALog.errorLog(ModuleName + " " + ChatViewController.className(), desc: #function + "fileUrlAuthozied FAILED") } } @@ -1362,7 +1387,46 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel controller.dismiss(animated: true) } - // MARK: ChatViewModelDelegate + // MARK: NEContactListener + + /// 好友(用户)信息变更回调 + /// - Parameter accountId: 用户 id + func onUserOrFriendInfoChanged(_ accountId: String) { + let sessionId = viewModel.sessionId + + if accountId == sessionId { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: DispatchWorkItem(block: { [weak self] in + let showName = ChatTeamCache.shared.getShowName(sessionId).name + self?.titleContent = showName + self?.title = showName + })) + } + viewModel.updateMessageInfo(accountId) + } + + /// 用户信息变更回调 + /// - Parameter users: 用户列表 + public func onUserProfileChanged(_ users: [V2NIMUser]) { + for user in users { + if let accountId = user.accountId { + onUserOrFriendInfoChanged(accountId) + if NEFriendUserCache.shared.getFriendInfo(accountId) == nil { + ChatTeamCache.shared.updateTeamMemberInfo(NEUserWithFriend(user: user)) + } + } + } + } + + /// 好友信息变更回调 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + guard let accountId = friendInfo.accountId else { + return + } + onUserOrFriendInfoChanged(accountId) + } + + // MARK: ChatviewModelDelegate open func didLeaveTeam() { weak var weakSelf = self @@ -1378,14 +1442,17 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - open func onRecvMessages(_ messages: [NIMMessage]) { + /// 收到消息 + /// - Parameter messages: 消息列表 + open func onRecvMessages(_ messages: [V2NIMMessage]) { operationView?.removeFromSuperview() insertRows() if isCurrentPage, UIApplication.shared.applicationState == .active { - viewmodel.markRead(messages: messages) { error in - NELog.infoLog( - ModuleName + " " + self.tag, + // 发送已读回执 + viewModel.markRead(messages: messages) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK markRead " + (error?.localizedDescription ?? "no error") ) } @@ -1394,23 +1461,24 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - open func willSend(_ message: NIMMessage) { + /// 消息即将发送 + /// - Parameter message: 消息 + open func sending(_ message: V2NIMMessage) { insertRows() } - open func send(_ message: NIMMessage, progress: Float) {} - - open func send(_ message: NIMMessage, didCompleteWithError error: Error?) { - if indexPathsWithMessags([message]).count > 0 { - tableViewReloadIndexs(indexPathsWithMessags([message])) - } + /// 消息发送成功 + /// - Parameter message: 消息 + public func sendSuccess(_ message: V2NIMMessage) { + let indexs = indexPathsWithMessags([message]) + tableViewReloadIndexs(indexs) } - private func indexPathsWithMessags(_ messages: [NIMMessage]) -> [IndexPath] { + private func indexPathsWithMessags(_ messages: [V2NIMMessage]) -> [IndexPath] { var indexPaths = [IndexPath]() for messageModel in messages { - for (i, model) in viewmodel.messages.enumerated() { - if model.message?.messageId == messageModel.messageId { + for (i, model) in viewModel.messages.enumerated() { + if model.message?.messageClientId == messageModel.messageClientId { indexPaths.append(IndexPath(row: i, section: 0)) } } @@ -1418,83 +1486,84 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return indexPaths } - open func onDeleteMessage(_ message: NIMMessage, atIndexs: [IndexPath], reloadIndex: [IndexPath]) { - if atIndexs.isEmpty { + public func onLoadMoreWithMessage(_ indexs: [IndexPath]) { + tableViewReloadIndexs(indexs) + } + + open func onDeleteMessage(_ messages: [V2NIMMessage], deleteIndexs: [IndexPath], reloadIndex: [IndexPath]) { + if deleteIndexs.isEmpty { return } - viewmodel.selectedMessages.removeAll(where: { $0.messageId == message.messageId }) + operationView?.removeFromSuperview() - tableViewDeleteIndexs(atIndexs) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in - self?.tableViewReloadIndexs(reloadIndex) - })) - } + tableViewReloadIndexs(reloadIndex) { [weak self] in + for index in reloadIndex { + if let numberOfRows = self?.tableView.numberOfRows(inSection: 0), index.row == numberOfRows - 1 { + self?.scrollTableViewToBottom() + } + } + } - open func updateDownloadProgress(_ message: NIMMessage, atIndex: IndexPath, progress: Float) { - tableViewUpdateDownload(atIndex) + for message in messages { + viewModel.messages.removeAll { $0.message?.messageClientId == message.messageClientId } + } + tableViewDeleteIndexs(deleteIndexs) + + // 如果消息为空(加载的消息全部被删除),则拉取更多数据 + if viewModel.messages.isEmpty { + loadMoreData() + } } - open func onRevokeMessage(_ message: NIMMessage, atIndexs: [IndexPath]) { + open func onRevokeMessage(_ message: V2NIMMessage, atIndexs: [IndexPath]) { if atIndexs.isEmpty { return } - viewmodel.selectedMessages.removeAll(where: { $0.messageId == message.messageId }) + viewModel.selectedMessages.removeAll(where: { $0.messageClientId == message.messageClientId }) operationView?.removeFromSuperview() - NELog.infoLog(className(), desc: "on revoke message at indexs \(atIndexs)") - tableViewReloadIndexs(atIndexs) - } - - open func onAddMessagePin(_ message: NIMMessage, atIndexs: [IndexPath]) { - tableViewReloadIndexs(atIndexs) + NEALog.infoLog(className(), desc: "on revoke message at indexs \(atIndexs)") + tableViewReloadIndexs(atIndexs) { [weak self] in + for index in atIndexs { + if let numberOfRows = self?.tableView.numberOfRows(inSection: 0), index.row == numberOfRows - 1 { + self?.scrollTableViewToBottom() + } + } + } } - open func onRemoveMessagePin(_ message: NIMMessage, atIndexs: [IndexPath]) { - tableViewReloadIndexs(atIndexs) + public func onMessagePinStatusChange(_ message: V2NIMMessage?, atIndexs: [IndexPath]) { + tableViewReloadIndexs(atIndexs) { [weak self] in + for index in atIndexs { + if let numberOfRows = self?.tableView.numberOfRows(inSection: 0), index.row == numberOfRows - 1 { + self?.scrollTableViewToBottom() + } + } + } } open func tableViewDeleteIndexs(_ indexs: [IndexPath]) { - tableView.beginUpdates() - tableView.deleteRows(at: indexs, with: .none) - tableView.endUpdates() + tableView.deleteData(indexs) } - open func tableViewReloadIndexs(_ indexs: [IndexPath]) { - weak var weakSelf = self - if #available(iOS 11.0, *) { - tableView.performBatchUpdates { - weakSelf?.tableView.reloadRows(at: indexs, with: .none) - } - } else { - tableView.beginUpdates() - tableView.reloadRows(at: indexs, with: .none) - tableView.endUpdates() + open func tableViewReloadIndexs(_ indexs: [IndexPath], _ completion: (() -> Void)? = nil) { + if isUploadingData { + return } - indexs.forEach { index in - if index.row == tableView.numberOfRows(inSection: 0) - 1 { - tableView.scrollToRow(at: index, at: .bottom, animated: true) - } + let indexs = indexs.filter { index in + index.row >= 0 && index.row < tableView.numberOfRows(inSection: 0) } - } - open func didReadedMessageIndexs() { - didRefreshTable() - } + if indexs.isEmpty { + return + } - open func tableViewUpdateDownload(_ index: IndexPath) { - if #available(iOS 11.0, *) { - tableView.performBatchUpdates { - tableView.reloadRows(at: [index], with: .none) - } - } else { - tableView.beginUpdates() - tableView.reloadRows(at: [index], with: .none) - tableView.endUpdates() + tableView.reloadData(indexs) { _ in + completion?() } } - open func didRefreshTable() { - getSessionInfo(session: viewmodel.session) + open func tableViewReload() { tableView.reloadData() } @@ -1536,30 +1605,23 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // MARK: audio play - func startPlaying(audioMessage: NIMMessage?, isSend: Bool) { - guard let message = audioMessage, let audio = message.messageObject as? NIMAudioObject else { + func startPlaying(audioMessage: V2NIMMessage?, isSend: Bool) { + guard let message = audioMessage, let audio = message.attachment as? V2NIMMessageAudioAttachment else { return } playingCell?.startAnimation(byRight: isSend) - if let path = audio.path, FileManager.default.fileExists(atPath: path) { - NELog.infoLog(className(), desc: #function + " play path : " + path) - - if viewmodel.getHandSetEnable() == true { - NIMSDK.shared().mediaManager.switch(.receiver) - } else { - NIMSDK.shared().mediaManager.switch(.speaker) - } + let path = audio.path ?? ChatMessageHelper.createFilePath(message) + if FileManager.default.fileExists(atPath: path) { + NEALog.infoLog(className(), desc: #function + " play path : " + path) NIMSDK.shared().mediaManager.play(path) } else { - NELog.infoLog(className(), desc: #function + " audio path is empty, play url : " + (audio.url ?? "")) + NEALog.infoLog(className(), desc: #function + " audio path is empty, play url : " + (audio.url ?? "")) playingCell?.stopAnimation(byRight: isSend) - ChatMessageHelper.downloadAudioFile(message: message) } } private func startPlay(cell: ChatAudioCellProtocol?, model: MessageAudioModel?) { - guard let audio = model?.message?.messageObject as? NIMAudioObject, - let isSend = model?.message?.isOutgoingMsg else { + guard let isSend = model?.message?.isSelf else { return } if playingModel == model { @@ -1578,7 +1640,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func stopPlay() { if NIMSDK.shared().mediaManager.isPlaying() { - playingCell?.startAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) + playingCell?.stopAnimation(byRight: playingModel?.message?.isSelf ?? true) NIMSDK.shared().mediaManager.stopPlay() } } @@ -1595,11 +1657,11 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // play open func playAudio(_ filePath: String, didBeganWithError error: Error?) { print(#function + "\(error?.localizedDescription ?? "")") - NIMSDK.shared().mediaManager.switch(viewmodel.getHandSetEnable() ? .receiver : .speaker) - if let e = error { + NIMSDK.shared().mediaManager.switch(viewModel.getHandSetEnable() ? .receiver : .speaker) + if error != nil { showErrorToast(error) // stop - playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) + playingCell?.stopAnimation(byRight: playingModel?.message?.isSelf ?? true) playingModel?.isPlaying = false } } @@ -1608,29 +1670,27 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel print(#function + "\(error?.localizedDescription ?? "")") showErrorToast(error) // stop - playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) + playingCell?.stopAnimation(byRight: playingModel?.message?.isSelf ?? true) playingModel?.isPlaying = false } open func stopPlayAudio(_ filePath: String, didCompletedWithError error: Error?) { print(#function + "\(error?.localizedDescription ?? "")") showErrorToast(error) - playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) + playingCell?.stopAnimation(byRight: playingModel?.message?.isSelf ?? true) playingModel?.isPlaying = false } open func playAudio(_ filePath: String, progress value: Float) {} open func playAudioInterruptionEnd() { - print(#function) - playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) + playingCell?.stopAnimation(byRight: playingModel?.message?.isSelf ?? true) playingModel?.isPlaying = false } open func playAudioInterruptionBegin() { - print(#function) // stop play - playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) + playingCell?.stopAnimation(byRight: playingModel?.message?.isSelf ?? true) playingModel?.isPlaying = false } @@ -1650,9 +1710,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel print("dur:\(dur)") if dur > 1 { - viewmodel.sendAudioMessage(filePath: fp) { error in - NELog.infoLog( - ModuleName + " " + self.tag, + viewModel.sendAudioMessage(filePath: fp) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendAudioMessage " + (error?.localizedDescription ?? "no error") ) self.showErrorToast(error) @@ -1668,9 +1728,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func recordAudioProgress(_ currentTime: TimeInterval) {} - open func recordAudioInterruptionBegin() { - print(#function) - } + open func recordAudioInterruptionBegin() {} // MARK: Private Method @@ -1685,37 +1743,26 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel tableView.reloadData() return } - if oldRows == viewmodel.messages.count { + if oldRows == viewModel.messages.count { tableView.reloadData() return } var indexs = [IndexPath]() - for (i, _) in viewmodel.messages.enumerated() { + for (i, _) in viewModel.messages.enumerated() { if i >= oldRows { indexs.append(IndexPath(row: i, section: 0)) } } if !indexs.isEmpty { - if #available(iOS 11.0, *) { - self.tableView.performBatchUpdates { - self.tableView.insertRows(at: indexs, with: .bottom) - } completion: { finished in - self.tableView.scrollToRow( - at: IndexPath(row: self.viewmodel.messages.count - 1, section: 0), + tableView.insertData(indexs) { [weak self] _ in + if let row = self?.tableView.numberOfRows(inSection: 0), row > 0 { + self?.tableView.scrollToRow( + at: IndexPath(row: row - 1, section: 0), at: .bottom, animated: false ) } - } else { - tableView.insertRows(at: indexs, with: .bottom) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: DispatchWorkItem(block: { - self.tableView.scrollToRow( - at: IndexPath(row: self.viewmodel.messages.count - 1, section: 0), - at: .bottom, - animated: false - ) - })) } } } @@ -1763,7 +1810,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } func getUserSelectVC() -> NEBaseSelectUserViewController { - NEBaseSelectUserViewController(sessionId: viewmodel.session.sessionId, showSelf: false) + NEBaseSelectUserViewController(sessionId: viewModel.sessionId, showSelf: false) } private func showUserSelectVC(text: String) { @@ -1777,8 +1824,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel addText += chatLocalizable("user_select_all") } else { if let m = model { - addText += m.showNameInTeam() - if let uid = m.nimUser?.userId { + addText += m.showNameInTeam() ?? "" + if let uid = m.nimUser?.user?.accountId { accid = uid } } @@ -1792,10 +1839,10 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel private func showErrorToast(_ error: Error?) { if let err = error as? NSError { switch err.code { - case noNetworkCode: + case protocolSendFailed: showToast(commonLocalizable("network_error")) default: - showToast(err.localizedDescription) + print(err.localizedDescription) } } } @@ -1819,8 +1866,6 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } case .recall: recallMessage() - case .collection: - collectionMessage() case .forward: forwardMessage() case .pin: @@ -1837,14 +1882,14 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func customOperation() {} open func copyMessage() { - if let model = viewmodel.operationModel as? MessageTextModel { + if let model = viewModel.operationModel as? MessageTextModel { if let text = model.message?.text, !text.isEmpty { UIPasteboard.general.string = text showToast(chatLocalizable("copy_success")) } else if let body = model.attributeStr?.string, !body.isEmpty { UIPasteboard.general.string = body showToast(chatLocalizable("copy_success")) - } else if let model = viewmodel.operationModel as? MessageRichTextModel { + } else if let model = viewModel.operationModel as? MessageRichTextModel { if let title = model.titleAttributeStr?.string, !title.isEmpty { UIPasteboard.general.string = title showToast(chatLocalizable("copy_success")) @@ -1854,15 +1899,21 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func deleteMessage() { - showAlert(message: chatLocalizable("message_delete_confirm")) { - self.viewmodel.deleteMessage { error in - self.showErrorToast(error) + showAlert(message: chatLocalizable("message_delete_confirm")) { [weak self] in + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + self?.showToast(commonLocalizable("network_error")) + return + } + + self?.viewModel.deleteMessage { error in + self?.showErrorToast(error) } } } open func showReplyMessageView(isReEdit: Bool = false) { - viewmodel.isReplying = true + viewModel.isReplying = true if chatInputView.chatInpuMode != .multipleReturn { view.addSubview(replyView) replyView.closeButton.addTarget(self, action: #selector(closeReply), for: .touchUpInside) @@ -1875,63 +1926,71 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ]) } - if let message = viewmodel.operationModel?.message { + if let message = viewModel.operationModel?.message { if isReEdit { - replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: viewmodel.operationModel?.replyText ?? "", + replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: viewModel.operationModel?.replyText ?? "", font: replyView.textLabel.font, color: replyView.textLabel.textColor) - if let replyMessage = viewmodel.getReplyMessageWithoutThread(message: message) as? MessageContentModel { - viewmodel.operationModel = replyMessage + viewModel.getReplyMessageWithoutThread(message: message) { model in + if let replyMessage = model as? MessageContentModel { + self.viewModel.operationModel = replyMessage + } } } else { var text = chatLocalizable("msg_reply") - if let uid = message.from { - var showName = ChatUserCache.getShowName(userId: uid, teamId: viewmodel.session.sessionId, false) - if viewmodel.session.sessionType != .P2P, - !IMKitClient.instance.isMySelf(uid) { + if let uid = message.senderId { + var (showName, _) = ChatTeamCache.shared.getShowName(uid, false) + if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) != .CONVERSATION_TYPE_P2P, + !IMKitClient.instance.isMe(uid) { addToAtUsers(addText: "@" + showName + "", isReply: true, accid: uid) } - let user = viewmodel.getUserInfo(userId: uid) - if let alias = user?.alias, !alias.isEmpty { - showName = alias - } + (showName, _) = ChatTeamCache.shared.getShowName(uid) text += " " + showName + text += ": \(ChatMessageHelper.contentOfMessage(message))" + replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, + font: replyView.textLabel.font, + color: replyView.textLabel.textColor) + chatInputView.textView.becomeFirstResponder() } - text += ": \(ChatMessageHelper.contentOfMessage(message))" - replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, - font: replyView.textLabel.font, - color: replyView.textLabel.textColor) - chatInputView.textView.becomeFirstResponder() } } } open func closeReply(button: UIButton?) { replyView.removeFromSuperview() - viewmodel.isReplying = false + viewModel.isReplying = false } open func recallMessage() { weak var weakSelf = self showAlert(message: chatLocalizable("message_revoke_confirm")) { - if let message = weakSelf?.viewmodel.operationModel?.message { - if message.messageType == .text { - weakSelf?.viewmodel.operationModel?.isRevokedText = true + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + + if let message = weakSelf?.viewModel.operationModel?.message { + if message.messageType == .MESSAGE_TYPE_TEXT { + weakSelf?.viewModel.operationModel?.isReedit = true } - if message.messageType == .custom, - let attach = NECustomAttachment.attachmentOfCustomMessage(message: message), attach.customType == customRichTextType { - weakSelf?.viewmodel.operationModel?.isRevokedText = true + if message.messageType == .MESSAGE_TYPE_CUSTOM, + let customType = NECustomAttachment.typeOfCustomMessage(message.attachment), + customType == customRichTextType { + weakSelf?.viewModel.operationModel?.isReedit = true } - let isPin = weakSelf?.viewmodel.operationModel?.isPined ?? false - weakSelf?.viewmodel.revokeMessage(message: message) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? ""), + let isPin = weakSelf?.viewModel.operationModel?.isPined ?? false + weakSelf?.viewModel.revokeMessage(message: message) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK revokeMessage " + (error?.localizedDescription ?? "no error") ) if let err = error as? NSError { - if err.code == 508 { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == ravokableTimeExpired { weakSelf?.showToast(chatLocalizable("ravokable_time_expired")) } else { weakSelf?.showToast(chatLocalizable("ravoked_failed")) @@ -1940,13 +1999,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // 自己撤回成功 & 收到对方撤回 都会走回调方法 onRevokeMessage // 撤回成功的逻辑统一在代理方法中处理 onRevokeMessage if isPin { - weakSelf?.viewmodel.removePinMessage(message) { error, pinItem, value in - } - } - weakSelf?.viewmodel.saveRevokeMessage(message) { error in - print("message id : ", message.messageId) - if let err = error { - NELog.infoLog(weakSelf?.className() ?? "chat view controller", desc: err.localizedDescription) + weakSelf?.viewModel.removePinMessage(message: message) { error, value in } } } @@ -1955,22 +2008,6 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - open func collectionMessage() { - if let message = viewmodel.operationModel?.message { - viewmodel.addColletion(message) { error, info in - NELog.infoLog( - ModuleName + " " + self.tag, - desc: #function + "CALLBACK addColletion " + (error?.localizedDescription ?? "no error") - ) - if error != nil { - self.showErrorToast(error) - } else { - self.showToast(chatLocalizable("collection_success")) - } - } - } - } - open func getForwardAlertController() -> NEBaseForwardAlertViewController { NEBaseForwardAlertViewController() } @@ -1981,8 +2018,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel let forwardAlert = getForwardAlertController() forwardAlert.setItems(items) forwardAlert.type = type - forwardAlert.context = ChatMessageHelper.getSessionName(session: viewmodel.session) forwardAlert.sureBlock = sureBlock + forwardAlert.context = ChatMessageHelper.getSessionName(conversationId: viewModel.conversationId) addChild(forwardAlert) view.addSubview(forwardAlert.view) @@ -1999,12 +2036,12 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel Router.shared.register(ContactSelectedUsersRouter) { param in var items = [ForwardItem]() - if let users = param["im_user"] as? [NIMUser] { - users.forEach { user in + if let users = param["im_user"] as? [V2NIMUser] { + for user in users { let item = ForwardItem() - item.uid = user.userId - item.avatar = user.userInfo?.avatarUrl - item.name = user.getShowName() + item.uid = user.accountId + item.avatar = user.avatar + item.name = user.name items.append(item) } @@ -2016,14 +2053,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return } - weakSelf?.viewmodel.forwardUserMessage(users, isMultiForward, depth, comment) { error in - if let err = error as? NSError { - if err.code != 0 { - weakSelf?.showErrorToast(err) - } - } else { - sureBlock?() - } + weakSelf?.viewModel.forwardUserMessage(users, isMultiForward, depth, comment) { error in + weakSelf?.showErrorToast(error) + sureBlock?() } } } @@ -2032,6 +2064,12 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel var param = [String: Any]() param["nav"] = weakSelf?.navigationController as Any param["limit"] = 6 + + // 转发人员选择页面不包含自己 + var filters = Set() + filters.insert(IMKitClient.instance.account()) + param["filters"] = filters + Router.shared.use(ContactUserSelectRouter, parameters: param, closure: nil) } @@ -2040,9 +2078,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel _ sureBlock: (() -> Void)? = nil) { weak var weakSelf = self Router.shared.register(ContactTeamDataRouter) { param in - if let team = param["team"] as? NIMTeam { + if let team = param["team"] as? V2NIMTeam { let item = ForwardItem() - item.avatar = team.avatarUrl + item.avatar = team.avatar item.name = team.getShowName() item.uid = team.teamId @@ -2054,14 +2092,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return } - weakSelf?.viewmodel.forwardTeamMessage(team, isMultiForward, depth, comment) { error in - if let err = error as? NSError { - if err.code != 0 { - weakSelf?.showErrorToast(err) - } - } else { - sureBlock?() - } + weakSelf?.viewModel.forwardTeamMessage(team, isMultiForward, depth, comment) { error in + weakSelf?.showErrorToast(error) + sureBlock?() } } } @@ -2076,35 +2109,49 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func forwardMessage() { - if let message = viewmodel.operationModel?.message { - viewmodel.selectedMessages = [message] + if let message = viewModel.operationModel?.message { + viewModel.selectedMessages = [message] didClickSingleForwardButton() } } open func pinMessage() { - guard let optModel = viewmodel.operationModel, !optModel.isPined else { + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + guard let optModel = viewModel.operationModel, !optModel.isPined else { return } if optModel.isRevoked == true { return } if let message = optModel.message { - viewmodel.pinMessage(message) { [weak self] error, pinItem, index in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), + viewModel.addPinMessage(message: message) { [weak self] error, index in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK pinMessage " + (error?.localizedDescription ?? "no error") ) if let err = error as? NSError { - if err.code == noNetworkCode { + if err.code == pinAlreadyExist { + return + } else if err.code == protocolSendFailed { self?.view.makeToast(commonLocalizable("network_error"), position: .center) + } else if err.code == pinLimitExceeded { + self?.view.makeToast(chatLocalizable("pin_limit_exceeded"), position: .center) } else { self?.view.makeToast(error?.localizedDescription, position: .center) } } else { // update UI if index >= 0 { - self?.tableViewReloadIndexs([IndexPath(row: index, section: 0)]) + self?.tableViewReloadIndexs([IndexPath(row: index, section: 0)]) { [weak self] in + if let numberOfRows = self?.tableView.numberOfRows(inSection: 0), index == numberOfRows - 1 { + self?.scrollTableViewToBottom() + } + } } } } @@ -2112,20 +2159,26 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func removePinMessage() { - guard let optModel = viewmodel.operationModel, optModel.isPined else { + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + guard let optModel = viewModel.operationModel, optModel.isPined else { return } if let message = optModel.message { - viewmodel.removePinMessage(message) { [weak self] error, pinItem, index in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), + viewModel.removePinMessage(message: message) { [weak self] error, index in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK removePinMessage " + (error?.localizedDescription ?? "no error") ) if let err = error as? NSError { - if err.code == 404 { + if err.code == pinNotExist { return - } else if err.code == noNetworkCode { + } else if err.code == protocolSendFailed { self?.view.makeToast(commonLocalizable("network_error"), position: .center) } else { self?.view.makeToast(error?.localizedDescription, position: .center) @@ -2133,7 +2186,11 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } else { // update UI if index >= 0 { - self?.tableViewReloadIndexs([IndexPath(row: index, section: 0)]) + self?.tableViewReloadIndexs([IndexPath(row: index, section: 0)]) { [weak self] in + if let numberOfRows = self?.tableView.numberOfRows(inSection: 0), index == numberOfRows - 1 { + self?.scrollTableViewToBottom() + } + } } } } @@ -2142,9 +2199,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func selectMessage() { isMutilSelect = true - if let model = viewmodel.operationModel, let msg = model.message { + if let model = viewModel.operationModel, let msg = model.message { model.isSelected = true - viewmodel.selectedMessages = [msg] + viewModel.selectedMessages = [msg] } navigationView.setMoreButtonTitle(chatLocalizable("cancel")) @@ -2156,8 +2213,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel func cancelMutilSelect() { isMutilSelect = false - viewmodel.selectedMessages.removeAll() - viewmodel.messages.forEach { model in + viewModel.selectedMessages.removeAll() + for model in viewModel.messages { model.isSelected = false } setMoreButton() @@ -2175,45 +2232,35 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // MARK: UITableViewDataSource, UITableViewDelegate open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let count = viewmodel.messages.count - print("numberOfRowsInSection count : ", count) - return count + viewModel.messages.count } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard indexPath.row < viewmodel.messages.count else { return NEBaseChatMessageCell() } - let model = viewmodel.messages[indexPath.row] - var reuseId = "" + guard indexPath.row < viewModel.messages.count else { return NEBaseChatMessageCell() } + let model = viewModel.messages[indexPath.row] + var reuseId = "\(NEBaseChatMessageCell.self)" if model.replyedModel?.isReplay == true, model.isRevoked == false { - if model.replyedModel?.message?.serverID == nil || - model.replyedModel?.message?.serverID.isEmpty == true { - if let message = model.message { - model.replyedModel = viewmodel.getReplyMessageWithoutThread(message: message) - } - } - - if let attch = NECustomAttachment.attachmentOfCustomMessage(message: model.message), - attch.customType == customRichTextType { + if let customType = NECustomAttachment.typeOfCustomMessage(model.message?.attachment), + customType == customRichTextType { reuseId = "\(MessageType.richText.rawValue)" } else { reuseId = "\(MessageType.reply.rawValue)" } } else { let key = "\(model.type.rawValue)" - if model.type == .custom, - let attch = NECustomAttachment.attachmentOfCustomMessage(message: model.message) { - if attch.customType == customMultiForwardType { + if model.type == .custom, let customType = NECustomAttachment.typeOfCustomMessage(model.message?.attachment) { + if customType == customMultiForwardType { reuseId = "\(MessageType.multiForward.rawValue)" - } else if attch.customType == customRichTextType { + } else if customType == customRichTextType { reuseId = "\(MessageType.richText.rawValue)" - } else if NEChatUIKitClient.instance.getRegisterCustomCell()["\(attch.customType)"] != nil { - reuseId = "\(attch.customType)" + } else if NEChatUIKitClient.instance.getRegisterCustomCell()["\(customType)"] != nil { + reuseId = "\(customType)" } else { reuseId = "\(NEBaseChatMessageCell.self)" } - } else if model.type == .time || model.type == .notification || model.type == .tip { + } else if model.type == .notification || model.type == .tip { reuseId = "\(MessageType.time.rawValue)" } else if cellRegisterDic[key] != nil { reuseId = key @@ -2225,40 +2272,30 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel let cell = tableView.dequeueReusableCell(withIdentifier: reuseId, for: indexPath) if let c = cell as? NEBaseChatMessageTipCell { if let m = model as? MessageTipsModel { - m.setText() c.setModel(m) } return c } else if let c = cell as? NEBaseChatMessageCell { c.delegate = self - if let m = model as? MessageContentModel { - // 更新好友昵称、头像 - if let from = model.message?.from, - let user = ChatUserCache.getUserInfo(from) { - if let uid = user.userId, - viewmodel.session.sessionType == .team || - viewmodel.session.sessionType == .superTeam { - m.fullName = ChatUserCache.getShowName(userId: uid, teamId: viewmodel.session.sessionId) - m.shortName = ChatUserCache.getShortName(name: user.showName(false) ?? "", length: 2) - } - m.avatar = user.userInfo?.avatarUrl - } - c.setModel(m, m.message?.isOutgoingMsg ?? false) - c.setSelect(m, isMutilSelect) - } + // 语音消息播放状态 if let audioCell = cell as? ChatAudioCellProtocol, let m = model as? MessageAudioModel, - m.message?.messageId == playingModel?.message?.messageId { + m.message?.messageClientId == playingModel?.message?.messageClientId { if NIMSDK.shared().mediaManager.isPlaying() { playingCell = audioCell - playingCell?.startAnimation(byRight: true) + m.isPlaying = true } } + if let m = model as? MessageContentModel { + c.setModel(m, m.message?.isSelf ?? false) + c.setSelect(m, isMutilSelect) + } + return c } else if let c = cell as? NEChatBaseCell, let m = model as? MessageContentModel { - c.setModel(m, m.message?.isOutgoingMsg ?? false) + c.setModel(m, m.message?.isSelf ?? false) return cell } else { return NEBaseChatMessageCell() @@ -2266,17 +2303,17 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard indexPath.row < viewmodel.messages.count else { return } + guard indexPath.row < viewModel.messages.count else { return } if isMutilSelect { - if indexPath.row < viewmodel.messages.count { - let model = viewmodel.messages[indexPath.row] + if indexPath.row < viewModel.messages.count { + let model = viewModel.messages[indexPath.row] if !model.isRevoked, let cell = tableView.cellForRow(at: indexPath) as? NEBaseChatMessageCell { model.isSelected = !model.isSelected - cell.seletedBtn.isSelected = model.isSelected - viewmodel.selectedMessages.removeAll(where: { $0.messageId == model.message?.messageId }) + cell.selectedButton.isSelected = model.isSelected + viewModel.selectedMessages.removeAll(where: { $0.messageClientId == model.message?.messageClientId }) if model.isSelected, let msg = model.message { - viewmodel.selectedMessages.append(msg) + viewModel.selectedMessages.append(msg) } } } @@ -2293,12 +2330,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - guard indexPath.row < viewmodel.messages.count else { return 0 } - let model = viewmodel.messages[indexPath.row] - if let m = model as? MessageTipsModel { - m.commonInit() - } - + guard indexPath.row < viewModel.messages.count else { return 0 } + let model = viewModel.messages[indexPath.row] return model.cellHeight() + chat_content_margin } @@ -2310,6 +2343,56 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel 0 } + public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + // 无更多消息 + if viewModel.messages.count < viewModel.messagPageNum { + return + } + + // 预加载 + let leaveCount = 10 // 剩余多少行开始预加载 + weak var weakSelf = self + if indexPath.row <= leaveCount, !isUploadingData, !uploadHasNoMore { + // 上拉预加载更多 + if !isUploadingData { + isUploadingData = true + viewModel.dropDownRemoteRefresh { error, count, messages in + if let err = error { + NEALog.errorLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK dropDownRemoteRefresh " + (err.localizedDescription) + ) + } else { + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK dropDownRemoteRefresh " + (error?.localizedDescription ?? "no error") + ) + if count <= 0 { + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + return + } + + // 无更多数据 + weakSelf?.uploadHasNoMore = true + weakSelf?.tableView.mj_header = nil + } else { + weakSelf?.tableViewReload() + if let num = weakSelf?.tableView.numberOfRows(inSection: 0), indexPath.row + count <= num { + weakSelf?.tableView.scrollToRow( + at: IndexPath(row: indexPath.row + count - 1, section: 0), + at: .top, + animated: false + ) + } + weakSelf?.isUploadingData = false + } + } + } + } + } + } + // MARK: UIScrollViewDelegate open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { @@ -2376,12 +2459,21 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func expandMoreAction() { - chatInputView.chatAddMoreView.configData(data: NEChatUIKitClient.instance.getMoreActionData(sessionType: viewmodel.session.sessionType)) + var data = NEChatUIKitClient.instance.getMoreActionData(sessionType: V2NIMConversationIdUtil.conversationType(viewModel.conversationId)) + if NEChatKitClient.instance.delegate == nil { + data = data.filter { item in + if item.type == .location { + return false + } + return true + } + } + chatInputView.chatAddMoreView.configData(data: NEChatUIKitClient.instance.getMoreActionData(sessionType: V2NIMConversationIdUtil.conversationType(viewModel.conversationId))) } open func showTextViewController(_ model: MessageContentModel?) { - let title = NECustomAttachment.titleOfRichText(message: model?.message) - let body = NECustomAttachment.bodyOfRichText(message: model?.message) ?? model?.message?.text + let title = NECustomAttachment.titleOfRichText(model?.message?.attachment) + let body = NECustomAttachment.bodyOfRichText(model?.message?.attachment) ?? model?.message?.text let textView = getTextViewController(title: title, body: body) textView.modalPresentationStyle = .fullScreen DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in @@ -2389,11 +2481,30 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel })) } + /// 单击消息 + /// - Parameters: + /// - cell: 消息所在单元格 + /// - model: 消息模型 + /// - replyIndex: 被回复消息的下标 open func didTapMessage(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ replyIndex: Int? = nil) { - if model?.type == .audio { - startPlay(cell: cell as? ChatAudioCellProtocol, model: model as? MessageAudioModel) + if model?.type == .audio, let audioObject = model?.message?.attachment as? V2NIMMessageAudioAttachment { + let path = audioObject.path ?? ChatMessageHelper.createFilePath(model?.message) + if !FileManager.default.fileExists(atPath: path) { + if let urlString = audioObject.url { + viewModel.downLoad(urlString, path, nil) { [weak self] _, error in + if error == nil { + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK downLoad") + self?.startPlay(cell: cell as? ChatAudioCellProtocol, model: model as? MessageAudioModel) + } else { + self?.showErrorToast(error) + } + } + } + } else { + startPlay(cell: cell as? ChatAudioCellProtocol, model: model as? MessageAudioModel) + } } else if model?.type == .image { - if let imageObject = model?.message?.messageObject as? NIMImageObject { + if let imageObject = model?.message?.attachment as? V2NIMMessageImageAttachment { var imageUrl = "" if let url = imageObject.url { @@ -2404,93 +2515,61 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } if imageUrl.count > 0 { - let showController = PhotoBrowserController( - urls: ChatMessageHelper.getUrls(messages: viewmodel.messages), - url: imageUrl - ) + var showImages = [imageUrl] + if let index = replyIndex, index >= 0 { + showImages = ChatMessageHelper.getUrls(messages: viewModel.messages) + } + + let showController = PhotoBrowserController(urls: showImages, url: imageUrl) showController.modalPresentationStyle = .overFullScreen present(showController, animated: false, completion: nil) } } } else if model?.type == .video, - let object = model?.message?.messageObject as? NIMVideoObject { + let object = model?.message?.attachment as? V2NIMMessageVideoAttachment { stopPlay() - weak var weakSelf = self - let videoPlayer = VideoPlayerViewController() - videoPlayer.modalPresentationStyle = .overFullScreen - if let path = object.path, FileManager.default.fileExists(atPath: path) == true { + + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) + if FileManager.default.fileExists(atPath: path) { let url = URL(fileURLWithPath: path) + let videoPlayer = VideoPlayerViewController() + videoPlayer.modalPresentationStyle = .overFullScreen videoPlayer.videoUrl = url - videoPlayer.totalTime = object.duration - print("video url : ", videoPlayer.videoUrl as Any) + videoPlayer.totalTime = Int(object.duration) present(videoPlayer, animated: true, completion: nil) - } else if let urlString = object.url, let path = object.path, - let videoModel = model as? MessageVideoModel { - print("fetch message attachment") - if let index = replyIndex, index >= 0 { + } else { + if let index = replyIndex, index >= 0, + index < tableView.numberOfRows(inSection: 0) { tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) } - videoModel.state = .Downalod - if let videoCell = cell as? NEBaseChatMessageCell { - videoCell.setModel(videoModel, videoModel.message?.isOutgoingMsg ?? false) - } - - viewmodel.downLoad(urlString, path) { progress in - NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ChatViewController"), desc: #function + "CALLBACK downLoad: \(progress)") - - videoModel.progress = progress - if progress >= 1.0 { - videoModel.state = .Success - } - videoModel.cell?.uploadProgress(byRight: videoModel.message?.isOutgoingMsg ?? true, progress) - } _: { error in - weakSelf?.showErrorToast(error) - } + downloadFile(cell, model, object.url, path) } } else if replyIndex != nil, model?.type == .text || model?.type == .reply { showTextViewController(model) } else if model?.type == .location { if let locationModel = model as? MessageLocationModel, let lat = locationModel.lat, let lng = locationModel.lng { - let mapDetail = NEDetailMapController(type: .detail) - mapDetail.currentPoint = CGPoint(x: lat, y: lng) - mapDetail.locationTitle = locationModel.title - mapDetail.subTitle = locationModel.subTitle - navigationController?.pushViewController(mapDetail, animated: true) + var params = [String: Any]() + params["type"] = NEMapType.detail.rawValue + params["nav"] = navigationController + params["lat"] = lat + params["lng"] = lng + params["locationTitle"] = locationModel.title + params["subTitle"] = locationModel.subTitle + Router.shared.use(NERouterUrl.LocationVCRouter, parameters: params) } } else if model?.type == .file, - let object = model?.message?.messageObject as? NIMFileObject, - let path = object.path { + let object = model?.message?.attachment as? V2NIMMessageFileAttachment { + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) if !FileManager.default.fileExists(atPath: path) { - if let urlString = object.url, let path = object.path, - let fileModel = model as? MessageFileModel { - if let index = replyIndex, index >= 0 { - tableView.scrollToRow(at: IndexPath(row: index, section: 0), - at: .middle, - animated: true) - } - fileModel.state = .Downalod - if let fileCell = cell as? NEBaseChatMessageCell { - fileCell.setModel(fileModel, fileModel.message?.isOutgoingMsg ?? false) - } - - viewmodel.downLoad(urlString, path) { [weak self] progress in - NELog.infoLog(ModuleName + " " + (self?.tag ?? "ChatViewController"), desc: #function + "downLoad file progress: \(progress)") - var newProgress = progress - if newProgress < 0 { - newProgress = abs(progress) / fileModel.size - } - fileModel.progress = newProgress - if newProgress >= 1.0 { - fileModel.state = .Success - } - fileModel.cell?.uploadProgress(byRight: fileModel.message?.isOutgoingMsg ?? true, newProgress) - - } _: { [weak self] error in - self?.showErrorToast(error) - } + if let index = replyIndex, index >= 0, + index < tableView.numberOfRows(inSection: 0) { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), + at: .middle, + animated: true) } + downloadFile(cell, model, object.url, path) } else { let url = URL(fileURLWithPath: path) interactionController.url = url @@ -2500,32 +2579,34 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel interactionController.presentOptionsMenu(from: view.bounds, in: view, animated: true) } } - } else if model?.type == .rtcCallRecord, let object = model?.message?.messageObject as? NIMRtcCallRecordObject { - var param = [String: AnyObject]() - param["remoteUserAccid"] = viewmodel.session.sessionId as AnyObject - param["currentUserAccid"] = NIMSDK.shared().loginManager.currentAccount() as AnyObject - param["remoteShowName"] = titleContent as AnyObject - if let user = viewmodel.repo.getUserInfo(userId: viewmodel.session.sessionId), let avatar = user.userInfo?.avatarUrl { - param["remoteAvatar"] = avatar as AnyObject - } - if object.callType == .audio { - param["type"] = NSNumber(integerLiteral: 1) as AnyObject - } else { - param["type"] = NSNumber(integerLiteral: 2) as AnyObject + } else if model?.type == .rtcCallRecord, + let attachment = model?.message?.attachment as? V2NIMMessageCallAttachment { + let sessionId = viewModel.sessionId + + var param = [String: Any]() + param["remoteUserAccid"] = sessionId + param["currentUserAccid"] = IMKitClient.instance.account() + param["remoteShowName"] = titleContent + param["type"] = attachment.type == 1 ? NSNumber(integerLiteral: 1) : NSNumber(integerLiteral: 2) + + if let user = viewModel.getShowName(sessionId).user { + param["remoteAvatar"] = user.user?.avatar } + Router.shared.use(CallViewRouter, parameters: param) - } else if model?.type == .custom, let attach = NECustomAttachment.attachmentOfCustomMessage(message: model?.message) { - if attach.customType == customRichTextType { + } else if model?.type == .custom, + let customType = NECustomAttachment.typeOfCustomMessage(model?.message?.attachment) { + if customType == customRichTextType { if replyIndex != nil { showTextViewController(model) } - } else if attach.customType == customMultiForwardType, - let data = NECustomAttachment.dataOfCustomMessage(message: model?.message) { + } else if customType == customMultiForwardType, + let data = NECustomAttachment.dataOfCustomMessage(model?.message?.attachment) { let url = data["url"] as? String let md5 = data["md5"] as? String - guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } - let fileName = multiForwardFileName + (model?.message?.messageId ?? "") - let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(fileName)").relativePath + guard let fileDirectory = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit/file/") else { return } + let fileName = multiForwardFileName + (model?.message?.messageClientId ?? "") + let filePath = fileDirectory + fileName let multiForwardVC = getMultiForwardViewController(url, filePath, md5) navigationController?.pushViewController(multiForwardVC, animated: true) } @@ -2533,25 +2614,62 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel print(#function + "message did tap, type:\(String(describing: model?.type.rawValue))") } } + + /// 下载附件(文件、视频消息) + /// - Parameters: + /// - cell: 当前单元格 + /// - model: 消息模型 + /// - url: 远端下载链接 + /// - path: 本地保存路径 + func downloadFile(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ url: String?, _ path: String) { + // 判断是否是视频或者文件对象 + guard let urlString = url, let fileModel = model as? MessageVideoModel else { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "MessageFileModel not exit") + return + } + + // 判断状态,如果是下载中不能进行预览 + if fileModel.state == .Downalod { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "downLoad state, click ingore") + return + } + + fileModel.state = .Downalod + if let fileCell = cell as? NEBaseChatMessageCell { + fileCell.setModel(fileModel, fileModel.message?.isSelf ?? false) + } + + viewModel.downLoad(urlString, path) { progress in + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "downLoad file progress: \(progress)") + fileModel.progress = progress + if progress >= 100 { + fileModel.state = .Success + } + fileModel.cell?.uploadProgress(byRight: fileModel.message?.isSelf ?? true, progress) + + } _: { [weak self] _, error in + self?.showErrorToast(error) + } + } } // MARK: NEMutilSelectBottomViewDelegate extension ChatViewController: NEMutilSelectBottomViewDelegate { /// 移除不可转发的消息 - /// - Parameters cancelSelect: 是否取消勾选 - func filterSelectedMessage(invalidMessages: [NIMMessage]) { + /// - Parameters invalidMessages: 不可转发的消息 + func filterSelectedMessage(invalidMessages: [V2NIMMessage]) { // 取消勾选 - for msg in viewmodel.selectedMessages { + for msg in viewModel.selectedMessages { if invalidMessages.contains(msg) { - for (row, model) in viewmodel.messages.enumerated() { - if msg.messageId == model.message?.messageId { + for (row, model) in viewModel.messages.enumerated() { + if msg.messageClientId == model.message?.messageClientId { let indexPath = IndexPath(row: row, section: 0) - let model = viewmodel.messages[indexPath.row] + let model = viewModel.messages[indexPath.row] if !model.isRevoked, let cell = tableView.cellForRow(at: indexPath) as? NEBaseChatMessageCell { model.isSelected = !model.isSelected - cell.seletedBtn.isSelected = model.isSelected + cell.selectedButton.isSelected = model.isSelected } } } @@ -2559,31 +2677,34 @@ extension ChatViewController: NEMutilSelectBottomViewDelegate { } // 无论UI上是否取消勾选,都需要移除该消息 - viewmodel.selectedMessages.removeAll(where: { invalidMessages.contains($0) }) + viewModel.selectedMessages.removeAll(where: { invalidMessages.contains($0) }) } - // 合并转发 + /// 点击合并转发 open func didClickMultiForwardButton() { + // 校验网络 if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { showToast(commonLocalizable("network_error")) return } - if viewmodel.selectedMessages.count > customMultiForwardLimitCount { + if viewModel.selectedMessages.count > customMultiForwardLimitCount { showToast(String(format: chatLocalizable("multiForward_forward_limit"), customMultiForwardLimitCount)) return } var depth = 0 - var invalidMessages = [NIMMessage]() - for msg in viewmodel.selectedMessages { - if msg.deliveryState == .failed || msg.isBlackListed { + var invalidMessages = [V2NIMMessage]() + for msg in viewModel.selectedMessages { + if msg.sendingState == .MESSAGE_SENDING_STATE_FAILED +// || msg.isBlackListed + { invalidMessages.append(msg) continue } // 解析消息中的depth - if let data = NECustomAttachment.dataOfCustomMessage(message: msg) { + if let data = NECustomAttachment.dataOfCustomMessage(msg.attachment) { if let dep = data["depth"] as? Int { if dep >= customMultiForwardMaxDepth { invalidMessages.append(msg) @@ -2601,17 +2722,19 @@ extension ChatViewController: NEMutilSelectBottomViewDelegate { showAlert(title: chatLocalizable("exception_description"), message: chatLocalizable("exist_invalid")) { [self] in filterSelectedMessage(invalidMessages: invalidMessages) - if !viewmodel.selectedMessages.isEmpty { + if !viewModel.selectedMessages.isEmpty { multiForwardForward(depth) } } } else { - if !viewmodel.selectedMessages.isEmpty { + if !viewModel.selectedMessages.isEmpty { multiForwardForward(depth) } } } + /// 合并转发 + /// - Parameter depth: 层数 open func multiForwardForward(_ depth: Int) { weak var weakSelf = self if IMKitClient.instance.getConfigCenter().teamEnable { @@ -2636,24 +2759,26 @@ extension ChatViewController: NEMutilSelectBottomViewDelegate { } } - // 逐条转发 + /// 点击逐条转发 open func didClickSingleForwardButton() { + // 校验网络 if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { showToast(commonLocalizable("network_error")) return } - if viewmodel.selectedMessages.count > customSingleForwardLimitCount { + if viewModel.selectedMessages.count > customSingleForwardLimitCount { showToast(String(format: chatLocalizable("per_item_forward_limit"), customSingleForwardLimitCount)) return } - var invalidMessages = [NIMMessage]() - for msg in viewmodel.selectedMessages { - if msg.messageType == .audio || - msg.messageType == .rtcCallRecord || - msg.deliveryState == .failed - || msg.isBlackListed { + var invalidMessages = [V2NIMMessage]() + for msg in viewModel.selectedMessages { + if msg.messageType == .MESSAGE_TYPE_AUDIO || + msg.messageType == .MESSAGE_TYPE_CALL || + msg.sendingState == .MESSAGE_SENDING_STATE_FAILED +// || msg.isBlackListed + { invalidMessages.append(msg) } } @@ -2662,17 +2787,18 @@ extension ChatViewController: NEMutilSelectBottomViewDelegate { showAlert(title: chatLocalizable("exception_description"), message: chatLocalizable("exist_invalid")) { [self] in filterSelectedMessage(invalidMessages: invalidMessages) - if !viewmodel.selectedMessages.isEmpty { + if !viewModel.selectedMessages.isEmpty { singleForward() } } } else { - if !viewmodel.selectedMessages.isEmpty { + if !viewModel.selectedMessages.isEmpty { singleForward() } } } + /// 逐条转发 open func singleForward() { weak var weakSelf = self if IMKitClient.instance.getConfigCenter().teamEnable { @@ -2697,16 +2823,23 @@ extension ChatViewController: NEMutilSelectBottomViewDelegate { } } - // 多选删除 + /// 多选删除 open func didClickDeleteButton() { + // 校验网络 if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { showToast(commonLocalizable("network_error")) return } + // 批量删除条数限制 + if viewModel.selectedMessages.count > deleteMessagesLimitCount { + showToast(String(format: chatLocalizable("selete_messages_limit"), deleteMessagesLimitCount)) + return + } + showAlert(message: chatLocalizable("message_delete_confirm")) { [weak self] in - if let messages = self?.viewmodel.selectedMessages { - self?.viewmodel.deleteMessages(messages: messages) { error in + if let messages = self?.viewModel.selectedMessages { + self?.viewModel.deleteMessages(messages: messages) { error in self?.showErrorToast(error) } } @@ -2722,7 +2855,7 @@ extension ChatViewController: ChatBaseCellDelegate { // 非群聊 // 禁言 // 多选 - guard viewmodel.session.sessionType == .team, + guard V2NIMConversationIdUtil.conversationType(viewModel.conversationId) == .CONVERSATION_TYPE_TEAM, !isMute, !isMutilSelect else { return @@ -2731,14 +2864,14 @@ extension ChatViewController: ChatBaseCellDelegate { var addText = "" var accid = "" - if let m = model, let from = m.message?.from { - accid = from - addText += ChatUserCache.getShowName(userId: from, teamId: viewmodel.session.sessionId, false) - } - - addText = "@" + addText + "" + if let m = model, let senderId = m.message?.senderId { + accid = senderId + let (name, _) = ChatTeamCache.shared.getShowName(senderId, false) + addText += name + addText = "@" + addText + "" - addToAtUsers(addText: addText, accid: accid, true) + addToAtUsers(addText: addText, accid: accid, true) + } } open func didTapAvatarView(_ cell: UITableViewCell, _ model: MessageContentModel?) { @@ -2749,7 +2882,7 @@ extension ChatViewController: ChatBaseCellDelegate { didTapHeadPortrait(model: model) } - open func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) { + open func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?, _ replyModel: MessageModel?) { if let tapClick = NEKitChatConfig.shared.ui.messageItemClick { tapClick(cell, model) return @@ -2762,26 +2895,22 @@ extension ChatViewController: ChatBaseCellDelegate { return } - var replyId: String? = model?.message?.repliedMessageId - if let yxReplyMsg = model?.message?.remoteExt?[keyReplyMsgKey] as? [String: Any] { - replyId = yxReplyMsg["idClient"] as? String - } - - if let id = replyId, !id.isEmpty { + if var replyModel = replyModel as? MessageContentModel { var index = -1 - var replyModel: MessageModel? - for (i, m) in viewmodel.messages.enumerated() { - if id == m.message?.messageId { + for (i, m) in viewModel.messages.enumerated() { + if replyModel.message?.messageClientId == m.message?.messageClientId { index = i - replyModel = m + if let m = m as? MessageContentModel { + replyModel = m + } break } } let replyCell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) - if let replyContentModel = replyModel as? MessageContentModel { - didTapMessage(replyCell, replyContentModel, index) - } + + didTapMessage(replyCell, replyModel, index) + } else { didTapMessage(cell, model) } @@ -2796,27 +2925,34 @@ extension ChatViewController: ChatBaseCellDelegate { return } - if playingCell?.messageId == model?.message?.messageId { + if playingCell?.messageId == model?.message?.messageClientId { if playingCell?.isPlaying == true { stopPlay() } } + if let m = model, let msg = m.message { - let messages = viewmodel.messages + let messages = viewModel.messages var index = -1 for i in 0 ..< messages.count { if let message = messages[i].message { - if message.messageId == msg.messageId { + if message.messageClientId == msg.messageClientId { index = i break } } } + if index >= 0 { - viewmodel.messages.remove(at: index) - viewmodel.messages.append(m) + viewModel.messages.remove(at: index) + tableViewDeleteIndexs([IndexPath(row: index, section: 0)]) + } + + viewModel.sendMessage(message: msg) { error in + if let err = error { + print("resend message error: \(err.localizedDescription)") + } } - viewmodel.resendMessage(message: msg) } } @@ -2829,25 +2965,25 @@ extension ChatViewController: ChatBaseCellDelegate { } if model?.type == .revoke, let message = model?.message, - message.messageType == .text || message.messageType == .custom { - if message.messageType == .custom { - guard let attach = NECustomAttachment.attachmentOfCustomMessage(message: message), - attach.customType == customRichTextType else { + message.messageType == .MESSAGE_TYPE_TEXT || message.messageType == .MESSAGE_TYPE_CUSTOM { + if message.messageType == .MESSAGE_TYPE_CUSTOM { + guard let customType = NECustomAttachment.typeOfCustomMessage(message.attachment), + customType == customRichTextType else { return } } - let data = NECustomAttachment.dataOfCustomMessage(message: model?.message) + let data = NECustomAttachment.dataOfCustomMessage(model?.message?.attachment) - let time = message.timestamp + let time = message.createTime let date = Date() let currentTime = date.timeIntervalSince1970 if currentTime - time >= 60 * 2 { showToast(chatLocalizable("editable_time_expired")) - didRefreshTable() + tableViewReload() return } - if message.remoteExt?[keyReplyMsgKey] != nil { - viewmodel.operationModel = model + if let remoteExt = getDictionaryFromJSONString(message.serverExtension ?? ""), remoteExt[keyReplyMsgKey] != nil { + viewModel.operationModel = model showReplyMessageView(isReEdit: true) } else { closeReply(button: nil) @@ -2855,21 +2991,22 @@ extension ChatViewController: ChatBaseCellDelegate { var attributeStr: NSMutableAttributedString? var text = "" - if message.messageType == .text, let txt = message.text { + if message.messageType == .MESSAGE_TYPE_TEXT, let txt = message.text { text = txt - } else if message.messageType == .custom, let body = data?["body"] as? String { + } else if message.messageType == .MESSAGE_TYPE_CUSTOM, let body = data?["body"] as? String { text = body } attributeStr = NSMutableAttributedString(string: text) attributeStr?.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)], range: NSMakeRange(0, text.utf16.count)) - if let remoteExt = message.remoteExt, let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { - dic.forEach { (key: String, value: AnyObject) in + if let remoteExt = getDictionaryFromJSONString(message.serverExtension ?? ""), + let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { + for (key, value) in dic { if let contentDic = value as? [String: AnyObject] { if let array = contentDic[atSegmentsKey] as? [AnyObject] { if let models = NSArray.yx_modelArray(with: MessageAtInfoModel.self, json: array) as? [MessageAtInfoModel] { - models.forEach { model in + for model in models { if var text = contentDic[atTextKey] as? String { if text.last == " " { text = String(text.prefix(text.count - 1)) @@ -2892,6 +3029,11 @@ extension ChatViewController: ChatBaseCellDelegate { // 标题填入 chatInputView.titleField.text = title expandButtonDidClick() + } else { + chatInputView.titleField.text = nil + if chatInputView.chatInpuMode != .normal { + didHideMultipleButtonClick() + } } chatInputView.textView.attributedText = attributeStr @@ -2904,21 +3046,27 @@ extension ChatViewController: ChatBaseCellDelegate { return } - if let msg = model?.message, msg.session?.sessionType == .team { - let readVC = getReadView(msg) + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + if let msg = model?.message, msg.conversationType == .CONVERSATION_TYPE_TEAM { + let readVC = getReadView(msg, viewModel.sessionId) navigationController?.pushViewController(readVC, animated: true) } } open func didTapSelectButton(_ cell: UITableViewCell, _ model: MessageContentModel?) { - viewmodel.selectedMessages.removeAll(where: { $0.messageId == model?.message?.messageId }) + viewModel.selectedMessages.removeAll(where: { $0.messageClientId == model?.message?.messageClientId }) if model?.isSelected == true, let msg = model?.message { - viewmodel.selectedMessages.append(msg) + viewModel.selectedMessages.append(msg) } } - open func getReadView(_ message: NIMMessage) -> NEBaseReadViewController { - ReadViewController(message: message) + open func getReadView(_ message: V2NIMMessage, _ teamId: String) -> NEBaseReadViewController { + ReadViewController(message: message, teamId: teamId) } open func loadDataFinish() {} @@ -2935,7 +3083,9 @@ extension ChatViewController: ChatBaseCellDelegate { open func expandButtonDidClick() { chatInputView.textView.resignFirstResponder() - scrollTableViewToBottom() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { + self.scrollTableViewToBottom() + })) operationView?.removeFromSuperview() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift index bf5be3c6..3c7316eb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift @@ -9,7 +9,7 @@ import UIKit @objcMembers open class MultiForwardViewController: ChatBaseViewController, UINavigationControllerDelegate, UITableViewDataSource, UITableViewDelegate, ChatBaseCellDelegate, UIDocumentInteractionControllerDelegate, MultiForwardViewModelDelegate { - public var viewmodel = MultiForwardViewModel() + public var viewModel = MultiForwardViewModel() private var messageAttachmentUrl: String? private var messageAttachmentFilePath: String = "" private var messageAttachmentMD5: String? @@ -27,7 +27,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewWillAppear(_ animated: Bool) { @@ -48,7 +48,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr override open func viewDidLoad() { super.viewDidLoad() - viewmodel.delegate = self + viewModel.delegate = self commonUI() loadData() } @@ -65,11 +65,11 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr forCellReuseIdentifier: "\(NEBaseChatMessageCell.self)" ) - NEChatUIKitClient.instance.getRegisterCustomCell().forEach { (key: String, value: UITableViewCell.Type) in + for (key, value) in NEChatUIKitClient.instance.getRegisterCustomCell() { cellRegisterDic[key] = value } - cellRegisterDic.forEach { (key: String, value: UITableViewCell.Type) in + for (key, value) in cellRegisterDic { tableView.register(value, forCellReuseIdentifier: key) } @@ -101,7 +101,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr func loadData() { view.makeToastActivity(.center) - viewmodel.loadData(messageAttachmentUrl, + viewModel.loadData(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) { [weak self] error in self?.view.hideToastActivity() @@ -121,7 +121,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr private func showErrorToast(_ error: Error?) { if let err = error as? NSError { switch err.code { - case noNetworkCode, -1009: + case protocolSendFailed, -1009: showToast(commonLocalizable("network_error")) default: showToast(err.localizedDescription) @@ -159,14 +159,12 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr // MARK: UITableViewDataSource, UITableViewDelegate open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let count = viewmodel.messages.count - print("numberOfRowsInSection count : ", count) - return count + viewModel.messages.count } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.messages[indexPath.row] + let model = viewModel.messages[indexPath.row] model.inMultiForward = true model.isPined = false var reuseId = "" @@ -176,20 +174,20 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr } else { let key = "\(model.type.rawValue)" if model.type == .custom { - if let attch = NECustomAttachment.attachmentOfCustomMessage(message: model.message) { - if attch.customType == customMultiForwardType { + if let customType = NECustomAttachment.typeOfCustomMessage(model.message?.attachment) { + if customType == customMultiForwardType { reuseId = "\(MessageType.multiForward.rawValue)" - } else if attch.customType == customRichTextType { + } else if customType == customRichTextType { reuseId = "\(MessageType.richText.rawValue)" - } else if NEChatUIKitClient.instance.getRegisterCustomCell()["\(attch.customType)"] != nil { - reuseId = "\(attch.customType)" + } else if NEChatUIKitClient.instance.getRegisterCustomCell()["\(customType)"] != nil { + reuseId = "\(customType)" } else { reuseId = "\(NEBaseChatMessageCell.self)" } } else { reuseId = "\(NEBaseChatMessageCell.self)" } - } else if model.type == .time || model.type == .notification || model.type == .tip { + } else if model.type == .notification || model.type == .tip { reuseId = "\(MessageType.time.rawValue)" } else if cellRegisterDic[key] != nil { reuseId = key @@ -222,7 +220,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - viewmodel.messages[indexPath.row].cellHeight() + chat_content_margin + viewModel.messages[indexPath.row].cellHeight() + chat_content_margin } open func getMultiForwardViewController(_ messageAttachmentUrl: String?, @@ -233,7 +231,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr // MARK: ChatBaseCellDelegate - open func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) { + open func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?, _ replyModel: MessageModel?) { if let tapClick = NEKitChatConfig.shared.ui.messageItemClick { tapClick(cell, model) return @@ -244,12 +242,12 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr return } - didTapMessage(cell, model) + didTapMessage(cell, model, nil) } open func didTapMessage(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ replyIndex: Int? = nil) { if model?.type == .image { - if let imageObject = model?.message?.messageObject as? NIMImageObject { + if let imageObject = model?.message?.attachment as? V2NIMMessageImageAttachment { var imageUrl = "" if let url = imageObject.url { @@ -261,112 +259,115 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr } if imageUrl.count > 0 { let showController = PhotoBrowserController( - urls: ChatMessageHelper.getUrls(messages: viewmodel.messages), + urls: ChatMessageHelper.getUrls(messages: viewModel.messages), url: imageUrl ) showController.modalPresentationStyle = .overFullScreen present(showController, animated: false, completion: nil) } } - } else if model?.type == .video, - let object = model?.message?.messageObject as? NIMVideoObject { - weak var weakSelf = self - let videoPlayer = VideoPlayerViewController() - videoPlayer.modalPresentationStyle = .overFullScreen - if let path = object.path, FileManager.default.fileExists(atPath: path) == true { + let object = model?.message?.attachment as? V2NIMMessageVideoAttachment { + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) + if FileManager.default.fileExists(atPath: path) { let url = URL(fileURLWithPath: path) + let videoPlayer = VideoPlayerViewController() + videoPlayer.modalPresentationStyle = .overFullScreen videoPlayer.videoUrl = url - videoPlayer.totalTime = object.duration - print("video url : ", videoPlayer.videoUrl as Any) + videoPlayer.totalTime = Int(object.duration) present(videoPlayer, animated: true, completion: nil) - } else if let urlString = object.url, let path = object.path, - let videoModel = model as? MessageVideoModel { - print("fetch message attachment") - if let index = replyIndex, index >= 0 { + } else { + if let index = replyIndex, index >= 0, + index < tableView.numberOfRows(inSection: 0) { tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) } - videoModel.state = .Downalod - if let videoCell = cell as? NEBaseChatMessageCell { - videoCell.setModel(videoModel, false) - } - - viewmodel.downLoad(urlString, path) { progress in - NELog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: #function + "CALLBACK downLoad: \(progress)") - - videoModel.progress = progress - if progress >= 1.0 { - videoModel.state = .Success - } - videoModel.cell?.uploadProgress(byRight: false, progress) - } _: { error in - weakSelf?.showErrorToast(error) - } + downloadFile(cell, model, object.url, path) } } else if model?.type == .location { if let locationModel = model as? MessageLocationModel, let lat = locationModel.lat, let lng = locationModel.lng { - let mapDetail = NEDetailMapController(type: .detail) - mapDetail.currentPoint = CGPoint(x: lat, y: lng) - mapDetail.locationTitle = locationModel.title - mapDetail.subTitle = locationModel.subTitle - navigationController?.pushViewController(mapDetail, animated: true) + var params = [String: Any]() + params["type"] = NEMapType.detail.rawValue + params["nav"] = navigationController + params["lat"] = lat + params["lng"] = lng + params["locationTitle"] = locationModel.title + params["subTitle"] = locationModel.subTitle + Router.shared.use(NERouterUrl.LocationVCRouter, parameters: params) } } else if model?.type == .file, - let object = model?.message?.messageObject as? NIMFileObject, - let path = object.path { + let object = model?.message?.attachment as? V2NIMMessageFileAttachment { + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) if !FileManager.default.fileExists(atPath: path) { - if let urlString = object.url, let path = object.path, - let fileModel = model as? MessageFileModel { - if let index = replyIndex, index >= 0 { - tableView.scrollToRow(at: IndexPath(row: index, section: 0), - at: .middle, - animated: true) - } - fileModel.state = .Downalod - if let fileCell = cell as? NEBaseChatMessageCell { - fileCell.setModel(fileModel, false) - } - - viewmodel.downLoad(urlString, path) { [weak self] progress in - NELog.infoLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + "downLoad file progress: \(progress)") - var newProgress = progress - if newProgress < 0 { - newProgress = abs(progress) / fileModel.size - } - fileModel.progress = newProgress - if newProgress >= 1.0 { - fileModel.state = .Success - } - fileModel.cell?.uploadProgress(byRight: false, newProgress) - - } _: { [weak self] error in - self?.showErrorToast(error) - } + if let index = replyIndex, index >= 0, + index < tableView.numberOfRows(inSection: 0) { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), + at: .middle, + animated: true) } + downloadFile(cell, model, object.url, path) } else { let url = URL(fileURLWithPath: path) interactionController.url = url interactionController.delegate = self // UIDocumentInteractionControllerDelegate - if interactionController.presentPreview(animated: true) {} else { + if interactionController.presentPreview(animated: true) {} + else { interactionController.presentOptionsMenu(from: view.bounds, in: view, animated: true) } } - } else if model?.type == .custom, let attach = NECustomAttachment.attachmentOfCustomMessage(message: model?.message) { - if attach.customType == customMultiForwardType, - let data = NECustomAttachment.dataOfCustomMessage(message: model?.message) { + } else if model?.type == .custom, + let customType = NECustomAttachment.typeOfCustomMessage(model?.message?.attachment) { + if customType == customMultiForwardType, + let data = NECustomAttachment.dataOfCustomMessage(model?.message?.attachment) { let url = data["url"] as? String let md5 = data["md5"] as? String - guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } - let fileName = multiForwardFileName + (model?.message?.messageId ?? "") - let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(fileName)").relativePath + guard let fileDirectory = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit/") else { return } + let fileName = multiForwardFileName + (model?.message?.messageClientId ?? "") + let filePath = fileDirectory + fileName let multiForwardVC = getMultiForwardViewController(url, filePath, md5) navigationController?.pushViewController(multiForwardVC, animated: true) } } else { print(#function + "message did tap, type:\(String(describing: model?.type.rawValue))") } + + /// 下载附件(文件、视频消息) + /// - Parameters: + /// - cell: 当前单元格 + /// - model: 消息模型 + /// - url: 远端下载链接 + /// - path: 本地保存路径 + func downloadFile(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ url: String?, _ path: String) { + // 判断是否是视频或者文件对象 + guard let urlString = url, let fileModel = model as? MessageVideoModel else { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "MessageFileModel not exit") + return + } + + // 判断状态,如果是下载中不能进行预览 + if fileModel.state == .Downalod { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "downLoad state, click ingore") + return + } + + fileModel.state = .Downalod + if let fileCell = cell as? NEBaseChatMessageCell { + fileCell.setModel(fileModel, false) + } + + viewModel.downLoad(urlString, path) { progress in + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "downLoad file progress: \(progress)") + fileModel.progress = progress + if progress >= 100 { + fileModel.state = .Success + } + fileModel.cell?.uploadProgress(byRight: false, progress) + + } _: { [weak self] _, error in + self?.showErrorToast(error) + } + } } // MARK: ChatBaseCellDelegate ignore protocol diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift index f19da8fd..1f2fb891 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift @@ -17,13 +17,13 @@ open class ForwardItem: NSObject { @objcMembers open class NEBaseForwardUserCell: UICollectionViewCell { - lazy var userHeader: NEUserHeaderView = { - let header = NEUserHeaderView(frame: .zero) - header.translatesAutoresizingMaskIntoConstraints = false - header.titleLabel.font = NEConstant.defaultTextFont(11.0) - header.clipsToBounds = true - header.accessibilityIdentifier = "id.forwardHeaderView" - return header + public lazy var userHeaderView: NEUserHeaderView = { + let headerView = NEUserHeaderView(frame: .zero) + headerView.translatesAutoresizingMaskIntoConstraints = false + headerView.titleLabel.font = NEConstant.defaultTextFont(11.0) + headerView.clipsToBounds = true + headerView.accessibilityIdentifier = "id.forwardHeaderView" + return headerView }() override public init(frame: CGRect) { @@ -36,12 +36,12 @@ open class NEBaseForwardUserCell: UICollectionViewCell { } func setupUI() { - contentView.addSubview(userHeader) + contentView.addSubview(userHeaderView) NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: contentView.leftAnchor), - userHeader.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - userHeader.widthAnchor.constraint(equalToConstant: 32.0), - userHeader.heightAnchor.constraint(equalToConstant: 32.0), + userHeaderView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + userHeaderView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + userHeaderView.widthAnchor.constraint(equalToConstant: 32.0), + userHeaderView.heightAnchor.constraint(equalToConstant: 32.0), ]) } } @@ -57,50 +57,50 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD var type = chatLocalizable("operation_forward") // 合并转发/逐条转发/转发 var context = "" - public let sureBtn = UIButton() - public let tip = UILabel() + public let sureButton = UIButton() + public let tipLabel = UILabel() public var contentViewCenterYAnchor: NSLayoutConstraint? - lazy var userCollection: UICollectionView = { - let flow = UICollectionViewFlowLayout() - flow.scrollDirection = .horizontal - flow.minimumLineSpacing = 9.5 - flow.minimumInteritemSpacing = 9.5 - let collection = UICollectionView(frame: .zero, collectionViewLayout: flow) - collection.translatesAutoresizingMaskIntoConstraints = false - collection.delegate = self - collection.dataSource = self - collection.backgroundColor = .clear - collection.showsHorizontalScrollIndicator = false - return collection + public lazy var userCollectionView: UICollectionView = { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + flowLayout.minimumLineSpacing = 9.5 + flowLayout.minimumInteritemSpacing = 9.5 + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.backgroundColor = .clear + collectionView.showsHorizontalScrollIndicator = false + return collectionView }() - lazy var contentView: UIView = { - let back = UIView() - back.backgroundColor = .white - back.translatesAutoresizingMaskIntoConstraints = false - back.clipsToBounds = true - back.layer.cornerRadius = 8.0 - return back + public lazy var contentView: UIView = { + let backView = UIView() + backView.backgroundColor = .white + backView.translatesAutoresizingMaskIntoConstraints = false + backView.clipsToBounds = true + backView.layer.cornerRadius = 8.0 + return backView }() - lazy var oneUserHead: NEUserHeaderView = { - let header = NEUserHeaderView(frame: .zero) - header.clipsToBounds = true - header.translatesAutoresizingMaskIntoConstraints = false - header.accessibilityIdentifier = "id.forwardHeaderView" - return header + public lazy var oneUserHeadView: NEUserHeaderView = { + let headerView = NEUserHeaderView(frame: .zero) + headerView.clipsToBounds = true + headerView.translatesAutoresizingMaskIntoConstraints = false + headerView.accessibilityIdentifier = "id.forwardHeaderView" + return headerView }() - lazy var oneUserName: UILabel = { - let name = UILabel() - name.textColor = .ne_darkText - name.font = NEConstant.defaultTextFont(14.0) - name.translatesAutoresizingMaskIntoConstraints = false - return name + public lazy var oneUserNameLabel: UILabel = { + let nameLabel = UILabel() + nameLabel.textColor = .ne_darkText + nameLabel.font = NEConstant.defaultTextFont(14.0) + nameLabel.translatesAutoresizingMaskIntoConstraints = false + return nameLabel }() - lazy var contentText: UILabel = { + public lazy var contentLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(14.0) @@ -112,7 +112,7 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD }() // 留言 - lazy var commentTextFeild: UITextField = { + public lazy var commentTextFeild: UITextField = { let textFeild = UITextField() textFeild.translatesAutoresizingMaskIntoConstraints = false textFeild.placeholder = chatLocalizable("leave_message") @@ -155,78 +155,78 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD view.backgroundColor = NEConstant.hexRGB(0x000000).withAlphaComponent(0.4) view.addSubview(contentView) contentViewCenterYAnchor = contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor) + contentViewCenterYAnchor?.isActive = true NSLayoutConstraint.activate([ contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - contentViewCenterYAnchor!, contentView.widthAnchor.constraint(equalToConstant: 276), contentView.heightAnchor.constraint(equalToConstant: 250), ]) - tip.translatesAutoresizingMaskIntoConstraints = false - tip.font = NEConstant.defaultTextFont(16.0) - tip.textColor = .ne_darkText - tip.text = chatLocalizable("send_to") - tip.accessibilityIdentifier = "id.forwardTitle" - contentView.addSubview(tip) + tipLabel.translatesAutoresizingMaskIntoConstraints = false + tipLabel.font = NEConstant.defaultTextFont(16.0) + tipLabel.textColor = .ne_darkText + tipLabel.text = chatLocalizable("send_to") + tipLabel.accessibilityIdentifier = "id.forwardTitle" + contentView.addSubview(tipLabel) NSLayoutConstraint.activate([ - tip.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), - tip.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), - tip.heightAnchor.constraint(equalToConstant: 18.0), + tipLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), + tipLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + tipLabel.heightAnchor.constraint(equalToConstant: 18.0), ]) - contentView.addSubview(oneUserHead) + contentView.addSubview(oneUserHeadView) NSLayoutConstraint.activate([ - oneUserHead.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), - oneUserHead.topAnchor.constraint(equalTo: tip.bottomAnchor, constant: 16), - oneUserHead.widthAnchor.constraint(equalToConstant: 32.0), - oneUserHead.heightAnchor.constraint(equalToConstant: 32.0), + oneUserHeadView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + oneUserHeadView.topAnchor.constraint(equalTo: tipLabel.bottomAnchor, constant: 16), + oneUserHeadView.widthAnchor.constraint(equalToConstant: 32.0), + oneUserHeadView.heightAnchor.constraint(equalToConstant: 32.0), ]) - contentView.addSubview(oneUserName) + contentView.addSubview(oneUserNameLabel) NSLayoutConstraint.activate([ - oneUserName.leftAnchor.constraint(equalTo: oneUserHead.rightAnchor, constant: 8.0), - oneUserName.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16.0), - oneUserName.centerYAnchor.constraint(equalTo: oneUserHead.centerYAnchor), + oneUserNameLabel.leftAnchor.constraint(equalTo: oneUserHeadView.rightAnchor, constant: 8.0), + oneUserNameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16.0), + oneUserNameLabel.centerYAnchor.constraint(equalTo: oneUserHeadView.centerYAnchor), ]) - contentView.addSubview(userCollection) + contentView.addSubview(userCollectionView) NSLayoutConstraint.activate([ - userCollection.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), - userCollection.rightAnchor.constraint( + userCollectionView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), + userCollectionView.rightAnchor.constraint( equalTo: contentView.rightAnchor, constant: -16.0 ), - userCollection.heightAnchor.constraint(equalToConstant: 32.0), - userCollection.topAnchor.constraint(equalTo: oneUserHead.topAnchor), + userCollectionView.heightAnchor.constraint(equalToConstant: 32.0), + userCollectionView.topAnchor.constraint(equalTo: oneUserHeadView.topAnchor), ]) - let textBack = UIView() - textBack.translatesAutoresizingMaskIntoConstraints = false - textBack.backgroundColor = NEConstant.hexRGB(0xF2F4F5) - textBack.clipsToBounds = true - textBack.layer.cornerRadius = 4.0 - contentView.addSubview(textBack) + let textBackView = UIView() + textBackView.translatesAutoresizingMaskIntoConstraints = false + textBackView.backgroundColor = NEConstant.hexRGB(0xF2F4F5) + textBackView.clipsToBounds = true + textBackView.layer.cornerRadius = 4.0 + contentView.addSubview(textBackView) NSLayoutConstraint.activate([ - textBack.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), - textBack.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16.0), - textBack.topAnchor.constraint(equalTo: oneUserHead.bottomAnchor, constant: 12.0), + textBackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), + textBackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16.0), + textBackView.topAnchor.constraint(equalTo: oneUserHeadView.bottomAnchor, constant: 12.0), ]) - textBack.addSubview(contentText) + textBackView.addSubview(contentLabel) NSLayoutConstraint.activate([ - contentText.leftAnchor.constraint(equalTo: textBack.leftAnchor, constant: 12), - contentText.rightAnchor.constraint(equalTo: textBack.rightAnchor, constant: -12), - contentText.topAnchor.constraint(equalTo: textBack.topAnchor, constant: 7), - contentText.bottomAnchor.constraint(equalTo: textBack.bottomAnchor, constant: -7), + contentLabel.leftAnchor.constraint(equalTo: textBackView.leftAnchor, constant: 12), + contentLabel.rightAnchor.constraint(equalTo: textBackView.rightAnchor, constant: -12), + contentLabel.topAnchor.constraint(equalTo: textBackView.topAnchor, constant: 7), + contentLabel.bottomAnchor.constraint(equalTo: textBackView.bottomAnchor, constant: -7), ]) - contentText.text = "[\(type)]\(context)的会话记录" + contentLabel.text = "[\(type)]\(context)的会话记录" // 留言 contentView.addSubview(commentTextFeild) NSLayoutConstraint.activate([ - commentTextFeild.leftAnchor.constraint(equalTo: textBack.leftAnchor), - commentTextFeild.rightAnchor.constraint(equalTo: textBack.rightAnchor), - commentTextFeild.topAnchor.constraint(equalTo: textBack.bottomAnchor, constant: 16), + commentTextFeild.leftAnchor.constraint(equalTo: textBackView.leftAnchor), + commentTextFeild.rightAnchor.constraint(equalTo: textBackView.rightAnchor), + commentTextFeild.topAnchor.constraint(equalTo: textBackView.bottomAnchor, constant: 16), commentTextFeild.heightAnchor.constraint(equalToConstant: 32), ]) @@ -252,33 +252,33 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD horizontalLine.bottomAnchor.constraint(equalTo: verticalLine.topAnchor), ]) - let canceBtn = UIButton() - canceBtn.translatesAutoresizingMaskIntoConstraints = false - canceBtn.addTarget(self, action: #selector(cancelClick), for: .touchUpInside) - canceBtn.setTitle(chatLocalizable("cancel"), for: .normal) - canceBtn.setTitleColor(.ne_greyText, for: .normal) - canceBtn.accessibilityIdentifier = "id.forwardCancelBtn" + let canceButton = UIButton() + canceButton.translatesAutoresizingMaskIntoConstraints = false + canceButton.addTarget(self, action: #selector(cancelClick), for: .touchUpInside) + canceButton.setTitle(chatLocalizable("cancel"), for: .normal) + canceButton.setTitleColor(.ne_greyText, for: .normal) + canceButton.accessibilityIdentifier = "id.forwardCancelBtn" - sureBtn.translatesAutoresizingMaskIntoConstraints = false - sureBtn.addTarget(self, action: #selector(sureClick), for: .touchUpInside) - sureBtn.setTitle(chatLocalizable("send"), for: .normal) - sureBtn.setTitleColor(.ne_blueText, for: .normal) - sureBtn.accessibilityIdentifier = "id.forwardSendBtn" + sureButton.translatesAutoresizingMaskIntoConstraints = false + sureButton.addTarget(self, action: #selector(sureClick), for: .touchUpInside) + sureButton.setTitle(chatLocalizable("send"), for: .normal) + sureButton.setTitleColor(.ne_blueText, for: .normal) + sureButton.accessibilityIdentifier = "id.forwardSendBtn" - contentView.addSubview(canceBtn) + contentView.addSubview(canceButton) NSLayoutConstraint.activate([ - canceBtn.leftAnchor.constraint(equalTo: contentView.leftAnchor), - canceBtn.bottomAnchor.constraint(equalTo: verticalLine.bottomAnchor), - canceBtn.topAnchor.constraint(equalTo: horizontalLine.bottomAnchor), - canceBtn.rightAnchor.constraint(equalTo: verticalLine.leftAnchor), + canceButton.leftAnchor.constraint(equalTo: contentView.leftAnchor), + canceButton.bottomAnchor.constraint(equalTo: verticalLine.bottomAnchor), + canceButton.topAnchor.constraint(equalTo: horizontalLine.bottomAnchor), + canceButton.rightAnchor.constraint(equalTo: verticalLine.leftAnchor), ]) - contentView.addSubview(sureBtn) + contentView.addSubview(sureButton) NSLayoutConstraint.activate([ - sureBtn.bottomAnchor.constraint(equalTo: verticalLine.bottomAnchor), - sureBtn.rightAnchor.constraint(equalTo: contentView.rightAnchor), - sureBtn.topAnchor.constraint(equalTo: horizontalLine.bottomAnchor), - sureBtn.leftAnchor.constraint(equalTo: verticalLine.rightAnchor), + sureButton.bottomAnchor.constraint(equalTo: verticalLine.bottomAnchor), + sureButton.rightAnchor.constraint(equalTo: contentView.rightAnchor), + sureButton.topAnchor.constraint(equalTo: horizontalLine.bottomAnchor), + sureButton.leftAnchor.constraint(equalTo: verticalLine.rightAnchor), ]) } @@ -305,24 +305,24 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD if datas.count == 1 { let item = datas[0] if let name = item.name { - oneUserHead.setTitle(name) - oneUserName.text = name + oneUserHeadView.setTitle(name) + oneUserNameLabel.text = name } else if let uid = item.uid { - oneUserHead.setTitle(uid) - oneUserName.text = uid + oneUserHeadView.setTitle(uid) + oneUserNameLabel.text = uid } if let url = item.avatar, !url.isEmpty { - oneUserHead.sd_setImage(with: URL(string: url), completed: nil) - oneUserHead.titleLabel.text = "" - oneUserHead.backgroundColor = .clear + oneUserHeadView.sd_setImage(with: URL(string: url), completed: nil) + oneUserHeadView.titleLabel.text = "" + oneUserHeadView.backgroundColor = .clear } else { - oneUserHead.backgroundColor = UIColor.colorWithString(string: item.uid) - oneUserHead.image = nil + oneUserHeadView.backgroundColor = UIColor.colorWithString(string: item.uid) + oneUserHeadView.image = nil } - userCollection.isHidden = true + userCollectionView.isHidden = true } else { - oneUserHead.isHidden = true - oneUserName.isHidden = true + oneUserHeadView.isHidden = true + oneUserNameLabel.isHidden = true } } @@ -360,16 +360,16 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD open func setCellModel(cell: NEBaseForwardUserCell, indexPath: IndexPath) -> UICollectionViewCell { let item = datas[indexPath.row] if let url = item.avatar, !url.isEmpty { - cell.userHeader.sd_setImage(with: URL(string: url), completed: nil) - cell.userHeader.titleLabel.text = "" - cell.userHeader.backgroundColor = .clear + cell.userHeaderView.sd_setImage(with: URL(string: url), completed: nil) + cell.userHeaderView.titleLabel.text = "" + cell.userHeaderView.backgroundColor = .clear } else { - cell.userHeader.backgroundColor = UIColor.colorWithString(string: item.uid) - cell.userHeader.image = nil + cell.userHeaderView.backgroundColor = UIColor.colorWithString(string: item.uid) + cell.userHeaderView.image = nil if let name = item.name { - cell.userHeader.setTitle(name) + cell.userHeaderView.setTitle(name) } else if let uid = item.uid { - cell.userHeader.setTitle(uid) + cell.userHeaderView.setTitle(uid) } } return cell diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift index c14e7c20..0e9d1eb7 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift @@ -4,36 +4,48 @@ import NEChatKit import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit let PinMessageDefaultType = 1000 @objcMembers -open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDataSource, UITableViewDelegate, PinMessageViewModelDelegate, PinMessageCellDelegate, UIDocumentInteractionControllerDelegate, NIMMediaManagerDelegate { - let viewmodel = PinMessageViewModel() +open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDataSource, UITableViewDelegate, PinMessageViewModelDelegate, PinMessageCellDelegate, UIDocumentInteractionControllerDelegate, NIMMediaManagerDelegate, NEIMKitClientListener { + let viewModel = PinMessageViewModel() - var session: NIMSession + /// 会话id + var conversationId: String? + /// 样式注册表 var cellClassDic: [String: NEBasePinMessageCell.Type] = [:] - // pin 列表内容最大宽度 + /// pin 列表内容最大宽度 public var pin_content_maxW = (kScreenWidth - 72) - + /// 正在播放的cell var playingCell: NEBasePinMessageAudioCell? var playingModel: MessageAudioModel? - public init(session: NIMSession) { - self.session = session + // 网络断开标志 + var networkBroken = false + + // 数据拉取标志 + var isLoadingData = false + + /// 初始化 + /// - Parameter conversationId: 会话id + public init(conversationId: String) { + self.conversationId = conversationId super.init(nibName: nil, bundle: nil) NIMSDK.shared().mediaManager.add(self) + IMKitClient.instance.addLoginListener(self) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } deinit { NIMSDK.shared().mediaManager.remove(self) + IMKitClient.instance.removeLoginListener(self) } private lazy var tableView: UITableView = { @@ -64,7 +76,7 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa override open func viewDidLoad() { super.viewDidLoad() - viewmodel.delegate = self + viewModel.delegate = self setupUI() loadData() } @@ -76,23 +88,29 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa func loadData() { weak var weakSelf = self - viewmodel.getPinitems(session: session) { error in + guard let cid = conversationId else { + return + } + + isLoadingData = true + viewModel.getPinitems(conversationId: cid) { error in + weakSelf?.isLoadingData = false if let err = error as? NSError { - if weakSelf?.session.sessionType == .team, err.code == 414 { + if V2NIMConversationIdUtil.conversationType(weakSelf?.conversationId ?? "") == .CONVERSATION_TYPE_TEAM, err.code == teamNotExistCode { weakSelf?.showToast(chatLocalizable("team_not_exist")) + } else if err.code == protocolTimeout { + weakSelf?.showToast(commonLocalizable("network_error")) } else { weakSelf?.showToast(err.localizedDescription) } } else { - weakSelf?.viewmodel.items.forEach { model in - ChatMessageHelper.downloadAudioFile(message: model.message) - } - weakSelf?.emptyView.isHidden = (weakSelf?.viewmodel.items.count ?? 0) > 0 + weakSelf?.emptyView.isHidden = (weakSelf?.viewModel.items.count ?? 0) > 0 weakSelf?.tableView.reloadData() } } } + /// UI 初始化 func setupUI() { title = chatLocalizable("operation_pin") navigationView.navTitle.text = chatLocalizable("operation_pin") @@ -114,50 +132,56 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa ]) cellClassDic = getRegisterCellDic() tableView.register(NEBasePinMessageTextCell.self, forCellReuseIdentifier: "\(NEBasePinMessageTextCell.self)") - cellClassDic.forEach { (key: String, value: NEBasePinMessageCell.Type) in + for (key, value) in cellClassDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ - + /// 列表点击回调 + /// - Parameter tableView: 列表视图对象 + /// - Parameter indexPath: 点击的索引 open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let item = viewmodel.items[indexPath.row] - if item.message.session?.sessionType == .P2P { - let session = item.message.session + if indexPath.row >= viewModel.items.count { + return + } + + let item = viewModel.items[indexPath.row] + if item.message.conversationType == .CONVERSATION_TYPE_P2P { + let conversationId = item.message.conversationId Router.shared.use( PushP2pChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any, + parameters: ["nav": navigationController as Any, + "conversationId": conversationId as Any, "anchor": item.message], closure: nil ) - } else if item.message.session?.sessionType == .team { - let session = item.message.session + } else if item.message.conversationType == .CONVERSATION_TYPE_TEAM { + let conversationId = item.message.conversationId Router.shared.use( PushTeamChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any, + parameters: ["nav": navigationController as Any, + "conversationId": conversationId as Any, "anchor": item.message], closure: nil ) } } + /// 列表数据绑定 + /// - parameter tableView: 列表视图对象 + /// - parameter indexPath: 索引 open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.items[indexPath.row] + if indexPath.row >= viewModel.items.count { + return UITableViewCell() + } + + let model = viewModel.items[indexPath.row] var reuseId = "\(model.chatmodel.type.rawValue)" if model.chatmodel.type == .custom, - let attach = NECustomAttachment.attachmentOfCustomMessage(message: model.chatmodel.message) { - if attach.customType == customMultiForwardType { + let customType = NECustomAttachment.typeOfCustomMessage(model.chatmodel.message?.attachment) { + if customType == customMultiForwardType { reuseId = "\(MessageType.multiForward.rawValue)" - } else if attach.customType == customRichTextType { + } else if customType == customRichTextType { reuseId = "\(MessageType.richText.rawValue)" } else { reuseId = "\(NEBasePinMessageTextCell.self)" @@ -171,42 +195,53 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa return cell } + /// 列表行数 + /// - parameter tableView: 列表视图对象 + /// - parameter section: 索引 + /// - returns: 行数 open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewmodel.items.count + viewModel.items.count } + /// 列表行高 + /// - parameter tableView: 列表视图对象 + /// - parameter indexPath: 索引 + /// - returns: 行高 open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.items[indexPath.row] - if let attach = NECustomAttachment.attachmentOfCustomMessage(message: model.message) { - if attach.customType == customMultiForwardType { + if indexPath.row >= viewModel.items.count { + return 0 + } + + let model = viewModel.items[indexPath.row] + if let customType = NECustomAttachment.typeOfCustomMessage(model.message.attachment) { + if customType == customMultiForwardType { return model.cellHeight(pinContentMaxW: pin_content_maxW) - 30 } } return model.cellHeight(pinContentMaxW: pin_content_maxW) } - func cancelPinActionClicked(item: PinMessageModel) { + /// 取消pin消息 + /// - Parameter item: pin消息对象 + open func cancelPinActionClicked(item: NEPinMessageModel) { weak var weakSelf = self if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { weakSelf?.showToast(commonLocalizable("network_error")) return } - weakSelf?.viewmodel.removePinMessage(item.message) { error, model in + weakSelf?.viewModel.removePinMessage(item.message) { error in if let err = error { - // weakSelf?.showToast(err.localizedDescription) + print(err.localizedDescription) } else { - if let index = weakSelf?.viewmodel.items.firstIndex(of: item) { - NotificationCenter.default.post(name: Notification.Name(removePinMessageNoti), object: item.message) - weakSelf?.viewmodel.items.remove(at: index) - weakSelf?.emptyView.isHidden = (weakSelf?.viewmodel.items.count ?? 0) > 0 - weakSelf?.tableView.reloadData() - weakSelf?.showToast(chatLocalizable("cancel_pin_success")) - } + weakSelf?.emptyView.isHidden = (weakSelf?.viewModel.items.count ?? 0) > 0 + weakSelf?.showToast(chatLocalizable("cancel_pin_success")) } } } - func copyActionClicked(item: PinMessageModel) { + /// 拷贝文本消息 + /// - Parameter item: pin消息对象 + func copyActionClicked(item: NEPinMessageModel) { weak var weakSelf = self let text = item.message.text let pasteboard = UIPasteboard.general @@ -214,7 +249,9 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa weakSelf?.view.makeToast(chatLocalizable("copy_success"), duration: 2, position: .center) } - func forwardActionClicked(item: PinMessageModel) { + /// 转发消息 + /// - Parameter item: pin消息对象 + func forwardActionClicked(item: NEPinMessageModel) { weak var weakSelf = self if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { weakSelf?.showToast(commonLocalizable("network_error")) @@ -223,7 +260,9 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa weakSelf?.forwardMessage(item.message) } - open func showAction(item: PinMessageModel) { + /// 弹出底操作悬浮框 + /// - Parameter item: pin消息对象 + open func showAction(item: NEPinMessageModel) { var actions = [UIAlertAction]() weak var weakSelf = self let cancelPinAction = UIAlertAction(title: chatLocalizable("operation_cancel_pin"), style: .default) { _ in @@ -231,14 +270,14 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa } actions.append(cancelPinAction) - if item.message.messageType == .text { + if item.message.messageType == .MESSAGE_TYPE_TEXT { let copyAction = UIAlertAction(title: chatLocalizable("operation_copy"), style: .default) { _ in weakSelf?.copyActionClicked(item: item) } actions.append(copyAction) } - if item.message.messageType != .audio { + if item.message.messageType != .MESSAGE_TYPE_AUDIO { let forwardAction = UIAlertAction(title: chatLocalizable("operation_forward"), style: .default) { _ in weakSelf?.forwardActionClicked(item: item) } @@ -255,25 +294,28 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa NEBaseForwardAlertViewController() } - func forwardMessageToUser(_ message: NIMMessage) { + /// 转发消息到单聊 + /// - Parameter message: 转发消息体 + func forwardMessageToUser(_ message: V2NIMMessage) { weak var weakSelf = self Router.shared.register(ContactSelectedUsersRouter) { param in print("user setting accids : ", param) var items = [ForwardItem]() - if let users = param["im_user"] as? [NIMUser] { - users.forEach { user in + if let users = param["im_user"] as? [V2NIMUser] { + for user in users { let item = ForwardItem() - item.uid = user.userId - item.avatar = user.userInfo?.avatarUrl - item.name = user.getShowName() + item.uid = user.accountId + item.avatar = user.avatar + item.name = user.name items.append(item) } let forwardAlert = weakSelf!.getForwardAlertController() forwardAlert.setItems(items) - if let session = self.viewmodel.session { - forwardAlert.context = ChatMessageHelper.getSessionName(session: session) + if let session = weakSelf?.viewModel.conversationId { + let content = ChatMessageHelper.getSessionName(conversationId: session) + forwardAlert.context = content } weakSelf?.addChild(forwardAlert) weakSelf?.view.addSubview(forwardAlert.view) @@ -283,9 +325,9 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa weakSelf?.showToast(commonLocalizable("network_error")) return } - weakSelf?.viewmodel.forwardUserMessage(message, users, comment) { error in + weakSelf?.viewModel.forwardUserMessage(message, users, comment) { result, error, progress in if let err = error as? NSError { - if err.code == noNetworkCode { + if err.code == protocolSendFailed { weakSelf?.showToast(commonLocalizable("network_error")) } else { weakSelf?.showToast(err.localizedDescription) @@ -298,31 +340,40 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa var param = [String: Any]() param["nav"] = weakSelf?.navigationController as Any param["limit"] = 6 + + // 标记列表-转发-人员选择页面不包含自己 + var filters = Set() + filters.insert(IMKitClient.instance.account()) + param["filters"] = filters + Router.shared.use(ContactUserSelectRouter, parameters: param, closure: nil) } - func forwardMessageToTeam(_ message: NIMMessage) { + /// 转发消息到群聊 + /// - Parameter message: 消息对象 + func forwardMessageToTeam(_ message: V2NIMMessage) { weak var weakSelf = self Router.shared.register(ContactTeamDataRouter) { param in - if let team = param["team"] as? NIMTeam { + if let team = param["team"] as? V2NIMTeam { let item = ForwardItem() - item.avatar = team.avatarUrl + item.avatar = team.avatar item.name = team.getShowName() item.uid = team.teamId let forwardAlert = weakSelf!.getForwardAlertController() forwardAlert.setItems([item]) - if let session = self.viewmodel.session { - forwardAlert.context = ChatMessageHelper.getSessionName(session: session) + if let conversationId = weakSelf?.viewModel.conversationId { + let content = ChatMessageHelper.getSessionName(conversationId: conversationId) + forwardAlert.context = content } forwardAlert.sureBlock = { comment in if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { weakSelf?.showToast(commonLocalizable("network_error")) return } - weakSelf?.viewmodel.forwardTeamMessage(message, team, comment) { error in + weakSelf?.viewModel.forwardTeamMessage(message, team, comment) { result, error, progress in if let err = error as? NSError { - if err.code == noNetworkCode { + if err.code == protocolSendFailed { weakSelf?.showToast(commonLocalizable("network_error")) } else { weakSelf?.showToast(err.localizedDescription) @@ -343,7 +394,7 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa ) } - open func forwardMessage(_ message: NIMMessage) { + open func forwardMessage(_ message: V2NIMMessage) { if IMKitClient.instance.getConfigCenter().teamEnable { weak var weakSelf = self let userAction = UIAlertAction(title: chatLocalizable("contact_user"), @@ -365,132 +416,241 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa } } - // MARK: PinMessageViewModelDelegate + open func tableViewReload(needLoad: Bool) { + if needLoad { + loadData() + } + tableView.reloadData() + } + + /// 刷新数据 + /// - Parameter model: 标记数据模型 + public func refreshModel(_ model: NEPinMessageModel) { + var index = -1 + for (i, item) in viewModel.items.enumerated() { + if item == model { + viewModel.items[i] = model + index = i + break + } + } - open func didNeedRefreshUI() { - loadData() + if index < 0 || index >= tableView.numberOfRows(inSection: 0) { + return + } + tableViewReload([IndexPath(row: index, section: 0)]) + } + + public func tableViewReload(_ indexs: [IndexPath]) { + tableView.reloadData(indexs) } - // MARK: PinMessageCellDelegate + public func tableViewDelete(_ indexs: [IndexPath]) { + if isLoadingData { + return + } - open func didClickMore(_ model: PinMessageModel?) { + let indexs = indexs.filter { $0.row >= 0 && $0.row < viewModel.items.count } + + if !indexs.isEmpty { + tableView.deleteData(indexs) + emptyView.isHidden = viewModel.items.count > 0 + } + } + + open func didClickMore(_ model: NEPinMessageModel?) { if let item = model { showAction(item: item) } } - open func didClickContent(_ model: PinMessageModel?, _ cell: NEBasePinMessageCell) { - NELog.infoLog(className(), desc: #function + "didClickContent") + /// 跳转视图显示控件 + /// - Parameter model: 标记对象 + open func toImageView(_ model: NEPinMessageModel?) { + if let object = model?.message.attachment as? V2NIMMessageImageAttachment { + var imageUrlString = "" - if model?.message.messageType == .audio { - startPlay(cell: cell, model: model) - } else if model?.message.messageType == .image { - if let imageObject = model?.message.messageObject as? NIMImageObject { - var imageUrl = "" + if let url = object.url { + imageUrlString = url - if let url = imageObject.url { - imageUrl = url - } else { - if let path = imageObject.path, FileManager.default.fileExists(atPath: path) { - imageUrl = path - } - } - if imageUrl.count > 0 { - let showController = PhotoBrowserController( - urls: [imageUrl], - url: imageUrl - ) - showController.modalPresentationStyle = .overFullScreen - present(showController, animated: false, completion: nil) + } else { + if let path = object.path, FileManager.default.fileExists(atPath: path) { + imageUrlString = path } } - } else if model?.message.messageType == .video, - let object = model?.message.messageObject as? NIMVideoObject { + + if imageUrlString.count > 0 { + let showController = PhotoBrowserController(urls: [imageUrlString], url: imageUrlString) + showController.modalPresentationStyle = .overFullScreen + present(showController, animated: false, completion: nil) + } + } + } + + /// 跳转视频播放器 + /// - Parameter model: 标记对象 + public func toVideoView(_ model: NEPinMessageModel?) { + if let object = model?.message.attachment as? V2NIMMessageVideoAttachment { stopPlay() - let videoPlayer = VideoPlayerViewController() - videoPlayer.modalPresentationStyle = .overFullScreen - videoPlayer.totalTime = object.duration + let player = VideoPlayerViewController() + player.totalTime = Int(object.duration) + player.modalPresentationStyle = .overFullScreen - if let path = object.path, FileManager.default.fileExists(atPath: path) == true { + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) + if FileManager.default.fileExists(atPath: path) == true { let url = URL(fileURLWithPath: path) - videoPlayer.videoUrl = url - present(videoPlayer, animated: true, completion: nil) - + player.videoUrl = url + present(player, animated: true, completion: nil) } else if let url = object.url, let remoteUrl = URL(string: url) { - videoPlayer.videoUrl = remoteUrl - present(videoPlayer, animated: true, completion: nil) + player.videoUrl = remoteUrl + present(player, animated: true, completion: nil) } + } + } - } else if model?.message.messageType == .text { - showTextViewController(model) - } else if model?.message.messageType == .location, let title = model?.message.text, - let locationObject = model?.message.messageObject as? NIMLocationObject { - let lat = locationObject.latitude + /// 跳转地图详情页 + /// - Parameter model: 标记对象 + public func toMapDetail(_ model: NEPinMessageModel?) { + if let title = model?.message.text, let locationObject = model?.message.attachment as? V2NIMMessageLocationAttachment { let lng = locationObject.longitude - let subTitle = locationObject.title - - let mapDetail = NEDetailMapController(type: .detail) - mapDetail.currentPoint = CGPoint(x: lat, y: lng) - mapDetail.locationTitle = title - mapDetail.subTitle = subTitle - navigationController?.pushViewController(mapDetail, animated: true) - } else if model?.message.messageType == .file, - let object = model?.message.messageObject as? NIMFileObject, - let path = object.path { + + let subTitle = locationObject.address + + let lat = locationObject.latitude + + var params = [String: Any]() + // 路由参数 + params["nav"] = navigationController + + params["type"] = NEMapType.detail.rawValue + + params["locationTitle"] = title + + params["subTitle"] = subTitle + + params["lat"] = lat + + params["lng"] = lng + + // 调用路由 + Router.shared.use(NERouterUrl.LocationVCRouter, parameters: params) + } + } + + /// 跳转文件查看器 + /// - Parameter model: 标记对象 + public func toFileDetail(_ model: NEPinMessageModel?) { + if let object = model?.message.attachment as? V2NIMMessageFileAttachment { + // 判断是否是文件对象 guard let fileModel = model?.pinFileModel as? PinMessageFileModel else { - NELog.infoLog(ModuleName + " " + className(), desc: #function + "PinMessageFileModel not exit") + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "PinMessageFileModel not exit") return } - + // 判断状态,如果是下载中不能进行预览 if fileModel.state == .Downalod { - NELog.infoLog(ModuleName + " " + className(), desc: #function + "downLoad state, click ingore") + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "downLoad state, click ingore") return } - if !FileManager.default.fileExists(atPath: path) { - if let urlString = object.url, let path = object.path { - fileModel.state = .Downalod - - viewmodel.downLoad(urlString, path) { [weak self] progress in - NELog.infoLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + "downLoad file progress: \(progress)") - var newProgress = progress - if newProgress < 0 { - newProgress = abs(progress) / fileModel.size - } - fileModel.progress = newProgress - if newProgress >= 1.0 { - fileModel.state = .Success - } - fileModel.cell?.uploadProgress(progress: newProgress) - } _: { error in - } + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) + if !FileManager.default.fileExists(atPath: path) { + // 本地文件不存在开始下载 + if let urlString = object.url { + downloadFile(fileModel, urlString, path) } } else { + // 有则直接加载 let url = URL(fileURLWithPath: path) interactionController.url = url interactionController.delegate = self + if interactionController.presentPreview(animated: true) {} else { interactionController.presentOptionsMenu(from: view.bounds, in: view, animated: true) } } - } else if model?.message.messageType == .custom, let attach = NECustomAttachment.attachmentOfCustomMessage(message: model?.message) { - if attach.customType == customRichTextType { + } + } + + /// 下载文件 + /// - Parameter fileModel: 文件对象 + /// - Parameter urlString: 下载地址 + /// - Parameter path: 保存路径 + open func downloadFile(_ fileModel: PinMessageFileModel, _ urlString: String, _ path: String) { + fileModel.state = .Downalod + + // 开始下载 + viewModel.downLoad(urlString, path) { [weak self] progress in + + NEALog.infoLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + "downLoad file progress: \(progress)") + + // 根据进度设置状态 + fileModel.progress = progress + + if progress >= 100 { + fileModel.state = .Success + } + // 更新ui进度 + fileModel.cell?.uploadProgress(progress: progress) + } _: { _, error in + NEALog.infoLog(self.className(), desc: "download error \(error?.localizedDescription ?? "")") + } + } + + /// 跳转文本显示器 + /// - Parameter model: 标记对象 + open func toTextViewShow(_ model: NEPinMessageModel?) { + if let customType = NECustomAttachment.typeOfCustomMessage(model?.message.attachment) { + if customType == customRichTextType { showTextViewController(model) - } else if attach.customType == customMultiForwardType, - let data = NECustomAttachment.dataOfCustomMessage(message: model?.message) { + + } else if customType == customMultiForwardType, + let data = NECustomAttachment.dataOfCustomMessage(model?.message.attachment) { let url = data["url"] as? String let md5 = data["md5"] as? String - guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } - let fileName = multiForwardFileName + (model?.message.messageId ?? "") - let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(fileName)").relativePath + + guard var fileDirectory = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit/") else { return } + let fileName = multiForwardFileName + (model?.message.messageClientId ?? "") + let filePath = fileDirectory + fileName let multiForwardVC = getMultiForwardViewController(url, filePath, md5) navigationController?.pushViewController(multiForwardVC, animated: true) } } } - private func startPlay(cell: NEBasePinMessageCell?, model: PinMessageModel?) { + /// 点击内容 + /// - Parameter model: 标记对象 + /// - Parameter cell: 内容视图显示控件 + open func didClickContent(_ model: NEPinMessageModel?, _ cell: NEBasePinMessageCell) { + NEALog.infoLog(className(), desc: #function + "didClickContent") + + if model?.message.messageType == .MESSAGE_TYPE_AUDIO { + startPlay(cell: cell, model: model) + + } else if model?.message.messageType == .MESSAGE_TYPE_IMAGE { + toImageView(model) + + } else if model?.message.messageType == .MESSAGE_TYPE_VIDEO { + toVideoView(model) + + } else if model?.message.messageType == .MESSAGE_TYPE_TEXT { + showTextViewController(model) + + } else if model?.message.messageType == .MESSAGE_TYPE_LOCATION { + toMapDetail(model) + + } else if model?.message.messageType == .MESSAGE_TYPE_FILE { + toFileDetail(model) + + } else if model?.message.messageType == .MESSAGE_TYPE_CUSTOM { + toTextViewShow(model) + } + } + + /// 开始播放 + /// - Parameter cell: 标记列表视图对象 + /// - Parameter model: 标记对象 + private func startPlay(cell: NEBasePinMessageCell?, model: NEPinMessageModel?) { guard let audioModel = model?.chatmodel as? MessageAudioModel else { return } @@ -513,23 +673,28 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa } } - func startPlaying(audioMessage: NIMMessage?) { - guard let message = audioMessage, let audio = message.messageObject as? NIMAudioObject else { + /// 开始播放 + /// - Parameter audioMessage: 音频消息对象 + func startPlaying(audioMessage: V2NIMMessage?) { + guard let message = audioMessage, let audio = message.attachment as? V2NIMMessageAudioAttachment else { return } playingCell?.startAnimation() - if let path = audio.path, FileManager.default.fileExists(atPath: path) == true { - NELog.infoLog(className(), desc: #function + " play path : " + path) - if viewmodel.getHandSetEnable() == true { - NIMSDK.shared().mediaManager.switch(.receiver) - } else { - NIMSDK.shared().mediaManager.switch(.speaker) + + let path = audio.path ?? ChatMessageHelper.createFilePath(message) + if !FileManager.default.fileExists(atPath: path) { + if let urlString = audio.url { + viewModel.downLoad(urlString, path, nil) { [weak self] _, error in + if error == nil { + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK downLoad") + NIMSDK.shared().mediaManager.play(path) + } else { + self?.showToast(error!.localizedDescription) + } + } } - NIMSDK.shared().mediaManager.play(path) } else { - NELog.infoLog(className(), desc: #function + " audio path is empty, play url : " + (audio.url ?? "")) - ChatMessageHelper.downloadAudioFile(message: message) - playingCell?.stopAnimation() + NIMSDK.shared().mediaManager.play(path) } } @@ -544,7 +709,7 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa // play open func playAudio(_ filePath: String, didBeganWithError error: Error?) { print(#function + "\(error?.localizedDescription ?? "")") - NIMSDK.shared().mediaManager.switch(viewmodel.getHandSetEnable() ? .receiver : .speaker) + NIMSDK.shared().mediaManager.switch(viewModel.getHandSetEnable() ? .receiver : .speaker) if let e = error { if e.localizedDescription.count > 0 { view.makeToast(e.localizedDescription) @@ -574,13 +739,11 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa open func playAudio(_ filePath: String, progress value: Float) {} open func playAudioInterruptionEnd() { - print(#function) playingCell?.stopAnimation() playingModel?.isPlaying = false } open func playAudioInterruptionBegin() { - print(#function) // stop play playingCell?.stopAnimation() playingModel?.isPlaying = false @@ -590,9 +753,9 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa cellClassDic } - open func showTextViewController(_ model: PinMessageModel?) { - let title = NECustomAttachment.titleOfRichText(message: model?.message) - let body = NECustomAttachment.bodyOfRichText(message: model?.message) ?? model?.message.text + open func showTextViewController(_ model: NEPinMessageModel?) { + let title = NECustomAttachment.titleOfRichText(model?.message.attachment) + let body = NECustomAttachment.bodyOfRichText(model?.message.attachment) ?? model?.message.text let textView = getTextViewController(title: title, body: body) textView.modalPresentationStyle = .fullScreen DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in @@ -612,8 +775,6 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa MultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) } - // MARK: UIDocumentInteractionControllerDelegate - open func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController { self } @@ -621,4 +782,20 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa open func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { controller.dismiss(animated: true) } + + // MARK: - NEIMKitClientListener + + public func onConnectStatus(_ status: V2NIMConnectStatus) { + if status == .CONNECT_STATUS_WAITING { + networkBroken = true + } + + if status == .CONNECT_STATUS_CONNECTED, networkBroken { + networkBroken = false + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: DispatchWorkItem(block: { [weak self] in + // 断网重连后不会重发标记回调,需要手动拉取 + self?.loadData() + })) + } + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift index ff999a40..f8c1c887 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift @@ -3,8 +3,9 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -15,18 +16,24 @@ open class NEBaseReadViewController: ChatBaseViewController, UIScrollViewDelegat public var line: UIView = .init() public var lineLeftCons: NSLayoutConstraint? public var readTableView = UITableView(frame: .zero, style: .plain) - public var readUsers = [NEKitUser]() - public var unReadUsers = [NEKitUser]() + public var readUsers = [NETeamMemberInfoModel]() + public var unReadUsers = [NETeamMemberInfoModel]() public let readButton = UIButton(type: .custom) public let unreadButton = UIButton(type: .custom) - private var message: NIMMessage - init(message: NIMMessage) { + private var message: V2NIMMessage + private var teamId: String + private let chatRepo = ChatRepo.shared + private let contactRepo = ContactRepo.shared + init(message: V2NIMMessage, teamId: String) { self.message = message + self.teamId = teamId super.init(nibName: nil, bundle: nil) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + message = V2NIMMessage() + teamId = "" + super.init(coder: coder) } override open func viewDidLoad() { @@ -51,7 +58,7 @@ open class NEBaseReadViewController: ChatBaseViewController, UIScrollViewDelegat unreadButton.setTitleColor(UIColor.ne_darkText, for: .normal) unreadButton.translatesAutoresizingMaskIntoConstraints = false unreadButton.addTarget(self, action: #selector(unreadButtonEvent), for: .touchUpInside) - readButton.accessibilityIdentifier = "id.tabUnRead" + unreadButton.accessibilityIdentifier = "id.tabUnRead" view.addSubview(readButton) NSLayoutConstraint.activate([ @@ -164,11 +171,12 @@ open class NEBaseReadViewController: ChatBaseViewController, UIScrollViewDelegat } } - func loadData(message: NIMMessage) { - NIMSDK.shared().chatManager.queryMessageReceiptDetail(message) { anError, receiptInfo in - print("anError:\(anError) receiptInfo:\(receiptInfo)") - if let error = anError as? NSError { - if error.code == noNetworkCode { + func loadData(message: V2NIMMessage) { + chatRepo.getTeamMessageReceiptDetail(message: message, memberAccountIds: []) { readReceiptDetail, error in + guard let readReceiptDetail = readReceiptDetail else { return } + let group = DispatchGroup() + if let error = error as? NSError { + if error.code == protocolSendFailed { self.showToast(commonLocalizable("network_error")) } else { self.showToast(error.localizedDescription) @@ -176,29 +184,39 @@ open class NEBaseReadViewController: ChatBaseViewController, UIScrollViewDelegat return } - for userId in receiptInfo?.readUserIds ?? [] { - if let uId = userId as? String, - let user = UserInfoProvider.shared.getUserInfo(userId: uId) { - self.readUsers.append(user) + self.readButton.setTitle("已读 (" + "\(readReceiptDetail.readAccountList.count)" + ")", for: .normal) + self.unreadButton.setTitle("未读 (" + "\(readReceiptDetail.unreadAccountList.count)" + ")", for: .normal) + + // 加载用户信息 + let loadUserIds = readReceiptDetail.readAccountList + readReceiptDetail.unreadAccountList + group.enter() + ChatTeamCache.shared.loadShowName(userIds: loadUserIds, teamId: self.teamId) { + // 已读用户 + for userId in readReceiptDetail.readAccountList { + if let memberInfo = ChatTeamCache.shared.getTeamMemberInfo(accountId: userId) { + self.readUsers.append(memberInfo) + } } - } - for userId in receiptInfo?.unreadUserIds ?? [] { - if let uId = userId as? String, - let user = UserInfoProvider.shared.getUserInfo(userId: uId) { - self.unReadUsers.append(user) + // 未读用户 + for userId in readReceiptDetail.unreadAccountList { + if let memberInfo = ChatTeamCache.shared.getTeamMemberInfo(accountId: userId) { + self.unReadUsers.append(memberInfo) + } } + + group.leave() } - self.readButton.setTitle("已读 (" + "\(self.readUsers.count)" + ")", for: .normal) - self.unreadButton.setTitle("未读 (" + "\(self.unReadUsers.count)" + ")", for: .normal) - self.readTableView.reloadData() - - if self.read, self.readUsers.count == 0 { - self.readTableView.isHidden = true - self.emptyView.isHidden = false - } else { - self.readTableView.isHidden = false - self.emptyView.isHidden = true + + group.notify(queue: .main) { + self.readTableView.reloadData() + if self.read, self.readUsers.count == 0 { + self.readTableView.isHidden = true + self.emptyView.isHidden = false + } else { + self.readTableView.isHidden = false + self.emptyView.isHidden = true + } } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift index afbec002..acfdb1a9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift @@ -4,10 +4,10 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import UIKit -public typealias DidSelectedAtRow = (_ index: Int, _ model: ChatTeamMemberInfoModel?) -> Void +public typealias DidSelectedAtRow = (_ index: Int, _ model: NETeamMemberInfoModel?) -> Void @objcMembers open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDelegate, @@ -16,8 +16,9 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe public var sessionId: String public var viewModel = TeamMemberSelectVM() public var selectedBlock: DidSelectedAtRow? - var teamInfo: ChatTeamInfoModel? - private var showSelf = true // 是否展示自己 + var teamInfo: NETeamInfoModel? + //// 是否展示自己 + private var showSelf = true var className = "SelectUserViewController" var isShowAtAll = true @@ -28,7 +29,9 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + sessionId = "" + showSelf = true + super.init(coder: coder) } override open func viewDidLoad() { @@ -39,28 +42,29 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe loadData() } + /// UI 内容初始化以及布局 func commonUI() { - let btn = UIButton(type: .custom) - btn.translatesAutoresizingMaskIntoConstraints = false - btn.accessibilityIdentifier = "id.arrowDown" - btn.setImage(UIImage.ne_imageNamed(name: "arrowDown"), for: .normal) - btn.addTarget(self, action: #selector(btnEvent), for: .touchUpInside) - view.addSubview(btn) + let button = UIButton(type: .custom) + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityIdentifier = "id.arrowDown" + button.setImage(UIImage.ne_imageNamed(name: "arrowDown"), for: .normal) + button.addTarget(self, action: #selector(btnEvent), for: .touchUpInside) + view.addSubview(button) if #available(iOS 11.0, *) { NSLayoutConstraint.activate([ - btn.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 16), - btn.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), - btn.widthAnchor.constraint(equalToConstant: 50), - btn.heightAnchor.constraint(equalToConstant: 50), + button.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 16), + button.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), + button.widthAnchor.constraint(equalToConstant: 50), + button.heightAnchor.constraint(equalToConstant: 50), ]) } else { // Fallback on earlier versions NSLayoutConstraint.activate([ - btn.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - btn.topAnchor.constraint(equalTo: view.topAnchor), - btn.widthAnchor.constraint(equalToConstant: 50), - btn.heightAnchor.constraint(equalToConstant: 50), + button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + button.topAnchor.constraint(equalTo: view.topAnchor), + button.widthAnchor.constraint(equalToConstant: 50), + button.heightAnchor.constraint(equalToConstant: 50), ]) } @@ -78,6 +82,7 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe label.heightAnchor.constraint(equalToConstant: 50), ]) + /// 内容列表 tableView.delegate = self tableView.dataSource = self tableView.sectionHeaderHeight = 0 @@ -108,7 +113,7 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe func loadData() { viewModel.fetchTeamMembers(sessionId: sessionId) { [weak self] error, team in - NELog.infoLog( + NEALog.infoLog( ModuleName + " " + (self?.className ?? "SelectUserViewController"), desc: "CALLBACK fetchTeamMembers " + (error?.localizedDescription ?? "no error") ) @@ -119,32 +124,33 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe // 人员选择页面移除自己 var selfIndex = -1 - if !(self?.showSelf ?? true), - let users = team?.users { + if !(self?.showSelf ?? true), let users = team?.users { for (index, user) in users.enumerated() { - if let u = team?.users[index].nimUser { - ChatUserCache.updateUserInfo(u) - } - if user.nimUser?.userId == IMKitLoginManager.instance.currentAccount() { - if user.teamMember?.type != .manager, let custom = team?.team?.clientCustomInfo, custom.count > 0, let json = getDictionaryFromJSONString(custom), let atValue = json[keyAllowAtAll] as? String, atValue == allowAtManagerValue { + if user.nimUser?.user?.accountId == IMKitClient.instance.account() { + if user.teamMember?.memberRole == .TEAM_MEMBER_ROLE_NORMAL, + let custom = team?.team?.serverExtension, custom.count > 0, + let json = getDictionaryFromJSONString(custom), + let atValue = json[keyAllowAtAll] as? String, atValue == allowAtManagerValue { self?.isShowAtAll = false } selfIndex = index } } - team?.users.remove(at: selfIndex) + if selfIndex >= 0 { + team?.users.remove(at: selfIndex) + } } // 根据身份+进群时间正序排序 if let users = team?.users { - var owner: ChatTeamMemberInfoModel? // 群主 - var managers = [ChatTeamMemberInfoModel]() // 管理员 - var normals = [ChatTeamMemberInfoModel]() // 普通成员 + var owner: NETeamMemberInfoModel? // 群主 + var managers = [NETeamMemberInfoModel]() // 管理员 + var normals = [NETeamMemberInfoModel]() // 普通成员 for user in users { - if user.teamMember?.type == .owner { + if user.teamMember?.memberRole == .TEAM_MEMBER_ROLE_OWNER { owner = user - } else if user.teamMember?.type == .manager { + } else if user.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { managers.append(user) } else { normals.append(user) @@ -152,11 +158,11 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe } managers.sort(by: { m1, m2 in - (m1.teamMember?.createTime ?? 0) < (m2.teamMember?.createTime ?? 0) + (m1.teamMember?.joinTime ?? 0) < (m2.teamMember?.joinTime ?? 0) }) normals.sort(by: { m1, m2 in - (m1.teamMember?.createTime ?? 0) < (m2.teamMember?.createTime ?? 0) + (m1.teamMember?.joinTime ?? 0) < (m2.teamMember?.joinTime ?? 0) }) if let owner = owner { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift index b9fc7f57..6a151c39 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift @@ -12,9 +12,9 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV UITableViewDataSource, UITableViewDelegate { public var userId: String? - let viewmodel = UserSettingViewModel() + let viewModel = UserSettingViewModel() - lazy var userHeader: NEUserHeaderView = { + public lazy var userHeaderView: NEUserHeaderView = { let imageView = NEUserHeaderView(frame: .zero) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.clipsToBounds = true @@ -23,7 +23,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV return imageView }() - lazy var addBtn: ExpandButton = { + public lazy var addButton: ExpandButton = { let button = ExpandButton() button.translatesAutoresizingMaskIntoConstraints = false button.setImage(coreLoader.loadImage("setting_add"), for: .normal) @@ -31,7 +31,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV return button }() - lazy var nameLabel: UILabel = { + public lazy var nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(12.0) @@ -41,22 +41,22 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV return label }() - lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - table + public lazy var contentTable: UITableView = { + let contentTable = UITableView() + contentTable.translatesAutoresizingMaskIntoConstraints = false + contentTable.backgroundColor = .clear + contentTable.dataSource = self + contentTable.delegate = self + contentTable.separatorColor = .clear + contentTable.separatorStyle = .none + contentTable.sectionHeaderHeight = 12.0 + contentTable .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + contentTable.sectionHeaderTopPadding = 0.0 } - return table + return contentTable }() public var cellClassDic = [Int: NEBaseUserSettingCell.Type]() @@ -67,16 +67,19 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() - viewmodel.delegate = self + viewModel.delegate = self if let uid = userId { - viewmodel.getUserSettingModel(uid) - contentTable.tableHeaderView = headerView() - contentTable.reloadData() + viewModel.getConversation(uid) { [weak self] error in + self?.viewModel.getUserSettingModel(uid) { [weak self] in + self?.contentTable.tableHeaderView = self?.headerView() + self?.contentTable.reloadData() + } + } } setupUI() } @@ -95,7 +98,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseUserSettingCell.Type) in + for (key, value) in cellClassDic { contentTable.register(value, forCellReuseIdentifier: "\(key)") } if let pan = navigationController?.interactivePopGestureRecognizer { @@ -104,80 +107,80 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV } open func headerView() -> UIView { - let header = UIView(frame: CGRect(x: 0, y: 0, width: view.width, height: 110)) - header.backgroundColor = .clear - let cornerBack = UIView() - cornerBack.layer.cornerRadius = 8.0 - cornerBack.backgroundColor = .white - cornerBack.translatesAutoresizingMaskIntoConstraints = false - header.addSubview(cornerBack) + let headerView = UIView(frame: CGRect(x: 0, y: 0, width: view.width, height: 110)) + headerView.backgroundColor = .clear + let cornerBackView = UIView() + cornerBackView.layer.cornerRadius = 8.0 + cornerBackView.backgroundColor = .white + cornerBackView.translatesAutoresizingMaskIntoConstraints = false + headerView.addSubview(cornerBackView) NSLayoutConstraint.activate([ - cornerBack.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -12), - cornerBack.leftAnchor.constraint(equalTo: header.leftAnchor, constant: 20), - cornerBack.widthAnchor.constraint(equalToConstant: kScreenWidth - 40), - cornerBack.heightAnchor.constraint(equalToConstant: 86.0), + cornerBackView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -12), + cornerBackView.leftAnchor.constraint(equalTo: headerView.leftAnchor, constant: 20), + cornerBackView.widthAnchor.constraint(equalToConstant: kScreenWidth - 40), + cornerBackView.heightAnchor.constraint(equalToConstant: 86.0), ]) - cornerBack.addSubview(userHeader) - let tap = UITapGestureRecognizer() - userHeader.addGestureRecognizer(tap) - tap.numberOfTapsRequired = 1 - tap.numberOfTouchesRequired = 1 - - if let url = viewmodel.userInfo?.userInfo?.avatarUrl, !url.isEmpty { - userHeader.sd_setImage(with: URL(string: url), completed: nil) - userHeader.setTitle("") - userHeader.backgroundColor = .clear - } else if let name = viewmodel.userInfo?.showName(false) { - userHeader.sd_setImage(with: nil) - userHeader.setTitle(name) - userHeader.backgroundColor = UIColor.colorWithString(string: viewmodel.userInfo?.userId) + cornerBackView.addSubview(userHeaderView) + let tapGesture = UITapGestureRecognizer() + userHeaderView.addGestureRecognizer(tapGesture) + tapGesture.numberOfTapsRequired = 1 + tapGesture.numberOfTouchesRequired = 1 + + if let url = viewModel.userInfo?.user?.avatar, !url.isEmpty { + userHeaderView.sd_setImage(with: URL(string: url), completed: nil) + userHeaderView.setTitle("") + userHeaderView.backgroundColor = .clear + } else if let name = viewModel.userInfo?.showName(false) { + userHeaderView.sd_setImage(with: nil) + userHeaderView.setTitle(name) + userHeaderView.backgroundColor = UIColor.colorWithString(string: viewModel.userInfo?.user?.accountId) } - nameLabel.text = viewmodel.userInfo?.showName() - cornerBack.addSubview(nameLabel) + nameLabel.text = viewModel.userInfo?.showName() + cornerBackView.addSubview(nameLabel) if IMKitClient.instance.getConfigCenter().teamEnable { NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: cornerBack.leftAnchor, constant: 16), - userHeader.topAnchor.constraint(equalTo: cornerBack.topAnchor, constant: 12), - userHeader.widthAnchor.constraint(equalToConstant: 42), - userHeader.heightAnchor.constraint(equalToConstant: 42), + userHeaderView.leftAnchor.constraint(equalTo: cornerBackView.leftAnchor, constant: 16), + userHeaderView.topAnchor.constraint(equalTo: cornerBackView.topAnchor, constant: 12), + userHeaderView.widthAnchor.constraint(equalToConstant: 42), + userHeaderView.heightAnchor.constraint(equalToConstant: 42), ]) nameLabel.font = NEConstant.defaultTextFont(12) nameLabel.textAlignment = .center NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: userHeader.leftAnchor, constant: -12.0), - nameLabel.rightAnchor.constraint(equalTo: userHeader.rightAnchor, constant: 12.0), - nameLabel.topAnchor.constraint(equalTo: userHeader.bottomAnchor, constant: 6.0), + nameLabel.leftAnchor.constraint(equalTo: userHeaderView.leftAnchor, constant: -12.0), + nameLabel.rightAnchor.constraint(equalTo: userHeaderView.rightAnchor, constant: 12.0), + nameLabel.topAnchor.constraint(equalTo: userHeaderView.bottomAnchor, constant: 6.0), ]) - cornerBack.addSubview(addBtn) - addBtn.addTarget(self, action: #selector(createDiscuss), for: .touchUpInside) + cornerBackView.addSubview(addButton) + addButton.addTarget(self, action: #selector(createDiscuss), for: .touchUpInside) NSLayoutConstraint.activate([ - addBtn.leftAnchor.constraint(equalTo: userHeader.rightAnchor, constant: 20.0), - addBtn.topAnchor.constraint(equalTo: userHeader.topAnchor), - addBtn.widthAnchor.constraint(equalToConstant: 42.0), - addBtn.heightAnchor.constraint(equalToConstant: 42.0), + addButton.leftAnchor.constraint(equalTo: userHeaderView.rightAnchor, constant: 20.0), + addButton.topAnchor.constraint(equalTo: userHeaderView.topAnchor), + addButton.widthAnchor.constraint(equalToConstant: 42.0), + addButton.heightAnchor.constraint(equalToConstant: 42.0), ]) } else { NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: cornerBack.leftAnchor, constant: 16), - userHeader.centerYAnchor.constraint(equalTo: cornerBack.centerYAnchor), - userHeader.widthAnchor.constraint(equalToConstant: 60), - userHeader.heightAnchor.constraint(equalToConstant: 60), + userHeaderView.leftAnchor.constraint(equalTo: cornerBackView.leftAnchor, constant: 16), + userHeaderView.centerYAnchor.constraint(equalTo: cornerBackView.centerYAnchor), + userHeaderView.widthAnchor.constraint(equalToConstant: 60), + userHeaderView.heightAnchor.constraint(equalToConstant: 60), ]) nameLabel.font = NEConstant.defaultTextFont(16) nameLabel.textAlignment = .left NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: userHeader.rightAnchor, constant: 16.0), - nameLabel.rightAnchor.constraint(equalTo: cornerBack.rightAnchor), - nameLabel.centerYAnchor.constraint(equalTo: userHeader.centerYAnchor), + nameLabel.leftAnchor.constraint(equalTo: userHeaderView.rightAnchor, constant: 16.0), + nameLabel.rightAnchor.constraint(equalTo: cornerBackView.rightAnchor), + nameLabel.centerYAnchor.constraint(equalTo: userHeaderView.centerYAnchor), ]) } - return header + return headerView } open func filterStackViewController() -> [UIViewController]? { @@ -195,7 +198,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV Router.shared.register(ContactSelectedUsersRouter) { param in print("user setting create disscuss : ", param) var convertParam = [String: Any]() - param.forEach { (key: String, value: Any) in + for (key, value) in param { if key == "names", let names = value as? String { convertParam[key] = "\(weakSelf?.nameLabel.text ?? "")、\(names)" } else { @@ -205,7 +208,12 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV weakSelf?.view.makeToastActivity(.center) Router.shared.use(TeamCreateDisuss, parameters: convertParam, closure: nil) } + + // 单聊设置-创建讨论组-人员选择页面不包含自己 var filters = Set() + filters.insert(IMKitClient.instance.account()) + + // 单聊设置-创建讨论组-人员选择页面不包含单聊对方 if let uid = userId { filters.insert(uid) } @@ -226,7 +234,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV weakSelf?.view.hideToastActivity() if let code = param["code"] as? Int, let teamid = param["teamId"] as? String, code == 0 { - let session = NIMSession(teamid, type: .team) + let conversationId = V2NIMConversationIdUtil.teamConversationId(teamid) DispatchQueue.main.async { if let allControllers = weakSelf?.filterStackViewController() { @@ -234,7 +242,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV Router.shared.use( PushTeamChatVCRouter, parameters: ["nav": weakSelf?.navigationController as Any, - "session": session as Any], + "conversationId": conversationId as Any], closure: nil ) } @@ -246,7 +254,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV } open func showUserInfo() { - if let user = viewmodel.userInfo { + if let user = viewModel.userInfo { Router.shared.use( ContactUserInfoPageRouter, parameters: ["nav": navigationController as Any, "user": user], @@ -266,12 +274,12 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV // MARK: UITableViewDataSource, UITableViewDelegate open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewmodel.cellDatas.count + viewModel.cellDatas.count } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] if let cell = tableView.dequeueReusableCell( withIdentifier: "\(model.type)", for: indexPath @@ -282,15 +290,14 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV return UITableViewCell() } - func getPinMessageViewController(session: NIMSession) -> NEBasePinMessageViewController { - NEBasePinMessageViewController(session: session) + func getPinMessageViewController(conversationId: String) -> NEBasePinMessageViewController { + NEBasePinMessageViewController(conversationId: conversationId) } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if indexPath.row == 0 { - if let accid = userId { - let session = NIMSession(accid, type: .P2P) - let pin = getPinMessageViewController(session: session) + if let accid = userId, let conversationId = V2NIMConversationIdUtil.p2pConversationId(accid) { + let pin = getPinMessageViewController(conversationId: conversationId) navigationController?.pushViewController(pin, animated: true) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift index 6b365f5d..421f4a92 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift @@ -12,14 +12,14 @@ open class TextViewController: ChatBaseViewController { let titleFont = UIFont.systemFont(ofSize: 24, weight: .semibold) let bodyFont = UIFont.systemFont(ofSize: 24) - lazy var scrollView: UIScrollView = { + public lazy var scrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.isScrollEnabled = true scrollView.translatesAutoresizingMaskIntoConstraints = false return scrollView }() - lazy var textView: UIView = { + public lazy var textView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .clear @@ -42,7 +42,7 @@ open class TextViewController: ChatBaseViewController { return view }() - lazy var titleLabel: CopyableLabel = { + public lazy var titleLabel: CopyableLabel = { let label = CopyableLabel() label.numberOfLines = 0 label.translatesAutoresizingMaskIntoConstraints = false @@ -52,7 +52,7 @@ open class TextViewController: ChatBaseViewController { return label }() - lazy var bodyLabel: CopyableLabel = { + public lazy var bodyLabel: CopyableLabel = { let label = CopyableLabel() label.numberOfLines = 0 label.translatesAutoresizingMaskIntoConstraints = false @@ -69,20 +69,20 @@ open class TextViewController: ChatBaseViewController { super.init(nibName: nil, bundle: nil) contentMaxWidth = kScreenWidth - leftRightMargin * 2 if let title = title { + titleLabel.copyString = title let titleAtt = NEEmotionTool.getAttWithStr(str: title, font: titleFont, CGPoint(x: 0, y: -3)) - titleLabel.copyString = titleAtt.string titleLabel.attributedText = titleAtt } if let body = body { + bodyLabel.copyString = body let bodyAtt = NEEmotionTool.getAttWithStr(str: body, font: bodyFont, CGPoint(x: 0, y: -3)) - bodyLabel.copyString = bodyAtt.string bodyLabel.attributedText = bodyAtt } } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -122,11 +122,11 @@ open class TextViewController: ChatBaseViewController { bodyLabel.preferredMaxLayoutWidth = contentMaxWidth scrollView.addSubview(textView) contentLabelTopAnchor = textView.topAnchor.constraint(equalTo: scrollView.topAnchor) + contentLabelTopAnchor?.isActive = true contentLabelLeftAnchor = textView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: leftRightMargin) + contentLabelLeftAnchor?.isActive = true NSLayoutConstraint.activate([ - contentLabelTopAnchor!, textView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), - contentLabelLeftAnchor!, textView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -leftRightMargin), ]) } @@ -142,7 +142,7 @@ open class TextViewController: ChatBaseViewController { } } - let textSize = label.attributedText?.finalSize(bodyFont, CGSize(width: contentMaxWidth, height: CGFloat.greatestFiniteMagnitude)) ?? .zero + let textSize = NSAttributedString.getRealSize(label.attributedText, bodyFont, CGSize(width: contentMaxWidth, height: CGFloat.greatestFiniteMagnitude)) let textViewHeight = kScreenHeight - kNavigationHeight - KStatusBarHeight if textSize.height <= textViewHeight { let offsetY = (textViewHeight - textSize.height) / 2 diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift index cc6471f8..3c5845b6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift @@ -32,7 +32,7 @@ open class EmojiPageView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override public var frame: CGRect { @@ -78,7 +78,7 @@ open class EmojiPageView: UIView { func reloadPage() { // reload时候记录上次位置 // guard let cPage = currentPage else { -// NELog.errorLog(className, desc: "❌currentPage is nil") +// NEALog.errorLog(className, desc: "❌currentPage is nil") // return // } if currentPage >= pages.count { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift index 1d6ec145..2aad5db6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift @@ -3,8 +3,9 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit + @objc public protocol InputEmoticonContainerViewDelegate: NSObjectProtocol { func selectedEmoticon(emoticonID: String, emotCatalogID: String, description: String) func didPressSend(sender: UIButton) @@ -46,7 +47,7 @@ open class InputEmoticonContainerView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setUpSubViews() { @@ -141,7 +142,7 @@ open class InputEmoticonContainerView: UIView { return emotionsCount / layoutCount + 1 } } else { - NELog.errorLog(classTag, desc: "❌count maybe nil") + NEALog.errorLog(classTag, desc: "❌count maybe nil") return 0 } } @@ -183,12 +184,12 @@ extension InputEmoticonContainerView { page: NSInteger) -> UIView { let subView = UIView() guard let layout = emoticon.layout else { - NELog.errorLog(classTag, desc: "layout is nil") + NEALog.errorLog(classTag, desc: "layout is nil") return UIView() } guard let emotions = emoticon.emoticons else { - NELog.errorLog(classTag, desc: "emoticon.emoticons is nil") + NEALog.errorLog(classTag, desc: "emoticon.emoticons is nil") return UIView() } @@ -250,7 +251,7 @@ extension InputEmoticonContainerView { startX: CGFloat, startY: CGFloat, iconWidth: CGFloat, iconHeight: CGFloat, emotion: NIMInputEmoticonCatalog) { guard let layout = emotion.layout else { - NELog.errorLog(classTag, desc: "❌emotion is nill") + NEALog.errorLog(classTag, desc: "❌emotion is nill") return } @@ -262,6 +263,7 @@ extension InputEmoticonContainerView { deleteIcon.setImage(UIImage.ne_imageNamed(name: "emoji_del_normal"), for: .normal) deleteIcon.setImage(UIImage.ne_imageNamed(name: "emoji_del_pressed"), for: .highlighted) deleteIcon.addTarget(self, action: #selector(onIconSelected), for: .touchUpInside) + deleteIcon.accessibilityIdentifier = "id.emojiDelete" let newX = CGFloat(coloumnIndex + 1) * layout.cellWidth + startX let newY = CGFloat(rowIndex) * layout.cellHeight + startY let deleteIconRect = CGRect( @@ -296,7 +298,7 @@ extension InputEmoticonContainerView: EmojiPageViewDelegate, EmojiPageViewDataSo var resultEmotion = NIMInputEmoticonCatalog() guard let totalData = totalCatalogData, let targetView = pageView else { - NELog.errorLog(classTag, desc: "❌totalCatalogData is nil") + NEALog.errorLog(classTag, desc: "❌totalCatalogData is nil") return UIView() } @@ -334,7 +336,7 @@ extension InputEmoticonContainerView: InputEmoticonTabViewDelegate { extension InputEmoticonContainerView: NIMInputEmoticonButtonDelegate { open func selectedEmoticon(emotion: NIMInputEmoticon, catalogID: String) { guard let emotionId = emotion.emoticonID else { - NELog.errorLog(classTag, desc: "❌emoticonID is nil") + NEALog.errorLog(classTag, desc: "❌emoticonID is nil") return } if emotion.type == .unicode { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift index acc9fb49..0573b912 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift @@ -3,8 +3,9 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit + @objc public protocol InputEmoticonTabViewDelegate: NSObjectProtocol { @objc optional func tabView(_ tabView: InputEmoticonTabView?, didSelectTabIndex index: Int) } @@ -21,7 +22,7 @@ open class InputEmoticonTabView: UIControl { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setUpSubViews() { @@ -37,36 +38,36 @@ open class InputEmoticonTabView: UIControl { open func selectTabIndex(_ index: Int) { for i in 0 ..< tabs.count { - let btn = tabs[i] - btn.isSelected = i == index + let button = tabs[i] + button.isSelected = i == index } } open func loadCatalogs(_ emoticonCatalogs: [NIMInputEmoticonCatalog]?) { - tabs.forEach { btn in - btn.removeFromSuperview() + for button in tabs { + button.removeFromSuperview() } - seps.forEach { view in + for view in seps { view.removeFromSuperview() } tabs.removeAll() seps.removeAll() guard let catalogs = emoticonCatalogs else { - NELog.errorLog(className, desc: "❌emoticonCatalogs is nil") + NEALog.errorLog(className, desc: "❌emoticonCatalogs is nil") return } - catalogs.forEach { catelog in + for catelog in catalogs { let button = UIButton() button.addTarget(self, action: #selector(onTouchTab), for: .touchUpInside) button.sizeToFit() - self.addSubview(button) + addSubview(button) tabs.append(button) let sep = UIView(frame: CGRect(x: 0, y: 0, width: 0.5, height: 35)) sep.backgroundColor = UIColor.ne_borderColor seps.append(sep) - self.addSubview(sep) + addSubview(sep) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonButton.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonButton.swift index 35f25ac0..9a56fab2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonButton.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonButton.swift @@ -3,8 +3,9 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit + public protocol NIMInputEmoticonButtonDelegate: NSObjectProtocol { func selectedEmoticon(emotion: NIMInputEmoticon, catalogID: String) } @@ -18,32 +19,32 @@ open class NIMInputEmoticonButton: UIButton { open class func iconButtonWithData(data: NIMInputEmoticon, catalogID: String, delegate: NIMInputEmoticonButtonDelegate) -> NIMInputEmoticonButton { - let icon = NIMInputEmoticonButton() - icon.addTarget(icon, action: #selector(onIconSelected), for: .touchUpInside) - icon.emotionData = data - icon.catalogID = catalogID - icon.isUserInteractionEnabled = true - icon.isExclusiveTouch = true - icon.contentMode = .scaleToFill - icon.delegate = delegate - icon.accessibilityIdentifier = "id.emoji" - icon.accessibilityValue = data.tag + let iconButton = NIMInputEmoticonButton() + iconButton.addTarget(iconButton, action: #selector(onIconSelected), for: .touchUpInside) + iconButton.emotionData = data + iconButton.catalogID = catalogID + iconButton.isUserInteractionEnabled = true + iconButton.isExclusiveTouch = true + iconButton.contentMode = .scaleToFill + iconButton.delegate = delegate + iconButton.accessibilityIdentifier = "id.emoji" + iconButton.accessibilityValue = data.tag switch data.type { case .unicode: - icon.setTitle(data.unicode, for: .normal) - icon.setTitle(data.unicode, for: .highlighted) - icon.titleLabel?.font = DefaultTextFont(32) + iconButton.setTitle(data.unicode, for: .normal) + iconButton.setTitle(data.unicode, for: .highlighted) + iconButton.titleLabel?.font = DefaultTextFont(32) default: let image = UIImage.ne_bundleImage(name: data.fileName ?? "") - icon.setImage(image, for: .normal) - icon.setImage(image, for: .highlighted) + iconButton.setImage(image, for: .normal) + iconButton.setImage(image, for: .highlighted) } - return icon + return iconButton } @objc func onIconSelected(sender: NIMInputEmoticonButton) { guard let data = emotionData, let id = catalogID else { - NELog.errorLog(classsTag, desc: "emotionData or catalogID maybe nil") + NEALog.errorLog(classsTag, desc: "emotionData or catalogID maybe nil") return } delegate?.selectedEmoticon(emotion: data, catalogID: id) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift index 60c43eee..9fce7a97 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift @@ -3,8 +3,9 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit + public enum NIMEmoticonType: NSInteger { case file = 0 case unicode @@ -100,7 +101,7 @@ open class NIMInputEmoticonManager: NSObject { let cataLog = NIMInputEmoticonCatalog() guard let infoDict = info, let emotions = emoticonsArray else { - NELog.errorLog(classTag, desc: "❌info or emoticonsArray is nil") + NEALog.errorLog(classTag, desc: "❌info or emoticonsArray is nil") return cataLog } cataLog.catalogID = infoDict["id"] as? String @@ -111,7 +112,7 @@ open class NIMInputEmoticonManager: NSObject { var id2Emoticons = [String: NIMInputEmoticon]() var resultEmotions = [NIMInputEmoticon]() - emotions.forEach { emoticonDict in + for emoticonDict in emotions { if let dict = (emoticonDict as? NSDictionary) { let emotion = NIMInputEmoticon() emotion.emoticonID = dict["id"] as? String @@ -151,7 +152,7 @@ open class NIMInputEmoticonManager: NSObject { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { - NELog.errorLog(classTag, desc: "❌catalogs is nil") + NEALog.errorLog(classTag, desc: "❌catalogs is nil") return emotion } @@ -171,7 +172,7 @@ open class NIMInputEmoticonManager: NSObject { open func emoticonByID(emoticonID: String) -> NIMInputEmoticon? { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { - NELog.errorLog(classTag, desc: "❌catalogs is nil") + NEALog.errorLog(classTag, desc: "❌catalogs is nil") return emotion } @@ -191,7 +192,7 @@ open class NIMInputEmoticonManager: NSObject { open func emoticonByCatalogID(catalogID: String, emoticonID: String) -> NIMInputEmoticon? { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { - NELog.errorLog(classTag, desc: "❌catalogs is nil") + NEALog.errorLog(classTag, desc: "❌catalogs is nil") return emotion } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.swift index ae841bf6..25000544 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.swift @@ -3,9 +3,12 @@ // found in the LICENSE file. import Foundation +import NEChatKit +import NECoreIM2Kit import NIMSDK + @objcMembers -public class ChatDeduplicationHelper: NSObject, NIMLoginManagerDelegate { +public class ChatDeduplicationHelper: NSObject, NEIMKitClientListener { // 单例变量 static let instance = ChatDeduplicationHelper() // 最多缓存数量,可外部修改 @@ -14,25 +17,27 @@ public class ChatDeduplicationHelper: NSObject, NIMLoginManagerDelegate { public var blackListMessageIds = Set() // 音频消息记录 public var recordAudioMessagePaths = Set() + // 发送中消息记录 + public var sendingMessageIds = Set() // 撤回消息记录 public var revokeMessageIds = Set() override private init() { super.init() - NIMSDK.shared().loginManager.add(self) + IMKitClient.instance.addLoginListener(self) } deinit { - NIMSDK.shared().loginManager.remove(self) + IMKitClient.instance.removeLoginListener(self) } - public func onLogin(_ step: NIMLoginStep) { - if step == .logout { + public func onLoginStatus(_ status: V2NIMLoginStatus) { + if status == .LOGIN_STATUS_LOGOUT { clearCache() } } - public func onKickout(_ result: NIMLoginKickoutResult) { + public func onKickedOffline(_ detail: V2NIMKickedOfflineDetail) { clearCache() } @@ -40,9 +45,22 @@ public class ChatDeduplicationHelper: NSObject, NIMLoginManagerDelegate { blackListMessageIds.removeAll() recordAudioMessagePaths.removeAll() revokeMessageIds.removeAll() + NEFriendUserCache.shared.removeAllFriendInfo() } // 是否已经发送过对应消息的提示 + public func isMessageSended(messageId: String) -> Bool { + if sendingMessageIds.contains(messageId) { + return true + } + if sendingMessageIds.count > limit { + sendingMessageIds.removeAll() + } + sendingMessageIds.insert(messageId) + return false + } + + // 是否已经发送过黑名单消息的提示 public func isBlackTipSended(messageId: String) -> Bool { if blackListMessageIds.contains(messageId) { return true diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift index 258da275..56ebc7a9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift @@ -7,14 +7,20 @@ import CommonCrypto import Foundation import NEChatKit import NECommonKit -import NECoreIMKit +import NECoreIM2Kit +import NECoreKit import NIMSDK @objcMembers public class ChatMessageHelper: NSObject { public static let repo = ChatRepo.shared - // 获取图片合适尺寸 + /// 获取图片合适尺寸 + /// - Parameters: + /// - maxSize: 最大宽高 + /// - size: 图片宽高 + /// - miniWH: 最小宽高 + /// - Returns: 消息列表中展示的尺寸 public class func getSizeWithMaxSize(_ maxSize: CGSize, size: CGSize, miniWH: CGFloat) -> CGSize { var realSize = CGSize.zero @@ -42,12 +48,27 @@ public class ChatMessageHelper: NSObject { return realSize } - public static func getSessionName(session: NIMSession, showAlias: Bool = true) -> String { - session.sessionType == .P2P ? ChatUserCache.getShowName(userId: session.sessionId, teamId: nil, showAlias) : repo.getTeamInfo(teamId: session.sessionId)?.teamName ?? "" + /// 获取会话昵称 + /// - Parameters: + /// - conversationId: 会话 id + /// - showAlias: 是否优先显示备注 + /// - Returns: 会话昵称 + public static func getSessionName(conversationId: String, showAlias: Bool = true) -> String { + guard let sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId) else { + return "" + } + if V2NIMConversationIdUtil.conversationType(conversationId) == .CONVERSATION_TYPE_P2P { + return NEFriendUserCache.shared.getShowName(sessionId).name + } else { + return ChatTeamCache.shared.getTeamInfo()?.name ?? "" + } } // MARK: message + /// 获取消息列表单元格注册列表 + /// - Parameter isFun: 是否是娱乐皮肤 + /// - Returns: 单元格注册列表 public static func getChatCellRegisterDic(isFun: Bool) -> [String: UITableViewCell.Type] { [ "\(MessageType.text.rawValue)": @@ -77,6 +98,9 @@ public class ChatMessageHelper: NSObject { ] } + /// 获取标记列表单元格注册列表 + /// - Parameter isFun: 是否是娱乐皮肤 + /// - Returns: 单元格注册列表 public static func getPinCellRegisterDic(isFun: Bool) -> [String: NEBasePinMessageCell.Type] { [ "\(MessageType.text.rawValue)": @@ -100,31 +124,36 @@ public class ChatMessageHelper: NSObject { ] } - public static func modelFromMessage(message: NIMMessage) -> MessageModel { + /// 构造消息体 + /// - Parameter message: 消息 + /// - Returns: 消息体 + public static func modelFromMessage(message: V2NIMMessage) -> MessageModel { var model: MessageModel switch message.messageType { - case .video: + case .MESSAGE_TYPE_VIDEO: model = MessageVideoModel(message: message) - case .text: + case .MESSAGE_TYPE_TEXT: model = MessageTextModel(message: message) - case .image: + case .MESSAGE_TYPE_IMAGE: model = MessageImageModel(message: message) - case .audio: + case .MESSAGE_TYPE_AUDIO: model = MessageAudioModel(message: message) - case .notification, .tip: + case .MESSAGE_TYPE_NOTIFICATION, .MESSAGE_TYPE_TIP: model = MessageTipsModel(message: message) - case .file: + case .MESSAGE_TYPE_FILE: model = MessageFileModel(message: message) - case .location: + case .MESSAGE_TYPE_LOCATION: model = MessageLocationModel(message: message) - case .rtcCallRecord: + case .MESSAGE_TYPE_CALL: model = MessageCallRecordModel(message: message) - case .custom: - if let attach = NECustomAttachment.attachmentOfCustomMessage(message: message) { - if attach.customType == customRichTextType { + case .MESSAGE_TYPE_CUSTOM: + if let type = NECustomAttachment.typeOfCustomMessage(message.attachment) { + if type == customMultiForwardType { + return MessageCustomModel(message: message, contentHeight: Int(customMultiForwardCellHeight)) + } + if type == customRichTextType { return MessageRichTextModel(message: message) } - return MessageCustomModel(message: message) } fallthrough default: @@ -135,12 +164,81 @@ public class ChatMessageHelper: NSObject { return model } + /// 构造消息体 + /// - Parameters: + /// - message: 消息 + /// - completion: 完成回调 + public static func modelFromMessage(message: V2NIMMessage, _ completion: @escaping (MessageModel) -> Void) { + var model: MessageModel + switch message.messageType { + case .MESSAGE_TYPE_VIDEO: + model = MessageVideoModel(message: message) + completion(model) + case .MESSAGE_TYPE_TEXT: + model = MessageTextModel(message: message) + completion(model) + case .MESSAGE_TYPE_IMAGE: + model = MessageImageModel(message: message) + completion(model) + case .MESSAGE_TYPE_AUDIO: + model = MessageAudioModel(message: message) + completion(model) + case .MESSAGE_TYPE_NOTIFICATION, .MESSAGE_TYPE_TIP: + // 查询通知消息中 targetId 的用户信息 + if message.messageType == .MESSAGE_TYPE_NOTIFICATION, + let attach = message.attachment as? V2NIMMessageNotificationAttachment, + var accIds = attach.targetIds { + if let senderId = message.senderId { + accIds.append(senderId) + } + + if let conversationId = message.conversationId, let tid = V2NIMConversationIdUtil.conversationTargetId(conversationId) { + ChatTeamCache.shared.loadShowName(userIds: accIds, teamId: tid) { + completion(MessageTipsModel(message: message)) + } + } else { + completion(MessageTipsModel(message: message)) + } + } else { + completion(MessageTipsModel(message: message)) + } + case .MESSAGE_TYPE_FILE: + model = MessageFileModel(message: message) + completion(model) + case .MESSAGE_TYPE_LOCATION: + model = MessageLocationModel(message: message) + completion(model) + case .MESSAGE_TYPE_CALL: + model = MessageCallRecordModel(message: message) + completion(model) + case .MESSAGE_TYPE_CUSTOM: + if let type = NECustomAttachment.typeOfCustomMessage(message.attachment) { + if type == customMultiForwardType { + completion(MessageCustomModel(message: message, contentHeight: Int(customMultiForwardCellHeight))) + return + } + if type == customRichTextType { + completion(MessageRichTextModel(message: message)) + return + } + } + fallthrough + default: + // 未识别的消息类型,默认为文本消息类型,text为未知消息体 + message.text = chatLocalizable("msg_unknown") + model = MessageTextModel(message: message) + completion(model) + } + } + /// 获取消息列表的中所以图片消息的 url + /// - Parameter messages: 消息列表 + /// - Returns: 图片路径列表 public static func getUrls(messages: [MessageModel]) -> [String] { - NELog.infoLog(ModuleName + " " + className(), desc: #function) + NEALog.infoLog(ModuleName + " " + className(), desc: #function) var urls = [String]() - messages.forEach { model in - if model.type == .image, let message = model.message?.messageObject as? NIMImageObject { + for model in messages { + if model.type == .image, let message = model.message?.attachment as? V2NIMMessageImageAttachment { if let url = message.url { urls.append(url) } else { @@ -153,20 +251,23 @@ public class ChatMessageHelper: NSObject { return urls } - // history message insert message at first of messages, send message add last of messages + /// 为消息体添加时间 + /// - Parameters: + /// - model: 消息体 + /// - lastModel: 最后一条消息 static func addTimeMessage(_ model: MessageModel, _ lastModel: MessageModel?) { guard let message = model.message else { - NELog.errorLog(ModuleName + " " + className(), desc: #function + ", model.message is nil") + NEALog.errorLog(ModuleName + " " + className(), desc: #function + ", model.message is nil") return } - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: \(String(describing: message.messageClientId))") if NotificationMessageUtils.isDiscussSeniorTeamNoti(message: message) { return } - let lastTs = lastModel?.message?.timestamp ?? 0.0 - let curTs = message.timestamp + let lastTs = lastModel?.message?.createTime ?? 0.0 + let curTs = message.createTime let dur = curTs - lastTs if (dur / 60) > 5 { let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: curTs)) @@ -174,37 +275,39 @@ public class ChatMessageHelper: NSObject { } } - public static func contentOfMessage(_ message: NIMMessage?) -> String { + /// 获取消息外显文案 + /// - Parameter message: 消息 + /// - Returns: 外显文案 + public static func contentOfMessage(_ message: V2NIMMessage?) -> String { switch message?.messageType { - case .text: + case .MESSAGE_TYPE_TEXT: if let t = message?.text { return t } else { return chatLocalizable("message_not_found") } - case .image: + case .MESSAGE_TYPE_IMAGE: return chatLocalizable("msg_image") - case .audio: + case .MESSAGE_TYPE_AUDIO: return chatLocalizable("msg_audio") - case .video: + case .MESSAGE_TYPE_VIDEO: return chatLocalizable("msg_video") - case .file: + case .MESSAGE_TYPE_FILE: return chatLocalizable("msg_file") - case .location: + case .MESSAGE_TYPE_LOCATION: return chatLocalizable("msg_location") - case .rtcCallRecord: - if let record = message?.messageObject as? NIMRtcCallRecordObject { - return record.callType == .audio ? chatLocalizable("msg_rtc_audio") : - chatLocalizable("msg_rtc_video") + case .MESSAGE_TYPE_CALL: + if let attachment = message?.attachment as? V2NIMMessageCallAttachment { + return attachment.type == 1 ? chatLocalizable("msg_rtc_audio") : chatLocalizable("msg_rtc_video") } return chatLocalizable("msg_rtc_call") - case .custom: - if let content = NECustomAttachment.contentOfRichText(message: message) { + case .MESSAGE_TYPE_CUSTOM: + if let content = NECustomAttachment.contentOfRichText(message?.attachment) { return content } - if let attach = NECustomAttachment.attachmentOfCustomMessage(message: message), - attach.customType == customMultiForwardType { + if let customType = NECustomAttachment.typeOfCustomMessage(message?.attachment), + customType == customMultiForwardType { return "[\(chatLocalizable("chat_history"))]" } @@ -215,14 +318,21 @@ public class ChatMessageHelper: NSObject { } /// 移除消息扩展字段中的 回复、@ - public static func clearForwardAtMark(_ forwardMessage: NIMMessage) { - forwardMessage.remoteExt?.removeValue(forKey: yxAtMsg) - forwardMessage.remoteExt?.removeValue(forKey: keyReplyMsgKey) - if forwardMessage.remoteExt?.count ?? 0 <= 0 { - forwardMessage.remoteExt = nil + /// - Parameter forwardMessage: 消息 + public static func clearForwardAtMark(_ forwardMessage: V2NIMMessage) { + guard var remoteExt = getDictionaryFromJSONString(forwardMessage.serverExtension ?? "") as? [String: Any] else { return } + remoteExt.removeValue(forKey: yxAtMsg) + remoteExt.removeValue(forKey: keyReplyMsgKey) + if remoteExt.count <= 0 { + remoteExt = [:] } + forwardMessage.serverExtension = getJSONStringFromDictionary(remoteExt) } + /// 构建合并转发消息附件的 header + /// - Parameters: + /// - messageCount: 消息数量 + /// - completion: 完成回调 public static func buildHeader(messageCount: Int) -> String { var dic = [String: Any]() dic["version"] = 0 // 功能版本 @@ -234,58 +344,117 @@ public class ChatMessageHelper: NSObject { return getJSONStringFromDictionary(dic) } - public static func buildBody(messages: [NIMMessage], + /// 构建合并转发消息附件的 body + /// - Parameters: + /// - messages: 消息 + /// - completion: 完成回调 + public static func buildBody(messages: [V2NIMMessage], _ completion: @escaping (String, [[String: Any]]) -> Void) { let enter = "\n" // 分隔符 var body = "" // 序列化结果 var abstracts = [[String: Any]]() // 摘要信息 - let group = DispatchGroup() for (i, msg) in messages.enumerated() { // 移除扩展字段中的 回复、@ 信息 - let remoteExt = msg.remoteExt + let remoteExt = msg.serverExtension clearForwardAtMark(msg) // 保存消息昵称和头像 - if let from = msg.from { - group.enter() - ChatUserCache.getUserInfo(from) { user, error in - if let user = user { - let senderNick = user.showName(false) - if msg.remoteExt != nil { - msg.remoteExt![mergedMessageNickKey] = senderNick - msg.remoteExt![mergedMessageAvatarKey] = user.userInfo?.avatarUrl ?? user.shortName(count: 2) - } else { - msg.remoteExt = [mergedMessageNickKey: senderNick as Any, - mergedMessageAvatarKey: user.userInfo?.avatarUrl as Any] - } - - // 摘要信息 - if i < 3 { - let content = ChatMessageHelper.contentOfMessage(msg) - abstracts.append(["senderNick": senderNick as Any, - "content": content, - "userAccId": from]) - } + if let from = msg.senderId { + let user = ChatTeamCache.shared.getTeamMemberInfo(accountId: from)?.nimUser ?? NEFriendUserCache.shared.getFriendInfo(from) ?? ChatUserCache.shared.getUserInfo(from) + if let user = user { + let senderNick = user.showNameWithAliasControl(false) + if var remoteExt = getDictionaryFromJSONString(msg.serverExtension ?? "") as? [String: Any] { + remoteExt[mergedMessageNickKey] = senderNick + remoteExt[mergedMessageAvatarKey] = user.user?.avatar ?? getShortName(senderNick ?? "") + msg.serverExtension = getJSONStringFromDictionary(remoteExt) + } else { + let remoteExt = [mergedMessageNickKey: senderNick as Any, + mergedMessageAvatarKey: user.user?.avatar as Any] + msg.serverExtension = getJSONStringFromDictionary(remoteExt) } - body.append(enter) - let data = NIMSDK.shared().conversationManager.encodeMessage(toData: msg) - if let stringData = String(data: data, encoding: .utf8) { - body.append(stringData) + + // 摘要信息 + if i < 3 { + let content = ChatMessageHelper.contentOfMessage(msg) + abstracts.append(["senderNick": senderNick as Any, + "content": content, + "userAccId": from]) } - group.leave() + } + if let stringData = ChatRepo.shared.messageSerialization(msg) { + body.append(enter + stringData) } } // 恢复扩展字段中的 回复、@ 信息 - msg.remoteExt = remoteExt + msg.serverExtension = remoteExt } - group.notify(queue: .main, work: DispatchWorkItem(block: { - completion(body, abstracts) - })) + completion(body, abstracts) } + /// 获取消息的客户端本地扩展信息(转换为[String: Any]) + /// - Parameter message: 消息 + /// - Returns: 客户端本地扩展信息 + public static func getMessageLocalExtension(message: V2NIMMessage) -> [String: Any]? { + guard let localExtension = message.localExtension else { return nil } + + if let localExt = getDictionaryFromJSONString(localExtension) as? [String: Any] { + return localExt + } + return nil + } + + /// 判断消息是否已撤回 + /// - Parameter message: 消息 + /// - Returns: 是否已撤回 + public static func isRevokeMessage(message: V2NIMMessage?) -> Bool { + guard let message = message else { return false } + + if let localExt = getMessageLocalExtension(message: message), + let isRevoke = localExt[revokeLocalMessage] as? Bool, isRevoke == true { + return true + } + return false + } + + /// 获取消息撤回前的内容(用于重新编辑) + /// - Parameter message: 消息 + /// - Returns: 撤回前的内容 + public static func getRevokeMessageContent(message: V2NIMMessage?) -> String? { + guard let message = message else { return nil } + + if let localExt = getMessageLocalExtension(message: message) { + if let content = localExt[revokeLocalMessageContent] as? String { + return content + } + } + return nil + } + + /// 查找回复信息键值对 + /// - Parameter message: 消息 + /// - Returns: 回复消息的 id + public static func getReplyDictionary(message: V2NIMMessage) -> [String: Any]? { + if let remoteExt = getDictionaryFromJSONString(message.serverExtension ?? ""), + let yxReplyMsg = remoteExt[keyReplyMsgKey] as? [String: Any] { + return yxReplyMsg + } + + return nil + } + + /// 全名后几位 + public static func getShortName(_ name: String, _ length: Int = 2) -> String { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", name: " + name) + return name + .count > length ? String(name[name.index(name.endIndex, offsetBy: -length)...]) : name + } + + /// 获取文件 MD5 值 + /// - Parameter fileURL: 文件 URL + /// - Returns: md5 值 public static func getFileChecksum(fileURL: URL) -> String? { // 打开文件,创建文件句柄 let file = FileHandle(forReadingAtPath: fileURL.path) @@ -317,15 +486,35 @@ public class ChatMessageHelper: NSObject { return md5String } - // 检测语音消息是否下载,非漫游的云端消息不会自动下载语音文件,需要手动下载 - public static func downloadAudioFile(message: NIMMessage) { - if message.messageType == .audio { - if let audio = message.messageObject as? NIMAudioObject { - if let path = audio.path, FileManager.default.fileExists(atPath: path) == false { - repo.downloadMessageAttachment(message) { error in - } - } - } + /// 构造消息附件的本地文件路径 + /// - Parameter message: 消息 + /// - Returns: 本地文件路径 + public static func createFilePath(_ message: V2NIMMessage?) -> String { + var path = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit") ?? "" + guard let attach = message?.attachment as? V2NIMMessageFileAttachment else { + return path + } + + switch message?.messageType { + case .MESSAGE_TYPE_AUDIO: + path = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit/audio/") ?? "" + case .MESSAGE_TYPE_IMAGE: + path = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit/image/") ?? "" + case .MESSAGE_TYPE_VIDEO: + path = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit/video/") ?? "" + default: + path = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit/file/") ?? "" + } + + if let messageClientId = message?.messageClientId { + path += messageClientId + } + + // 后缀(例如:.png) + if let ext = attach.ext { + path += ext } + + return path } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatTeamCache.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatTeamCache.swift new file mode 100644 index 00000000..dca88a32 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatTeamCache.swift @@ -0,0 +1,159 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import Foundation +import NEChatKit +import NECoreIM2Kit +import NIMSDK + +public class ChatTeamCache: NSObject { + public static let shared = ChatTeamCache() + private var teamInfo: V2NIMTeam? + private var cacheTeamMemberInfoDic = [String: NETeamMemberInfoModel]() + + override private init() { + super.init() + } + + public func updateTeamInfo(_ team: V2NIMTeam?) { + guard let teamId = team?.teamId else { return } + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: \(teamId)") + teamInfo = team + } + + public func updateTeamMemberInfo(_ userFriend: NEUserWithFriend?) { + guard let userId = userFriend?.user?.accountId, !NEFriendUserCache.shared.isFriend(userId) else { + return + } + + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", userId:\(userId)") + + if let teamMemberInfo = cacheTeamMemberInfoDic[userId] { + teamMemberInfo.nimUser = userFriend + } else { + let teamMemberInfo = NETeamMemberInfoModel() + teamMemberInfo.nimUser = userFriend + cacheTeamMemberInfoDic[userId] = teamMemberInfo + } + } + + public func updateTeamMemberInfo(_ teamMember: V2NIMTeamMember?) { + guard let teamMember = teamMember else { + return + } + + let accountId = teamMember.accountId + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:\(accountId)") + + if let teamMemberInfo = cacheTeamMemberInfoDic[accountId] { + teamMemberInfo.teamMember = teamMember + teamMemberInfo.nimUser = teamMemberInfo.nimUser ?? NEFriendUserCache.shared.getFriendInfo(accountId) + } else { + let teamMemberInfo = NETeamMemberInfoModel() + teamMemberInfo.teamMember = teamMember + teamMemberInfo.nimUser = teamMemberInfo.nimUser ?? NEFriendUserCache.shared.getFriendInfo(accountId) + cacheTeamMemberInfoDic[accountId] = teamMemberInfo + } + } + + public func updateTeamMemberInfo(_ teamMember: NETeamMemberInfoModel?) { + guard let teamMember = teamMember, + let accountId = teamMember.teamMember?.accountId else { + return + } + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:\(accountId)") + cacheTeamMemberInfoDic[accountId] = teamMember + } + + /// 获取缓存的群聊信息 + public func getTeamInfo() -> V2NIMTeam? { + teamInfo + } + + /// 获取缓存的群成员信息 + public func getTeamMemberInfo(accountId: String) -> NETeamMemberInfoModel? { + cacheTeamMemberInfoDic[accountId] + } + + /// 删除群成员信息缓存 + public func removeTeamMemberInfo(_ accountId: String) { + if let _ = cacheTeamMemberInfoDic[accountId] { + cacheTeamMemberInfoDic.removeValue(forKey: accountId) + } + } + + /// 删除所有信息缓存 + public func removeAllTeamInfo() { + teamInfo = nil + cacheTeamMemberInfoDic.removeAll() + } + + /// 获取缓存群成员名字,team: 备注 > 群昵称 > 昵称 > ID + public func getShowName(_ accountId: String, + _ showAlias: Bool = true) -> (name: String, user: NEUserWithFriend?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", userId: " + accountId) + // 好友缓存 + var (fullName, user) = NEFriendUserCache.shared.getShowName(accountId, showAlias) + + // 非好友缓存 + if user == nil { + (fullName, user) = ChatUserCache.shared.getShowName(accountId, showAlias) + } + + // 群成员缓存 + if let teamMember = cacheTeamMemberInfoDic[accountId] { + if teamMember.nimUser?.user?.accountId == accountId || + teamMember.teamMember?.accountId == accountId { + if let teamNick = teamMember.teamMember?.teamNick, !teamNick.isEmpty { + fullName = teamNick + } + + if showAlias, let alias = user?.friend?.alias, !alias.isEmpty { + fullName = alias + } + + return (fullName, user) + } + } + return (fullName, user) + } + + // 获取展示的群成员名字, 备注 > 群昵称 > 昵称 > ID + public func loadShowName(userIds: [String], teamId: String, _ completion: @escaping () -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: \(teamId)") + var loadUserIds = Set() // 需要查询用户信息的ID + var loadMemberIds = Set() // 需要查询群成员信息的ID + let group = DispatchGroup() + + for userId in userIds { + if !NEFriendUserCache.shared.isFriend(userId) { + loadUserIds.insert(userId) + } + + if cacheTeamMemberInfoDic[userId] == nil { + loadMemberIds.insert(userId) + } + } + + // 先查询用户信息 + group.enter() + ContactRepo.shared.getFriendInfoList(accountIds: Array(loadUserIds)) { users, error in + users?.forEach { ChatUserCache.shared.updateUserInfo($0) } + group.leave() + } + + // 再查询群成员信息 + if !loadMemberIds.isEmpty { + group.enter() + TeamRepo.shared.getTeamMemberList(teamId, Array(loadMemberIds)) { [weak self] teamMember, error in + teamMember?.forEach { self?.updateTeamMemberInfo($0) } + group.leave() + } + } + + group.notify(queue: .main) { + completion() + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatUserCache.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatUserCache.swift new file mode 100644 index 00000000..e9687d2c --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatUserCache.swift @@ -0,0 +1,57 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import Foundation +import NECoreIM2Kit +import NIMSDK + +/// 用户信息缓存,主要缓存非好友用户 +public class ChatUserCache: NSObject { + public static let shared = ChatUserCache() + + // 非好友列表,聊天页面销毁时同步清空 + public var noUserCache = [String: NEUserWithFriend]() + + override private init() { + super.init() + } + + // 添加(更新)非好友信息 + public func updateUserInfo(_ user: V2NIMUser?) { + guard let userId = user?.accountId else { return } + noUserCache[userId]?.user = user + } + + // 添加(更新)非好友信息 + public func updateUserInfo(_ user: NEUserWithFriend?) { + guard let userId = user?.user?.accountId else { return } + noUserCache[userId] = user + } + + /// 获取缓存的非好友信息 + public func getUserInfo(_ accountId: String) -> NEUserWithFriend? { + noUserCache[accountId] + } + + /// 删除非好友信息缓存 + public func removeUserInfo(_ accountId: String) { + if let _ = noUserCache[accountId] { + noUserCache.removeValue(forKey: accountId) + } + } + + /// 删除所有非好友信息缓存 + public func removeAllUserInfo() { + noUserCache.removeAll() + } + + /// 获取缓存用户名字,p2p: 备注 > 昵称 > ID + public func getShowName(_ userId: String, + _ showAlias: Bool = true) -> (String, NEUserWithFriend?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", userId: " + userId) + let user = getUserInfo(userId) + let fullName = user?.showName(showAlias) ?? userId + return (fullName, user) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift index 1856b5a4..fefac910 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift @@ -4,120 +4,87 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NEChatKit +import NECoreIM2Kit import NIMSDK @objcMembers open class MessageUtils: NSObject { - open class func textMessage(text: String) -> NIMMessage { - let message = NIMMessage() - message.setting = messageSetting() - message.text = text - return message - } - - open class func textMessage(text: String, remoteExt: [String: Any]?) -> NIMMessage { - let message = NIMMessage() - message.setting = messageSetting() - message.text = text - if remoteExt?.count ?? 0 > 0 { - message.remoteExt = remoteExt + open class func textMessage(text: String, remoteExt: [String: Any]?) -> V2NIMMessage { + let message = V2NIMMessageCreator.createTextMessage(text) + if let remoteExt = remoteExt { + message.serverExtension = getJSONStringFromDictionary(remoteExt) } return message } - open class func imageMessage(image: UIImage) -> NIMMessage { - imageMessage(imageObject: NIMImageObject(image: image)) + open class func textMessage(text: String) -> V2NIMMessage { + V2NIMMessageCreator.createTextMessage(text) } - open class func imageMessage(path: String) -> NIMMessage { - imageMessage(imageObject: NIMImageObject(filepath: path)) + open class func forwardMessage(message: V2NIMMessage) -> V2NIMMessage { + V2NIMMessageCreator.createForwardMessage(message) } - open class func imageMessage(data: Data, ext: String) -> NIMMessage { - imageMessage(imageObject: NIMImageObject(data: data, extension: ext)) + open class func imageMessage(path: String, + name: String?, + sceneName: String?, + width: Int32, + height: Int32) -> V2NIMMessage { + V2NIMMessageCreator.createImageMessage(path, + name: name, + sceneName: sceneName ?? V2NIMStorageSceneConfig.default_IM().sceneName, + width: width, + height: height) } - open class func imageMessage(imageObject: NIMImageObject) -> NIMMessage { - let message = NIMMessage() - let option = NIMImageOption() - option.compressQuality = 0.8 - imageObject.option = option - message.messageObject = imageObject - message.apnsContent = chatLocalizable("send_picture") - message.setting = messageSetting() - return message + open class func audioMessage(filePath: String, + name: String?, + sceneName: String?, + duration: Int32) -> V2NIMMessage { + V2NIMMessageCreator.createAudioMessage(filePath, name: name, + sceneName: sceneName ?? V2NIMStorageSceneConfig.default_IM().sceneName, + duration: duration) } - open class func audioMessage(filePath: String) -> NIMMessage { - let messageObject = NIMAudioObject(sourcePath: filePath) - let message = NIMMessage() - message.messageObject = messageObject - message.apnsContent = chatLocalizable("send_voice") - message.setting = messageSetting() - return message + open class func videoMessage(filePath: String, + name: String?, + sceneName: String?, + width: Int32, + height: Int32, + duration: Int32) -> V2NIMMessage { + V2NIMMessageCreator.createVideoMessage(filePath, + name: name, + sceneName: sceneName ?? V2NIMStorageSceneConfig.default_IM().sceneName, + duration: duration, + width: width, + height: height) } - open class func videoMessage(filePath: String) -> NIMMessage { - let messageObject = NIMVideoObject(sourcePath: filePath) - let message = NIMMessage() - message.messageObject = messageObject - message.apnsContent = chatLocalizable("send_video") - message.setting = messageSetting() - return message + open class func locationMessage(lat: Double, + lng: Double, + address: String) -> V2NIMMessage { + V2NIMMessageCreator.createLocationMessage(lat, longitude: lng, address: address) } - open class func locationMessage(_ lat: Double, _ lng: Double, _ title: String, _ address: String) -> NIMMessage { - let messageObject = NIMLocationObject(latitude: lat, longitude: lng, title: address) - let message = NIMMessage() - message.messageObject = messageObject - message.text = title - message.apnsContent = chatLocalizable("send_location") - message.setting = messageSetting() - return message + open class func fileMessage(filePath: String, + displayName: String?, + sceneName: String?) -> V2NIMMessage { + V2NIMMessageCreator.createFileMessage(filePath, + name: displayName, + sceneName: sceneName ?? V2NIMStorageSceneConfig.default_IM().sceneName) } - open class func fileMessage(filePath: String, displayName: String?) -> NIMMessage { - let messageObject = NIMFileObject(sourcePath: filePath) - if let dpName = displayName { - messageObject.displayName = dpName - } - let message = NIMMessage() - message.messageObject = messageObject - message.apnsContent = chatLocalizable("send_file") - message.setting = messageSetting() - return message - } - - open class func fileMessage(data: Data, displayName: String?) -> NIMMessage { - let dpName = displayName ?? "" - let pointIndex = dpName.lastIndex(of: ".") ?? dpName.startIndex - let suffix = dpName[dpName.index(after: pointIndex) ..< dpName.endIndex] - let messageObject = NIMFileObject(data: data, extension: String(suffix)) - messageObject.displayName = dpName - let message = NIMMessage() - message.messageObject = messageObject - message.apnsContent = chatLocalizable("send_file") - message.setting = messageSetting() - return message + open class func customMessage(text: String, + rawAttachment: String) -> V2NIMMessage { + V2NIMMessageCreator.createCustomMessage(text, rawAttachment: rawAttachment) } - open class func customMessage(attachment: NIMCustomAttachment?, - remoteExt: [String: Any]?, - apnsContent: String?) -> NIMMessage { - let messageObject = NIMCustomObject() - messageObject.attachment = attachment - let message = NIMMessage() - message.messageObject = messageObject - message.apnsContent = apnsContent - message.remoteExt = remoteExt - message.setting = messageSetting() - return message + open class func tipMessage(text: String) -> V2NIMMessage { + V2NIMMessageCreator.createTipsMessage(text) } - open class func messageSetting() -> NIMMessageSetting { - let setting = NIMMessageSetting() - setting.teamReceiptEnabled = SettingProvider.shared.getMessageRead() - return setting + open class func messageConfig() -> V2NIMMessageConfig { + ChatRepo.shared.messageConfig() } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift index 2f8e97d8..343b9d8a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift @@ -5,7 +5,7 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK @@ -15,179 +15,128 @@ public enum TeamType { } open class NotificationMessageUtils: NSObject { - open class func textForNotification(message: NIMMessage) -> String { - if message.messageType != .notification { + open class func textForNotification(message: V2NIMMessage) -> String { + if message.messageType != .MESSAGE_TYPE_NOTIFICATION { return "" } - if let object = message.messageObject as? NIMNotificationObject { - switch object.notificationType { - case .team: - return textForTeamNotificationMessage(message: message) - case .superTeam: - return "" - case .netCall: - return "" - case .chatroom: - return "" - default: - return "" - } + if message.attachment is V2NIMMessageNotificationAttachment { + let text = textForTeamNotificationMessage(message: message) + return text + } else { + return "" } - return "" } /// 是否是群通知 - open class func isDiscussSeniorTeamNoti(message: NIMMessage) -> Bool { - if let object = message.messageObject as? NIMNotificationObject, - let _ = object.content as? NIMTeamNotificationContent { + open class func isDiscussSeniorTeamNoti(message: V2NIMMessage) -> Bool { + if message.attachment is V2NIMMessageNotificationAttachment { return true } return false } - open class func isDiscussSeniorTeamUpdateCustomNoti(message: NIMMessage) -> Bool { - if let object = message.messageObject as? NIMNotificationObject { - guard let content = object.content as? NIMTeamNotificationContent else { - return false - } - - // 转移讨论组的通知 - if content.operationType == .transferOwner, - teamType(message: message) == .discussTeam { - return true - } - - if content.operationType != .update { - return false - } - guard let attach = content.attachment as? NIMUpdateTeamInfoAttachment, - let tag = attach.values?.keys.first?.intValue else { - return false - } - - // 18:客户端自定义拓展字段, 19: 服务器自定义拓展字段 - if tag == 18 || tag == 19 { - return true - } - } - return false - } - - open class func isTeamLeaveOrDismiss(message: NIMMessage) -> (isLeave: Bool, isDismiss: Bool) { + open class func isTeamLeaveOrDismiss(message: V2NIMMessage) -> (isLeave: Bool, isDismiss: Bool) { var leave = false var dismiss = false - if let object = message.messageObject as? NIMNotificationObject, object.notificationType == .team { - if let content = object.content as? NIMTeamNotificationContent { - switch content.operationType { - case .leave: - leave = true - - case .dismiss: - dismiss = true - - @unknown default: - break - } + if let content = message.attachment as? V2NIMMessageNotificationAttachment { + switch content.type { + case .MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE: + leave = true + case .MESSAGE_NOTIFICATION_TYPE_TEAM_DISMISS: + dismiss = true + default: + break } } return (leave, dismiss) } - open class func textForTeamNotificationMessage(message: NIMMessage) -> String { + open class func textForTeamNotificationMessage(message: V2NIMMessage) -> String { var text = chatLocalizable("unknown_system_message") - if let object = message.messageObject as? NIMNotificationObject { - if let content = object.content as? NIMTeamNotificationContent { - let fromName = fromName(message: message) - let toNames = toName(message: message) - let toFirstName = toNames.first ?? "" - let teamName = teamName(message: message) - var toNamestext = toNames.first ?? "" - if toNames.count > 1 { - toNamestext = toNames.joined(separator: "、") + if let content = message.attachment as? V2NIMMessageNotificationAttachment { + let fromName = fromName(message: message) + let toNames = toName(message: message) + let toFirstName = toNames.first ?? "" + let teamName = teamName(message: message) + var toNamestext = toNames.first ?? "" + if toNames.count > 1 { + toNamestext = toNames.joined(separator: "、") + } + switch content.type { + case .MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE: + text = fromName + chatLocalizable("invite") + toNamestext + chatLocalizable("enter") + chatLocalizable("group_chat") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_DISMISS: + text = fromName + chatLocalizable("dissolve") + chatLocalizable("group_chat") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_KICK: + text = fromName + chatLocalizable("kick") + toNamestext + chatLocalizable("out") + chatLocalizable("group_chat") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_UPDATE_TINFO: + text = "update team info" + text = textOfUpdateTeam( + fromName: fromName, + teamName: teamName, + content: content + ) + case .MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE: + text = fromName + chatLocalizable("leave") + chatLocalizable("group_chat") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_APPLY_PASS: + if fromName == toNamestext { + text = fromName + chatLocalizable("join") + chatLocalizable("group_chat") + } else { + text = fromName + chatLocalizable("pass") + toNamestext } - switch content.operationType { - case .invite: - text = fromName + chatLocalizable("invite") + toNamestext + chatLocalizable("enter") + chatLocalizable("group_chat") - case .dismiss: - text = fromName + chatLocalizable("dissolve") + chatLocalizable("group_chat") - case .kick: - text = fromName + chatLocalizable("kick") + toNamestext + chatLocalizable("out") + chatLocalizable("group_chat") - case .update: - text = textOfUpdateTeam( - fromName: fromName, - teamName: teamName, - content: content - ) - case .leave: - text = fromName + chatLocalizable("leave") + chatLocalizable("group_chat") - case .applyPass: - if fromName == toNamestext { - text = fromName + chatLocalizable("join") + chatLocalizable("group_chat") - } else { - text = fromName + chatLocalizable("pass") + toNamestext - } - case .transferOwner: - text = fromName + chatLocalizable("transfer") + toFirstName - case .addManager: - text = toNamestext + chatLocalizable("added_manager") - case .removeManager: - text = toFirstName + chatLocalizable("removed_manager") - case .acceptInvitation: - text = fromName + chatLocalizable("accept") + toNamestext - case .mute: - var mute = false - if let atta = content.attachment as? NIMMuteTeamMemberAttachment { - mute = atta.flag - } - if let atta = content.attachment as? NIMMuteSuperTeamMemberAttachment { - mute = atta.flag - } - // text = mute ? chatLocalizable("team_all_mute") : chatLocalizable("team_all_no_mute") - text = "\(toNamestext) \(mute ? chatLocalizable("mute") : chatLocalizable("not_mute"))" + case .MESSAGE_NOTIFICATION_TYPE_TEAM_OWNER_TRANSFER: + text = fromName + chatLocalizable("transfer") + toFirstName + case .MESSAGE_NOTIFICATION_TYPE_TEAM_ADD_MANAGER: + text = toNamestext + chatLocalizable("added_manager") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_REMOVE_MANAGER: + text = toFirstName + chatLocalizable("removed_manager") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE_ACCEPT: + text = fromName + chatLocalizable("accept") + toNamestext + case .MESSAGE_NOTIFICATION_TYPE_TEAM_BANNED_TEAM_MEMBER: + text = "\(toNamestext) \(content.chatBanned ? chatLocalizable("mute") : chatLocalizable("not_mute"))" - default: - text = chatLocalizable("unknown_system_message") - } + default: + text = chatLocalizable("unknown_system_message") } + return text + } else { + return text } - return text } - open class func fromName(message: NIMMessage) -> String { - if let object = message.messageObject as? NIMNotificationObject { - if let content = object.content as? NIMTeamNotificationContent { - if content.sourceID == NIMSDK.shared().loginManager.currentAccount() { - return chatLocalizable("You") + " " - } else { - if let sourceId = content.sourceID { - return ChatUserCache.getShowName(userId: sourceId, teamId: message.session?.sessionId) - } - } + open class func fromName(message: V2NIMMessage) -> String { + if let sourceId = message.senderId { + if sourceId == IMKitClient.instance.account() { + return chatLocalizable("You") + " " + } else { + let (name, _) = ChatTeamCache.shared.getShowName(sourceId) + return name } + } else { + return "" } - return "" } - open class func toName(message: NIMMessage) -> [String] { + open class func toName(message: V2NIMMessage) -> [String] { var toNames = [String]() - guard let object = message.messageObject as? NIMNotificationObject, - let content = object.content as? NIMTeamNotificationContent, - let targetIDs = content.targetIDs else { + guard let content = message.attachment as? V2NIMMessageNotificationAttachment, + let targetIDs = content.targetIds else { return toNames } + for targetID in targetIDs { - if targetID == NIMSDK.shared().loginManager.currentAccount() { + if targetID == IMKitClient.instance.account() { toNames.append(chatLocalizable("You") + " ") } else { - toNames - .append(ChatUserCache.getShowName(userId: targetID, teamId: message.session?.sessionId)) + let (name, _) = ChatTeamCache.shared.getShowName(targetID) + toNames.append(name) } } return toNames } - open class func teamName(message: NIMMessage) -> String { + open class func teamName(message: V2NIMMessage) -> String { let teamtype = teamType(message: message) switch teamtype { case .advanceTeam: @@ -197,115 +146,97 @@ open class NotificationMessageUtils: NSObject { } } - open class func teamType(message: NIMMessage) -> TeamType { - let team = TeamProvider.shared.getTeam(teamId: message.session?.sessionId ?? "") - if team?.isDisscuss() == true { - return .discussTeam - } else { - return .advanceTeam + open class func teamType(message: V2NIMMessage) -> TeamType { + if let team = ChatTeamCache.shared.getTeamInfo() { + if team.isDisscuss() == true { + return .discussTeam + } else { + return .advanceTeam + } } + return .advanceTeam } private class func textOfUpdateTeam(fromName: String, teamName: String, - content: NIMTeamNotificationContent) -> String { + content: V2NIMMessageNotificationAttachment) -> String { var text = fromName + chatLocalizable("has_updated") + teamName - if let attach = content.attachment as? NIMUpdateTeamInfoAttachment { - if let tag = attach.values { - let string = getShowString(fromName, teamName, tag) - if string.count > 0 { - text = string - } - } - } - if let attach = content.attachment as? NIMMuteTeamMemberAttachment { - if attach.flag == false { - text = teamName + chatLocalizable("team_all_mute") - } else { - text = teamName + chatLocalizable("team_all_no_mute") - } - } - return text - } - private class func getShowString(_ fromName: String, - _ teamName: String, - _ tag: [NSNumber: String]) -> String { - var text = "" + guard let updatedTeamInfo = content.updatedTeamInfo else { return text } // 群名 - if let value = tag[3] { - text = fromName + " " + chatLocalizable("has_updated") + teamName + - chatLocalizable("team_name") + chatLocalizable("to") + "\"" + value + "\"" + if let name = updatedTeamInfo.name { + return fromName + " " + chatLocalizable("has_updated") + teamName + + chatLocalizable("team_name") + chatLocalizable("to") + "\"" + name + "\"" } // 群简介 - if let _ = tag[14] { - text = fromName + " " + chatLocalizable("has_updated") + teamName + + if updatedTeamInfo.intro != nil { + return fromName + " " + chatLocalizable("has_updated") + teamName + chatLocalizable("team_intro") } // 群公告 - if let _ = tag[15] { - text = fromName + " " + chatLocalizable("has_updated") + teamName + + if updatedTeamInfo.announcement != nil { + return fromName + " " + chatLocalizable("has_updated") + teamName + chatLocalizable("team_anouncement") } - // 群验证方式 - if let _ = tag[16] { - text = fromName + " " + chatLocalizable("has_updated") + teamName + - chatLocalizable("team_join_mode") - } - - // 客户端自定义拓展字段 - if let _ = tag[18] { - text = fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_custom_info") - } - - // 服务器自定义拓展字段(SDK 无法直接修改这个字段, 请调用服务器接口) - if let _ = tag[19] { - text = fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_custom_info") - } - // 头像 - if let _ = tag[20] { - text = fromName + " " + chatLocalizable("has_updated") + teamName + + if updatedTeamInfo.avatar != nil { + return fromName + " " + chatLocalizable("has_updated") + teamName + chatLocalizable("team_avatar") } + // 群验证方式 + if updatedTeamInfo.joinMode.rawValue != -1 { + return fromName + " " + chatLocalizable("has_updated") + teamName + + chatLocalizable("team_join_mode") + } + // 被邀请模式 - if let _ = tag[21] { - text = fromName + " " + chatLocalizable("has_updated") + + if updatedTeamInfo.agreeMode.rawValue != -1 { + return fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_be_invited_author") } // 邀请权限,仅高级群有效 - if let value = tag[22] { - text = fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_permission") + " \"" + - chatLocalizable("team_be_invited_permission") + "\" " + chatLocalizable("to") + "\"" + (value == "0" ? chatLocalizable("only_team_owner") : chatLocalizable("user_select_all")) + "\"" + if updatedTeamInfo.inviteMode.rawValue != -1 { + return fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_permission") + " \"" + + chatLocalizable("team_be_invited_permission") + "\" " + chatLocalizable("to") + "\"" + (updatedTeamInfo.inviteMode == .TEAM_INVITE_MODE_MANAGER ? chatLocalizable("only_team_owner") : chatLocalizable("user_select_all")) + "\"" } // 更新群信息权限,仅高级群有效 - if let value = tag[23] { - text = fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_permission") + " \"" + - chatLocalizable("team_update_info_permission") + "\" " + chatLocalizable("to") + "\"" + (value == "0" ? chatLocalizable("only_team_owner") : chatLocalizable("user_select_all")) + "\"" + if updatedTeamInfo.updateInfoMode.rawValue != -1 { + return fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_permission") + " \"" + + chatLocalizable("team_update_info_permission") + "\" " + chatLocalizable("to") + "\"" + (updatedTeamInfo.updateInfoMode == .TEAM_UPDATE_INFO_MODE_MANAGER ? chatLocalizable("only_team_owner") : chatLocalizable("user_select_all")) + "\"" } // 更新群客户端自定义拓展字段权限 - if let _ = tag[24] { - text = fromName + " " + chatLocalizable("has_updated") + + if updatedTeamInfo.updateExtensionMode.rawValue != -1 { + return fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_update_client_custom") } - // 群禁言模式 - if let value = tag[100] { - if value == "1" || value == "3" { - text = chatLocalizable("team_all_mute") - } else if value == "0" { - text = chatLocalizable("team_all_no_mute") - } + // 群整体禁言 + if updatedTeamInfo.chatBannedMode.rawValue != -1 { + return updatedTeamInfo.chatBannedMode == .TEAM_CHAT_BANNED_MODE_BANNED_NORMAL ? chatLocalizable("team_all_mute") : chatLocalizable("team_all_no_mute") } + // 客户端自定义拓展字段 + if let serverExt = updatedTeamInfo.serverExtension, let extDic = getDictionaryFromJSONString(serverExt) { + // @所有人权限 + if let allowAt = extDic[keyAllowAtAll] as? String { + if allowAt == allowAtManagerValue { + return chatLocalizable("team_at_permission") + chatLocalizable("only_team_owner") + } + if allowAt == allowAtAllValue { + return chatLocalizable("team_at_permission") + chatLocalizable("everyone") + } + } + } else { + return fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_custom_info") + } return text } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift index 83e54bf7..88e26b1f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift @@ -13,7 +13,7 @@ open class ReplyMessageUtil: NSObject { } if model.type == .reply { - if let content = NECustomAttachment.contentOfRichText(message: model.message) { + if let content = NECustomAttachment.contentOfRichText(model.message?.attachment) { return text + content } text += "\(model.message?.text ?? chatLocalizable("message_not_found"))" diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift index 78b8ddeb..bc70136f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift @@ -11,19 +11,19 @@ open class MessageAudioModel: MessageContentModel { public var duration: Int = 0 public var isPlaying = false - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .audio var audioW = 96.0 let audioTotalWidth = kScreenWidth <= 325 ? 230 : 265.0 // contentSize - if let obj = message?.messageObject as? NIMAudioObject { - duration = obj.duration / 1000 + if let obj = message?.attachment as? V2NIMMessageAudioAttachment { + duration = Int(obj.duration / 1000) if duration > 2 { audioW = min(Double(duration) * 8 + audioW, audioTotalWidth) } } contentSize = CGSize(width: audioW, height: chat_min_h) - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift index 56acfafe..e35783d4 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift @@ -11,16 +11,20 @@ import UIKit open class MessageCallRecordModel: MessageContentModel { public var attributeStr: NSMutableAttributedString? - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .rtcCallRecord var isAuiodRecord = false - if let object = message?.messageObject as? NIMRtcCallRecordObject, let isSend = message?.isOutgoingMsg { + + if let attach = message?.attachment as? V2NIMMessageCallAttachment { attributeStr = NSMutableAttributedString() + let callType = attach.type + let callStatus = attach.status var image: UIImage? var bound = CGRect.zero let offset: CGFloat = -1 - if object.callType == .audio { + + if callType == 1 { isAuiodRecord = true image = coreLoader.loadImage("audio_record") bound = CGRect(x: 0, y: offset - 5, width: 24, height: 24) @@ -28,20 +32,26 @@ open class MessageCallRecordModel: MessageContentModel { image = coreLoader.loadImage("video_record") bound = CGRect(x: 0, y: offset, width: 24, height: 14) } - switch object.callStatus { - case .complete: - var timeString = "00:00" - if let duration = object.durations[NIMSDK.shared().loginManager.currentAccount()] { - timeString = Date.getFormatPlayTime(duration.doubleValue) + + switch callStatus { + case 1: + var duration: TimeInterval = 0 + for durationModel in attach.durations { + if durationModel.accountId == message?.senderId { + duration = TimeInterval(durationModel.duration) + break + } } + + let timeString = Date.getFormatPlayTime(duration) attributeStr?.append(NSAttributedString(string: chatLocalizable("call_complete") + " \(timeString)")) - case .canceled: + case 2: attributeStr?.append(NSAttributedString(string: chatLocalizable("call_canceled"))) - case .rejected: + case 3: attributeStr?.append(NSAttributedString(string: chatLocalizable("call_rejected"))) - case .timeout: + case 4: attributeStr?.append(NSAttributedString(string: chatLocalizable("call_timeout"))) - case .busy: + case 5: attributeStr?.append(NSAttributedString(string: chatLocalizable("call_busy"))) default: break @@ -49,7 +59,7 @@ open class MessageCallRecordModel: MessageContentModel { let attachment = NSTextAttachment() attachment.image = image attachment.bounds = bound - if isSend { + if message?.isSelf == true { attributeStr?.append(NSAttributedString(string: " ")) attributeStr?.append(NSAttributedString(attachment: attachment)) } else { @@ -57,17 +67,15 @@ open class MessageCallRecordModel: MessageContentModel { attributeStr?.insert(NSAttributedString(attachment: attachment), at: 0) } - attributeStr?.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize), range: NSMakeRange(0, attributeStr?.length ?? 0)) + attributeStr?.addAttribute(NSAttributedString.Key.font, value: messageTextFont, range: NSMakeRange(0, attributeStr?.length ?? 0)) attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: NEKitChatConfig.shared.ui.messageProperties.messageTextColor, range: NSMakeRange(0, attributeStr?.length ?? 0)) } - let textSize = attributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize), CGSize(width: chat_content_maxW, height: CGFloat.greatestFiniteMagnitude)) ?? .zero - + let textSize = NSAttributedString.getRealSize(attributeStr, messageTextFont, messageMaxSize) var h = chat_min_h h = textSize.height + (isAuiodRecord ? 20 : 24) contentSize = CGSize(width: textSize.width + chat_cell_margin * 2, height: h) - - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift index 5616d902..8d287f59 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift @@ -5,18 +5,21 @@ import CoreAudio import Foundation -import NECoreIMKit +import NECoreIM2Kit import NIMSDK -import simd @objcMembers open class MessageContentModel: NSObject, MessageModel { + public var type: MessageType = .custom + public var message: V2NIMMessage? + public var offset: CGFloat = 0 + public var contentSize = CGSize(width: 32.0, height: chat_min_h) + public var height: CGFloat = 48 open func cellHeight() -> CGFloat { CGFloat(height) + offset } - public var isReplay: Bool = false public var showSelect: Bool = false // 多选按钮是否展示 public var isSelected: Bool = false // 多选是否选中 public var inMultiForward: Bool = false { // 是否是合并消息中的子消息 @@ -31,20 +34,16 @@ open class MessageContentModel: NSObject, MessageModel { } } - public var pinAccount: String? - public var pinShowName: String? - public var type: MessageType = .custom - public var message: NIMMessage? - public var contentSize = CGSize(width: 32.0, height: chat_min_h) - public var height: CGFloat = 48 + public var avatar: String? public var shortName: String? // 昵称 > uid public var fullName: String? // 备注 >(群昵称)> 昵称 > uid - public var avatar: String? - public var replyText: String? public var fullNameHeight: CGFloat = 0 - public var isRevokedText: Bool = false - public var timeOut = false + public var readCount: Int = 0 + public var unreadCount: Int = 0 + + public var isReplay: Bool = false + public var replyText: String? public var replyedModel: MessageModel? { didSet { if let reply = replyedModel as? MessageContentModel, reply.isReplay == true { @@ -55,11 +54,13 @@ open class MessageContentModel: NSObject, MessageModel { } } + public var isReedit: Bool = false + public var timeOut = false public var isRevoked: Bool = false { didSet { if isRevoked { type = .revoke - if let time = message?.timestamp { + if let time = message?.createTime { let date = Date() let currentTime = date.timeIntervalSince1970 if currentTime - time > 60 * 2 { @@ -67,7 +68,7 @@ open class MessageContentModel: NSObject, MessageModel { } } // 只有文本消息,才计算可编辑按钮的宽度 - if let isSend = message?.isOutgoingMsg, isSend, message?.messageType == .text, timeOut == false { + if let isSend = message?.isSelf, isSend, message?.messageType == .MESSAGE_TYPE_TEXT, timeOut == false { contentSize = CGSize(width: 218, height: chat_min_h) } else { contentSize = CGSize(width: 130, height: chat_min_h) @@ -91,15 +92,9 @@ open class MessageContentModel: NSObject, MessageModel { } } - public var isPined: Bool = false { - didSet { - if isPined { - height += chat_pin_height - } else if oldValue { - height -= chat_pin_height - } - } - } + public var pinAccount: String? + public var pinShowName: String? + public var isPined: Bool = false // 是否显示时间 public var timeContent: String? { @@ -110,12 +105,15 @@ open class MessageContentModel: NSObject, MessageModel { } } - public required init(message: NIMMessage?) { + public let messageTextFont = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + public let messageMaxSize = CGSize(width: chat_content_maxW, height: CGFloat.greatestFiniteMagnitude) + + public required init(message: V2NIMMessage?) { self.message = message - if message?.session?.sessionType == .team, - !IMKitClient.instance.isMySelf(message?.from) { + if message?.conversationType == .CONVERSATION_TYPE_TEAM, + !IMKitClient.instance.isMe(message?.senderId) { fullNameHeight = NEKitChatConfig.shared.ui.messageProperties.showTeamMessageNick ? 20 : 0 } - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift index fcecc360..2df9aa86 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift @@ -2,17 +2,21 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NECoreIM2Kit import NIMSDK import UIKit @objc open class MessageCustomModel: MessageContentModel { - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .custom - if let attachment = NECustomAttachment.attachmentOfCustomMessage(message: message) { - contentSize = CGSize(width: 0, height: Int(attachment.cellHeight)) - height = contentSize.height + chat_content_margin * 2 + fullNameHeight - } + } + + public init(message: V2NIMMessage?, contentHeight: Int) { + super.init(message: message) + type = .custom + contentSize = CGSize(width: 0, height: contentHeight) + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift index 4a64f250..cd10df0b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift @@ -7,27 +7,21 @@ import NIMSDK import UIKit @objcMembers -open class MessageFileModel: MessageContentModel { +open class MessageFileModel: MessageVideoModel { public var displayName: String? public var path: String? - public var url: String? public var fileLength: Int64? - - public var progress: Float = 0 public var size: Float = 0 - public var state = DownloadState.Success - public weak var cell: NEChatBaseCell? - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .file - if let fileObject = message?.messageObject as? NIMFileObject { - displayName = fileObject.displayName + if let fileObject = message?.attachment as? V2NIMMessageFileAttachment { + displayName = fileObject.name path = fileObject.path - url = fileObject.url - fileLength = fileObject.fileLength + fileLength = Int64(fileObject.size) } contentSize = chat_file_size - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift index ccb7bd0f..8557abe3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift @@ -4,29 +4,33 @@ // found in the LICENSE file. import Foundation +import NEChatKit import NIMSDK @objcMembers open class MessageImageModel: MessageContentModel { - public var imageUrl: String? + public var urlString: String? - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .image - if let imageObject = message?.messageObject as? NIMImageObject { + if let imageObject = message?.attachment as? V2NIMMessageImageAttachment { if let path = imageObject.path, FileManager.default.fileExists(atPath: path) { - imageUrl = path - } else { - imageUrl = imageObject.url + urlString = path + } else if let url = imageObject.url { + if imageObject.ext?.lowercased() != ".gif" { + urlString = ResourceRepo.shared.imageThumbnailURL(url) + } + urlString = url } contentSize = ChatMessageHelper.getSizeWithMaxSize( chat_pic_size, - size: imageObject.size, + size: CGSize(width: Int(imageObject.width), height: Int(imageObject.height)), miniWH: chat_min_h ) } else { contentSize = chat_pic_size } - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift index d193d92e..556386fd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift @@ -12,16 +12,16 @@ open class MessageLocationModel: MessageContentModel { public var title: String? public var subTitle: String? - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .location - if let locationObject = message?.messageObject as? NIMLocationObject { + if let locationObject = message?.attachment as? V2NIMMessageLocationAttachment { lat = locationObject.latitude lng = locationObject.longitude - subTitle = locationObject.title + subTitle = locationObject.address title = message?.text contentSize = CGSize(width: 242, height: 140) } - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift index db8fb8a9..cf68229c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift @@ -32,35 +32,45 @@ public enum MessageType: Int { @objc public protocol MessageModel: NSObjectProtocol { - var message: NIMMessage? { get set } - // 气泡区域的大小 不包含气泡上下到cell上下的边距 - var contentSize: CGSize { get set } + var message: V2NIMMessage? { get set } + var type: MessageType { get set } + + // 宽高 + var contentSize: CGSize { get set } // 气泡区域的大小 不包含气泡上下到cell上下的边距 + var offset: CGFloat { get set } var height: CGFloat { get set } -// 名字后2位 - var shortName: String? { get set } -// 名字全长 - var fullName: String? { get set } + func cellHeight() -> CGFloat + + // 名称头像 + var shortName: String? { get set } // 名字后2位 + var fullName: String? { get set } // 名字全长 var avatar: String? { get set } - var type: MessageType { get set } - var isRevoked: Bool { get set } + + // 标记 var isPined: Bool { get set } -// userID var pinAccount: String? { get set } var pinShowName: String? { get set } -// 被回复的消息 - var replyedModel: MessageModel? { get set } - var replyText: String? { get set } - var isRevokedText: Bool { get set } + + // 回复 var isReplay: Bool { get set } + var replyedModel: MessageModel? { get set } // 被回复的消息 + var replyText: String? { get set } + + // 撤回 + var isRevoked: Bool { get set } // 消息是否已撤回 + var isReedit: Bool { get set } // 撤回消息是否可以重新编辑 + + // 已读未读 + var readCount: Int { get set } + var unreadCount: Int { get set } + + // 多选 var showSelect: Bool { get set } // 多选按钮是否展示 var isSelected: Bool { get set } // 多选是否选中 + var inMultiForward: Bool { get set } // 是否是合并消息中的子消息 var timeContent: String? { get set } // 具体时间 - init(message: NIMMessage?) - - var offset: CGFloat { get set } - - func cellHeight() -> CGFloat + init(message: V2NIMMessage?) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift index ce115450..744ab459 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift @@ -12,8 +12,8 @@ open class MessageRichTextModel: MessageTextModel { public var titleAttributeStr: NSMutableAttributedString? public var titleTextHeight: CGFloat = 0 - public required init(message: NIMMessage?) { - guard let data = NECustomAttachment.dataOfCustomMessage(message: message), + public required init(message: V2NIMMessage?) { + guard let data = NECustomAttachment.dataOfCustomMessage(message?.attachment), let title = data["title"] as? String else { super.init(message: message) return @@ -30,12 +30,11 @@ open class MessageRichTextModel: MessageTextModel { font: font ) - let textSize = titleAttributeStr?.finalSize(font, CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) ?? .zero - + let textSize = NSAttributedString.getRealSize(titleAttributeStr, messageTextFont, messageMaxSize) titleTextHeight = textSize.height - contentSize = CGSize(width: max(contentSize.width, textSize.width + chat_content_margin * 2), + contentSize = CGSize(width: max(textWidght, textSize.width) + chat_content_margin * 2, height: contentSize.height + titleTextHeight + (body.isEmpty ? 0 : chat_content_margin)) - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift index 0f92533a..2a519382 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift @@ -11,22 +11,23 @@ import NIMSDK open class MessageTextModel: MessageContentModel { public var attributeStr: NSMutableAttributedString? public var textHeight: CGFloat = 0 + public var textWidght: CGFloat = 0 - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .text attributeStr = NEEmotionTool.getAttWithStr( str: message?.text ?? "", - font: UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + font: messageTextFont ) - if let remoteExt = message?.remoteExt, let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { - dic.forEach { (key: String, value: AnyObject) in + if let remoteExt = getDictionaryFromJSONString(message?.serverExtension ?? ""), let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { + for (_, value) in dic { if let contentDic = value as? [String: AnyObject] { if let array = contentDic[atSegmentsKey] as? [AnyObject] { if let models = NSArray.yx_modelArray(with: MessageAtInfoModel.self, json: array) as? [MessageAtInfoModel] { - models.forEach { model in + for model in models { if attributeStr?.length ?? 0 > model.end { attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_blueText, range: NSMakeRange(model.start, model.end - model.start + atRangeOffset)) } @@ -37,10 +38,10 @@ open class MessageTextModel: MessageContentModel { } } - let textSize = attributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize), CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) ?? .zero - + let textSize = NSAttributedString.getRealSize(attributeStr, messageTextFont, messageMaxSize) textHeight = textSize.height + textWidght = textSize.width contentSize = CGSize(width: textSize.width + chat_content_margin * 2, height: textHeight + chat_content_margin * 2) - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift index b2e1c071..4e6b578f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift @@ -10,29 +10,23 @@ import NIMSDK open class MessageTipsModel: MessageContentModel { var text: String? - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) - commonInit() - } - - func setText() { + type = .tip if let msg = message { - if msg.messageType == .notification { + if msg.messageType == .MESSAGE_TYPE_NOTIFICATION { text = NotificationMessageUtils.textForNotification(message: msg) type = .notification - } else if msg.messageType == .tip { + } else if msg.messageType == .MESSAGE_TYPE_TIP { text = msg.text type = .tip } } - } - func commonInit() { - setText() let font: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.timeTextSize) - contentSize = text?.finalSize(font, CGSize(width: chat_content_maxW, height: CGFloat.greatestFiniteMagnitude)) ?? .zero - height = ceil(contentSize.height) + contentSize = String.getRealSize(text, font, messageMaxSize) + height = contentSize.height + chat_content_margin * 3 // time if let time = timeContent, !time.isEmpty { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift index 50fd4e6e..8ed5f362 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift @@ -13,24 +13,23 @@ public enum DownloadState: Int { } @objcMembers -open class MessageVideoModel: MessageContentModel { - public var imageUrl: String? +open class MessageVideoModel: MessageImageModel { public var state = DownloadState.Success - public var progress: Float = 0 + public var progress: UInt = 0 public weak var cell: NEChatBaseCell? - public required init(message: NIMMessage?) { + + public required init(message: V2NIMMessage?) { super.init(message: message) type = .video - if let videoObject = message?.messageObject as? NIMVideoObject { - imageUrl = videoObject.url + if let videoObject = message?.attachment as? V2NIMMessageVideoAttachment { contentSize = ChatMessageHelper.getSizeWithMaxSize( chat_pic_size, - size: videoObject.coverSize, + size: CGSize(width: videoObject.width, height: videoObject.height), miniWH: chat_min_h ) } else { contentSize = chat_pic_size } - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEPinMessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEPinMessageModel.swift new file mode 100644 index 00000000..c732e9e6 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEPinMessageModel.swift @@ -0,0 +1,93 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIM2Kit +import NIMSDK +import UIKit + +@objcMembers +open class NEPinMessageModel: NSObject { + var chatmodel: MessageModel = MessageTextModel(message: nil) + var message: V2NIMMessage + var item: V2NIMMessagePin + var conversationId: String? + var repo = ChatRepo.shared + var pinFileModel: PinMessageFileModel? + + init(message: V2NIMMessage, item: V2NIMMessagePin) { + self.message = message + conversationId = item.messageRefer?.conversationId + self.item = item + super.init() + chatmodel = modelFromMessage(message: message) + if chatmodel.type == .file { + pinFileModel = PinMessageFileModel() + if let filemodel = chatmodel as? MessageFileModel { + pinFileModel?.size = filemodel.size + } + } + } + + private func modelFromMessage(message: V2NIMMessage) -> MessageModel { + let model = ChatMessageHelper.modelFromMessage(message: message) + model.fullName = message.senderId + model.shortName = ChatMessageHelper.getShortName(message.senderId ?? "") + return model + } + + open func cellHeight(pinContentMaxW: CGFloat) -> CGFloat { + var height = chatmodel.contentSize.height + let titleFont: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize, weight: .semibold) + let bodyFont: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) + let maxSize = CGSize(width: pinContentMaxW, height: CGFloat.greatestFiniteMagnitude) + + if let textModel = chatmodel as? MessageTextModel { + // 文本消息最多显示 3 行 + let textSize = NSAttributedString.getRealSize(textModel.attributeStr, bodyFont, maxSize, 3) + height = textSize.height + } + + if let textModel = chatmodel as? MessageRichTextModel { + // 换行消息中的标题最多显示 1 行 + let titleSize = NSAttributedString.getRealSize(textModel.titleAttributeStr, titleFont, maxSize, 1) + height = titleSize.height + + // 换行消息中的内容最多显示 2 行 + let textSize = NSAttributedString.getRealSize(textModel.attributeStr, bodyFont, maxSize, 2) + height += textSize.height + } + + height += 100 + + if chatmodel.replyedModel?.isReplay == true { + height += 12 + } + + return height + } + + open func getReplyMessageWithoutThread(message: V2NIMMessage, _ completion: @escaping (MessageModel?) -> Void) { + var replyId: String? = message.threadReply?.messageClientId + if let remoteExt = getDictionaryFromJSONString(message.serverExtension ?? ""), + let yxReplyMsg = remoteExt[keyReplyMsgKey] as? [String: Any] { + replyId = yxReplyMsg["idClient"] as? String + } + + guard let id = replyId, !id.isEmpty else { + completion(nil) + return + } + + repo.getMessageListByIds([id]) { [weak self] messages, error in + if let m = messages?.first { + let model = self?.modelFromMessage(message: m) + model?.isReplay = true + completion(model) + } else { + completion(nil) + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift index c8b768ae..681f51cb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift @@ -7,7 +7,7 @@ import UIKit @objcMembers open class PinMessageFileModel: NSObject { - public var progress: Float = 0 + public var progress: UInt = 0 public var size: Float = 0 public var state = DownloadState.Success diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift deleted file mode 100644 index e1fe22af..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NEChatKit -import NECoreIMKit -import NIMSDK -import UIKit - -@objcMembers -open class PinMessageModel: NSObject { - var chatmodel: MessageModel = MessageTextModel(message: nil) - var message: NIMMessage - var item: NIMMessagePinItem - var session: NIMSession - var repo = ChatRepo.shared - var pinFileModel: PinMessageFileModel? - - init(message: NIMMessage, item: NIMMessagePinItem) { - self.message = message - session = item.session - self.item = item - super.init() - chatmodel = modelFromMessage(message: message) - if chatmodel.type == .file { - pinFileModel = PinMessageFileModel() - if let filemodel = chatmodel as? MessageFileModel { - pinFileModel?.size = filemodel.size - } - } - } - - private func modelFromMessage(message: NIMMessage) -> MessageModel { - let model = ChatMessageHelper.modelFromMessage(message: message) - - if let uid = message.from { - let user = ChatUserCache.getUserInfo(uid) - let fullName = ChatUserCache.getShowName(userId: uid, teamId: session.sessionId) - model.avatar = user?.userInfo?.avatarUrl - model.fullName = fullName - model.shortName = ChatUserCache.getShortName(name: user?.showName(false) ?? "", length: 2) - } - -// model.replyedModel = getReplyMessageWithoutThread(message: message) -// if let pin = repo.searchMessagePinHistory(message) { -// model.isPined = true -// model.pinAccount = pin.accountID -// let pinID = pin.accountID ?? NIMSDK.shared().loginManager.currentAccount() -// model.pinShowName = getShowName(userId: pinID, teamId: session.sessionId) -// } else { -// model.isPined = false -// } - return model - } - - open func cellHeight(pinContentMaxW: CGFloat) -> CGFloat { - var height = chatmodel.contentSize.height - if let textModel = chatmodel as? MessageTextModel { - // 文本消息最多显示 3 行 - let textSize = textModel.attributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize), CGSize(width: pinContentMaxW, height: CGFloat.greatestFiniteMagnitude), 3) ?? .zero - height = textSize.height - } - - if let textModel = chatmodel as? MessageRichTextModel { - // 换行消息中的标题最多显示 1 行 - let titleSize = textModel.titleAttributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize, weight: .semibold), CGSize(width: pinContentMaxW, height: CGFloat.greatestFiniteMagnitude), 1) ?? .zero - height = titleSize.height - - // 换行消息中的内容最多显示 2 行 - let textSize = textModel.attributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize), CGSize(width: pinContentMaxW, height: CGFloat.greatestFiniteMagnitude), 2) ?? .zero - height += textSize.height - } - - height += 100 - - if chatmodel.replyedModel?.isReplay == true { - height += 12 - } - - return height - } - - open func getReplyMessageWithoutThread(message: NIMMessage) -> MessageModel? { - var replyId: String? = message.repliedMessageId - if let yxReplyMsg = message.remoteExt?[keyReplyMsgKey] as? [String: Any] { - replyId = yxReplyMsg["idClient"] as? String - } - - guard let id = replyId, !id.isEmpty else { - return nil - } - - if let m = ConversationProvider.shared.messagesInSession(session, messageIds: [id])? - .first { - let model = modelFromMessage(message: m) - model.isReplay = true - return model - } - let message = NIMMessage() - let model = modelFromMessage(message: message) - model.isReplay = true - return model - } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift index e0c1ece7..0ecd0526 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift @@ -3,7 +3,8 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NEChatKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -17,7 +18,7 @@ public protocol ChatBaseCellDelegate: NSObjectProtocol { func didLongPressAvatar(_ cell: UITableViewCell, _ model: MessageContentModel?) // 单击消息体 - func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) + func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?, _ replyModel: MessageModel?) // 长按消息体 func didLongPressMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) @@ -45,7 +46,6 @@ protocol ChatAudioCellProtocol { @objcMembers open class NEBaseChatMessageCell: NEChatBaseCell { - private let bubbleWidth: CGFloat = 218 // 气泡默认宽度 private let pinLabelMaxWidth: CGFloat = 280 // pin 文案最大宽度 public weak var delegate: ChatBaseCellDelegate? public var contentModel: MessageContentModel? // 消息模型 @@ -60,7 +60,6 @@ open class NEBaseChatMessageCell: NEChatBaseCell { public var bubbleHLeft: NSLayoutConstraint? // 左侧气泡高度布局约束 public var pinImageLeft = UIImageView() // 左侧标记图片 public var pinLabelLeft = UILabel() // 左侧标记文案 - public var pinLabelLeftTopAnchor: NSLayoutConstraint? // 左侧标记文案顶部布局约束 private var pinLabelHLeft: NSLayoutConstraint? // 左侧标记文案宽度布局约束 private var pinLabelWLeft: NSLayoutConstraint? // 左侧标记文案高度布局约束 public var fullNameLabel = UILabel() // 群昵称(只在群聊中有效) @@ -74,20 +73,22 @@ open class NEBaseChatMessageCell: NEChatBaseCell { public var bubbleHRight: NSLayoutConstraint? // 右侧气泡高度布局约束 public var pinImageRight = UIImageView() // 右侧标记图片 public var pinLabelRight = UILabel() // 右侧标记文案 - public var pinLabelRightTopAnchor: NSLayoutConstraint? // 右侧标记文案顶部布局约束 private var pinLabelHRight: NSLayoutConstraint? // 右侧标记文案宽度布局约束 private var pinLabelWRight: NSLayoutConstraint? // 右侧标记文案高度布局约束 // 已读未读视图 public var readView = CirleProgressView(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) public var activityView = ChatActivityIndicatorView() // 消息状态视图 - public var seletedBtn = UIButton(type: .custom) // 多选按钮 + public var selectedButton = UIButton(type: .custom) // 多选按钮 + public var selectedButtonCenterYAnchor: NSLayoutConstraint? // 多选按钮中心 Y 布局约束 public var timeLabel = UILabel() // 消息时间 public var timeLabelHeightAnchor: NSLayoutConstraint? // 消息时间高度约束 // 已读未读点击手势 private var tapGesture: UITapGestureRecognizer? + public let messageTextFont = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) initProperty() @@ -97,7 +98,7 @@ open class NEBaseChatMessageCell: NEChatBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } deinit { @@ -186,10 +187,10 @@ open class NEBaseChatMessageCell: NEChatBaseCell { activityView.accessibilityIdentifier = "id.status" - seletedBtn.translatesAutoresizingMaskIntoConstraints = false - seletedBtn.setImage(.ne_imageNamed(name: "unselect"), for: .normal) - seletedBtn.setImage(.ne_imageNamed(name: "select"), for: .selected) - seletedBtn.addTarget(self, action: #selector(selectButtonClicked), for: .touchUpInside) + selectedButton.translatesAutoresizingMaskIntoConstraints = false + selectedButton.setImage(.ne_imageNamed(name: "unselect"), for: .normal) + selectedButton.setImage(.ne_imageNamed(name: "select"), for: .selected) + selectedButton.addTarget(self, action: #selector(selectButtonClicked), for: .touchUpInside) } open func baseCommonUI() { @@ -198,12 +199,12 @@ open class NEBaseChatMessageCell: NEChatBaseCell { // time contentView.addSubview(timeLabel) - timeLabelHeightAnchor = timeLabel.heightAnchor.constraint(equalToConstant: chat_timeCellH) + timeLabelHeightAnchor = timeLabel.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + timeLabelHeightAnchor?.isActive = true NSLayoutConstraint.activate([ timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), timeLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0), timeLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -0), - timeLabelHeightAnchor!, ]) baseCommonUILeft() @@ -213,9 +214,9 @@ open class NEBaseChatMessageCell: NEChatBaseCell { open func baseCommonUILeft() { contentView.addSubview(avatarImageLeft) avatarImageLeftAnchor = avatarImageLeft.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16) + avatarImageLeftAnchor?.isActive = true NSLayoutConstraint.activate([ avatarImageLeft.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: chat_content_margin), - avatarImageLeftAnchor!, avatarImageLeft.widthAnchor.constraint(equalToConstant: 32), avatarImageLeft.heightAnchor.constraint(equalToConstant: 32), ]) @@ -229,35 +230,32 @@ open class NEBaseChatMessageCell: NEChatBaseCell { ]) contentView.addSubview(fullNameLabel) - fullNameH = fullNameLabel.heightAnchor.constraint(equalToConstant: 0) + fullNameH = fullNameLabel.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + fullNameH?.isActive = true NSLayoutConstraint.activate([ fullNameLabel.leftAnchor.constraint(equalTo: avatarImageLeft.rightAnchor, constant: chat_content_margin), fullNameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), fullNameLabel.topAnchor.constraint(equalTo: avatarImageLeft.topAnchor), - fullNameH!, ]) // bubbleImageLeft contentView.addSubview(bubbleImageLeft) bubbleTopAnchorLeft = bubbleImageLeft.topAnchor.constraint(equalTo: fullNameLabel.bottomAnchor, constant: 0) - bubbleWLeft = bubbleImageLeft.widthAnchor.constraint(equalToConstant: bubbleWidth) - bubbleHLeft = bubbleImageLeft.heightAnchor.constraint(equalToConstant: bubbleWidth) - NSLayoutConstraint.activate([ - bubbleTopAnchorLeft!, - bubbleImageLeft.leftAnchor.constraint(equalTo: avatarImageLeft.rightAnchor, constant: chat_content_margin), - bubbleWLeft!, - bubbleHLeft!, - ]) + bubbleTopAnchorLeft?.isActive = true + bubbleWLeft = bubbleImageLeft.widthAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + bubbleWLeft?.isActive = true + bubbleHLeft = bubbleImageLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + bubbleHLeft?.isActive = true + bubbleImageLeft.leftAnchor.constraint(equalTo: avatarImageLeft.rightAnchor, constant: chat_content_margin).isActive = true contentView.addSubview(pinLabelLeft) - pinLabelLeftTopAnchor = pinLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.bottomAnchor, constant: 4) - pinLabelHLeft = pinLabelLeft.heightAnchor.constraint(equalToConstant: 0) + pinLabelHLeft = pinLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + pinLabelHLeft?.isActive = true pinLabelWLeft = pinLabelLeft.widthAnchor.constraint(equalToConstant: pinLabelMaxWidth) + pinLabelWLeft?.isActive = true NSLayoutConstraint.activate([ - pinLabelLeftTopAnchor!, + pinLabelLeft.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4), pinLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: 14), - pinLabelWLeft!, - pinLabelHLeft!, ]) contentView.addSubview(pinImageLeft) @@ -286,19 +284,19 @@ open class NEBaseChatMessageCell: NEChatBaseCell { ]) contentView.addSubview(bubbleImageRight) - bubbleWRight = bubbleImageRight.widthAnchor.constraint(equalToConstant: bubbleWidth) - bubbleHRight = bubbleImageRight.heightAnchor.constraint(equalToConstant: bubbleWidth) + bubbleWRight = bubbleImageRight.widthAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + bubbleWRight?.isActive = true + bubbleHRight = bubbleImageRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + bubbleHRight?.isActive = true NSLayoutConstraint.activate([ bubbleImageRight.topAnchor.constraint(equalTo: avatarImageRight.topAnchor, constant: 0), bubbleImageRight.rightAnchor.constraint(equalTo: avatarImageRight.leftAnchor, constant: -chat_content_margin), - bubbleWRight!, - bubbleHRight!, ]) // activityView contentView.addSubview(activityView) activityView.translatesAutoresizingMaskIntoConstraints = false - activityView.failBtn.addTarget(self, action: #selector(resend), for: .touchUpInside) + activityView.failButton.addTarget(self, action: #selector(resend), for: .touchUpInside) NSLayoutConstraint.activate([ activityView.rightAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: -chat_content_margin), activityView.centerYAnchor.constraint(equalTo: bubbleImageRight.centerYAnchor, constant: 0), @@ -315,24 +313,22 @@ open class NEBaseChatMessageCell: NEChatBaseCell { readView.heightAnchor.constraint(equalToConstant: 16), ]) -// seletedBtn - contentView.addSubview(seletedBtn) +// selectedButton + contentView.addSubview(selectedButton) NSLayoutConstraint.activate([ - seletedBtn.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), - seletedBtn.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), - seletedBtn.widthAnchor.constraint(equalToConstant: 18), - seletedBtn.heightAnchor.constraint(equalToConstant: 18), + selectedButton.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + selectedButton.widthAnchor.constraint(equalToConstant: 18), + selectedButton.heightAnchor.constraint(equalToConstant: 18), ]) contentView.addSubview(pinLabelRight) - pinLabelRightTopAnchor = pinLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: 4) - pinLabelHRight = pinLabelRight.heightAnchor.constraint(equalToConstant: 0) + pinLabelHRight = pinLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + pinLabelHRight?.isActive = true pinLabelWRight = pinLabelRight.widthAnchor.constraint(equalToConstant: 210) + pinLabelWRight?.isActive = true NSLayoutConstraint.activate([ - pinLabelRightTopAnchor!, + pinLabelRight.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4), pinLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: 0), - pinLabelWRight!, - pinLabelHRight!, ]) contentView.addSubview(pinImageRight) @@ -391,13 +387,11 @@ open class NEBaseChatMessageCell: NEChatBaseCell { // MARK: event open func tapAvatar(tap: UITapGestureRecognizer) { - print(#function) delegate?.didTapAvatarView(self, contentModel) } open func tapMessage(tap: UITapGestureRecognizer) { - print(#function) - delegate?.didTapMessageView(self, contentModel) + delegate?.didTapMessageView(self, contentModel, contentModel?.replyedModel) } open func longPressAvatar(longPress: UITapGestureRecognizer) { @@ -407,7 +401,6 @@ open class NEBaseChatMessageCell: NEChatBaseCell { } open func longPress(longPress: UILongPressGestureRecognizer) { - print(#function) switch longPress.state { case .began: print("state:begin") @@ -431,12 +424,11 @@ open class NEBaseChatMessageCell: NEChatBaseCell { } open func tapReadView(tap: UITapGestureRecognizer) { - print(#function) delegate?.didTapReadView(self, contentModel) } open func selectButtonClicked() { - seletedBtn.isSelected = !seletedBtn.isSelected + selectedButton.isSelected = !selectedButton.isSelected if let model = contentModel { model.isSelected = !model.isSelected } @@ -447,13 +439,13 @@ open class NEBaseChatMessageCell: NEChatBaseCell { open func setSelect(_ model: MessageContentModel, _ enableSelect: Bool = false) { // 多选框 - seletedBtn.isHidden = model.isRevoked || !enableSelect - seletedBtn.isSelected = model.isSelected + selectedButton.isHidden = model.isRevoked || !enableSelect + selectedButton.isSelected = model.isSelected avatarImageLeftAnchor?.constant = enableSelect ? 42 : 16 } override open func setModel(_ model: MessageContentModel) { - setModel(model, model.message?.isOutgoingMsg ?? false) + setModel(model, model.message?.isSelf ?? false) } override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { @@ -480,9 +472,14 @@ open class NEBaseChatMessageCell: NEChatBaseCell { bubbleW?.constant = model.contentSize.width bubbleH?.constant = model.contentSize.height + selectedButtonCenterYAnchor = selectedButton.centerYAnchor.constraint(equalTo: isSend ? bubbleImageRight.centerYAnchor : bubbleImageLeft.centerYAnchor) + selectedButtonCenterYAnchor?.priority = .defaultHigh + selectedButtonCenterYAnchor?.isActive = true // avatar nameLabel.text = model.shortName + nameLabel.isHidden = true + avatarImage.backgroundColor = .clear if let avatarURL = model.avatar, !avatarURL.isEmpty { avatarImage .sd_setImage(with: URL(string: avatarURL)) { image, error, type, url in @@ -494,14 +491,14 @@ open class NEBaseChatMessageCell: NEChatBaseCell { avatarImage.image = nil nameLabel.isHidden = false avatarImage.backgroundColor = UIColor - .colorWithString(string: model.message?.from) + .colorWithString(string: model.message?.senderId) } } } else { avatarImage.image = nil nameLabel.isHidden = false avatarImage.backgroundColor = UIColor - .colorWithString(string: model.message?.from) + .colorWithString(string: model.message?.senderId) } if model.fullNameHeight > 0 { @@ -516,60 +513,52 @@ open class NEBaseChatMessageCell: NEChatBaseCell { fullNameH?.constant = CGFloat(model.fullNameHeight) if isSend { - switch model.message?.deliveryState { - case .delivering: + switch model.message?.sendingState { + case .MESSAGE_SENDING_STATE_SENDING: activityView.messageStatus = .sending - case .deliveried: - // 同一个账号,在多端登录,被对方拉黑,需要根据isBlackListed判断,进而更新信息状态 - if let isBlackMsg = model.message?.isBlackListed, isBlackMsg { - activityView.messageStatus = .failed - } else { - activityView.messageStatus = .successed - } - case .failed: + case .MESSAGE_SENDING_STATE_SUCCEEDED: + activityView.messageStatus = .successed + case .MESSAGE_SENDING_STATE_FAILED: activityView.messageStatus = .failed - default: break + default: + activityView.messageStatus = .sending } } - if isSend, model.message?.deliveryState == .deliveried { - if model.message?.session?.sessionType == .P2P { - let receiptEnable = model.message?.setting?.teamReceiptEnabled ?? false + if isSend, model.message?.sendingState == .MESSAGE_SENDING_STATE_SUCCEEDED { + if model.message?.conversationType == .CONVERSATION_TYPE_P2P { + let receiptEnable = model.message?.messageConfig?.readReceiptEnabled ?? false if receiptEnable, - IMKitClient.instance.getSettingRepo().getShowReadStatus(), + !model.isRevoked, + SettingRepo.shared.getShowReadStatus(), NEKitChatConfig.shared.ui.messageProperties.showP2pMessageStatus == true { readView.isHidden = false - if let read = model.message?.isRemoteRead, read { + if model.readCount == 1, model.unreadCount == 0 { readView.progress = 1 } else { readView.progress = 0 } - // 未读消息需要判断是否被拉黑,拉黑情况,已读未读状态不展示。 - if let isBlackMsg = model.message?.isBlackListed, isBlackMsg { - readView.isHidden = true - } else { - readView.isHidden = false - } - } else { readView.isHidden = true } - } else if model.message?.session?.sessionType == .team { - let receiptEnable = model.message?.setting?.teamReceiptEnabled ?? false + } else if model.message?.conversationType == .CONVERSATION_TYPE_TEAM { + let receiptEnable = model.message?.messageConfig?.readReceiptEnabled ?? false if receiptEnable, - IMKitClient.instance.getSettingRepo().getShowReadStatus(), + !model.isRevoked, + SettingRepo.shared.getShowReadStatus(), NEKitChatConfig.shared.ui.messageProperties.showTeamMessageStatus == true { readView.isHidden = false - let readCount = model.message?.teamReceiptInfo?.readCount ?? 0 - let unreadCount = model.message?.teamReceiptInfo?.unreadCount ?? 0 - let total = Float(readCount + unreadCount) - if (readCount + unreadCount) >= NEKitChatConfig.shared.maxReadingNum { + var total = ChatTeamCache.shared.getTeamInfo()?.memberCount ?? 0 + if model.readCount + model.unreadCount != 0 { + total = model.readCount + model.unreadCount + 1 + } + if total >= NEKitChatConfig.shared.maxReadingNum { readView.isHidden = true return } - if total > 0 { - let progress = Float(readCount) / total + if total - 1 > 0 { + let progress = Float(model.readCount) / Float(total - 1) readView.progress = progress if progress >= 1.0 { tapGesture?.isEnabled = false @@ -607,7 +596,7 @@ open class NEBaseChatMessageCell: NEChatBaseCell { /// 更新标记状态 open func updatePinStatus(_ model: MessageContentModel) { - guard let isSend = model.message?.isOutgoingMsg else { + guard let isSend = model.message?.isSelf else { return } let pinLabel = isSend ? pinLabelRight : pinLabelLeft @@ -620,21 +609,18 @@ open class NEBaseChatMessageCell: NEChatBaseCell { contentView.backgroundColor = model.isPined ? NEKitChatConfig.shared.ui .messageProperties.signalBgColor : .clear if model.isPined { - let pinText = model.message?.session?.sessionType == .P2P ? chatLocalizable("pin_text_P2P") : chatLocalizable("pin_text_team") + let pinText = model.message?.conversationType == .CONVERSATION_TYPE_P2P ? chatLocalizable("pin_text_P2P") : chatLocalizable("pin_text_team") if model.pinAccount == nil { pinLabel.text = chatLocalizable("You") + " " + pinText - } else if let account = model.pinAccount, account == NIMSDK.shared().loginManager.currentAccount() { + } else if let account = model.pinAccount, account == IMKitClient.instance.account() { pinLabel.text = chatLocalizable("You") + " " + pinText } else if let text = model.pinShowName { pinLabel.text = text + pinText } pinImage.image = UIImage.ne_imageNamed(name: "msg_pin") - let size = String.getTextRectSize( - pinLabel.text ?? pinText, - font: UIFont.systemFont(ofSize: 12.0), - size: CGSize(width: pinLabelMaxWidth, height: CGFloat.greatestFiniteMagnitude) - ) + let showText = pinLabel.text ?? pinText + let size = String.getRealSize(showText, .systemFont(ofSize: 12), CGSize(width: pinLabelMaxWidth, height: CGFloat.greatestFiniteMagnitude)) pinLabelH?.constant = CGFloat(chat_pin_height) pinLabelW?.constant = min(size.width + 1, pinLabelMaxWidth) } else { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift index fa9858c8..d3a185c0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift @@ -8,6 +8,7 @@ import UIKit @objcMembers open class NEBaseChatMessageTipCell: UITableViewCell { var timeLabelHeightAnchor: NSLayoutConstraint? // 消息时间高度约束 + var contentLabelCenterYAnchor: NSLayoutConstraint? // 消息内容中心Y约束 override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -17,22 +18,23 @@ open class NEBaseChatMessageTipCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { contentView.addSubview(timeLabel) timeLabelHeightAnchor = timeLabel.heightAnchor.constraint(equalToConstant: 22) + timeLabelHeightAnchor?.isActive = true NSLayoutConstraint.activate([ timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), timeLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), timeLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), - timeLabelHeightAnchor!, ]) contentView.addSubview(contentLabel) + contentLabelCenterYAnchor = contentLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) + contentLabelCenterYAnchor?.isActive = true NSLayoutConstraint.activate([ - contentLabel.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 4), contentLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), contentLabel.widthAnchor.constraint(equalToConstant: chat_content_maxW), ]) @@ -42,10 +44,12 @@ open class NEBaseChatMessageTipCell: UITableViewCell { // time if let time = model.timeContent, !time.isEmpty { timeLabelHeightAnchor?.constant = chat_timeCellH + contentLabelCenterYAnchor?.constant = chat_timeCellH / 2 timeLabel.text = time timeLabel.isHidden = false } else { timeLabelHeightAnchor?.constant = 0 + contentLabelCenterYAnchor?.constant = 0 timeLabel.text = "" timeLabel.isHidden = true } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift index 334d7de9..a47bb2b0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift @@ -60,14 +60,14 @@ open class NEBaseChatTeamMemberCell: UITableViewCell { ]) } - open func configure(_ model: ChatTeamMemberInfoModel) { - if let url = model.nimUser?.userInfo?.avatarUrl, !url.isEmpty { + open func configure(_ model: NETeamMemberInfoModel) { + if let url = model.nimUser?.user?.avatar, !url.isEmpty { headerView.sd_setImage(with: URL(string: url), completed: nil) headerView.setTitle("") } else { headerView.image = nil - headerView.setTitle(model.showNickInTeam()) - headerView.backgroundColor = UIColor.colorWithString(string: model.nimUser?.userId) + headerView.setTitle(model.showNickInTeam() ?? "") + headerView.backgroundColor = UIColor.colorWithString(string: model.nimUser?.user?.accountId) } nameLabel.text = model.atNameInTeam() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift index 8d059d2e..9b9554b6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift @@ -15,7 +15,7 @@ open class NEBaseUserSettingCell: CornerCell { return label }() - public lazy var arrow: UIImageView = { + public lazy var arrowImageView: UIImageView = { let imageView = UIImageView(image: coreLoader.loadImage("arrowRight")) imageView.translatesAutoresizingMaskIntoConstraints = false return imageView diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSelectCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSelectCell.swift index 86a8fadf..9d99df1b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSelectCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSelectCell.swift @@ -26,7 +26,7 @@ open class NEBaseUserSettingSelectCell: NEBaseUserSettingCell { open func setupUI() { contentView.addSubview(titleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowImageView) contentView.addSubview(subTitleLabel) NSLayoutConstraint.activate([ diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift index 86a43d98..3f7ed17f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift @@ -53,7 +53,7 @@ open class OperationCell: UICollectionViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } // @objc func tapEvent(tap: UITapGestureRecognizer) { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift index a5ce8d4d..3b3b4694 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift @@ -71,7 +71,7 @@ open class NEBasePinMessageAudioCell: NEBasePinMessageCell { } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) if let m = item.chatmodel as? MessageAudioModel { audioTimeLabel.text = "\(m.duration)" + "s" diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift index 1bdb169c..78dd1e04 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift @@ -9,8 +9,8 @@ import UIKit @objc public protocol PinMessageCellDelegate { - func didClickMore(_ model: PinMessageModel?) - func didClickContent(_ model: PinMessageModel?, _ cell: NEBasePinMessageCell) + func didClickMore(_ model: NEPinMessageModel?) + func didClickContent(_ model: NEPinMessageModel?, _ cell: NEBasePinMessageCell) } @objcMembers @@ -19,13 +19,13 @@ open class NEBasePinMessageCell: UITableViewCell { public var contentHeight: NSLayoutConstraint? - public var pinModel: PinMessageModel? + public var pinModel: NEPinMessageModel? public var delegate: PinMessageCellDelegate? public var contentGesture: UITapGestureRecognizer? - lazy var headerView: NEUserHeaderView = { + public lazy var headerView: NEUserHeaderView = { let header = NEUserHeaderView(frame: .zero) header.titleLabel.font = NEConstant.defaultTextFont(12) header.titleLabel.textColor = UIColor.white @@ -35,7 +35,7 @@ open class NEBasePinMessageCell: UITableViewCell { return header }() - lazy var nameLabel: UILabel = { + public lazy var nameLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 12.0) label.textColor = .ne_darkText @@ -44,11 +44,12 @@ open class NEBasePinMessageCell: UITableViewCell { return label }() - lazy var timeLabel: UILabel = { + public lazy var timeLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 12.0) label.textColor = .ne_greyText label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.time" return label }() @@ -84,21 +85,21 @@ open class NEBasePinMessageCell: UITableViewCell { open func setupUI() { contentView.backgroundColor = .clear + backView.translatesAutoresizingMaskIntoConstraints = false backView.backgroundColor = UIColor.white + backView.clipsToBounds = true + backView.layer.cornerRadius = 8.0 contentView.addSubview(backView) backLeftConstraint = backView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20) + backLeftConstraint?.isActive = true backRightConstraint = backView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20) - + backRightConstraint?.isActive = true NSLayoutConstraint.activate([ - backLeftConstraint!, - backRightConstraint!, backView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), backView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) - backView.clipsToBounds = true - backView.layer.cornerRadius = 8.0 backView.addSubview(headerView) NSLayoutConstraint.activate([ @@ -114,16 +115,16 @@ open class NEBasePinMessageCell: UITableViewCell { imageView.translatesAutoresizingMaskIntoConstraints = false backView.addSubview(imageView) - let moreBtn = UIButton() - moreBtn.addTarget(self, action: #selector(moreClick), for: .touchUpInside) - moreBtn.accessibilityIdentifier = "id.moreAction" - moreBtn.translatesAutoresizingMaskIntoConstraints = false - backView.addSubview(moreBtn) + let moreButton = UIButton() + moreButton.addTarget(self, action: #selector(moreClick), for: .touchUpInside) + moreButton.accessibilityIdentifier = "id.moreAction" + moreButton.translatesAutoresizingMaskIntoConstraints = false + backView.addSubview(moreButton) NSLayoutConstraint.activate([ - moreBtn.rightAnchor.constraint(equalTo: backView.rightAnchor), - moreBtn.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), - moreBtn.widthAnchor.constraint(equalToConstant: 50), - moreBtn.heightAnchor.constraint(equalToConstant: 40), + moreButton.rightAnchor.constraint(equalTo: backView.rightAnchor), + moreButton.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), + moreButton.widthAnchor.constraint(equalToConstant: 50), + moreButton.heightAnchor.constraint(equalToConstant: 40), ]) NSLayoutConstraint.activate([ @@ -156,14 +157,13 @@ open class NEBasePinMessageCell: UITableViewCell { line.backgroundColor = .ne_greyLine } - open func configure(_ item: PinMessageModel) { + open func configure(_ item: NEPinMessageModel) { pinModel = item headerView.configHeadData(headUrl: item.chatmodel.avatar, name: item.chatmodel.fullName ?? "", - uid: item.chatmodel.message?.from ?? "") + uid: item.chatmodel.message?.senderId ?? "") nameLabel.text = item.chatmodel.fullName - print("config time : ", item.message.timestamp) - timeLabel.text = String.stringFromDate(date: Date(timeIntervalSince1970: item.message.timestamp)) + timeLabel.text = String.stringFromDate(date: Date(timeIntervalSince1970: item.message.createTime)) contentWidth?.constant = item.chatmodel.contentSize.width contentHeight?.constant = item.chatmodel.contentSize.height diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift index 524b562a..f11ff48d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift @@ -29,7 +29,7 @@ open class NEBasePinMessageDefaultCell: NEBasePinMessageTextCell { super.setupUI() } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) contentLabel.text = chatLocalizable("unkonw_pin_message") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift index edab5229..3cd4e96f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift @@ -8,22 +8,22 @@ import UIKit @objcMembers open class NEBasePinMessageFileCell: NEBasePinMessageCell { public lazy var stateView: FileStateView = { - let state = FileStateView() - state.translatesAutoresizingMaskIntoConstraints = false - state.backgroundColor = .clear - return state + let stateView = FileStateView() + stateView.translatesAutoresizingMaskIntoConstraints = false + stateView.backgroundColor = .clear + return stateView }() public var bubbleImage = UIImageView() - lazy var imgView: UIImageView = { - let view_img = UIImageView() - view_img.translatesAutoresizingMaskIntoConstraints = false - view_img.backgroundColor = .clear - return view_img + public lazy var imgView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = .clear + return imageView }() - lazy var titleLabel: UILabel = { + public lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.isUserInteractionEnabled = false @@ -34,7 +34,7 @@ open class NEBasePinMessageFileCell: NEBasePinMessageCell { return label }() - lazy var sizeLabel: UILabel = { + public lazy var sizeLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor(hexString: "#999999") @@ -43,7 +43,7 @@ open class NEBasePinMessageFileCell: NEBasePinMessageCell { return label }() - lazy var labelView: UIView = { + public lazy var labelView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.isUserInteractionEnabled = false @@ -131,56 +131,55 @@ open class NEBasePinMessageFileCell: NEBasePinMessageCell { } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) - if let fileObject = item.message.messageObject as? NIMFileObject { + if let fileObject = item.message.attachment as? V2NIMMessageFileAttachment { if let fileModel = item.pinFileModel { fileModel.cell = self if fileModel.state == .Success { stateView.state = .FileOpen } else { stateView.state = .FileDownload - stateView.setProgress(fileModel.progress) - if fileModel.progress >= 1 { + stateView.setProgress(Float(fileModel.progress)) + if fileModel.progress >= 100 { fileModel.state = .Success } } } var imageName = "file_unknown" - var displayName = "未知文件" - if let filePath = fileObject.path as? NSString { - displayName = filePath.lastPathComponent - switch filePath.pathExtension.lowercased() { - case file_doc_support: - imageName = "file_doc" - case file_xls_support: - imageName = "file_xls" - case file_img_support: - imageName = "file_img" - case file_ppt_support: - imageName = "file_ppt" - case file_txt_support: - imageName = "file_txt" - case file_audio_support: - imageName = "file_audio" - case file_vedio_support: - imageName = "file_vedio" - case file_zip_support: - imageName = "file_zip" - case file_pdf_support: - imageName = "file_pdf" - case file_html_support: - imageName = "file_html" - case "key", "keynote": - imageName = "file_keynote" - default: - imageName = "file_unknown" - } + let suffix = (fileObject.name as NSString).pathExtension.lowercased() + switch suffix { + case file_doc_support: + imageName = "file_doc" + case file_xls_support: + imageName = "file_xls" + case file_img_support: + imageName = "file_img" + case file_ppt_support: + imageName = "file_ppt" + case file_txt_support: + imageName = "file_txt" + case file_audio_support: + imageName = "file_audio" + case file_vedio_support: + imageName = "file_vedio" + case file_zip_support: + imageName = "file_zip" + case file_pdf_support: + imageName = "file_pdf" + case file_html_support: + imageName = "file_html" + case "key", "keynote": + imageName = "file_keynote" + default: + imageName = "file_unknown" } + imgView.image = UIImage.ne_imageNamed(name: imageName) - titleLabel.text = fileObject.displayName ?? displayName - let size_B = Double(fileObject.fileLength) + titleLabel.text = fileObject.name + + let size_B = Double(fileObject.size) var size_str = String(format: "%.1f B", size_B) if size_B > 1e3 { let size_KB = size_B / 1e3 @@ -198,7 +197,7 @@ open class NEBasePinMessageFileCell: NEBasePinMessageCell { } } - open func uploadProgress(progress: Float) { - stateView.setProgress(progress) + open func uploadProgress(progress: UInt) { + stateView.setProgress(Float(progress)) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift index 23198d9d..da2bea39 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift @@ -51,10 +51,10 @@ open class NEBasePinMessageImageCell: NEBasePinMessageCell { } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) - if let m = item.chatmodel as? MessageImageModel, let imageUrl = m.imageUrl { + if let m = item.chatmodel as? MessageImageModel, let imageUrl = m.urlString { if imageUrl.hasPrefix("http") { contentImageView.sd_setImage( with: URL(string: imageUrl), diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift index 517a4488..65e8cebb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift @@ -31,9 +31,18 @@ open class NEBasePinMessageLocationCell: NEBasePinMessageCell { label.text = chatLocalizable("no_map_plugin") label.textAlignment = .center label.textColor = UIColor.ne_greyText + label.isHidden = true return label }() + let pointImageView = UIImageView() + + public lazy var mapImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + var mapView: UIView? override open func awakeFromNib() { @@ -58,79 +67,82 @@ open class NEBasePinMessageLocationCell: NEBasePinMessageCell { override open func setupUI() { super.setupUI() - let back = UIView() - back.backgroundColor = UIColor.white - contentView.addSubview(back) - back.translatesAutoresizingMaskIntoConstraints = false - back.clipsToBounds = true - back.layer.cornerRadius = 4 - back.layer.borderWidth = 1 - back.layer.borderColor = UIColor.ne_outlineColor.cgColor - - backView.addSubview(back) - contentWidth = back.widthAnchor.constraint(equalToConstant: chat_content_maxW) - contentHeight = back.heightAnchor.constraint(equalToConstant: chat_content_maxW) + let contentBackView = UIView() + contentBackView.backgroundColor = UIColor.white + contentView.addSubview(contentBackView) + contentBackView.translatesAutoresizingMaskIntoConstraints = false + contentBackView.clipsToBounds = true + contentBackView.layer.cornerRadius = 4 + contentBackView.layer.borderWidth = 1 + contentBackView.layer.borderColor = UIColor.ne_outlineColor.cgColor + + backView.addSubview(contentBackView) + contentWidth = contentBackView.widthAnchor.constraint(equalToConstant: chat_content_maxW) + contentHeight = contentBackView.heightAnchor.constraint(equalToConstant: chat_content_maxW) NSLayoutConstraint.activate([ contentWidth!, contentHeight!, - back.leftAnchor.constraint(equalTo: headerView.leftAnchor), - back.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), + contentBackView.leftAnchor.constraint(equalTo: headerView.leftAnchor), + contentBackView.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), ]) - back.addSubview(locationTitleLabel) + contentBackView.addSubview(locationTitleLabel) NSLayoutConstraint.activate([ - locationTitleLabel.leftAnchor.constraint(equalTo: back.leftAnchor, constant: 16), - locationTitleLabel.rightAnchor.constraint(equalTo: back.rightAnchor, constant: -16), - locationTitleLabel.topAnchor.constraint(equalTo: back.topAnchor, constant: 10), + locationTitleLabel.leftAnchor.constraint(equalTo: contentBackView.leftAnchor, constant: 16), + locationTitleLabel.rightAnchor.constraint(equalTo: contentBackView.rightAnchor, constant: -16), + locationTitleLabel.topAnchor.constraint(equalTo: contentBackView.topAnchor, constant: 10), ]) - back.addSubview(subTitleLabel) + contentBackView.addSubview(subTitleLabel) NSLayoutConstraint.activate([ subTitleLabel.leftAnchor.constraint(equalTo: locationTitleLabel.leftAnchor), subTitleLabel.rightAnchor.constraint(equalTo: locationTitleLabel.rightAnchor), subTitleLabel.topAnchor.constraint(equalTo: locationTitleLabel.bottomAnchor, constant: 4), ]) - if let map = NEChatKitClient.instance.delegate?.getCellMapView?() as? UIView { - mapView = map - back.addSubview(map) - map.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - map.leftAnchor.constraint(equalTo: back.leftAnchor), - map.bottomAnchor.constraint(equalTo: back.bottomAnchor), - map.rightAnchor.constraint(equalTo: back.rightAnchor), - map.topAnchor.constraint(equalTo: subTitleLabel.bottomAnchor, constant: 4), - ]) - - let pointImage = UIImageView() - pointImage.translatesAutoresizingMaskIntoConstraints = false - pointImage.image = coreLoader.loadImage("location_point") - map.addSubview(pointImage) - NSLayoutConstraint.activate([ - pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), - pointImage.bottomAnchor.constraint(equalTo: map.bottomAnchor, constant: -30), - ]) - } else { - back.addSubview(emptyLabel) - NSLayoutConstraint.activate([ - emptyLabel.leftAnchor.constraint(equalTo: back.leftAnchor), - emptyLabel.rightAnchor.constraint(equalTo: back.rightAnchor), - emptyLabel.bottomAnchor.constraint(equalTo: back.bottomAnchor, constant: -40), - ]) - } - mapView?.isUserInteractionEnabled = false + contentBackView.addSubview(mapImageView) + NSLayoutConstraint.activate([ + mapImageView.leftAnchor.constraint(equalTo: contentBackView.leftAnchor), + mapImageView.bottomAnchor.constraint(equalTo: contentBackView.bottomAnchor), + mapImageView.rightAnchor.constraint(equalTo: contentBackView.rightAnchor), + mapImageView.topAnchor.constraint(equalTo: subTitleLabel.bottomAnchor, constant: 4), + ]) + + pointImageView.translatesAutoresizingMaskIntoConstraints = false + pointImageView.image = coreLoader.loadImage("location_point") + mapImageView.addSubview(pointImageView) + NSLayoutConstraint.activate([ + pointImageView.centerXAnchor.constraint(equalTo: mapImageView.centerXAnchor), + pointImageView.bottomAnchor.constraint(equalTo: mapImageView.bottomAnchor, constant: -30), + ]) + + contentBackView.addSubview(emptyLabel) + NSLayoutConstraint.activate([ + emptyLabel.leftAnchor.constraint(equalTo: contentBackView.leftAnchor), + emptyLabel.rightAnchor.constraint(equalTo: contentBackView.rightAnchor), + emptyLabel.bottomAnchor.constraint(equalTo: contentBackView.bottomAnchor, constant: -40), + ]) + if let gesture = contentGesture { - back.addGestureRecognizer(gesture) + contentBackView.addGestureRecognizer(gesture) } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) if let m = item.chatmodel as? MessageLocationModel { locationTitleLabel.text = m.title subTitleLabel.text = m.subTitle - if let lat = m.lat, let lng = m.lng, let map = mapView { - NEChatKitClient.instance.delegate?.setMapviewLocation?(lat: lat, lng: lng, mapview: map) + if let lat = m.lat, let lng = m.lng { + if let url = NEChatKitClient.instance.delegate?.getMapImageUrl?(lat: lat, lng: lng) { + NEALog.infoLog(className(), desc: #function + "location image url = \(url)") + mapImageView.sd_setImage(with: URL(string: url)) + emptyLabel.isHidden = true + pointImageView.isHidden = false + } else { + emptyLabel.isHidden = false + pointImageView.isHidden = true + } } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift index 4498509b..7b5cc0fa 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift @@ -19,7 +19,7 @@ open class NEBasePinMessageMultiForwardCell: NEBasePinMessageCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { @@ -45,9 +45,9 @@ open class NEBasePinMessageMultiForwardCell: NEBasePinMessageCell { } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) - guard let data = NECustomAttachment.dataOfCustomMessage(message: item.chatmodel.message) else { + guard let data = NECustomAttachment.dataOfCustomMessage(item.chatmodel.message?.attachment) else { return } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift index 539049e7..37cdd91f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift @@ -6,7 +6,7 @@ import UIKit @objcMembers open class NEBasePinMessageRichTextCell: NEBasePinMessageTextCell { - lazy var titleLabel: UILabel = { + public lazy var titleLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) label.textColor = .ne_darkText @@ -48,7 +48,7 @@ open class NEBasePinMessageRichTextCell: NEBasePinMessageTextCell { titleLabel.addGestureRecognizer(titleGesture) } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) if let model = item.chatmodel as? MessageRichTextModel { titleLabel.attributedText = model.titleAttributeStr diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift index 74fcbc82..6bf09770 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift @@ -6,13 +6,14 @@ import UIKit @objcMembers open class NEBasePinMessageTextCell: NEBasePinMessageCell { - lazy var contentLabel: UILabel = { + public lazy var contentLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) label.textColor = .ne_darkText label.translatesAutoresizingMaskIntoConstraints = false label.isUserInteractionEnabled = true label.numberOfLines = 3 + label.accessibilityIdentifier = "id.message" return label }() @@ -64,7 +65,7 @@ open class NEBasePinMessageTextCell: NEBasePinMessageCell { } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) if let model = item.chatmodel as? MessageTextModel { contentLabel.attributedText = model.attributeStr diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift index 26b70bc1..e40ec909 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift @@ -2,19 +2,20 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NIMSDK import UIKit @objcMembers open class NEBasePinMessageVideoCell: NEBasePinMessageImageCell { - lazy var stateView: VideoStateView = { + public lazy var stateView: VideoStateView = { let state = VideoStateView() state.translatesAutoresizingMaskIntoConstraints = false state.backgroundColor = .clear return state }() - lazy var videoTimeLabel: UILabel = { + public lazy var videoTimeLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = .white @@ -23,7 +24,7 @@ open class NEBasePinMessageVideoCell: NEBasePinMessageImageCell { return label }() - lazy var timeView: UIView = { + public lazy var timeView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(videoTimeLabel) @@ -67,27 +68,20 @@ open class NEBasePinMessageVideoCell: NEBasePinMessageImageCell { stateView.isUserInteractionEnabled = false } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) - if let videoObject = item.chatmodel.message?.messageObject as? NIMVideoObject { - if let path = videoObject.coverUrl { - contentImageView.sd_setImage( - with: URL(string: path), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } else { - contentImageView.sd_setImage( - with: URL(string: videoObject.coverUrl ?? ""), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } + if let videoObject = item.chatmodel.message?.attachment as? V2NIMMessageVideoAttachment { + // 获取首帧 + let videoUrl = videoObject.url ?? "" + let thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + contentImageView.sd_setImage( + with: URL(string: thumbUrl), + placeholderImage: nil, + options: .retryFailed, + progress: nil, + completed: nil + ) if videoObject.duration > 0 { timeView.isHidden = false diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift index 93483e69..e731c67c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift @@ -3,20 +3,22 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NEChatKit +import NECoreIM2Kit import UIKit @objcMembers open class UserBaseTableViewCell: UITableViewCell { - public lazy var avatarImage: UIImageView = { - let avatarImage = UIImageView() - avatarImage.backgroundColor = UIColor(hexString: "#537FF4") - avatarImage.translatesAutoresizingMaskIntoConstraints = false - avatarImage.clipsToBounds = true - avatarImage.isUserInteractionEnabled = true - avatarImage.contentMode = .scaleAspectFill - avatarImage.accessibilityIdentifier = "id.avatar" - return avatarImage + /// 用户头像 + public lazy var avatarImageView: UIImageView = { + let avatarImageView = UIImageView() + avatarImageView.backgroundColor = UIColor(hexString: "#537FF4") + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + avatarImageView.clipsToBounds = true + avatarImageView.isUserInteractionEnabled = true + avatarImageView.contentMode = .scaleAspectFill + avatarImageView.accessibilityIdentifier = "id.avatar" + return avatarImageView }() public lazy var nameLabel: UILabel = { @@ -38,7 +40,7 @@ open class UserBaseTableViewCell: UITableViewCell { return titleLabel }() - public var userModel: NEKitUser? + public var userModel: NETeamMemberInfoModel? override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -48,50 +50,50 @@ open class UserBaseTableViewCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func baseCommonUI() { selectionStyle = .none backgroundColor = .white - contentView.addSubview(avatarImage) + contentView.addSubview(avatarImageView) contentView.addSubview(nameLabel) contentView.addSubview(titleLabel) // name NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), - nameLabel.rightAnchor.constraint(equalTo: avatarImage.rightAnchor), - nameLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor), - nameLabel.bottomAnchor.constraint(equalTo: avatarImage.bottomAnchor), + nameLabel.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), + nameLabel.rightAnchor.constraint(equalTo: avatarImageView.rightAnchor), + nameLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor), + nameLabel.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), ]) titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.text = "placeholder" } - open func setModel(_ model: NEKitUser) { + open func setModel(_ model: NETeamMemberInfoModel) { userModel = model - nameLabel.text = model.shortName(showAlias: false, count: 2) - titleLabel.text = model.showName() + nameLabel.text = ChatMessageHelper.getShortName(model.showNickInTeam() ?? "") + titleLabel.text = model.atNameInTeam() - if let avatarURL = model.userInfo?.avatarUrl, !avatarURL.isEmpty { - avatarImage + if let avatarURL = model.nimUser?.user?.avatar, !avatarURL.isEmpty { + avatarImageView .sd_setImage(with: URL(string: avatarURL)) { [weak self] image, error, type, url in if image != nil { - self?.avatarImage.image = image + self?.avatarImageView.image = image self?.nameLabel.isHidden = true - self?.avatarImage.backgroundColor = .clear + self?.avatarImageView.backgroundColor = .clear } else { - self?.avatarImage.image = nil + self?.avatarImageView.image = nil self?.nameLabel.isHidden = false - self?.avatarImage.backgroundColor = UIColor.colorWithString(string: model.userId) + self?.avatarImageView.backgroundColor = UIColor.colorWithString(string: model.teamMember?.accountId) } } } else { - avatarImage.image = nil + avatarImageView.image = nil nameLabel.isHidden = false - avatarImage.backgroundColor = UIColor.colorWithString(string: model.userId) + avatarImageView.backgroundColor = UIColor.colorWithString(string: model.teamMember?.accountId) } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift index d61d5336..476e062e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift @@ -16,22 +16,22 @@ public enum ChatSendMessageStatus: Int { open class ChatActivityIndicatorView: UIView { public var messageStatus: ChatSendMessageStatus? { didSet { - failBtn.isHidden = true - activity.isHidden = true - activity.stopAnimating() + failButton.isHidden = true + activityView.isHidden = true + activityView.stopAnimating() switch messageStatus { case .sending: - self.isHidden = false - activity.isHidden = false - failBtn.isHidden = true - activity.startAnimating() + isHidden = false + activityView.isHidden = false + failButton.isHidden = true + activityView.startAnimating() case .failed: - self.isHidden = false - activity.isHidden = true - failBtn.isHidden = false + isHidden = false + activityView.isHidden = true + failButton.isHidden = false case .successed: - self.isHidden = true + isHidden = true default: print("default") @@ -45,31 +45,31 @@ open class ChatActivityIndicatorView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { backgroundColor = .clear - addSubview(failBtn) - addSubview(activity) + addSubview(failButton) + addSubview(activityView) NSLayoutConstraint.activate([ - failBtn.topAnchor.constraint(equalTo: topAnchor), - failBtn.leftAnchor.constraint(equalTo: leftAnchor), - failBtn.bottomAnchor.constraint(equalTo: bottomAnchor), - failBtn.rightAnchor.constraint(equalTo: rightAnchor), + failButton.topAnchor.constraint(equalTo: topAnchor), + failButton.leftAnchor.constraint(equalTo: leftAnchor), + failButton.bottomAnchor.constraint(equalTo: bottomAnchor), + failButton.rightAnchor.constraint(equalTo: rightAnchor), ]) NSLayoutConstraint.activate([ - activity.topAnchor.constraint(equalTo: topAnchor), - activity.leftAnchor.constraint(equalTo: leftAnchor), - activity.bottomAnchor.constraint(equalTo: bottomAnchor), - activity.rightAnchor.constraint(equalTo: rightAnchor), + activityView.topAnchor.constraint(equalTo: topAnchor), + activityView.leftAnchor.constraint(equalTo: leftAnchor), + activityView.bottomAnchor.constraint(equalTo: bottomAnchor), + activityView.rightAnchor.constraint(equalTo: rightAnchor), ]) } // MARK: lazy Method - public lazy var failBtn: UIButton = { + public lazy var failButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.imageView?.contentMode = .center @@ -78,10 +78,10 @@ open class ChatActivityIndicatorView: UIView { return button }() - private lazy var activity: UIActivityIndicatorView = { - let activity = UIActivityIndicatorView() - activity.translatesAutoresizingMaskIntoConstraints = false - activity.color = .gray - return activity + private lazy var activityView: UIActivityIndicatorView = { + let activityView = UIActivityIndicatorView() + activityView.translatesAutoresizingMaskIntoConstraints = false + activityView.color = .gray + return activityView }() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift index a60b7cf4..80ccdee1 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift @@ -13,20 +13,20 @@ open class ChatBrokenNetworkView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { backgroundColor = HexRGB(0xFEE3E6) - addSubview(content) + addSubview(contentLabel) NSLayoutConstraint.activate([ - content.leftAnchor.constraint(equalTo: leftAnchor, constant: 15), - content.centerYAnchor.constraint(equalTo: centerYAnchor), - content.rightAnchor.constraint(equalTo: rightAnchor, constant: -15), + contentLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 15), + contentLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + contentLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -15), ]) } - private lazy var content: UILabel = { + private lazy var contentLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = DefaultTextFont(14) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatRecordView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatRecordView.swift index 87118f70..c91ce70b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatRecordView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatRecordView.swift @@ -26,7 +26,7 @@ open class ChatRecordView: UIView, UIGestureRecognizerDelegate { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { @@ -35,6 +35,7 @@ open class ChatRecordView: UIView, UIGestureRecognizerDelegate { topTipLabel.font = UIFont.systemFont(ofSize: 12) topTipLabel.textColor = .ne_lightText topTipLabel.textAlignment = .center + topTipLabel.isHidden = true // 不展示 addSubview(topTipLabel) NSLayoutConstraint.activate([ topTipLabel.topAnchor.constraint(equalTo: topAnchor, constant: 0), diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift index df1b9fb5..ce4566ff 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift @@ -64,7 +64,7 @@ open class CirleProgressView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } private func drawCircle(progress: Float) { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift index 19b1b58c..0efe4ccf 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift @@ -57,7 +57,8 @@ open class MessageOperationView: UIView, UICollectionViewDataSource, UICollectio } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + collcetionView = UICollectionView(frame: .zero) + super.init(coder: coder) } // MARK: UICollectionViewDataSource diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift index 4d054363..280b3a5e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift @@ -88,6 +88,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, let button = ExpandButton() button.translatesAutoresizingMaskIntoConstraints = false button.backgroundColor = .clear + button.accessibilityIdentifier = "id.chatExpandButton" return button }() @@ -101,13 +102,80 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, public var textviewLeftConstraint: NSLayoutConstraint? public var textviewRightConstraint: NSLayoutConstraint? + public var multipleLineView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.white + view.clipsToBounds = true + view.layer.cornerRadius = 6.0 + view.isHidden = true + + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOpacity = 0.5 + view.layer.shadowOffset = CGSize(width: 3, height: 3) + view.layer.shadowRadius = 5 + view.layer.masksToBounds = false + return view + }() + + public var titleField: UITextField = { + let textField = UITextField() + textField.translatesAutoresizingMaskIntoConstraints = false + textField.font = UIFont.systemFont(ofSize: 18.0) + textField.textColor = .ne_darkText + textField.returnKeyType = .send + textField.attributedPlaceholder = NSAttributedString(string: coreLoader.localizable("multiple_line_placleholder"), attributes: [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText]) + textField.addTarget(self, action: #selector(textFieldChange), for: .editingChanged) + return textField + }() + + public var multipleLineExpandButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = .clear + return button + }() + + public var multipleSendButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = .clear + button.setImage(coreLoader.loadImage("multiple_send_image"), for: .normal) + return button + }() + + public lazy var emojiView: UIView = { + let backView = UIView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) + let view = + InputEmoticonContainerView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) + view.delegate = self + backView.isHidden = true + + backView.backgroundColor = UIColor.clear + backView.addSubview(view) + let tap = UITapGestureRecognizer() + backView.addGestureRecognizer(tap) + tap.addTarget(self, action: #selector(missClickEmoj)) + return backView + }() + + public lazy var chatAddMoreView: NEChatMoreActionView = { + let view = NEChatMoreActionView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + view.delegate = self + return view + }() + + public var multipleLineViewHeight: NSLayoutConstraint? + override init(frame: CGRect) { super.init(frame: frame) commonUI() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } deinit { @@ -154,31 +222,6 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, atCache?.clean() } - // MARK: ===================== lazy method ===================== - - public lazy var emojiView: UIView = { - let backView = UIView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) - let view = - InputEmoticonContainerView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) - view.delegate = self - backView.isHidden = true - - backView.backgroundColor = UIColor.clear - backView.addSubview(view) - let tap = UITapGestureRecognizer() - backView.addGestureRecognizer(tap) - tap.addTarget(self, action: #selector(missClickEmoj)) - return backView - }() - - public lazy var chatAddMoreView: NEChatMoreActionView = { - let view = NEChatMoreActionView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - view.delegate = self - return view - }() - open func textViewDidChange(_ textView: UITextView) { delegate?.textFieldDidChange(textView.text) } @@ -267,6 +310,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, let addString = NEEmotionTool.getAttWithStr(str: text, font: .systemFont(ofSize: 16)) mutaString.replaceCharacters(in: range, with: addString) textView.attributedText = mutaString + textView.accessibilityValue = text DispatchQueue.main.async { textView.selectedRange = NSMakeRange(range.location + addString.length, 0) } @@ -305,6 +349,9 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, if let findRange = findShowPosition(range: range, attribute: textView.attributedText) { textView.selectedRange = NSMakeRange(findRange.location + findRange.length, 0) } + + textView.scrollRangeToVisible(NSMakeRange(textView.selectedRange.location, 1)) + textView.accessibilityValue = getRealSendText(textView.attributedText) } @available(iOS 10.0, *) @@ -314,12 +361,6 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, return true } -// @available(iOS 10.0, *) -// open func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { -// -// return true -// } - open func buttonEvent(button: UIButton) { button.isSelected = !button.isSelected if button.tag - 5 != 2, button != currentButton { @@ -349,37 +390,32 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, textView.deleteBackward() print("delete ward") } else { - if let font = textView.font { - let attribute = NEEmotionTool.getAttWithStr( - str: description, - font: font, - CGPoint(x: 0, y: -4) - ) - print("attribute : ", attribute) - let mutaAttribute = NSMutableAttributedString() - if let origin = textView.attributedText { - mutaAttribute.append(origin) - } - attribute.enumerateAttribute( - NSAttributedString.Key.attachment, - in: NSMakeRange(0, attribute.length) - ) { value, range, stop in - if let neAttachment = value as? NEEmotionAttachment { - print("ne attachment bounds ", neAttachment.bounds) - } - } - mutaAttribute.append(attribute) - mutaAttribute.addAttribute( - NSAttributedString.Key.font, - value: font, - range: NSMakeRange(0, mutaAttribute.length) - ) - textView.attributedText = mutaAttribute - textView.scrollRangeToVisible(NSMakeRange(textView.attributedText.length, 1)) - } + let range = textView.selectedRange + let attribute = NEEmotionTool.getAttWithStr(str: description, font: .systemFont(ofSize: 16)) + let mutaAttribute = NSMutableAttributedString(attributedString: textView.attributedText) + mutaAttribute.insert(attribute, at: range.location) + textView.attributedText = mutaAttribute + textView.selectedRange = NSMakeRange(range.location + attribute.length, 0) } } + /// 点击富文本图片 + public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + textView.becomeFirstResponder() + + var offset = characterRange.location + // 修复iOS 14.1,点击空白识别为点击最后一个富文本图片的问题,待优化 + if characterRange.location + characterRange.length == textView.text.count { + offset += 1 + } + + if let newPosition = textView.position(from: textView.beginningOfDocument, offset: offset) { + textView.selectedTextRange = textView.textRange(from: newPosition, to: newPosition) + } + + return true + } + open func didPressSend(sender: UIButton) { sendText(textView: textView) } @@ -415,10 +451,6 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, delegate?.endRecord(insideView: insideView) } -// func textFieldChangeNoti() { -// delegate?.textFieldDidChange(textField) -// } - func getRealSendText(_ attribute: NSAttributedString) -> String? { let muta = NSMutableString() @@ -493,8 +525,8 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, open func getAtRemoteExtension() -> [String: Any]? { var atDic = [String: Any]() - NELog.infoLog(className(), desc: "at range cache : \(atRangeCache)") - atRangeCache.forEach { (key: String, value: MessageAtCacheModel) in + NEALog.infoLog(className(), desc: "at range cache : \(atRangeCache)") + for (key, value) in atRangeCache { if let userValue = atDic[value.accid] as? [String: AnyObject], var array = userValue[atSegmentsKey] as? [Any], let object = value.atModel.yx_modelToJSONObject() { array.append(object) if var dic = atDic[value.accid] as? [String: Any] { @@ -510,7 +542,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, atDic[value.accid] = dic } } - NELog.infoLog(className(), desc: "at dic value : \(atDic)") + NEALog.infoLog(className(), desc: "at dic value : \(atDic)") if atDic.count > 0 { return [yxAtMsg: atDic] } @@ -588,50 +620,6 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, } } - public var multipleLineView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.white - view.clipsToBounds = true - view.layer.cornerRadius = 6.0 - view.isHidden = true - - view.layer.shadowColor = UIColor.black.cgColor - view.layer.shadowOpacity = 0.5 - view.layer.shadowOffset = CGSize(width: 3, height: 3) - view.layer.shadowRadius = 5 - view.layer.masksToBounds = false - return view - }() - - public var titleField: UITextField = { - let text = UITextField() - text.translatesAutoresizingMaskIntoConstraints = false - text.font = UIFont.systemFont(ofSize: 18.0) - text.textColor = .ne_darkText - text.returnKeyType = .send - text.attributedPlaceholder = NSAttributedString(string: coreLoader.localizable("multiple_line_placleholder"), attributes: [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText]) - text.addTarget(self, action: #selector(textFieldChange), for: .editingChanged) - return text - }() - - public var multipleLineExpandButton: ExpandButton = { - let button = ExpandButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.backgroundColor = .clear - return button - }() - - public var multipleSendButton: ExpandButton = { - let button = ExpandButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.backgroundColor = .clear - button.setImage(coreLoader.loadImage("multiple_send_image"), for: .normal) - return button - }() - - public var multipleLineViewHeight: NSLayoutConstraint? - func setupMultipleLineView() { addSubview(multipleLineView) multipleLineViewHeight = multipleLineView.heightAnchor.constraint(equalToConstant: 400) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift index 2498f77e..7bd4fa37 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift @@ -32,7 +32,7 @@ open class NEChatMoreActionView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func configData(data: [NEMoreItemModel]) { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift index 7f151a83..144e73e8 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift @@ -19,25 +19,25 @@ open class NEInputMoreCell: UICollectionViewCell { } func setupViews() { - contentView.addSubview(avatarImage) + contentView.addSubview(avatarImageView) contentView.addSubview(titleLabel) NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: contentView.leftAnchor), - avatarImage.topAnchor.constraint(equalTo: contentView.topAnchor), - avatarImage.widthAnchor.constraint(equalToConstant: NEMoreCell_Image_Size.width), - avatarImage.heightAnchor.constraint(equalToConstant: NEMoreCell_Image_Size.height), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: NEMoreCell_Image_Size.width), + avatarImageView.heightAnchor.constraint(equalToConstant: NEMoreCell_Image_Size.height), ]) NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: avatarImage.bottomAnchor), + titleLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor), titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor), titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor), titleLabel.heightAnchor.constraint(equalToConstant: NEMoreCell_Title_Height), ]) } - lazy var avatarImage: UIImageView = { + public lazy var avatarImageView: UIImageView = { let imageView = UIImageView() imageView.isUserInteractionEnabled = true imageView.translatesAutoresizingMaskIntoConstraints = false @@ -45,19 +45,19 @@ open class NEInputMoreCell: UICollectionViewCell { return imageView }() - lazy var titleLabel: UILabel = { - let title = UILabel() - title.textColor = UIColor.ne_greyText - title.font = UIFont.systemFont(ofSize: 10) - title.textAlignment = .center - title.translatesAutoresizingMaskIntoConstraints = false - title.accessibilityIdentifier = "id.menuIcon" - return title + public lazy var titleLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.textColor = UIColor.ne_greyText + titleLabel.font = UIFont.systemFont(ofSize: 10) + titleLabel.textAlignment = .center + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.accessibilityIdentifier = "id.menuIcon" + return titleLabel }() func config(_ itemModel: NEMoreItemModel) { cellData = itemModel - avatarImage.image = itemModel.customImage == nil ? itemModel.image : itemModel.customImage + avatarImageView.image = itemModel.customImage == nil ? itemModel.image : itemModel.customImage titleLabel.text = itemModel.title } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift index ce90656f..dc59e387 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift @@ -21,15 +21,15 @@ open class NEMutilSelectBottomView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupSubview() { // 逐条转发 addSubview(singleForwardButton) buttonTopAnchor = singleForwardButton.topAnchor.constraint(equalTo: topAnchor, constant: 12) + buttonTopAnchor?.isActive = true NSLayoutConstraint.activate([ - buttonTopAnchor!, singleForwardButton.centerXAnchor.constraint(equalTo: centerXAnchor), singleForwardButton.widthAnchor.constraint(equalToConstant: 48), singleForwardButton.heightAnchor.constraint(equalToConstant: 48), diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift index 380faa5a..749bca93 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift @@ -50,7 +50,7 @@ open class ReplyView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } // @objc func closeButtonEvent(button: UIButton) { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapAddressCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapAddressCell.swift deleted file mode 100644 index b60e2c77..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapAddressCell.swift +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NEChatKit -import UIKit - -@objcMembers -open class NEMapAddressCell: UITableViewCell { - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - selectionStyle = .none - setupSubviews() - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupSubviews() { - selectionStyle = .none - contentView.addSubview(locationImg) - contentView.addSubview(selectImg) - contentView.addSubview(title) - contentView.addSubview(subTitle) - contentView.addSubview(bottomLine) - - NSLayoutConstraint.activate([ - locationImg.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 15), - locationImg.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 17), - locationImg.heightAnchor.constraint(equalToConstant: 18), - locationImg.widthAnchor.constraint(equalToConstant: 18), - ]) - - NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: locationImg.rightAnchor, constant: 7), - title.centerYAnchor.constraint(equalTo: locationImg.centerYAnchor), - title.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -70), - ]) - - NSLayoutConstraint.activate([ - subTitle.leftAnchor.constraint(equalTo: title.leftAnchor), - subTitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), - subTitle.rightAnchor.constraint(equalTo: title.rightAnchor), - ]) - - NSLayoutConstraint.activate([ - selectImg.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -13), - selectImg.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - ]) - - NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 12), - bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -12), - bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -1), - bottomLine.heightAnchor.constraint(equalToConstant: 1), - ]) - } - - public lazy var locationImg: UIImageView = { - let location = UIImageView(image: UIImage.ne_imageNamed(name: "chat_loacaiton_img")) - location.translatesAutoresizingMaskIntoConstraints = false - return location - }() - - public lazy var selectImg: UIImageView = { - let img = UIImageView(image: UIImage.ne_imageNamed(name: "chat_map_select")) - img.translatesAutoresizingMaskIntoConstraints = false - return img - }() - - public lazy var title: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = UIColor.ne_darkText - label.font = UIFont.systemFont(ofSize: 16) - label.text = "" - return label - }() - - public lazy var subTitle: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = UIColor.ne_emptyTitleColor - label.font = UIFont.systemFont(ofSize: 14) - label.text = "" - return label - }() - - private lazy var bottomLine: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.ne_navLineColor - return view - }() - - func configure(_ model: ChatLocaitonModel, _ select: Bool) { - title.attributedText = model.attribute - var distanceStr = "" - if model.distance > 0 { - if model.distance <= 1000 { - distanceStr = "\(model.distance)m" - } else { - let kilometer = model.distance / 1000 - distanceStr = "\(kilometer)km" - } - subTitle.text = "\(distanceStr)\(chatLocalizable("distance_inner"))|\(model.address)" - } else { - subTitle.text = model.address - } - - if select == true { - selectImg.isHidden = false - } else { - selectImg.isHidden = true - } - } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapGuideBottomView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapGuideBottomView.swift deleted file mode 100644 index 97f79ba7..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapGuideBottomView.swift +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import UIKit - -@objc -public protocol NEMapGuideBottomViewDelegate: NSObjectProtocol { - func didClickGuide() -} - -@objcMembers -open class NEMapGuideBottomView: UIView { - public weak var delegate: NEMapGuideBottomViewDelegate? - - override public init(frame: CGRect) { - super.init(frame: frame) - setupSubviews() - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupSubviews() { - backgroundColor = .white - addSubview(guideBtn) - addSubview(title) - addSubview(subtitle) - - NSLayoutConstraint.activate([ - guideBtn.topAnchor.constraint(equalTo: topAnchor, constant: 16), - guideBtn.rightAnchor.constraint(equalTo: rightAnchor, constant: -12), - guideBtn.widthAnchor.constraint(equalToConstant: 40), - guideBtn.heightAnchor.constraint(equalToConstant: 40), - ]) - - NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: leftAnchor, constant: 12), - title.rightAnchor.constraint(equalTo: rightAnchor, constant: -52), - title.topAnchor.constraint(equalTo: topAnchor, constant: 16), - ]) - - NSLayoutConstraint.activate([ - subtitle.leftAnchor.constraint(equalTo: title.leftAnchor), - subtitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), - subtitle.rightAnchor.constraint(equalTo: rightAnchor, constant: -52), - ]) - } - - lazy var guideBtn: UIButton = { - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setImage(UIImage.ne_imageNamed(name: "chat_map_path"), for: .normal) - button.setImage(UIImage.ne_imageNamed(name: "chat_map_path"), for: .highlighted) - button.addTarget(self, action: #selector(guideBtnClick), for: .touchUpInside) - return button - }() - - lazy var title: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = UIFont.systemFont(ofSize: 16) - label.textColor = UIColor.ne_darkText - label.text = "" - return label - }() - - lazy var subtitle: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = UIFont.systemFont(ofSize: 14) - label.textColor = UIColor.ne_emptyTitleColor - label.text = "" - - return label - }() - - func guideBtnClick() { - if let delegate = delegate { - delegate.didClickGuide() - } - } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift index 8637638b..2d0ac801 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift @@ -5,74 +5,56 @@ import Foundation import NEChatKit import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK -@objc -public enum LoadMessageDirection: Int { - case old = 1 - case new -} - @objc public protocol ChatViewModelDelegate: NSObjectProtocol { - func onRecvMessages(_ messages: [NIMMessage]) - func willSend(_ message: NIMMessage) - func send(_ message: NIMMessage, didCompleteWithError error: Error?) - func send(_ message: NIMMessage, progress: Float) - func didReadedMessageIndexs() - func onDeleteMessage(_ message: NIMMessage, atIndexs: [IndexPath], reloadIndex: [IndexPath]) - func onRevokeMessage(_ message: NIMMessage, atIndexs: [IndexPath]) - func onAddMessagePin(_ message: NIMMessage, atIndexs: [IndexPath]) - func onRemoveMessagePin(_ message: NIMMessage, atIndexs: [IndexPath]) - func updateDownloadProgress(_ message: NIMMessage, atIndex: IndexPath, progress: Float) + func onRecvMessages(_ messages: [V2NIMMessage]) + func sending(_ message: V2NIMMessage) + func sendSuccess(_ message: V2NIMMessage) + @objc optional func send(_ message: V2NIMMessage, progress: Float) + func onLoadMoreWithMessage(_ indexs: [IndexPath]) + func onDeleteMessage(_ messages: [V2NIMMessage], deleteIndexs: [IndexPath], reloadIndex: [IndexPath]) + func onRevokeMessage(_ message: V2NIMMessage, atIndexs: [IndexPath]) + func onMessagePinStatusChange(_ message: V2NIMMessage?, atIndexs: [IndexPath]) func remoteUserEditing() func remoteUserEndEditing() func didLeaveTeam() func didDismissTeam() - func didRefreshTable() - func onTeamMemberChange(team: NIMTeam) + func tableViewReload() @objc optional func showErrorToast(error: Error?) @objc optional func getMessageModel(model: MessageModel) @objc optional func selectedMessagesChanged(_ count: Int) } -let revokeLocalMessage = "revoke_message_local" -let revokeLocalMessageContent = "revoke_message_local_content" -let removePinMessageNoti = "remove_pin_message_noti" - @objcMembers -open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDelegate, - NIMConversationManagerDelegate, NIMSystemNotificationManagerDelegate, ChatExtendProviderDelegate, FriendProviderDelegate, NIMTeamManagerDelegate { - public var team: NIMTeam? - /// 当前成员的群成员对象类 - public var teamMember: NIMTeamMember? - public var session: NIMSession +open class ChatViewModel: NSObject, NEChatListener, NENotiListener { + public var conversationId: String + public var sessionId: String public var messages = [MessageModel]() public weak var delegate: ChatViewModelDelegate? // 多选选中的消息 - public var selectedMessages = [NIMMessage]() { + public var selectedMessages = [V2NIMMessage]() { didSet { delegate?.selectedMessagesChanged?(selectedMessages.count) } } // 上拉时间戳 - private var newMsg: NIMMessage? + private var newMsg: V2NIMMessage? // 下拉时间戳 - private var oldMsg: NIMMessage? + private var oldMsg: V2NIMMessage? - public var repo = ChatRepo.shared + public let chatRepo = ChatRepo.shared + public let contactRepo = ContactRepo.shared public var operationModel: MessageContentModel? public var isReplying = false - public let messagPageNum: UInt = 100 - - // 可信时间戳 - public var credibleTimestamp: TimeInterval = 0 - public var anchor: NIMMessage? + public let messagPageNum: Int = 100 + public var anchor: V2NIMMessage? public var isHistoryChat = false @@ -80,379 +62,56 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg public var deletingMsgDic = Set() - init(session: NIMSession) { - NELog.infoLog(ModuleName + " ChatViewModel", desc: #function + ", sessionId:" + session.sessionId) - self.session = session + override init() { + conversationId = "" + sessionId = "" + super.init() + } + + init(conversationId: String) { + NEALog.infoLog(ModuleName + " " + ChatViewModel.className(), desc: #function + ", conversationId:\(conversationId)") + self.conversationId = conversationId + sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId) ?? "" anchor = nil super.init() - repo.addChatDelegate(delegate: self) - repo.addContactDelegate(delegate: self) - repo.addSessionDelegate(delegate: self) - repo.addSystemNotificationDelegate(delegate: self) - repo.addChatExtendDelegate(delegate: self) - repo.addTeamDelegate(delegate: self) - addObserver() + chatRepo.addChatListener(self) } - init(session: NIMSession, anchor: NIMMessage?) { - NELog.infoLog(ModuleName + " ChatViewModel", desc: #function + ", sessionId:" + session.sessionId) - self.session = session + init(conversationId: String, anchor: V2NIMMessage?) { + NEALog.infoLog(ModuleName + " " + ChatViewModel.className(), desc: #function + ", conversationId:\(conversationId)") + self.conversationId = conversationId self.anchor = anchor + sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId) ?? "" super.init() if anchor != nil { isHistoryChat = true } - repo.addChatDelegate(delegate: self) - repo.addContactDelegate(delegate: self) - repo.addSessionDelegate(delegate: self) - repo.addSystemNotificationDelegate(delegate: self) - repo.addChatExtendDelegate(delegate: self) - repo.addTeamDelegate(delegate: self) - addObserver() - } - - func addObserver() { - NotificationCenter.default.addObserver(self, selector: #selector(removePinNoti), name: Notification.Name(removePinMessageNoti), object: nil) - } - - func removePinNoti(_ noti: Notification) { - if let message = noti.object as? NIMMessage { - removeLocalPinMessage(message) - delegate?.didRefreshTable() - } - } - - /// 发送文本消息(当前会话) - open func sendTextMessage(text: String, remoteExt: [String: Any]?, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") - if text.count <= 0 { - return - } - repo.sendMessage( - message: MessageUtils.textMessage(text: text, remoteExt: remoteExt), - session: session, - completion - ) - } - - /// 发送文本消息(当前会话) - open func sendTextMessage(text: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") - if text.count <= 0 { - return - } - repo.sendMessage( - message: MessageUtils.textMessage(text: text), - session: session, - completion - ) - } - - /// 发送文本消息(非当前会话) - open func sendTextMessage(text: String, session: NIMSession, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") - if text.count <= 0 { - return - } - repo.sendMessage( - message: MessageUtils.textMessage(text: text), - session: session, - completion - ) - } - - open func sendAudioMessage(filePath: String, _ completion: @escaping (Error?) -> Void) { - if ChatDeduplicationHelper.instance.isRecordAudioSended(path: filePath) == true { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ",duplicate send audio at filePath:" + filePath) - return - } - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", filePath:" + filePath) - repo.sendMessage( - message: MessageUtils.audioMessage(filePath: filePath), - session: session, - completion - ) - } - - open func sendImageMessage(image: UIImage, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", image.size: \(image.size)") - repo.sendMessage( - message: MessageUtils.imageMessage(image: image), - session: session, - completion - ) - } - - open func sendImageMessage(data: Data, ext: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", image data count: \(data.count)") - repo.sendMessage( - message: MessageUtils.imageMessage(data: data, ext: ext), - session: session, - completion - ) - } - - open func sendImageMessage(path: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", image path: \(path)") - repo.sendMessage( - message: MessageUtils.imageMessage(path: path), - session: session, - completion - ) - } - - open func sendVideoMessage(url: URL, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ",video url.path:" + url.path) - weak var weakSelf = self - - convertVideoToMP4(videoURL: url) { url, error in - if let p = url?.path, let s = weakSelf?.session { - weakSelf?.repo.sendMessage( - message: MessageUtils.videoMessage(filePath: p), - session: s, - completion - ) - } else { - NELog.errorLog("chat veiw model", desc: "convert mov to mp4 failed") - } - } - } - - func convertVideoToMP4(videoURL: URL, completion: @escaping (URL?, Error?) -> Void) { - let outputFileName = NIMKitFileLocationHelper.genFilename(withExt: "mp4") - guard let outputPath = NIMKitFileLocationHelper.filepath(forVideo: outputFileName) else { - return - } - let asset = AVURLAsset(url: videoURL, options: nil) - let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) - let outputUrl = URL(fileURLWithPath: outputPath) - session?.outputURL = outputUrl - session?.outputFileType = AVFileType.mp4 - session?.shouldOptimizeForNetworkUse = true - session?.exportAsynchronously { - DispatchQueue.main.async { - if session?.status == AVAssetExportSession.Status.completed { - completion(outputUrl, nil) - } else { - completion(nil, nil) - } - } - } - } - - open func sendLocationMessage(_ model: ChatLocaitonModel, _ completion: @escaping (Error?) -> Void) { - let message = MessageUtils.locationMessage(model.lat, model.lng, model.title, model.address) - repo.sendMessage(message: message, session: session, completion) - } - - open func sendFileMessage(filePath: String, displayName: String?, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", filePath:\(filePath)") - repo.sendMessage( - message: MessageUtils.fileMessage(filePath: filePath, displayName: displayName), - session: session, - completion - ) + chatRepo.addChatListener(self) } - open func sendFileMessage(data: Data, displayName: String?, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", data.count:\(data.count)") - repo.sendMessage( - message: MessageUtils.fileMessage(data: data, displayName: displayName), - session: session, - completion - ) - } - - /// 发送自定义消息(当前会话) - open func sendCustomMessage(attachment: NIMCustomAttachment, - remoteExt: [String: Any]?, - apnsConstent: String?, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", apnsConstent:\(String(describing: apnsConstent))") - repo.sendMessage( - message: MessageUtils.customMessage(attachment: attachment, - remoteExt: remoteExt, - apnsContent: apnsConstent), - session: session, - completion - ) - } - - /// 发送自定义消息 - open func sendCustomMessage(attachment: NIMCustomAttachment, - remoteExt: [String: Any]?, - apnsConstent: String?, - session: NIMSession, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", apnsConstent:\(String(describing: apnsConstent))") - repo.sendMessage( - message: MessageUtils.customMessage(attachment: attachment, - remoteExt: remoteExt, - apnsContent: apnsConstent), - session: session, - completion - ) - } - - open func sendBlackListTip(_ errorSession: NIMSession?, _ message: NIMMessage) { -// if DeduplicationHelper.instance.isBlackTipSended(messageId: message.messageId) == true { -// NELog.infoLog(ModuleName + " " + className(), desc: #function + "sendBlackListTip") -// return -// } - guard let eSession = errorSession else { - return - } - NELog.infoLog(ModuleName + " " + className(), desc: #function + "sendBlackListTip") - let content = chatLocalizable("black_list_tip") - let tip = NIMMessage() - let object = NIMTipObject(attach: nil, callbackExt: nil) - tip.messageObject = object - tip.text = content - let setting = NIMMessageSetting() - setting.shouldBeCounted = false - tip.setting = setting - repo.saveMessageToDB(tip, eSession) { [weak self] error in - NELog.infoLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + "save black tip list tip result \(error?.localizedDescription ?? "")") - if let model = self?.modelFromMessage(message: tip) { - self?.messages.append(model) - if let currentSid = self?.session.sessionId, let errorSid = errorSession?.sessionId, currentSid == errorSid { - self?.delegate?.willSend(tip) - } - } + /// 根据会话id列表清空相应会话的未读数 + public func clearUnreadCount() { + ConversationProvider.shared.clearUnreadCountByIds([conversationId]) { result, error in + NEALog.infoLog(ModuleName, desc: #function + " error" + (error?.localizedDescription ?? "")) } } - // 动态查询历史消息解决方案 - open func getMessagesModelDynamically(_ order: NIMMessageSearchOrder, message: NIMMessage?, - _ completion: @escaping (Error?, NSInteger, [MessageModel]?) - -> Void) { - let param = NIMGetMessagesDynamicallyParam() - param.limit = messagPageNum - param.session = session - param.order = order - if let msg = message { - if order == .desc { - param.endTime = msg.timestamp - } else { - param.startTime = msg.timestamp - } - param.anchorClientId = msg.messageId - param.anchorServerId = msg.serverID - } + /// 加载数据 + /// - Parameter completion: 完成回调 + open func loadData(_ completion: @escaping (Error?, NSInteger, NSInteger, Int) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) weak var weakSelf = self - repo.getMessagesDynamically(param) { error, isReliable, messages in - if let messageArray = messages, messageArray.count > 0 { - var count = 0 - var readMsg: NIMMessage? - if order == .desc { - weakSelf?.oldMsg = messageArray.last - readMsg = messageArray.first - } else { - readMsg = messageArray.last - weakSelf?.newMsg = messageArray.last - } - for msg in messageArray { - // 是否需要进行重复消息过滤 - var needFilter = msg.serverID.isEmpty - if let object = msg.messageObject as? NIMNotificationObject, - let content = object.content as? NIMTeamNotificationContent, - content.operationType == .invite { - needFilter = true - } - - if needFilter { - if weakSelf?.filterInviteSet.contains(msg.messageId) == true { - continue - } else { - weakSelf?.filterInviteSet.insert(msg.messageId) - } - } - - print("message text : ", msg.text as Any) - if let model = weakSelf?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - weakSelf?.filterRevokeMessage([model]) - if order == .desc { - weakSelf?.addTimeForHistoryMessage(model) - weakSelf?.messages.insert(model, at: 0) - count += 1 - } else { - if let last = weakSelf?.messages.last { - ChatMessageHelper.addTimeMessage(model, last) - } - weakSelf?.messages.append(model) - count += 1 - } - } - } - - // 第一条消息默认显示时间 - if let firstModel = weakSelf?.messages.first, - let msg = firstModel.message { - let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: msg.timestamp)) - firstModel.timeContent = timeText - } - weakSelf?.checkAudioFile(messages: weakSelf?.messages) - completion(error, count, weakSelf?.messages) - - if weakSelf?.session.sessionType == .P2P { - if let nearMsg = readMsg { - weakSelf?.markRead(messages: [nearMsg]) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), - desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") - ) - } - } - } else if weakSelf?.session.sessionType == .team { - weakSelf?.markRead(messages: messageArray) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), - desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") - ) - } - weakSelf?.refreshReceipts(messages: messageArray) - } - - let group = DispatchGroup() - for msg in messageArray { - if let object = msg.messageObject as? NIMNotificationObject, - let content = object.content as? NIMTeamNotificationContent { - let targetIDs = content.targetIDs ?? [] - targetIDs.forEach { uid in - if ChatUserCache.getUserInfo(uid) == nil { - group.enter() - ChatUserCache.getUserInfo(uid) { _, _ in - group.leave() - } - } - } - } - } - - group.notify(queue: .main) { - weakSelf?.delegate?.didRefreshTable() - } - - } else { - weakSelf?.checkAudioFile(messages: weakSelf?.messages) - completion(error, 0, weakSelf?.messages) - } - } - } - open func queryRoamMsgHasMoreTime_v2(_ completion: @escaping (Error?, NSInteger, NSInteger, Int) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - weak var weakSelf = self // 记录可信时间戳 if anchor == nil { - weakSelf?.getMessagesModelDynamically(.desc, message: nil) { error, count, models in - NELog.infoLog( - ModuleName + " " + self.className(), - desc: "CALLBACK getMessageHistory " + (error?.localizedDescription ?? "no error") + weakSelf?.getHistoryMessage(order: .QUERY_DIRECTION_DESC, message: nil) { error, count, models in + NEALog.infoLog( + ModuleName + " " + ChatViewModel.className(), + desc: "CALLBACK getMessageList " + (error?.localizedDescription ?? "no error") ) completion(error, count, 0, 0) + weakSelf?.loadMoreWithMessage(models) } - } else { // 有锚点消息,从两个方向拉去消息 weakSelf?.newMsg = weakSelf?.anchor @@ -462,204 +121,277 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg var moreEnd = 0 var newEnd = 0 - var historyDatas = [MessageModel]() - var newDatas = [MessageModel]() + var historyDatas = [V2NIMMessage]() + var newDatas = [V2NIMMessage]() + var loadMessages = [V2NIMMessage]() var err: Error? group.enter() - weakSelf?.getMessagesModelDynamically(.desc, message: weakSelf?.anchor) { error, value, models in + weakSelf?.getHistoryMessage(order: .QUERY_DIRECTION_DESC, message: weakSelf?.anchor) { error, value, models in moreEnd = value if error != nil { err = error } - if let ms = models { - historyDatas.append(contentsOf: ms) - } - print("drop down remote refresh : ", historyDatas.count) + + historyDatas.append(contentsOf: models) + loadMessages.append(contentsOf: historyDatas) + + group.enter() if let anchorMessage = weakSelf?.anchor { - let model = self.modelFromMessage(message: anchorMessage) - weakSelf?.filterRevokeMessage([model]) - if NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: anchorMessage) == false { + loadMessages.append(anchorMessage) + weakSelf?.modelFromMessage(message: anchorMessage) { model in weakSelf?.messages.append(model) + group.leave() } } - weakSelf?.getMessagesModelDynamically(.asc, message: weakSelf?.anchor) { error, value, models in - NELog.infoLog( - ModuleName + " " + self.className(), + + group.enter() + weakSelf?.getHistoryMessage(order: .QUERY_DIRECTION_ASC, message: weakSelf?.anchor) { error, value, models in + NEALog.infoLog( + ModuleName + " " + ChatViewModel.className(), desc: "CALLBACK pullRemoteRefresh " + (error?.localizedDescription ?? "no error") ) newEnd = value if err != nil { err = error } - if let ms = models { - newDatas.append(contentsOf: ms) - } - print("pull remote refresh : ", newDatas.count) + + newDatas.append(contentsOf: models) + loadMessages.append(contentsOf: newDatas) group.leave() } + group.leave() } - group.notify(queue: DispatchQueue.main, execute: { + group.notify(queue: .main) { completion(err, moreEnd, newEnd, historyDatas.count) - }) + weakSelf?.loadMoreWithMessage(loadMessages) + } } } - // 查询本地历史消息 - open func getMessageHistory(_ message: NIMMessage?, - _ completion: @escaping (Error?, NSInteger, [MessageModel]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + (message?.messageId ?? "nil")) - ChatProvider.shared.getMessageHistory( - session: session, - message: message, - limit: messagPageNum - ) { [weak self] error, messages in - if let messageArray = messages, messageArray.count > 0 { - self?.oldMsg = messageArray.first - for msg in messageArray { - if let model = self?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - if let last = self?.messages.last { - ChatMessageHelper.addTimeMessage(model, last) - } - self?.filterRevokeMessage([model]) - self?.messages.append(model) + /// 查询回复 + /// - Parameters: + /// - model: 消息体 + /// - completion: 完成回调 + func loadReply(_ model: MessageModel, _ completion: @escaping () -> Void) { + if model.replyedModel != nil, + model.replyedModel?.message?.messageServerId == nil || + model.replyedModel?.message?.messageServerId?.isEmpty == true { + if let message = model.message { + getReplyMessageWithoutThread(message: message) { replyedModel in + if let reply = replyedModel as? MessageContentModel, + model.replyText != ReplyMessageUtil.textForReplyModel(model: reply) { + model.replyedModel = replyedModel + } else { + model.replyText = chatLocalizable("message_not_found") } + completion() } - completion(error, messageArray.count, self?.messages) - // mark read - self?.markRead(messages: messageArray) { error in - NELog.infoLog( - ModuleName + " " + (self?.className() ?? "ChatViewModel"), - desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") - ) - } - - } else { - completion(error, 0, self?.messages) + return } } + completion() } - // 查询更多本地历史消息 - open func getMoreMessageHistory(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function) + /// 加载消息的更多信息(回复、标记、发送者信息) + /// - Parameter messageArray: 消息列表 + func loadMoreWithMessage(_ messageArray: [V2NIMMessage]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) weak var weakSelf = self - - let messageParam = oldMsg ?? newMsg - - ChatProvider.shared.getMessageHistory( - session: session, - message: messageParam, - limit: messagPageNum - ) { [weak self] error, messages in - if let messageArray = messages, messageArray.count > 0 { - weakSelf?.oldMsg = messageArray.first - - // 如果可信就使用本次请求数据,如果不可信就去远端拉去数据,并更新可信时间戳 - let isCredible = weakSelf? - .isMessageCredible(message: messageArray.first ?? NIMMessage()) - if let isTrust = isCredible, isTrust { - for msg in messageArray.reversed() { - if let model = self?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - self?.addTimeForHistoryMessage(model) - self?.messages.insert(model, at: 0) + let conversationId = weakSelf?.conversationId ?? "" + let group = DispatchGroup() + let sema = DispatchSemaphore(value: 0) + + DispatchQueue.global().async { [self] in + + // 群聊需要获取群昵称 + if V2NIMConversationIdUtil.conversationType(conversationId) != .CONVERSATION_TYPE_P2P { + let userIds = messages.compactMap { $0.message?.senderId } + loadShowName(userIds, weakSelf?.sessionId) { + // 获取头像昵称 + for model in weakSelf?.messages ?? [] { + if let uid = model.message?.senderId, let (fullName, userFriend) = weakSelf?.getShowName(uid) { + model.avatar = userFriend?.user?.avatar + model.fullName = fullName + model.shortName = ChatMessageHelper.getShortName(userFriend?.showName(false) ?? "") } } - completion(error, messageArray.count, self?.messages) - } else { - let option = NIMHistoryMessageSearchOption() - option.startTime = 0 - option.endTime = self?.oldMsg?.timestamp ?? 0 - option.limit = self?.messagPageNum ?? 100 - option.sync = true - weakSelf?.getRemoteHistoryMessage( - direction: .old, - updateCredible: true, - option: option, - completion - ) + sema.signal() } + sema.wait() + } - weakSelf?.markRead(messages: messageArray) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), - desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") - ) + // 查询回复 + for model in messages { + group.enter() + loadReply(model) { + group.leave() } - } else { - if let messageArray = messages, messageArray.isEmpty, - weakSelf?.credibleTimestamp ?? 0 > 0 { - // 如果远端拉倒了信息 就去更新可信时间戳,拉不到就不更新。 - let option = NIMHistoryMessageSearchOption() - option.startTime = 0 - option.endTime = self?.oldMsg?.timestamp ?? 0 - option.limit = self?.messagPageNum ?? 100 - weakSelf?.getRemoteHistoryMessage( - direction: .old, - updateCredible: true, - option: option, - completion - ) - } else { - completion(error, 0, self?.messages) + } + + // 查找标记记录 + group.enter() + chatRepo.searchMessagePinHistory(conversationId: weakSelf?.conversationId ?? "") { [weak self] pinList, error in + + if let pinList = pinList { + let userIds = pinList.map(\.operatorId) + group.enter() + self?.loadShowName(userIds, weakSelf?.sessionId) { + for pin in pinList { + // if pin.updateTime < weakSelf?.messages.first?.message?.createTime ?? 0 { + // break + // } + for model in weakSelf?.messages ?? [] { + if model.message?.messageClientId == pin.messageRefer?.messageClientId { + model.isPined = true + model.pinAccount = pin.operatorId + model.pinShowName = self?.getShowName(pin.operatorId).name + break + } + } + } + group.leave() + } } + group.leave() + } + + // 获取消息已读未读 + group.enter() + getMessageReceipts(messages: messageArray) { reloadIndexs, error in + NEALog.infoLog( + ModuleName + " " + ChatViewModel.className(), + desc: "CALLBACK getP2PMessageReceipt " + (error?.localizedDescription ?? "no error") + ) + group.leave() + } + + group.notify(queue: .main) { + weakSelf?.delegate?.tableViewReload() + } + } + + // 下载语音附件 + downloadAudioFile(messages) + } + + /// 更新消息发送者的信息 + /// - Parameter accid: 发送者 accid + func updateMessageInfo(_ accid: String?) { + guard let accid = accid else { return } + + let (showName, user) = getShowName(accid) + var indexPaths = [IndexPath]() + for (i, model) in messages.enumerated() { + // 更新消息发送者昵称和头像 + if model.message?.senderId == accid { + model.fullName = showName + model.shortName = ChatMessageHelper.getShortName(showName) + model.avatar = user?.user?.avatar + indexPaths.append(IndexPath(row: i, section: 0)) + } + + // 更新标记者昵称 + if model.isPined, model.pinAccount == accid { + model.pinShowName = showName + indexPaths.append(IndexPath(row: i, section: 0)) } } + delegate?.onLoadMoreWithMessage(indexPaths) } - // 查询远端历史消息 - open func getRemoteHistoryMessage(direction: LoadMessageDirection, updateCredible: Bool, - option: NIMHistoryMessageSearchOption, - _ completion: @escaping (Error?, NSInteger, - [MessageModel]?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", direction: \(direction.rawValue)") + /// 查询历史消息 + /// - Parameters: + /// - order: 查询方向 + /// - message: 锚点消息 + /// - completion: 完成回调 + open func getHistoryMessage(order: V2NIMQueryDirection, + message: V2NIMMessage?, + _ completion: @escaping (Error?, NSInteger, [V2NIMMessage]) + -> Void) { + let opt = V2NIMMessageListOption() + opt.limit = messagPageNum + opt.anchorMessage = message + opt.conversationId = conversationId + opt.direction = order + + if let msg = message { + if order == .QUERY_DIRECTION_DESC { + opt.endTime = msg.createTime + } else { + opt.beginTime = msg.createTime + } + } + weak var weakSelf = self - repo.getHistoryMessage(session: session, option: option) { error, messages in - if error == nil { - if let messageArray = messages, messageArray.count > 0 { - if direction == .old { - weakSelf?.oldMsg = messageArray.last - } else { - weakSelf?.newMsg = messageArray.first + chatRepo.getMessageList(option: opt) { error, messages in + if let messageArray = messages, messageArray.count > 0 { + let group = DispatchGroup() + + if order == .QUERY_DIRECTION_DESC { + weakSelf?.oldMsg = messageArray.last + } else { + weakSelf?.newMsg = messageArray.last + } + for msg in messageArray { + // 是否需要进行重复消息过滤 + var needFilter = msg.messageServerId?.isEmpty + if let object = msg.attachment as? V2NIMMessageNotificationAttachment, + object.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE { + needFilter = true } - for msg in messageArray { - if let model = weakSelf?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - weakSelf?.addTimeForHistoryMessage(model) - weakSelf?.messages.insert(model, at: 0) + + if needFilter == true, let messageId = msg.messageClientId { + if weakSelf?.filterInviteSet.contains(messageId) == true { + continue + } else { + weakSelf?.filterInviteSet.insert(messageId) } } - if let updateMessage = messageArray.first, updateCredible { - // 更新可信时间戳 - weakSelf?.credibleTimestamp = updateMessage.timestamp - weakSelf?.repo - .updateIncompleteSessions(messages: [updateMessage]) { error, recentSessions in - if error != nil { - NELog.errorLog( - ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), - desc: "❌updateIncompleteSessions failed,error = \(error!)" - ) - } - } + group.enter() + weakSelf?.modelFromMessage(message: msg) { model in + if weakSelf?.messages.contains(where: { $0.message?.messageClientId == model.message?.messageClientId }) == false { + weakSelf?.messages.append(model) + } + group.leave() } - completion(error, messageArray.count, weakSelf?.messages) - } else { - completion(error, 0, weakSelf?.messages) + } + + group.notify(queue: .main) { + weakSelf?.messages.sort(by: { model1, model2 in + (model1.message?.createTime ?? 0) < (model2.message?.createTime ?? 0) + }) + + // 显示时间 + weakSelf?.addTimeForHistoryMessage() + + // 回调消息列表 + completion(error, messageArray.count, messageArray) + } + + // 标记已读 + weakSelf?.markRead(messages: messageArray) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewModel.className(), + desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") + ) } } else { - completion(error, 0, nil) + completion(error, 0, []) } } } - // 下拉获取历史消息 - open func dropDownRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) + /// 下拉获取历史消息 + /// - Parameter completion: 完成回调 + open func dropDownRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [V2NIMMessage]) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + // 首次会话下拉,没有锚点消息 || 锚点消息被删除,需要手动设置锚点消息 - if oldMsg == nil || !messages.contains(where: { $0.message?.messageId == oldMsg?.messageId }) { + if oldMsg == nil || !messages.contains(where: { $0.message?.messageClientId == oldMsg?.messageClientId }) { for msg in messages { if let mmsg = msg.message { oldMsg = mmsg @@ -672,504 +404,446 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg oldMsg = nil } - getMessagesModelDynamically(.desc, message: oldMsg, completion) - NELog.infoLog(ModuleName + " " + className(), desc: #function) + getHistoryMessage(order: .QUERY_DIRECTION_DESC, message: oldMsg) { [weak self] error, count, messages in + completion(error, count, messages) + if count > 0 { + self?.loadMoreWithMessage(messages) + } + } } - // 上拉获取最新消息 - open func pullRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) + /// 上拉获取最新消息 + /// - Parameter completion: 完成回调 + open func pullRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [V2NIMMessage]) -> Void) { - getMessagesModelDynamically(.asc, message: newMsg, completion) - NELog.infoLog(ModuleName + " " + className(), desc: #function) - } - - // 搜索历史记录查询的本地消息 - open func searchMessageHistory(direction: LoadMessageDirection, startTime: TimeInterval, - endTime: TimeInterval, - _ completion: @escaping (Error?, NSInteger, [MessageModel]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", direction: \(direction.rawValue)") - let option = NIMMessageSearchOption() - option.startTime = startTime - option.endTime = endTime - option.order = .asc - option.limit = messagPageNum - weak var weakSelf = self - - repo.searchMessages(session, option: option) { error, messages in - if error == nil { - if let messageArray = messages, messageArray.count > 0 { - if direction == .old { - weakSelf?.oldMsg = messageArray.first - } else { - weakSelf?.newMsg = messageArray.last - } - for msg in messageArray { - if let model = weakSelf?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - ChatMessageHelper.addTimeMessage(model, weakSelf?.messages.last) - weakSelf?.messages.append(model) + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + getHistoryMessage(order: .QUERY_DIRECTION_ASC, message: newMsg) { [weak self] error, count, messages in + completion(error, count, messages) + self?.loadMoreWithMessage(messages) + } + } + + /// 下载语音消息附件 + /// - Parameter models: 消息列表 + open func downloadAudioFile(_ models: [MessageModel]) { + DispatchQueue.global().async { [weak self] in + for model in models { + if model.type == .audio, let audioAttach = model.message?.attachment as? V2NIMMessageAudioAttachment { + let path = audioAttach.path ?? ChatMessageHelper.createFilePath(model.message) + if !FileManager.default.fileExists(atPath: path) { + if let urlString = audioAttach.url { + self?.downLoad(urlString, path, nil) { _, error in + if error == nil { + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK downLoad") + } + } } } - completion(error, messageArray.count, weakSelf?.messages) - } else { - completion(error, 0, weakSelf?.messages) } - } else { - completion(error, 0, nil) } } } - // 判断消息是否可信 - open func isMessageCredible(message: NIMMessage) -> Bool { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - return credibleTimestamp <= 0 || message.timestamp >= credibleTimestamp - } + /// 发送消息 + /// - Parameters: + /// - message: 需要发送的消息体 + /// - conversationId: 会话id + /// - completion: 回调 + open func sendMessage(message: V2NIMMessage, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text: \(String(describing: message.text))") - open func markRead(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") - if session.sessionType == .P2P { - markReadInP2P(messages: messages, completion) - } else if session.sessionType == .team { - markReadInTeam(messages: messages, completion) - } - // mark session read - weak var weakself = self - repo.markMessageRead(session) { error in - if error != nil { - NELog.errorLog( - ModuleName + " " + (weakself?.className() ?? "ChatViewModel"), - desc: "❌markReadInSession failed,error = \(error!)" - ) - } + chatRepo.sendMessage(message: message, + conversationId: conversationId ?? self.conversationId) { result, error, pro in + completion(error) } } - // 单人会话消息已读标记 - private func markReadInP2P(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") - for message in messages.reversed() { - if message.isReceivedMsg { - let param = NIMMessageReceipt(message: message) - repo.markP2pMessageRead(param: param, completion) - break - } + /// 发送文本消息 + /// - Parameters: + /// - text: 文本内容 + /// - remoteExt: 扩展字段 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendTextMessage(text: String, + conversationId: String? = nil, + remoteExt: [String: Any]?, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") + if text.count <= 0 { + completion(nil) + return } - completion(nil) - } - // 群消息已读标记 - private func markReadInTeam(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") - var receipts = [NIMMessageReceipt]() - for message in messages { - let receiptEnable = message.setting?.teamReceiptEnabled ?? false - if receiptEnable, !message.isTeamReceiptSended { - let receipt = NIMMessageReceipt(message: message) - receipts.append(receipt) - } - } - let receiptsChunk = receipts.chunk(50) - for receipt in receiptsChunk { - repo.markTeamMessageRead(param: receipt) { error, failedReceipts in - print("!! chatViewModel markReadInTeam error:\(String(describing: error))") - completion(error) - } + let message = MessageUtils.textMessage(text: text, remoteExt: remoteExt) + sendMessage(message: message, conversationId: conversationId) { error in + completion(error) } } - // 删除消息 - open func deleteMessage(_ completion: @escaping (Error?) -> Void) { - guard let message = operationModel?.message else { - NELog.errorLog(ModuleName + " " + className(), desc: #function + ", message is nil") + /// 发送语音消息 + /// - Parameters: + /// - filePath: 语音文件路径 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendAudioMessage(filePath: String, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + if ChatDeduplicationHelper.instance.isRecordAudioSended(path: filePath) == true { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ",duplicate send audio at filePath:" + filePath) return } - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - // 已撤回的消息不能删除 - if operationModel?.isRevoked == true { - return + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", filePath:" + filePath) + let message = MessageUtils.audioMessage(filePath: filePath, name: nil, sceneName: nil, duration: 0) + sendMessage(message: message, conversationId: conversationId) { error in + completion(error) } + } - if deletingMsgDic.contains(message.messageId) { - return - } - deletingMsgDic.insert(message.messageId) - if message.serverID.count <= 0 { - repo.deleteMessage(message: message) - deleteMessageUpdateUI(message) - deletingMsgDic.remove(message.messageId) - completion(nil) - return + /// 发送图片消息 + /// - Parameters: + /// - path: 图片文件路径 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendImageMessage(path: String, + name: String? = "image", + width: Int32 = 0, + height: Int32 = 0, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", image path: \(path)") + let message = MessageUtils.imageMessage(path: path, name: name, sceneName: nil, width: width, height: height) + sendMessage(message: message, conversationId: conversationId) { error in + completion(error) } - weak var weakSelf = self + } - if message.serverID == "0" { - repo.deleteMessage(message: message) - deleteMessageUpdateUI(message) - weakSelf?.deletingMsgDic.remove(message.messageId) - completion(nil) - return - } + /// 发送视频消息 + /// - Parameters: + /// - url: 视频文件路径 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendVideoMessage(url: URL, + name: String? = "video", + width: Int32 = 0, + height: Int32 = 0, + duration: Int32 = 0, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ",video url.path:" + url.path) + weak var weakSelf = self - repo.deleteServerMessage(message: message, ext: nil) { error in - if error == nil { - weakSelf?.deleteMessageUpdateUI(message) + convertVideoToMP4(videoURL: url) { url, error in + if let path = url?.path, let conversationId = weakSelf?.conversationId { + let message = MessageUtils.videoMessage(filePath: path, name: name, sceneName: nil, width: width, height: height, duration: duration) + weakSelf?.sendMessage(message: message, conversationId: conversationId) { error in + completion(error) + } } else { - completion(error) + NEALog.errorLog("chat veiw model", desc: "convert mov to mp4 failed") + completion(NSError(domain: "convert mov to mp4 failed", code: 414)) } - weakSelf?.deletingMsgDic.remove(message.messageId) } } - open func deleteMessages(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", message count:\(messages.count)") - var localMsgs = [NIMMessage]() - var remoteMsgs = [NIMMessage]() - for msg in messages { - if deletingMsgDic.contains(msg.messageId) { - continue - } - deletingMsgDic.insert(msg.messageId) - if msg.serverID.count <= 0 { - localMsgs.append(msg) - } else { - remoteMsgs.append(msg) - } - } - - localMsgs.forEach { msg in - repo.deleteMessage(message: msg) - deleteMessageUpdateUI(msg) - deletingMsgDic.remove(msg.messageId) + /// 将视频格式转为 MP4 + /// - Parameters: + /// - videoURL: 视频文件路径 + /// - completion: 完成回调 + func convertVideoToMP4(videoURL: URL, completion: @escaping (URL?, Error?) -> Void) { + let outputFileName = NIMKitFileLocationHelper.genFilename(withExt: "mp4") + guard let outputPath = NIMKitFileLocationHelper.filepath(forVideo: outputFileName) else { + return } - - weak var weakSelf = self - repo.deleteRemoteMessages(messages: remoteMsgs, exts: nil) { error in - if error == nil { - remoteMsgs.forEach { msg in - weakSelf?.deleteMessageUpdateUI(msg) - weakSelf?.deletingMsgDic.remove(msg.messageId) + let asset = AVURLAsset(url: videoURL, options: nil) + let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) + let outputUrl = URL(fileURLWithPath: outputPath) + session?.outputURL = outputUrl + session?.outputFileType = AVFileType.mp4 + session?.shouldOptimizeForNetworkUse = true + session?.exportAsynchronously { + DispatchQueue.main.async { + if session?.status == AVAssetExportSession.Status.completed { + completion(outputUrl, nil) + } else { + completion(nil, nil) } - } else { - completion(error) } } } - // 回复消息 - open func replyMessage(_ message: NIMMessage, _ target: NIMMessage, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - repo.replyMessage(message, target) { error in + /// 发送地理位置消息 + /// - Parameters: + /// - model: 位置信息 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendLocationMessage(model: ChatLocaitonModel, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + let message = MessageUtils.locationMessage(lat: model.lat, + lng: model.lng, + address: model.title + model.address) + message.text = model.title + sendMessage(message: message, conversationId: conversationId) { error in completion(error) } } - open func replyMessageWithoutThread(message: NIMMessage, - target: NIMMessage, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - repo.replyMessageWithoutThread(message: message, session: session, target: target) { error in + /// 发送文件消息 + /// - Parameters: + /// - filePath: 源文件路径 + /// - displayName: 文件展示名称 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendFileMessage(filePath: String, + displayName: String?, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", filePath:\(filePath)") + let message = MessageUtils.fileMessage(filePath: filePath, displayName: displayName, sceneName: nil) + sendMessage(message: message, conversationId: conversationId) { error in completion(error) } } - // 撤回消息 - open func revokeMessage(message: NIMMessage, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - repo.revokeMessage(message: message) { error in - if error == nil { - self.revokeMessageUpdateUI(message) - } + /// 发送自定义消息 + /// - Parameters: + /// - text: 文本内容 + /// - rawAttachment: 附件内容 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendCustomMessage(text: String, + rawAttachment: String, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text:\(text)") + let message = MessageUtils.customMessage(text: text, rawAttachment: rawAttachment) + sendMessage(message: message, conversationId: conversationId) { error in completion(error) } } - // 消息重发 - @discardableResult - open func resendMessage(message: NIMMessage) -> NSError? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - ChatDeduplicationHelper.instance.clearCache() - return repo.resendMessage(message: message) - } + /// 发送拉黑提示消息 + /// - Parameter errConversationId: 会话 id + open func sendBlackListTip(_ errConversationId: String) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "sendBlackListTip") - // 从本地获取用户信息 - open func getUserInfo(userId: String) -> NEKitUser? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", userId:" + userId) - return repo.getUserInfo(userId: userId) - } - - // 获取指定的群成员 - open func getTeamMember(userId: String, teamId: String) -> NIMTeamMember? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", userId:" + userId) - return repo.getTeamMemberList(userId: userId, teamId: teamId) - } - - // 系统通知回调 - // 自定义系统通知回调 - open func onReceive(_ notification: NIMCustomSystemNotification) { - NELog.infoLog( - ModuleName + " " + className(), - desc: #function + ", notification.description:" + notification.description - ) - print("on receive custom noti : ", notification) - if session.sessionType != .P2P { - return - } - if session.sessionId != notification.sender { - return - } - if let content = notification.content, - let dic = getDictionaryFromJSONString(content) as? [String: Any], - let typing = dic["typing"] as? Int { - if typing == 1 { - delegate?.remoteUserEditing() - } else { - delegate?.remoteUserEndEditing() + let tip = MessageUtils.tipMessage(text: chatLocalizable("black_list_tip")) + chatRepo.saveMessageToDB(message: tip, conversationId: errConversationId) { [weak self] _, error in + if let currentSid = self?.conversationId, currentSid == errConversationId { + self?.modelFromMessage(message: tip) { model in + self?.messages.append(model) + self?.delegate?.sending(tip) + } } } } - // MARK: FriendProviderDelegate + /// 发送消息已读回执 + /// - Parameters: + /// - messages: 需要发送已读回执的消息 + /// - completion: 完成回调 + open func markRead(messages: [V2NIMMessage], _ completion: @escaping (Error?) -> Void) {} - open func onFriendChanged(user: NEKitUser) { - ChatUserCache.updateUserInfo(user) - } + /// 获取消息已读未读回执 + /// - Parameters: + /// - messages: 消息列表 + /// - completion: 完成回调 + open func getMessageReceipts(messages: [V2NIMMessage], + _ completion: @escaping ([IndexPath], Error?) -> Void) {} - open func onUserInfoChanged(user: NEKitUser) { - ChatUserCache.updateUserInfo(user) - } + /// 删除消息 + /// - Parameter completion: 完成回调 + open func deleteMessage(_ completion: @escaping (Error?) -> Void) { + guard let message = operationModel?.message, + let messageId = message.messageClientId else { + NEALog.errorLog(ModuleName + " " + className(), desc: #function + ", message is nil") + return + } + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId:\(messageId)") - open func onBlackListChanged() {} + // 已撤回的消息不能删除 + if operationModel?.isRevoked == true { + return + } - // MARK: NIMChatManagerDelegate + if deletingMsgDic.contains(messageId) { + return + } + deletingMsgDic.insert(messageId) - // 收到消息 - open func onRecvMessages(_ messages: [NIMMessage]) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count), first.messageID: \(messages.first?.messageId ?? "")") - var count = 0 - for msg in messages { - if msg.session?.sessionId == session.sessionId { - if msg.serverID.count <= 0, msg.messageType != .custom { - continue - } - if msg.isDeleted == true { - continue - } - if NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) { - continue - } - if let object = msg.messageObject as? NIMNotificationObject { - if let content = object.content as? NIMTeamNotificationContent, content.operationType == .invite { - if filterInviteSet.contains(msg.messageId) { - continue - } else { - filterInviteSet.insert(msg.messageId) - } - } + weak var weakSelf = self + // 本地消息 + if !(message.messageServerId?.isEmpty == false) { + chatRepo.deleteMessage(message: message, onlyDeleteLocal: true) { error in + if error == nil { + weakSelf?.deleteMessageUpdateUI([message]) + weakSelf?.deletingMsgDic.remove(messageId) } - /* 后续解散群离开群弹框优化 - if msg.messageType == .notification, session.sessionType == .team { - if team?.clientCustomInfo?.contains(discussTeamKey) == true { - return - } - let value = NotificationMessageUtils.isTeamLeaveOrDismiss(message: msg) - if value.isLeave == true { - delegate?.didLeaveTeam() - } else if value.isDismiss == true { - delegate?.didDismissTeam() - } - - }*/ - count += 1 - // 自定义消息处理 - newMsg = msg - let model = modelFromMessage(message: msg) - ChatMessageHelper.addTimeMessage(model, self.messages.last) - self.messages.append(model) - } - } - if count > 0 { delegate?.onRecvMessages(messages) } - } - - open func willSend(_ message: NIMMessage) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - print("\(#function)") - - if message.session?.sessionId != session.sessionId { + completion(error) + } return } - // 自定义消息发送之前的处理 - if newMsg == nil { - newMsg = message - } - var isResend = false - for (i, msg) in messages.enumerated() { - if message.messageId == msg.message?.messageId { - messages[i].message = message - isResend = true - break + if message.messageServerId == "0" { + chatRepo.deleteMessage(message: message, onlyDeleteLocal: true) { error in + if error == nil { + weakSelf?.deleteMessageUpdateUI([message]) + weakSelf?.deletingMsgDic.remove(messageId) + } + completion(error) } + return } - if !isResend { - let model = modelFromMessage(message: message) - ChatMessageHelper.addTimeMessage(model, messages.last) - filterRevokeMessage([model]) - messages.append(model) + chatRepo.deleteMessage(message: message, onlyDeleteLocal: false) { error in + if error == nil { + weakSelf?.deleteMessageUpdateUI([message]) + weakSelf?.deletingMsgDic.remove(messageId) + } + completion(error) } - - delegate?.willSend(message) } - // 发送消息进度回调 - open func send(_ message: NIMMessage, progress: Float) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - print("\(#function) progress\(progress)") - delegate?.send(message, progress: progress) - } + /// 批量删除消息 + /// - Parameters: + /// - messages: 需要删除的消息 + /// - completion: 完成回调 + open func deleteMessages(messages: [V2NIMMessage], _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", message count:\(messages.count)") + var localMsgs = [V2NIMMessage]() + var remoteMsgs = [V2NIMMessage]() + for msg in messages { + guard let messageId = msg.messageClientId else { + continue + } - // 发送消息完成回调 - open func send(_ message: NIMMessage, didCompleteWithError error: Error?) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - print("\(#function) message deliveryState:\(message.deliveryState) error:\(error)") - for (i, msg) in messages.enumerated() { - if message.messageId == msg.message?.messageId { - messages[i].message = message - break + if deletingMsgDic.contains(messageId) { + continue } - } - // 判断发送失败原因是否是因为在黑名单中 - if error != nil { - if let err = error as NSError? { - if err.code == inBlackListCode { - weak var weakSelf = self - DispatchQueue.main.async { - weakSelf?.sendBlackListTip(message.session, message) - } - } + + deletingMsgDic.insert(messageId) + if !(msg.messageServerId?.isEmpty == false) { + localMsgs.append(msg) + } else { + remoteMsgs.append(msg) } } - delegate?.send(message, didCompleteWithError: error) - } - -// MARK: ChatExtendProviderDelegate - - // 添加标记消息回调 - open func onNotifyAddMessagePin(pinItem: NIMMessagePinItem) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + pinItem.messageId) - var index = -1 - for (i, model) in messages.enumerated() { - if pinItem.messageServerID == model.message?.serverID { - messages[i].isPined = true - let pinID = pinItem.accountID ?? NIMSDK.shared().loginManager.currentAccount() - messages[i].pinAccount = pinID - messages[i].pinShowName = ChatUserCache.getShowName(userId: pinID, teamId: session.sessionId) - index = i - break + weak var weakSelf = self + chatRepo.deleteMessages(messages: localMsgs, onlyDeleteLocal: true) { error in + if error == nil { + weakSelf?.deleteMessageUpdateUI(localMsgs) + for msg in localMsgs { + if let msgId = msg.messageClientId { + weakSelf?.deletingMsgDic.remove(msgId) + } + } } + completion(error) } - if index >= 0, let msg = messages[index].message { - delegate?.onAddMessagePin(msg, atIndexs: [IndexPath(row: index, section: 0)]) - } - } - // 移除标记消息回调 - open func onNotifyRemoveMessagePin(pinItem: NIMMessagePinItem) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + pinItem.messageId) - var index = -1 - for (i, model) in messages.enumerated() { - if pinItem.messageServerID == model.message?.serverID { - if !messages[i].isPined { - return + chatRepo.deleteMessages(messages: remoteMsgs, onlyDeleteLocal: false) { error in + if error == nil { + weakSelf?.deleteMessageUpdateUI(remoteMsgs) + for msg in remoteMsgs { + if let msgId = msg.messageClientId { + weakSelf?.deletingMsgDic.remove(msgId) + } } - messages[i].isPined = false - messages[i].pinAccount = nil - messages[i].pinShowName = nil - index = i - break } - } - if index >= 0, let msg = messages[index].message { - delegate?.onRemoveMessagePin(msg, atIndexs: [IndexPath(row: index, section: 0)]) + completion(error) } } - open func onNotifySyncStickTopSessions(_ response: NIMSyncStickTopSessionResponse) {} - - open func onNotifyAddStickTopSession(_ newInfo: NIMStickTopSessionInfo) {} - - open func onNotifyRemoveStickTopSession(_ removedInfo: NIMStickTopSessionInfo) {} - -// MARK: collection + /// 回复消息 + /// - Parameters: + /// - message: 新生成的消息 + /// - target: 被回复的消息 + open func replyMessage(_ message: V2NIMMessage, + _ target: V2NIMMessage, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId:\(String(describing: message.messageClientId))") + chatRepo.replyMessage(message: message, target: target, completion) + } - func addColletion(_ message: NIMMessage, - completion: @escaping (NSError?, NIMCollectInfo?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - let param = NIMAddCollectParams() - var string: String? - if message.messageType == .text { - string = message.text - param.type = 1024 - } else { - switch message.messageType { - case .audio: - if let obj = message.messageObject as? NIMAudioObject { - string = obj.url - } - param.type = message.messageType.rawValue - case .image: - if let obj = message.messageObject as? NIMImageObject { - string = obj.url - } - param.type = message.messageType.rawValue - case .video: - if let obj = message.messageObject as? NIMVideoObject { - string = obj.url - } - param.type = message.messageType.rawValue - default: - param.type = 0 - } - param.data = string ?? "" + /// 回复消息(不使用 thread ) + /// - Parameters: + /// - message: 新生成的消息 + /// - target: 被回复的消息 + open func replyMessageWithoutThread(message: V2NIMMessage, + target: V2NIMMessage, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId:\(String(describing: message.messageClientId))") + chatRepo.replyMessageWithoutThread(message: message, conversationId: conversationId, target: target) { result, error, progress in + completion(error) } - param.uniqueId = message.serverID - repo.collectMessage(param, completion) } -// MARK: revoke + /// 撤回消息 + /// - Parameters: + /// - message: 消息 + /// - completion: 完成回调 + open func revokeMessage(message: V2NIMMessage, _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId:\(String(describing: message.messageClientId))") - // 撤回消息回调 - open func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - guard let msg = notification.message else { - return + var muta = [String: Any]() + muta[revokeLocalMessage] = true + if message.messageType == .MESSAGE_TYPE_TEXT { + muta[revokeLocalMessageContent] = message.text + } + if message.messageType == .MESSAGE_TYPE_CUSTOM { + if let title = NECustomAttachment.titleOfRichText(message.attachment), !title.isEmpty { + muta[revokeLocalMessageContent] = title + } + if let body = NECustomAttachment.bodyOfRichText(message.attachment), !body.isEmpty { + muta[revokeLocalMessageContent] = body + } } - NELog.infoLog(ModuleName + className(), desc: #function + "messageId:\(msg.messageId), serverID:\(msg.serverID)") - - revokeMessageUpdateUI(msg) + let revokeParams = V2NIMMessageRevokeParams() + revokeParams.serverExtension = getJSONStringFromDictionary(muta) + chatRepo.revokeMessage(message: message, revokeParams: revokeParams) { error in + if error == nil { + self.revokeMessageUpdateUI(message) + } + completion(error) + } } - open func onRecvMessageReceipts(_ receipts: [NIMMessageReceipt]) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", receipts.count: \(receipts.count)") - print( - "chatViewModel: :\(receipts.count) messageId:\(receipts.first?.messageId) messageId:\(receipts.first?.timestamp)" - ) - delegate?.didReadedMessageIndexs() + /// 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - Returns: 名称和好友信息 + open func getShowName(_ accountId: String, + _ showAlias: Bool = true) -> (name: String, user: NEUserWithFriend?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:" + accountId) + return NEFriendUserCache.shared.getShowName(accountId, showAlias) } + /// 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - completion: 完成回调 + open func loadShowName(_ accountIds: [String], + _ teamId: String? = nil, + _ completion: @escaping () -> Void) {} + + /// 获取消息所支持的操作列表 + /// - Parameter model: 消息模型 + /// - Returns: 操作列表 open func avalibleOperationsForMessage(_ model: MessageContentModel?) -> [OperationItem]? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", pinAccount: " + (model?.pinAccount ?? "nil")) + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", pinAccount: " + (model?.pinAccount ?? "nil")) var items = [OperationItem]() /// 消息发送中的消息只能删除(文本可复制) - if model?.message?.deliveryState == .delivering { + if model?.message?.sendingState == .MESSAGE_SENDING_STATE_SENDING { switch model?.message?.messageType { - case .text: + case .MESSAGE_TYPE_TEXT: items.append(contentsOf: [ OperationItem.copyItem(), OperationItem.deleteItem(), @@ -1183,11 +857,10 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg } /// 发送失败 || 黑名单中的消息 || 话单消息 只能多选和删除(文本可复制) - if model?.message?.deliveryState == .failed || - model?.message?.messageType == .rtcCallRecord || - model?.message?.isBlackListed == true { + if model?.message?.sendingState == .MESSAGE_SENDING_STATE_FAILED || + model?.message?.messageType == .MESSAGE_TYPE_CALL { switch model?.message?.messageType { - case .text: + case .MESSAGE_TYPE_TEXT: items.append(contentsOf: [ OperationItem.copyItem(), OperationItem.deleteItem(), @@ -1205,7 +878,7 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg /// 消息发送成功 let pinItem = model?.isPined == false ? OperationItem.pinItem() : OperationItem.removePinItem() switch model?.message?.messageType { - case .location: + case .MESSAGE_TYPE_LOCATION: items.append(contentsOf: [ OperationItem.replayItem(), OperationItem.forwardItem(), @@ -1213,7 +886,7 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg OperationItem.deleteItem(), OperationItem.selectItem(), ]) - case .text: + case .MESSAGE_TYPE_TEXT: items = [ OperationItem.copyItem(), OperationItem.replayItem(), @@ -1222,7 +895,7 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg OperationItem.deleteItem(), OperationItem.selectItem(), ] - case .image, .video, .file: + case .MESSAGE_TYPE_IMAGE, .MESSAGE_TYPE_VIDEO, .MESSAGE_TYPE_FILE: items = [ OperationItem.replayItem(), OperationItem.forwardItem(), @@ -1230,16 +903,16 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg OperationItem.deleteItem(), OperationItem.selectItem(), ] - case .audio: + case .MESSAGE_TYPE_AUDIO: items = [ OperationItem.replayItem(), pinItem, OperationItem.deleteItem(), OperationItem.selectItem(), ] - case .custom: - if let attach = NECustomAttachment.attachmentOfCustomMessage(message: model?.message) { - if attach.customType == customRichTextType { + case .MESSAGE_TYPE_CUSTOM: + if let customType = NECustomAttachment.typeOfCustomMessage(model?.message?.attachment) { + if customType == customRichTextType { items = [ OperationItem.copyItem(), ] @@ -1255,6 +928,7 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg // 未知消息体 items = [ OperationItem.deleteItem(), + OperationItem.selectItem(), ] } default: @@ -1267,298 +941,357 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg } // 自己发送且非未知消息可以撤回 - if model?.message?.from == NIMSDK.shared().loginManager.currentAccount() { - if model?.message?.messageType == .custom, - NECustomAttachment.dataOfCustomMessage(message: model?.message) == nil { + if model?.message?.isSelf == true { + if model?.message?.messageType == .MESSAGE_TYPE_CUSTOM, + NECustomAttachment.dataOfCustomMessage(model?.message?.attachment) == nil { return items } - items.append(OperationItem.recallItem()) + items.append(OperationItem.recallItem()) + } + return items + } + + /// 消息列表添加时间 + private func addTimeForHistoryMessage() { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + for (i, model) in messages.enumerated() { + if i == 0, let createTime = model.message?.createTime { + // 第一条消息默认显示时间 + let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: createTime)) + model.timeContent = timeText + continue + } + + if let message = model.message, NotificationMessageUtils.isDiscussSeniorTeamNoti(message: message) { + continue + } + + // 当前消息时间 - 上一条消息时间 > 5s, 则显示当前消息的创建时间 + let lastModel = messages[i - 1] + let lastTime = lastModel.message?.createTime ?? 0.0 + let curTime = model.message?.createTime ?? 0 + let dur = curTime - lastTime + if (dur / 60) > 5 { + let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: curTime)) + model.timeContent = timeText + } } - return items } - private func indexPathsForTeamMarkRead(_ receipts: [NIMMessageReceipt]) -> [IndexPath] { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", receipts.count: \(receipts.count)") - var indexs = [IndexPath]() -// find messages that need to update UI - for receipt in receipts { - for (i, model) in messages.enumerated() { - if model.message?.messageId == receipt.messageId { - indexs.append(IndexPath(row: i, section: 0)) + /// 构建聊天页面UI显示model + /// - Parameters: + /// - message: 消息 + /// - completion: 完成回调 + open func modelFromMessage(message: V2NIMMessage, _ completion: @escaping (MessageModel) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + ChatMessageHelper.modelFromMessage(message: message) { [weak self] model in + if ChatMessageHelper.isRevokeMessage(message: model.message) { + if let content = ChatMessageHelper.getRevokeMessageContent(message: model.message) { + model.isReedit = true + model.message?.text = content } + model.isRevoked = true } - } - print("mark read indexs:\(indexs)") - return indexs - } - private func indexPathsForP2PMarkRead(_ receipts: [NIMMessageReceipt]) -> [IndexPath] { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", receipts.count: \(receipts.count)") - var updateIndexs = [IndexPath]() -// find messages that need to update UI - var i = messages.count - 1 - for model in messages.reversed() { - if let msg = model.message, msg.isRemoteRead { - updateIndexs.append(IndexPath(row: i, section: 0)) - break - } else { - updateIndexs.append(IndexPath(row: i, section: 0)) - i -= 1 + if let uid = message.senderId, + let (fullName, user) = self?.getShowName(uid) { + model.avatar = user?.user?.avatar + model.fullName = fullName + model.shortName = ChatMessageHelper.getShortName(user?.showName(false) ?? ChatMessageHelper.getShortName(fullName)) } + + if let replyModel = self?.getReplyMessageWithoutThread(message: message) { + model.replyedModel = replyModel + } + self?.delegate?.getMessageModel?(model: model) + completion(model) } - return updateIndexs } - private func addTimeForHistoryMessage(_ model: MessageModel) { - guard let first = messages.first, - let firstMsg = first.message else { - NELog.errorLog(ModuleName + " " + className(), desc: #function + ", model.message is nil") - return - } + /// 构建聊天页面UI显示model + /// - Parameter message: 消息 + /// - Returns: 消息体 + open func modelFromMessage(message: V2NIMMessage) -> MessageModel { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + let model = ChatMessageHelper.modelFromMessage(message: message) - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + firstMsg.messageId) - if NotificationMessageUtils.isDiscussSeniorTeamNoti(message: firstMsg) { - return + if ChatMessageHelper.isRevokeMessage(message: model.message) { + if let content = ChatMessageHelper.getRevokeMessageContent(message: model.message) { + model.isReedit = true + model.message?.text = content + } + model.isRevoked = true } - let firstTs = firstMsg.timestamp - let curTs = model.message?.timestamp ?? 0.0 - let dur = firstTs - curTs - if (dur / 60) > 5 { - let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: firstTs)) - first.timeContent = timeText + if let uid = message.senderId { + let (fullName, user) = getShowName(uid) + model.avatar = user?.user?.avatar + model.fullName = fullName + model.shortName = ChatMessageHelper.getShortName(user?.showName(false) ?? ChatMessageHelper.getShortName(fullName)) } - } - // 构建聊天页面UI显示model - open func modelFromMessage(message: NIMMessage) -> MessageModel { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - let model = ChatMessageHelper.modelFromMessage(message: message) - if let uid = message.from { - let user = ChatUserCache.getUserInfo(uid) - let fullName = ChatUserCache.getShowName(userId: uid, teamId: session.sessionId) - model.avatar = user?.userInfo?.avatarUrl - model.fullName = fullName - model.shortName = ChatUserCache.getShortName(name: user?.showName(false) ?? "", length: 2) - } - model.replyedModel = getReplyMessageWithoutThread(message: message) - if let pin = repo.searchMessagePinHistory(message) { - model.isPined = true - model.pinAccount = pin.accountID - let pinID = pin.accountID ?? NIMSDK.shared().loginManager.currentAccount() - model.pinShowName = ChatUserCache.getShowName(userId: pinID, teamId: session.sessionId) - } else { - model.isPined = false + if let replyModel = getReplyMessageWithoutThread(message: message) { + model.replyedModel = replyModel } delegate?.getMessageModel?(model: model) return model } - // 查找回复消息 - open func getReplyMessage(message: NIMMessage) -> MessageModel? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - guard let id = message.repliedMessageId, id.count > 0 else { + /// 查找回复消息,优先不使用 thread 方案 (不进行远端拉取) + /// - Parameters: + /// - message: 需要查找回复的消息 + /// - completion: 完成回调 + open func getReplyMessageWithoutThread(message: V2NIMMessage) -> MessageModel? { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + var replyId: String? = message.threadReply?.messageClientId + let replyDic = ChatMessageHelper.getReplyDictionary(message: message) + replyId = replyDic?["idClient"] as? String + guard let replyId = replyId, !replyId.isEmpty else { return nil } - if let m = ConversationProvider.shared.messagesInSession(session, messageIds: [id])? - .first { - let model = modelFromMessage(message: m) - model.isReplay = true - return model + + for model in messages { + if model.message?.messageClientId == replyId { + model.isReplay = true + return model + } } - let message = NIMMessage() - let model = modelFromMessage(message: message) + + let model = MessageTextModel(message: nil) model.isReplay = true + if let replySenderId = replyDic?["from"] as? String { + model.fullName = replySenderId + } return model } - open func getReplyMessageWithoutThread(message: NIMMessage) -> MessageModel? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - - var replyId: String? = message.repliedMessageId - if let yxReplyMsg = message.remoteExt?[keyReplyMsgKey] as? [String: Any] { - replyId = yxReplyMsg["idClient"] as? String + /// 查找回复消息,优先不使用 thread 方案,已加载的消息中没有则去远端查 + /// - Parameters: + /// - message: 需要查找回复的消息 + /// - fetch: 是否远端查询 + /// - completion: 完成回调 + open func getReplyMessageWithoutThread(message: V2NIMMessage, + _ completion: @escaping (MessageModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + var replyId: String? = message.threadReply?.messageClientId + let replyDic = ChatMessageHelper.getReplyDictionary(message: message) + replyId = replyDic?["idClient"] as? String + guard let replyId = replyId, !replyId.isEmpty else { + completion(nil) + return } - guard let id = replyId, !id.isEmpty else { - return nil + // 先去已加载的消息中查 + for model in messages { + if model.message?.messageClientId == replyId { + model.isReplay = true + completion(model) + return + } } - if let m = ConversationProvider.shared.messagesInSession(session, messageIds: [id])? - .first { - let model = modelFromMessage(message: m) - model.isReplay = true - return model + // 已加载的消息中没有则去远端查 + chatRepo.getMessageListByIds([replyId]) { [weak self] messages, error in + if let m = messages?.first { + self?.modelFromMessage(message: m) { model in + model.isReplay = true + completion(model) + } + } else { + completion(nil) + } } - let message = NIMMessage() - let model = modelFromMessage(message: message) - model.isReplay = true - return model } - func deleteMessageUpdateUI(_ message: NIMMessage) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) + @discardableResult + func deleteMessageModel(_ message: V2NIMMessage) -> (deleteIndexs: [Int], reloadIndexs: [Int]) { + var deleteIndexs = [Int]() + var reloadIndexs = [Int]() var index = -1 var replyIndex = [Int]() var hasFind = false + for (i, model) in messages.enumerated() { if hasFind { - var replyId: String? = model.message?.repliedMessageId - if let yxReplyMsg = model.message?.remoteExt?[keyReplyMsgKey] as? [String: Any] { + var replyId: String? = model.message?.threadReply?.messageClientId + if let remoteExt = getDictionaryFromJSONString(model.message?.serverExtension ?? ""), + let yxReplyMsg = remoteExt[keyReplyMsgKey] as? [String: Any] { replyId = yxReplyMsg["idClient"] as? String } - if let id = replyId, !id.isEmpty, id == message.messageId { + if let id = replyId, !id.isEmpty, id == message.messageClientId { messages[i].replyText = chatLocalizable("message_not_found") replyIndex.append(i) } } else { - if model.message?.messageId == message.messageId { + if model.message?.messageClientId == message.messageClientId { index = i hasFind = true } } } - var indexs = [IndexPath]() - var reloadIndexs = [IndexPath]() if index >= 0 { -// remove time tip - let last = index - 1 - if last >= 0, let timeModel = messages[last] as? MessageTipsModel, - timeModel.type == .time { - messages.removeSubrange(last ... index) - indexs.append(IndexPath(row: last, section: 0)) - indexs.append(IndexPath(row: index, section: 0)) - for replyIdx in replyIndex { - reloadIndexs.append(IndexPath(row: replyIdx - 2, section: 0)) - } - } else { - messages.remove(at: index) - indexs.append(IndexPath(row: index, section: 0)) - for replyIdx in replyIndex { - reloadIndexs.append(IndexPath(row: replyIdx - 1, section: 0)) - } + deleteIndexs.append(index) + for replyIdx in replyIndex { + reloadIndexs.append(replyIdx) + } + } + + return (deleteIndexs, reloadIndexs) + } + + /// 删除消息更新UI + /// - Parameter message: 消息 + func deleteMessageUpdateUI(_ messages: [V2NIMMessage]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages count: \(messages.count)") + var deleteIndexs = Set() + var reloadIndexs = Set() + for message in messages { + let indexs = deleteMessageModel(message) + + for index in indexs.deleteIndexs { + deleteIndexs.insert(index) + } + + for index in indexs.reloadIndexs { + reloadIndexs.insert(index) } } - delegate?.onDeleteMessage(message, atIndexs: indexs, reloadIndex: reloadIndexs) + let deleteIndexPaths = deleteIndexs.map { IndexPath(row: $0, section: 0) } + let reloadIndexPaths = reloadIndexs.map { IndexPath(row: $0, section: 0) } + + delegate?.onDeleteMessage(messages, deleteIndexs: deleteIndexPaths, reloadIndex: reloadIndexPaths) } - func revokeMessageUpdateUI(_ message: NIMMessage) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) + /// 撤回消息更新UI + /// - Parameter message: 消息 + func revokeMessageUpdateUI(_ message: V2NIMMessage) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") var index = -1 - var replyIndex = [Int]() + var indexs = [IndexPath]() var hasFind = false + // 遍历查找回复该条消息的消息 for (i, model) in messages.enumerated() { if hasFind { - var replyId: String? = model.message?.repliedMessageId - if let yxReplyMsg = model.message?.remoteExt?[keyReplyMsgKey] as? [String: Any] { + var replyId: String? = model.message?.threadReply?.messageClientId + if let remoteExt = getDictionaryFromJSONString(model.message?.serverExtension ?? ""), + let yxReplyMsg = remoteExt[keyReplyMsgKey] as? [String: Any] { replyId = yxReplyMsg["idClient"] as? String } - if let id = replyId, !id.isEmpty, id == message.messageId { - replyIndex.append(i) + if let id = replyId, !id.isEmpty, id == message.messageClientId { + messages[i].replyText = chatLocalizable("message_not_found") + indexs.append(IndexPath(row: i, section: 0)) } } else { - if model.message?.serverID == message.serverID { + if model.message?.messageServerId == message.messageServerId { index = i hasFind = true } } } - var indexs = [IndexPath]() if index >= 0 { messages[index].isRevoked = true messages[index].replyedModel = nil messages[index].isPined = false - indexs.append(IndexPath(row: index, section: 0)) - } - for replyIdx in replyIndex { - messages[replyIdx].replyText = chatLocalizable("message_not_found") - indexs.append(IndexPath(row: replyIdx, section: 0)) + // 是否可以重新编辑 + if let content = ChatMessageHelper.getRevokeMessageContent(message: messages[index].message) { + messages[index].isReedit = true + messages[index].message?.text = content + } + + indexs.append(IndexPath(row: index, section: 0)) } delegate?.onRevokeMessage(message, atIndexs: indexs) } - open func fetchMessageAttachment(_ message: NIMMessage, didCompleteWithError error: Error?) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - } - - open func fetchMessageAttachment(_ message: NIMMessage, progress: Float) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - } - - open func fetchMessageAttachment(_ message: NIMMessage, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - repo.downloadMessageAttachment(message, completion) - } - - open func downLoad(_ urlString: String, _ filePath: String, _ progress: NIMHttpProgressBlock?, - _ completion: NIMDownloadCompleteBlock?) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) - repo.downloadSource(urlString, filePath, progress, completion) - } - - // 转发消息 - open func forwardMessage(_ forwardMessages: [NIMMessage], - _ session: NIMSession, + /// 下载附件 + /// - Parameters: + /// - urlString: 远端 url + /// - filePath: 本地路径 + /// - progress: 下载进度回调 + /// - completion: 完成回调 + open func downLoad(_ urlString: String, + _ filePath: String, + _ progress: ((UInt) -> Void)?, + _ completion: ((String?, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: " + urlString) + ResourceRepo.shared.downLoad(urlString, filePath, progress, completion) + } + + /// 逐条转发消息 + /// - Parameters: + /// - forwardMessages: 需要逐条转发的消息列表 + /// - conversationId: 转发的会话 id + /// - comment: 留言 + /// - completion: 完成回调 + open func forwardMessage(_ forwardMessages: [V2NIMMessage], + _ conversationId: String, _ comment: String?, _ completion: @escaping (Error?) -> Void) { for message in forwardMessages { - if let forwardMessage = repo.makeForwardMessage(message) { - ChatMessageHelper.clearForwardAtMark(forwardMessage) - repo.sendForwardMessage(forwardMessage, session) + let forwardMessage = MessageUtils.forwardMessage(message: message) + ChatMessageHelper.clearForwardAtMark(forwardMessage) + chatRepo.sendMessage(message: forwardMessage, conversationId: conversationId) { result, error, pro in } } if let text = comment, !text.isEmpty { - sendTextMessage(text: text, session: session, completion) + sendTextMessage(text: text, conversationId: conversationId, remoteExt: nil, completion) } else { completion(nil) } } - // 合并转发消息 - open func forwardMultiMessage(_ forwardMessages: [NIMMessage], - _ toSession: NIMSession, - _ depth: Int = 0, - _ comment: String?, + /// 合并转发消息 + /// - Parameters: + /// - forwardMessages: 需要合并的消息列表 + /// - toconversationId: 转发的会话 id + /// - users: 需要转发的好友列表 + /// - depth: 合并转发消息的深度 + /// - comment: 留言 + /// - completion: 完成回调 + open func forwardMultiMessage(forwardMessages: [V2NIMMessage], + toconversationId: String, + users: [V2NIMUser]? = nil, + depth: Int = 0, + comment: String?, _ completion: @escaping (Error?) -> Void) { if forwardMessages.count <= 0 { if let text = comment, !text.isEmpty { - sendTextMessage(text: text, session: toSession, completion) + sendTextMessage(text: text, conversationId: toconversationId, remoteExt: nil, completion) } else { completion(nil) } return } - let fromSession = session + let fromSession = conversationId let header = ChatMessageHelper.buildHeader(messageCount: forwardMessages.count) ChatMessageHelper.buildBody(messages: forwardMessages) { body, abstracts in let multiForwardMsg = header + body let fileName = multiForwardFileName + "\(Int(Date().timeIntervalSince1970))" - if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(fileName)") - if let multiForwardMsgData = multiForwardMsg.data(using: .utf8) { - do { - try multiForwardMsgData.write(to: filePath) - } catch { - completion(NSError(domain: chatLocalizable("forward_failed"), code: 414)) - print("Error writing string to file: \(error)") - return - } + if var filePath = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit/file/") { + filePath += fileName + + do { + try multiForwardMsg.write(toFile: filePath, atomically: true, encoding: .utf8) + } catch { + completion(NSError(domain: chatLocalizable("forward_failed"), code: 414)) + print("Error writing string to file: \(error)") + return } - NIMSDK.shared().resourceManager.upload(filePath.path, progress: nil) { [weak self] url, error in + let fileTask = ResourceRepo.shared.createUploadFileTask(filePath) + ResourceRepo.shared.upload(fileTask, nil) { [weak self] url, error in if let err = error { completion(err) - } else if let url = url { + } else if let url = url, let filePath = URL(string: filePath) { let md5 = ChatMessageHelper.getFileChecksum(fileURL: filePath) // 删除本地文件 @@ -1569,34 +1302,41 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg } var data = [String: Any]() - data["sessionId"] = toSession.sessionId - data["sessionName"] = ChatMessageHelper.getSessionName(session: fromSession, showAlias: false) + data["sessionId"] = V2NIMConversationIdUtil.conversationTargetId(toconversationId) data["url"] = url data["md5"] = md5 data["depth"] = depth data["abstracts"] = abstracts + data["sessionName"] = ChatMessageHelper.getSessionName(conversationId: fromSession, showAlias: false) var jsonData = [String: Any]() jsonData["data"] = data jsonData["messageType"] = "custom" jsonData["type"] = customMultiForwardType - let attah = NECustomAttachment(customType: customMultiForwardType, - cellHeight: customMultiForwardCellHeight, - data: jsonData) - self?.sendCustomMessage(attachment: attah, - remoteExt: nil, - apnsConstent: "[\(chatLocalizable("chat_history"))]", - session: toSession) { error in - if let err = error { - completion(err) - } else { - if let text = comment, !text.isEmpty { - self?.sendTextMessage(text: text, session: toSession, completion) - } else { - completion(nil) + // 转发给好友 + if let users = users { + for user in users { + if let uid = user.accountId, let cid = V2NIMConversationIdUtil.p2pConversationId(uid) { + self?.sendCustomMessage(text: "[\(chatLocalizable("chat_history"))]", + rawAttachment: getJSONStringFromDictionary(jsonData), conversationId: cid) { error in + if let text = comment, !text.isEmpty { + self?.sendTextMessage(text: text, conversationId: cid, remoteExt: nil, completion) + } + completion(error) + } } } + return + } + + // 转发到群聊 + self?.sendCustomMessage(text: "[\(chatLocalizable("chat_history"))]", + rawAttachment: getJSONStringFromDictionary(jsonData), conversationId: toconversationId) { error in + if let text = comment, !text.isEmpty { + self?.sendTextMessage(text: text, conversationId: toconversationId, remoteExt: nil, completion) + } + completion(error) } } } @@ -1604,253 +1344,462 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg } } - open func forwardUserMessage(_ users: [NIMUser], + /// 转发消息给好友 + /// - Parameters: + /// - users: 好友列表 + /// - isMultiForward: 是否是合并转发 + /// - depth: 合并转发深度 + /// - comment: 留言 + /// - completion: 完成回调 + open func forwardUserMessage(_ users: [V2NIMUser], _ isMultiForward: Bool, _ depth: Int, _ comment: String?, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") // 排序(发送时间正序) let forwardMessages = selectedMessages.sorted { msg1, msg2 in - msg1.timestamp < msg2.timestamp + msg1.createTime < msg2.createTime } - users.forEach { user in - if let uid = user.userId { - let session = NIMSession(uid, type: .P2P) - if isMultiForward { - forwardMultiMessage(forwardMessages, session, depth, comment, completion) - } else { - forwardMessage(forwardMessages, session, comment, completion) + if isMultiForward { + forwardMultiMessage(forwardMessages: forwardMessages, + toconversationId: conversationId, + users: users, + depth: depth, + comment: comment, + completion) + } else { + for user in users { + if let uid = user.accountId, let conversationId = V2NIMConversationIdUtil.p2pConversationId(uid) { + forwardMessage(forwardMessages, conversationId, comment, completion) } } } } - open func forwardTeamMessage(_ team: NIMTeam, + /// 转发消息到群聊 + /// - Parameters: + /// - team: 群聊 + /// - isMultiForward: 是否是合并转发 + /// - depth: 合并转发深度 + /// - comment: 留言 + /// - completion: 完成回调 + open func forwardTeamMessage(_ team: V2NIMTeam, _ isMultiForward: Bool, _ depth: Int, _ comment: String?, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") - if let tid = team.teamId { - let session = NIMSession(tid, type: .team) - if isMultiForward { - forwardMultiMessage(selectedMessages, session, depth, comment, completion) - } else { - forwardMessage(selectedMessages, session, comment, completion) - } + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") + guard let conversationId = V2NIMConversationIdUtil.teamConversationId(team.teamId) else { + return } - } - // 标记消息 - open func pinMessage(_ message: NIMMessage, - _ completion: @escaping (Error?, NIMMessagePinItem?, Int) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - let item = NIMMessagePinItem(message: message) - guard let _ = NIMSDK.shared().conversationManager.messages(in: session, messageIds: [message.messageId]) else { - return + // 排序(发送时间正序) + let forwardMessages = selectedMessages.sorted { msg1, msg2 in + msg1.createTime < msg2.createTime + } + + if isMultiForward { + forwardMultiMessage(forwardMessages: forwardMessages, + toconversationId: conversationId, + depth: depth, + comment: comment, + completion) + } else { + forwardMessage(selectedMessages, conversationId, comment, completion) } + } - repo.addMessagePin(item) { [weak self] error, pinItem in + /// 标记消息 + /// - Parameters: + /// - message: 消息 + /// - completion: 完成回调 + open func addPinMessage(message: V2NIMMessage, + _ completion: @escaping (Error?, Int) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + chatRepo.addMessagePin(message: message, serverExtension: "") { [weak self] error in + var index = -1 if error != nil { - completion(error, nil, -1) + completion(error, index) } else { - var index = -1 - if let messages = self?.messages { - for (i, model) in messages.enumerated() { - if message.messageId == model.message?.messageId, !messages[i].isPined { - messages[i].isPined = true - messages[i].pinAccount = NIMSDK.shared().loginManager.currentAccount() - messages[i].pinShowName = ChatUserCache.getShowName( - userId: NIMSDK.shared().loginManager.currentAccount(), - teamId: message.session?.sessionId - ) - self?.messages = messages - index = i - break - } + for (i, model) in (self?.messages ?? []).enumerated() { + if message.messageClientId == model.message?.messageClientId, !(self?.messages[i].isPined == true) { + self?.messages[i].isPined = true + self?.messages[i].pinAccount = IMKitClient.instance.account() + self?.messages[i].pinShowName = self?.getShowName(IMKitClient.instance.account()).name + index = i + break } } - completion(nil, pinItem, index) + completion(nil, index) } } } - // 取消消息标记 - open func removePinMessage(_ message: NIMMessage, - _ completion: @escaping (Error?, NIMMessagePinItem?, Int) - -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - guard let _ = NIMSDK.shared().conversationManager.messages(in: session, messageIds: [message.messageId]) else { - return - } - let item = NIMMessagePinItem(message: message) - weak var weakSelf = self - repo.removeMessagePin(item) { error, pinItem in + /// 取消消息标记 + /// - Parameters: + /// - message: 消息 + /// - completion: 完成回调 + open func removePinMessage(message: V2NIMMessage, + _ completion: @escaping (Error?, Int) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + chatRepo.removeMessagePin(messageRefer: message, serverExtension: "") { [weak self] error in if error != nil { - completion(error, nil, -1) + completion(error, -1) } else { - let index = weakSelf?.removeLocalPinMessage(message) ?? -1 - completion(nil, pinItem, index) + let index = self?.removeLocalPinMessage(message) ?? -1 + completion(nil, index) } } } - // 发送正在输入中状态 - open func sendInputTypingState() { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - if session.sessionType == .P2P { - setTypingCustom(1) - } + /// 获取听筒模式 + /// - Returns: 听筒模式 + open func getHandSetEnable() -> Bool { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + return SettingRepo.shared.getHandsetMode() } - // 发送结束输入中状态 - open func sendInputTypingEndState() { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - if session.sessionType == .P2P { - setTypingCustom(0) + @discardableResult + /// 移除缓存数据的标记状态 + /// - Parameter message: 消息 + /// - Returns: 消息下标 + private func removeLocalPinMessage(_ message: V2NIMMessage) -> Int { + var index = -1 + + for (i, model) in messages.enumerated() { + if message.messageClientId == model.message?.messageClientId, messages[i].isPined { + messages[i].isPined = false + messages[i].pinAccount = nil + index = i + break + } } + return index } - func setTypingCustom(_ typing: Int) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", typing: \(typing)") - let message = NIMMessage() - if message.setting == nil { - message.setting = NIMMessageSetting() + /// 消息即将发送 + /// - Parameter message: 消息 + open func sendingMsg(_ message: V2NIMMessage) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId:\(String(describing: message.messageClientId))") + print("\(#function)") + + // 消息不是当前会话的消息,不处理(转发) + if message.conversationId != conversationId { + return + } + + // 拉黑消息避免重复发送 + if message.messageType == .MESSAGE_TYPE_TIP, + ChatDeduplicationHelper.instance.isMessageSended(messageId: message.messageClientId ?? "") { + return + } + + // 自定义消息发送之前的处理 + if newMsg == nil { + newMsg = message } - message.setting?.apnsEnabled = false - message.setting?.shouldBeCounted = false - let noti = - NIMCustomSystemNotification(content: getJSONStringFromDictionary(["typing": typing])) - repo.sendCustomNotification(noti, session) { error in - if let err = error { - print("send noti success :", err) + var isResend = false + for (i, msg) in messages.enumerated() { + if message.messageClientId == msg.message?.messageClientId { + messages[i].message = message + isResend = true + break } } - } - open func getHandSetEnable() -> Bool { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - return repo.getHandsetMode() - } + if !isResend { + let model = modelFromMessage(message: message) + ChatMessageHelper.addTimeMessage(model, messages.last) + messages.append(model) + } - open func getMessageRead() -> Bool { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - return repo.getMessageRead() + delegate?.sending(message) } - // 本地保存撤回消息 - open func saveRevokeMessage(_ message: NIMMessage, _ completion: @escaping (Error?) -> Void) { - let messageNew = NIMMessage() - messageNew.text = chatLocalizable("message_recalled") - var muta = [String: Any]() - muta[revokeLocalMessage] = true - if message.messageType == .text { - muta[revokeLocalMessageContent] = message.text + /// 消息发送完成 + /// - Parameters: + /// - message: 消息 + /// - error: 错误信息 + @nonobjc open func sendMsgSuccess(_ message: V2NIMMessage) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + + if message.conversationId != conversationId { + return } - if message.messageType == .custom { - if let title = NECustomAttachment.titleOfRichText(message: message), !title.isEmpty { - muta[revokeLocalMessageContent] = title - } - if let body = NECustomAttachment.bodyOfRichText(message: message), !body.isEmpty { - muta[revokeLocalMessageContent] = body + + for (i, msg) in messages.enumerated() { + if message.messageClientId == msg.message?.messageClientId { + messages[i].message = message + break } } - messageNew.timestamp = message.timestamp - messageNew.from = message.from - messageNew.localExt = muta - messageNew.remoteExt = message.remoteExt - let setting = NIMMessageSetting() - setting.shouldBeCounted = false - setting.isSessionUpdate = false - messageNew.setting = setting - repo.saveMessageToDB(messageNew, session, completion) + + delegate?.sendSuccess(message) } - open func filterRevokeMessage(_ messages: [MessageModel]) { - messages.forEach { model in - if let isRevoke = model.message?.localExt?[revokeLocalMessage] as? Bool, isRevoke == true { - if let content = model.message?.localExt?[revokeLocalMessageContent] as? String, content.count > 0 { - model.isRevokedText = true - model.message?.text = content + /// 消息发送失败 + /// - Parameters: + /// - message: 消息 + /// - error: 错误信息 + open func sendMsgFailed(_ message: V2NIMMessage, _ error: Error?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", error: \(String(describing: error))") + + // 判断发送失败原因是否是因为在黑名单中 + if error != nil { + if let err = error as NSError? { + if err.code == inBlackListCode, let conversationId = message.conversationId { + weak var weakSelf = self + DispatchQueue.main.async { + weakSelf?.sendBlackListTip(conversationId) + if conversationId == weakSelf?.conversationId { + weakSelf?.delegate?.sendSuccess(message) + } + } } - model.isRevoked = true } } + + delegate?.sendSuccess(message) } - // 刷新已读回执 - open func refreshReceipts(messages: [NIMMessage]) { - if session.sessionType != .team { - return - } - if repo.settingProvider.getMessageRead() == false { - return - } - print("refresh team id : ", session.sessionId) - var receiptsMessages = [NIMMessage]() - messages.forEach { message in - if message.setting?.teamReceiptEnabled == true { - receiptsMessages.append(message) + // MARK: - NEChatListener + + /// 收到消息 + /// - Parameter messages: 消息列表 + public func onReceiveMessages(_ messages: [V2NIMMessage]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count), first.messageID: \(messages.first?.messageClientId ?? "")") + + for msg in messages { + guard let messageId = msg.messageClientId, + V2NIMConversationIdUtil.conversationTargetId(msg.conversationId ?? "") == sessionId else { + return + } + + if !(msg.messageServerId?.isEmpty == false), msg.messageType != .MESSAGE_TYPE_CUSTOM { + continue + } + + if filterInviteSet.contains(messageId) { + continue + } else { + filterInviteSet.insert(messageId) + } + + newMsg = msg + + modelFromMessage(message: msg) { [weak self] model in + ChatMessageHelper.addTimeMessage(model, self?.messages.last) + self?.downloadAudioFile([model]) + self?.loadReply(model) { + self?.messages.append(model) + self?.messages.sort { m1, m2 in + (m1.message?.createTime ?? 0) < (m2.message?.createTime ?? 0) + } + self?.delegate?.onRecvMessages(messages) + } } } - for receipt in receiptsMessages.chunk(50) { - repo.refreshReceipts(receipt) + } + + /// 消息撤回回调 + /// - Parameter revokeNotifications: 撤回通知 + public func onMessageRevokeNotifications(_ revokeNotifications: [V2NIMMessageRevokeNotification]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", revokeNotifications.count: \(revokeNotifications.count)") + for revokeNoti in revokeNotifications { + if revokeNoti.messageRefer?.conversationId != conversationId { + continue + } + if revokeNoti.messageRefer?.messageClientId?.isEmpty == true { + continue + } + + for model in messages { + if let msg = model.message, msg.messageClientId == revokeNoti.messageRefer?.messageClientId { + model.message!.localExtension = revokeNoti.serverExtension + revokeMessageUpdateUI(msg) + break + } + } } } - @discardableResult - private func removeLocalPinMessage(_ message: NIMMessage) -> Int { - var index = -1 + /// 消息删除成功回调。当本地端或多端同步删除消息成功时会触发该回调。 + /// - Parameter messageDeletedNotification: 删除通知 + public func onMessageDeletedNotifications(_ messageDeletedNotification: [V2NIMMessageDeletedNotification]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageDeletedNotification.count: \(messageDeletedNotification.count)") - for (i, model) in messages.enumerated() { - if message.messageId == model.message?.messageId, messages[i].isPined { - messages[i].isPined = false - messages[i].pinAccount = nil - index = i - break + var deleteMessages = [V2NIMMessage]() + for message in messageDeletedNotification { + if message.messageRefer.conversationId != conversationId { + continue + } + if message.messageRefer.messageClientId?.isEmpty == true { + continue + } + + for model in messages { + if let msg = model.message, msg.messageClientId == message.messageRefer.messageClientId { + deleteMessages.append(msg) + } } } - return index + + deleteMessageUpdateUI(deleteMessages) + } + + /// 消息清空成功回调。当本地端或多端同步清空消息成功时会触发该回调。 + /// - Parameter clearHistoryNotification: 清空通知 + public func onClearHistoryNotifications(_ clearHistoryNotification: [V2NIMClearHistoryNotification]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", clearHistoryNotification.count: \(clearHistoryNotification.count)") } -// MARK: NIMConversationManagerDelegate + /// 消息pin状态回调通知 + /// - Parameter pinNotification: 消息pin状态变化通知数据 + public func onMessagePinNotification(_ pinNotification: V2NIMMessagePinNotification) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:\(String(describing: pinNotification.pin?.messageRefer?.messageClientId)), pinStatus:\(pinNotification.pinState.rawValue)") + if pinNotification.pinState == .MESSAGE_PIN_STEATE_PINNED { + // 置顶 + var index = -1 + for (i, model) in messages.enumerated() { + if pinNotification.pin?.messageRefer?.messageServerId == model.message?.messageServerId { + messages[i].isPined = true + let pinID = pinNotification.pin?.operatorId ?? IMKitClient.instance.account() + messages[i].pinAccount = pinID - // 多端登录删除消息 - open func onRecvMessagesDeleted(_ messages: [NIMMessage], exts: [String: String]?) { - messages.forEach { message in - if message.session?.sessionId != session.sessionId { - return + if let _ = getShowName(pinID).user { + messages[i].pinShowName = getShowName(pinID).name + } else { + loadShowName([pinID], sessionId) { [weak self] in + self?.messages[i].pinShowName = self?.getShowName(pinID).name + if let msg = self?.messages[i].message { + self?.delegate?.onMessagePinStatusChange(msg, atIndexs: [IndexPath(row: i, section: 0)]) + } + } + } + index = i + break + } } - if message.messageId.count <= 0 { - return + if index >= 0, let msg = messages[index].message { + delegate?.onMessagePinStatusChange(msg, atIndexs: [IndexPath(row: index, section: 0)]) + } + } else if pinNotification.pinState == .MESSAGE_PIN_STEATE_NOT_PINNED { + // 取消置顶 + var index = -1 + for (i, model) in messages.enumerated() { + if pinNotification.pin?.messageRefer?.messageServerId == model.message?.messageServerId { + if !messages[i].isPined { + return + } + messages[i].isPined = false + messages[i].pinAccount = nil + messages[i].pinShowName = nil + index = i + break + } + } + if index >= 0, let msg = messages[index].message { + delegate?.onMessagePinStatusChange(msg, atIndexs: [IndexPath(row: index, section: 0)]) } - deleteMessageUpdateUI(message) } } - func fetchPinMessage(_ completion: @escaping () -> Void) { - repo.fetchPinMessage(session.sessionId, session.sessionType) { error, items in - completion() + /// 消息评论状态回调 + /// - Parameter notification: 快捷评论通知数据 + public func onMessageQuickCommentNotification(_ notification: V2NIMMessageQuickCommentNotification) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", quickComment.index: \(notification.quickComment.index)") + } + + /// 收到点对点已读回执 + /// - Parameter readReceipts: 已读回执 + public func onReceiveP2PMessageReadReceipts(_ readReceipts: [V2NIMP2PMessageReadReceipt]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", readReceipts.count: \(readReceipts.count)") + var reloadIndexPaths: [IndexPath] = [] + for readReceipt in readReceipts { + if readReceipt.conversationId != conversationId { + continue + } + + for (i, model) in messages.enumerated() { + if model.message?.isSelf == false { + continue + } + + if model.message?.messageConfig?.readReceiptEnabled == false { + continue + } + + if let msgCreateTime = model.message?.createTime, msgCreateTime <= readReceipt.timestamp { + if model.readCount == 1, model.unreadCount == 0 { + continue + } + + model.readCount = 1 + model.unreadCount = 0 + reloadIndexPaths.append(IndexPath(row: i, section: 0)) + } + } } + delegate?.onLoadMoreWithMessage(reloadIndexPaths) } - // 检查音频消息是否有附件 - open func checkAudioFile(messages: [MessageModel]?) { - messages?.forEach { model in - if let message = model.message { - ChatMessageHelper.downloadAudioFile(message: message) + /// 收到群已读回执 + /// - Parameter readReceipts: 已读回执 + public func onReceiveTeamMessageReadReceipts(_ readReceipts: [V2NIMTeamMessageReadReceipt]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", readReceipts.count: \(readReceipts.count)") + var reloadIndexPaths: [IndexPath] = [] + for readReceipt in readReceipts { + if readReceipt.conversationId != conversationId { + continue + } + + for (i, model) in messages.enumerated() { + if model.message?.isSelf == false { + continue + } + + if model.message?.messageConfig?.readReceiptEnabled == false { + continue + } + + if model.message?.messageClientId == readReceipt.messageClientId { + model.readCount = readReceipt.readCount + model.unreadCount = readReceipt.unreadCount + reloadIndexPaths.append(IndexPath(row: i, section: 0)) + } } } + delegate?.onLoadMoreWithMessage(reloadIndexPaths) + } + + /// 消息发送进度 + /// - Parameters: + /// - message: 消息 + /// - progress: 进度 + public func sendMessageProgress(_ message: V2NIMMessage, _ progress: UInt) { + if progress == 0 { + // 消息即将发送 + sendingMsg(message) + } } - open func onTeamMemberChanged(_ team: NIMTeam) { - if session.sessionType == .team, session.sessionId == team.teamId { - self.team = team - delegate?.onTeamMemberChange(team: team) + /// 消息发送成功 + /// - Parameter result: 成功结果 + public func sendMessageSuccess(_ result: V2NIMSendMessageResult?) { + if let msg = result?.message { + sendMsgSuccess(msg) } } + + /// 消息发送失败 + /// - Parameters: + /// - message: 消息 + /// - error: 错误信息 + public func sendMessageFailed(_ message: V2NIMMessage, _ error: NSError) { + sendMsgFailed(message, error) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift index 38b018c0..ac6100e9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift @@ -24,7 +24,7 @@ open class MultiForwardViewModel: NSObject { if FileManager.default.fileExists(atPath: filePath) { decodeMesssage(filePath: filePath, md5: md5, completion) } else if let urlString = messageAttachmentUrl { - downLoad(urlString, filePath, nil) { [weak self] error in + downLoad(urlString, filePath, nil) { [weak self] _, error in self?.decodeMesssage(filePath: filePath, md5: md5, completion) } } @@ -48,8 +48,7 @@ open class MultiForwardViewModel: NSObject { let subStringData = strData?.components(separatedBy: "\n") if let msgCount = subStringData?.count, msgCount > 1 { for i in 1 ..< msgCount { - if let msgData = subStringData?[i].data(using: .utf8) { - let msg = NIMSDK.shared().conversationManager.decodeMessage(from: msgData) + if let msgString = subStringData?[i], let msg = ChatRepo.shared.messageDeserialization(msgString) { let model = modelFromMessage(message: msg) ChatMessageHelper.addTimeMessage(model, messages.last) messages.append(model) @@ -62,33 +61,40 @@ open class MultiForwardViewModel: NSObject { } } - open func modelFromMessage(message: NIMMessage) -> MessageModel { + open func modelFromMessage(message: V2NIMMessage) -> MessageModel { var model: MessageModel switch message.messageType { - case .audio: + case .MESSAGE_TYPE_AUDIO: message.text = chatLocalizable("msg_audio") model = MessageTextModel(message: message) - case .rtcCallRecord: + case .MESSAGE_TYPE_CALL: message.text = chatLocalizable("msg_rtc_call") - if let object = message.messageObject as? NIMRtcCallRecordObject { - message.text = object.callType == .audio ? chatLocalizable("msg_rtc_audio") : chatLocalizable("msg_rtc_video") + if let attachment = message.attachment as? V2NIMMessageCallAttachment { + message.text = attachment.type == 1 ? chatLocalizable("msg_rtc_audio") : chatLocalizable("msg_rtc_video") } model = MessageTextModel(message: message) default: model = ChatMessageHelper.modelFromMessage(message: message) } - model.fullName = message.remoteExt?[mergedMessageNickKey] as? String - model.shortName = ChatUserCache.getShortName(name: model.fullName ?? "", length: 2) - model.avatar = message.remoteExt?[mergedMessageAvatarKey] as? String + if let remoteExt = getDictionaryFromJSONString(message.serverExtension ?? "") { + model.fullName = remoteExt[mergedMessageNickKey] as? String + model.shortName = ChatMessageHelper.getShortName(model.fullName ?? "") + model.avatar = remoteExt[mergedMessageAvatarKey] as? String + } else { + model.fullName = message.senderId + model.shortName = ChatMessageHelper.getShortName(model.fullName ?? "") + } delegate?.getMessageModel?(model: model) return model } - open func downLoad(_ urlString: String, _ filePath: String, _ progress: NIMHttpProgressBlock?, - _ completion: NIMDownloadCompleteBlock?) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) - repo.downloadSource(urlString, filePath, progress, completion) + open func downLoad(_ urlString: String, + _ filePath: String, + _ progress: ((UInt) -> Void)?, + _ completion: ((String?, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) + ResourceRepo.shared.downLoad(urlString, filePath, progress, completion) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/P2PChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/P2PChatViewModel.swift new file mode 100644 index 00000000..afabc73c --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/P2PChatViewModel.swift @@ -0,0 +1,190 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import CoreText +import Foundation +import NEChatKit +import NECoreIM2Kit +import NIMSDK + +@objcMembers +open class P2PChatViewModel: ChatViewModel { + /// 重写初始化方法 + override init(conversationId: String) { + super.init(conversationId: conversationId) + chatRepo.addNotiListener(self) + } + + /// 重写初始化方法 + override init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId, anchor: anchor) + chatRepo.addNotiListener(self) + } + + /// 重写 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - Returns: 名称和好友信息 + override open func getShowName(_ accountId: String, + _ showAlias: Bool = true) -> (name: String, user: NEUserWithFriend?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:" + accountId) + if NEFriendUserCache.shared.isFriend(accountId) { + return NEFriendUserCache.shared.getShowName(accountId, showAlias) + } else { + return ChatUserCache.shared.getShowName(accountId, showAlias) + } + } + + /// 重写 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - completion: 完成回调 + override open func loadShowName(_ accountIds: [String], + _ teamId: String? = nil, + _ completion: @escaping () -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", count: \(accountIds.count)") + NEFriendUserCache.shared.loadShowName(accountIds) { users in + for user in users ?? [] { + // 非好友,单独缓存 + if let uid = user.user?.accountId, !NEFriendUserCache.shared.isFriend(uid) { + ChatUserCache.shared.updateUserInfo(user) + } + } + completion() + } + } + + /// 重写 发送消息已读回执 + /// - Parameters: + /// - messages: 需要发送已读回执的消息 + /// - completion: 完成回调 + override open func markRead(messages: [V2NIMMessage], _ completion: @escaping ((any Error)?) -> Void) { + markReadInP2P(messages: messages, completion) + } + + /// 单人会话消息发送已读回执 + /// - Parameters: + /// - messages: 需要发送已读回执的消息 + /// - completion: 完成回调 + private func markReadInP2P(messages: [V2NIMMessage], _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") + for message in messages { + if !message.isSelf { + chatRepo.markP2pMessageRead(message: message, completion) + return + } + } + completion(nil) + } + + /// 重写获取消息已读未读回执 + /// - Parameters: + /// - messages: 消息列表 + /// - completion: 完成回调 + override open func getMessageReceipts(messages: [V2NIMMessage], + _ completion: @escaping ([IndexPath], Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") + getP2PMessageReceipt(messages: messages, completion) + } + + /// 获取 P2P 消息已读未读回执 + /// - Parameters: + /// - messages: 消息列表 + /// - completion: 完成回调 + func getP2PMessageReceipt(messages: [V2NIMMessage], _ completion: @escaping ([IndexPath], Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + chatRepo.getP2PMessageReceipt(conversationId: conversationId) { readReceipt, error in + if let readReceipt = readReceipt { + var reloadIndexs = [IndexPath]() + for (i, model) in self.messages.enumerated() { + if model.message?.isSelf == false { + continue + } + + if model.message?.messageConfig?.readReceiptEnabled == false { + continue + } + + if let msgCreateTime = model.message?.createTime, msgCreateTime <= readReceipt.timestamp { + if model.readCount == 1, model.unreadCount == 0 { + continue + } + + model.readCount = 1 + model.unreadCount = 0 + reloadIndexs.append(IndexPath(row: i, section: 0)) + } + } + completion(reloadIndexs, error) + } else { + completion([], error) + } + } + } + + /// 发送正在输入中状态 + open func sendInputTypingState() { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + if V2NIMConversationIdUtil.conversationType(conversationId) == .CONVERSATION_TYPE_P2P { + setTypingCustom(1) + } + } + + /// 发送结束输入中状态 + open func sendInputTypingEndState() { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + if V2NIMConversationIdUtil.conversationType(conversationId) == .CONVERSATION_TYPE_P2P { + setTypingCustom(0) + } + } + + /// 发送输入状态 + /// - Parameter typing: 输入状态: 1-正在输入, 0-结束输入 + func setTypingCustom(_ typing: Int) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", typing: \(typing)") + let content = getJSONStringFromDictionary(["typing": typing]) + let param = V2NIMSendCustomNotificationParams() + chatRepo.sendCustomNotification(converstaionId: conversationId, content: content, params: param) { error in + if let err = error { + print("send noti success :", err) + } + } + } + + // MARK: - NENotiListener + + /// 收到自定义系统通知回调 + /// 用于展示对方输入状态 + /// - Parameter customNotifications: 自定义系统通知 + public func onReceiveCustomNotifications(_ customNotifications: [V2NIMCustomNotification]) { + NEALog.infoLog( + ModuleName + " " + className(), + desc: #function + ", customNotifications.count:\(customNotifications.count)" + ) + + // 只处理单聊的输入状态 + if V2NIMConversationIdUtil.conversationType(conversationId) != .CONVERSATION_TYPE_P2P { + return + } + + for notification in customNotifications { + // 只处理当前会话的输入状态 + if sessionId != notification.senderId { + continue + } + + if let content = notification.content, + let dic = getDictionaryFromJSONString(content) as? [String: Any], + let typing = dic["typing"] as? Int { + if typing == 1 { + delegate?.remoteUserEditing() + } else { + delegate?.remoteUserEndEditing() + } + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift index 4890d90d..e6679eb5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift @@ -8,177 +8,194 @@ import UIKit @objc public protocol PinMessageViewModelDelegate: NSObjectProtocol { - func didNeedRefreshUI() + func tableViewReload(needLoad: Bool) + func tableViewReload(_ indexPaths: [IndexPath]) + func tableViewDelete(_ indexPaths: [IndexPath]) + func refreshModel(_ model: NEPinMessageModel) } @objcMembers -open class PinMessageViewModel: NSObject, ChatExtendProviderDelegate, NIMChatManagerDelegate, NIMConversationManagerDelegate { +open class PinMessageViewModel: NSObject, NEChatListener { public let chatRepo = ChatRepo.shared - public var items = [PinMessageModel]() + public var items = [NEPinMessageModel]() public var delegate: PinMessageViewModelDelegate? - public var session: NIMSession? + public var conversationId: String? override public init() { super.init() - chatRepo.addChatDelegate(delegate: self) - chatRepo.addChatExtendDelegate(delegate: self) - NIMSDK.shared().conversationManager.add(self) + chatRepo.addChatListener(self) } - open func onRecvMessagesDeleted(_ messages: [NIMMessage], exts: [String: String]?) { - for message in messages { - if message.session?.sessionId == session?.sessionId { - delegate?.didNeedRefreshUI() - break - } - } - } - - open func getPinitems(session: NIMSession, _ completion: @escaping (Error?) -> Void) { + open func getPinitems(conversationId: String, _ completion: @escaping (Error?) -> Void) { + let group = DispatchGroup() weak var weakSelf = self - self.session = session - chatRepo.fetchPinMessage(session.sessionId, session.sessionType) { error, pinItems in + self.conversationId = conversationId + chatRepo.searchMessagePinHistory(conversationId: conversationId) { pinItems, error in if let pins = pinItems { - if error == nil { - weakSelf?.items.removeAll() - } - var remoteMessages = [NIMMessagePinItem]() - var pinDic = [String: NIMMessagePinItem]() - pins.forEach { item in - if let message = ConversationProvider.shared.messagesInSession(item.session, messageIds: [item.messageId])?.first { - let pinModel = PinMessageModel(message: message, item: item) - weakSelf?.items.append(pinModel) - weakSelf?.items.sort { model1, model2 in - model1.message.timestamp > model2.message.timestamp + let messageRefers = pins.compactMap(\.messageRefer) + group.enter() + weakSelf?.chatRepo.getMessageListByRefers(messageRefers) { messages, error in + if let messages = messages { + weakSelf?.items.removeAll() + for message in messages { + for item in pins { + if message.messageClientId == item.messageRefer?.messageClientId { + let pinModel = NEPinMessageModel(message: message, item: item) + weakSelf?.items.append(pinModel) + } + } } } else { - remoteMessages.append(item) - pinDic[item.messageServerID] = item + completion(error) } + group.leave() } - if remoteMessages.count <= 0 { - completion(error) - } else { - var infos = [NIMChatExtendBasicInfo]() - remoteMessages.forEach { item in - let info = NIMChatExtendBasicInfo() - info.type = session.sessionType - info.fromAccount = item.messageFromAccount - info.toAccount = item.messageToAccount - info.messageID = item.messageId - info.serverID = item.messageServerID - info.timestamp = item.messageTime - infos.append(info) + + group.notify(queue: .main) { + weakSelf?.items.sort { model1, model2 in + model1.message.createTime > model2.message.createTime } - weakSelf?.chatRepo.fetchHistoryMessages(infos, false) { err, mapTable in - let enums = mapTable?.objectEnumerator() - while let message = enums?.nextObject() as? NIMMessage { - print("fetchHistoryMessages ", message.messageId) - if let item = pinDic[message.serverID] { - let pinModel = PinMessageModel(message: message, item: item) - weakSelf?.items.append(pinModel) - weakSelf?.items.sort { model1, model2 in - model1.message.timestamp > model2.message.timestamp - } - } - } - completion(err) + completion(error) + weakSelf?.loadMoreWithModel(items: weakSelf?.items) { + weakSelf?.delegate?.tableViewReload(needLoad: false) } } - } else { completion(error) } } } - open func removePinMessage(_ message: NIMMessage, - _ completion: @escaping (Error?, NIMMessagePinItem?) + open func loadMoreWithModel(items: [NEPinMessageModel]?, _ completion: @escaping () -> Void) { + guard let items = items else { + completion() + return + } + + let userIds = items.compactMap(\.message.senderId) + if let conversationId = conversationId, let teamId = V2NIMConversationIdUtil.conversationTargetId(conversationId) { + ChatTeamCache.shared.loadShowName(userIds: userIds, teamId: teamId) { + for item in items { + let (name, user) = ChatTeamCache.shared.getShowName(item.chatmodel.message?.senderId ?? "") + item.chatmodel.avatar = user?.user?.avatar + item.chatmodel.fullName = name + item.chatmodel.shortName = ChatMessageHelper.getShortName(user?.showName(false) ?? "") + } + completion() + } + } + } + + open func removePinMessage(_ message: V2NIMMessage, + _ completion: @escaping (Error?) -> Void) { - NELog.infoLog("PinMessageViewModel", desc: #function + ", messageId: " + message.messageId) - let item = NIMMessagePinItem(message: message) - chatRepo.removeMessagePin(item) { error, pinItem in - completion(error, pinItem) + NEALog.infoLog("PinMessageViewModel", desc: #function + ", messageId: \(String(describing: message.messageClientId))") + chatRepo.removeMessagePin(messageRefer: message, serverExtension: "") { error in + completion(error) } } - open func sendTextMessage(text: String, session: NIMSession, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") + open func sendTextMessage(text: String, conversationId: String, _ completion: @escaping (V2NIMSendMessageResult?, Error?, UInt) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") if text.count <= 0 { return } chatRepo.sendMessage( message: MessageUtils.textMessage(text: text), - session: session, + conversationId: conversationId, completion ) } - open func forwardUserMessage(_ message: NIMMessage, - _ users: [NIMUser], + open func forwardUserMessage(_ message: V2NIMMessage, + _ users: [V2NIMUser], _ comment: String?, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - users.forEach { user in - if let uid = user.userId { - let session = NIMSession(uid, type: .P2P) - if let forwardMessage = chatRepo.makeForwardMessage(message) { - ChatMessageHelper.clearForwardAtMark(forwardMessage) - chatRepo.sendForwardMessage(forwardMessage, session) - } + _ completion: @escaping (V2NIMSendMessageResult?, Error?, UInt) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: \(String(describing: message.messageClientId))") + for user in users { + if let uid = user.accountId, let conversationId = V2NIMConversationIdUtil.p2pConversationId(uid) { + let forwardMessage = MessageUtils.forwardMessage(message: message) + ChatMessageHelper.clearForwardAtMark(forwardMessage) + chatRepo.sendMessage(message: forwardMessage, conversationId: conversationId, completion) if let text = comment { - sendTextMessage(text: text, session: session) { error in - print("sendTextMessage error: \(String(describing: error))") - } + sendTextMessage(text: text, conversationId: conversationId, completion) } } } } - open func forwardTeamMessage(_ message: NIMMessage, - _ team: NIMTeam, + open func forwardTeamMessage(_ message: V2NIMMessage, + _ team: V2NIMTeam, _ comment: String?, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - if let tid = team.teamId { - let session = NIMSession(tid, type: .team) - if let forwardMessage = chatRepo.makeForwardMessage(message) { - ChatMessageHelper.clearForwardAtMark(forwardMessage) - chatRepo.sendForwardMessage(forwardMessage, session) - } - if let text = comment { - sendTextMessage(text: text, session: session, completion) - } + _ completion: @escaping (V2NIMSendMessageResult?, Error?, UInt) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: \(String(describing: message.messageClientId))") + let conversationId = V2NIMConversationIdUtil.teamConversationId(team.teamId) ?? "" + let forwardMessage = MessageUtils.forwardMessage(message: message) + ChatMessageHelper.clearForwardAtMark(forwardMessage) + chatRepo.sendMessage(message: forwardMessage, conversationId: conversationId, completion) + if let text = comment { + sendTextMessage(text: text, conversationId: conversationId, completion) } } - // MARK: NIMChatManagerDelegate + open func downLoad(_ urlString: String, + _ filePath: String, + _ progress: ((UInt) -> Void)?, + _ completion: ((String?, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) + ResourceRepo.shared.downLoad(urlString, filePath, progress, completion) + } - open func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { -// items = [PinMessageModel]() - delegate?.didNeedRefreshUI() + open func getHandSetEnable() -> Bool { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + return SettingRepo.shared.getHandsetMode() } - // MARK: ChatExtendProviderDelegate + func onDeleteIndexPath(_ messageRefers: [V2NIMMessageRefer?]) { + var indexs = [IndexPath]() + for messageRefer in messageRefers { + for (i, model) in items.enumerated() { + if model.message.messageClientId == messageRefer?.messageClientId { + indexs.append(IndexPath(row: i, section: 0)) + } + } + } - open func onNotifyAddMessagePin(pinItem: NIMMessagePinItem) { -// items = [PinMessageModel]() - delegate?.didNeedRefreshUI() + for messageRefer in messageRefers { + items.removeAll { $0.message.messageClientId == messageRefer?.messageClientId } + } + + delegate?.tableViewDelete(indexs) } - open func onNotifyRemoveMessagePin(pinItem: NIMMessagePinItem) { -// items = [PinMessageModel]() - delegate?.didNeedRefreshUI() + // MARK: - NEChatListener + + /// 收到消息撤回回调 + /// - Parameter revokeNotifications: 消息撤回通知数据 + public func onMessageRevokeNotifications(_ revokeNotifications: [V2NIMMessageRevokeNotification]) { + delegate?.tableViewReload(needLoad: true) } - open func downLoad(_ urlString: String, _ filePath: String, _ progress: NIMHttpProgressBlock?, - _ completion: NIMDownloadCompleteBlock?) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) - chatRepo.downloadSource(urlString, filePath, progress, completion) + /// 消息pin状态回调通知 + /// - Parameter pinNotification: 消息pin状态变化通知数据 + public func onMessagePinNotification(_ pinNotification: V2NIMMessagePinNotification) { + switch pinNotification.pinState { + case .MESSAGE_PIN_STEATE_NOT_PINNED: + let messageRefer = pinNotification.pin?.messageRefer + onDeleteIndexPath([messageRefer]) + case .MESSAGE_PIN_STEATE_PINNED: + delegate?.tableViewReload(needLoad: true) + case .MESSAGE_PIN_STEATE_UPDATED: + delegate?.tableViewReload(needLoad: true) + default: + break + } } - open func getHandSetEnable() -> Bool { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - return chatRepo.getHandsetMode() + /// 消息被删除通知 + /// - Parameter messageDeletedNotification: 被删除的消息列表 + public func onMessageDeletedNotifications(_ messageDeletedNotification: [V2NIMMessageDeletedNotification]) { + let messageRefers = messageDeletedNotification.map(\.messageRefer) + onDeleteIndexPath(messageRefers) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift index 22885f04..ee431315 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift @@ -5,36 +5,66 @@ import CoreText import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK @objc public protocol TeamChatViewModelDelegate: ChatViewModelDelegate { - func onTeamRemoved(team: NIMTeam) - func onTeamUpdate(team: NIMTeam) - func onTeamMemberUpdate(team: NIMTeam) + @objc optional func onTeamRemoved(team: V2NIMTeam) + @objc optional func onTeamUpdate(team: V2NIMTeam) + @objc optional func onTeamMemberUpdate(_ teamMembers: [V2NIMTeamMember]) } @objcMembers -open class TeamChatViewModel: ChatViewModel { - private let className = "TeamChatViewModel" +open class TeamChatViewModel: ChatViewModel, NETeamListener, NEContactListener { + public let teamRepo = TeamRepo.shared + public var team: V2NIMTeam? + /// 当前成员的群成员对象类 + public var teamMember: V2NIMTeamMember? - override init(session: NIMSession, anchor: NIMMessage?) { - super.init(session: session, anchor: anchor) - NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId: " + session.sessionId) - repo.addTeamDelegate(delegate: self) - getTeamMember() + override init(conversationId: String) { + super.init(conversationId: conversationId) + teamRepo.addTeamListener(self) + contactRepo.addContactListener(self) } - open func getTeam(teamId: String) -> NIMTeam? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + teamId) - return repo.getTeamInfo(teamId: teamId) + override init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId, anchor: anchor) + teamRepo.addTeamListener(self) + contactRepo.addContactListener(self) + getTeamMember {} } - open func fetchTeamInfo(teamId: String, - _ completion: @escaping (NSError?, NIMTeam?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + teamId) - repo.getTeamInfo(teamId: teamId) { [weak self] error, team in + /// 重写 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - Returns: 名称和好友信息 + override open func getShowName(_ accountId: String, _ showAlias: Bool = true) -> (name: String, user: NEUserWithFriend?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:" + accountId) + return ChatTeamCache.shared.getShowName(accountId, showAlias) + } + + /// 重写 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - completion: 完成回调 + override open func loadShowName(_ accountIds: [String], + _ teamId: String?, + _ completion: @escaping () -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId:\(String(describing: teamId))") + guard let teamId = teamId else { + return + } + + ChatTeamCache.shared.loadShowName(userIds: accountIds, teamId: teamId, completion) + } + + open func getTeamInfo(teamId: String, + _ completion: @escaping (Error?, V2NIMTeam?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: " + teamId) + teamRepo.getTeamInfo(teamId) { [weak self] team, error in if error == nil { self?.team = team } @@ -42,42 +72,169 @@ open class TeamChatViewModel: ChatViewModel { } } -// MARK: NIMTeamManagerDelegate + open func getTeamMemberInfo(teamId: String, + _ completion: @escaping (Error?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: " + teamId) + teamRepo.getTeamWithMembers(teamId, + .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL) { [weak self] error, teamInfoModel in + if error == nil { + self?.team = teamInfoModel?.team + } + completion(error, teamInfoModel) + } + } + + /// 获取自己的群成员信息 + public func getTeamMember(_ completion: @escaping () -> Void) { + teamRepo.getTeamMember(sessionId, IMKitClient.instance.account()) { [weak self] member, error in + self?.teamMember = member + completion() + } + } + + /// 重写发送消息已读回执 + /// - Parameters: + /// - messages: 需要发送已读回执的消息 + /// - completion: 完成回调 + override open func markRead(messages: [V2NIMMessage], _ completion: @escaping ((any Error)?) -> Void) { + markReadInTeam(messages: messages, completion) + } + + /// 群消息发送已读回执 + /// - Parameters: + /// - messages: 需要发送已读回执的消息 + /// - completion: 完成回调 + private func markReadInTeam(messages: [V2NIMMessage], _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") - open func onTeamRemoved(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + (team.teamId ?? "nil")) - if session.sessionId == team.teamId { + var markMessages = [V2NIMMessage]() + for message in messages { + if message.messageServerId != nil, !message.isSelf, message.messageConfig?.readReceiptEnabled == true { + markMessages.append(message) + } + } + chatRepo.markTeamMessageRead(messages: markMessages, completion) + } + + /// 重写获取消息已读未读回执 + /// - Parameters: + /// - messages: 消息列表 + /// - completion: 完成回调 + override open func getMessageReceipts(messages: [V2NIMMessage], + _ completion: @escaping ([IndexPath], Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") + getTeamMessageReceipts(messages: messages, completion) + } + + /// 获取群消息已读未读回执 + /// - Parameters: + /// - messages: 消息列表 + /// - completion: 完成回调 + func getTeamMessageReceipts(messages: [V2NIMMessage], _ completion: @escaping ([IndexPath], Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + let sendMessages = messages.filter { msg in + msg.messageServerId != nil && msg.isSelf && msg.messageType != .MESSAGE_TYPE_NOTIFICATION && msg.messageType != .MESSAGE_TYPE_TIP + } + + if sendMessages.isEmpty { + completion([], nil) + return + } + + chatRepo.getTeamMessageReceipts(messages: sendMessages) { readReceipts, error in + var reloadIndexs = [IndexPath]() + readReceipts?.forEach { readReceipt in + for (i, model) in self.messages.enumerated() { + if model.message?.isSelf == false { + continue + } + + if model.message?.messageConfig?.readReceiptEnabled == false { + continue + } + + if model.message?.messageClientId == readReceipt.messageClientId { + if model.readCount == readReceipt.readCount, + model.unreadCount == readReceipt.unreadCount { + continue + } + + model.readCount = readReceipt.readCount + model.unreadCount = readReceipt.unreadCount + reloadIndexs.append(IndexPath(row: i, section: 0)) + } + } + } + completion(reloadIndexs, error) + } + } + + // MARK: - NEContactListener + + /// 用户信息变更回调 + /// - Parameter users: 用户列表 + public func onUserProfileChanged(_ users: [V2NIMUser]) { + for item in users { + let userFriend = NEUserWithFriend(user: item) + ChatTeamCache.shared.updateTeamMemberInfo(userFriend) + updateMessageInfo(item.accountId) + } + } + + /// 好友信息变更回调 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + let userFriend = NEUserWithFriend(friend: friendInfo) + ChatTeamCache.shared.updateTeamMemberInfo(userFriend) + updateMessageInfo(friendInfo.accountId) + } + + // MARK: - NETeamListener + + public func onTeamDismissed(_ team: V2NIMTeam) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: " + (team.teamId)) + if sessionId == team.teamId { if let delegate = delegate as? TeamChatViewModelDelegate { - delegate.onTeamRemoved(team: team) + delegate.onTeamRemoved?(team: team) } } } - open func onTeamUpdated(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + (team.teamId ?? "nil")) - if session.sessionId == team.teamId { + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: " + (team.teamId)) + if sessionId == team.teamId { self.team = team if let delegate = delegate as? TeamChatViewModelDelegate { - delegate.onTeamUpdate(team: team) + delegate.onTeamUpdate?(team: team) } } } - open func onTeamMemberUpdated(_ team: NIMTeam, withMembers memberIDs: [String]?) { - guard let membersIds = memberIDs else { - return + /// 群成员加入回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { + for teamMember in teamMembers { + guard teamMember.teamId == team?.teamId else { break } + + ChatTeamCache.shared.updateTeamMemberInfo(teamMember) + updateMessageInfo(teamMember.accountId) } - for memberId in membersIds { - if let user = UserInfoProvider.shared.getUserInfo(userId: memberId) { - ChatUserCache.updateUserInfo(user) + } + + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + for teamMember in teamMembers { + guard teamMember.teamId == team?.teamId else { break } + + if teamMember.accountId == self.teamMember?.accountId { + self.teamMember = teamMember } + + ChatTeamCache.shared.updateTeamMemberInfo(teamMember) + updateMessageInfo(teamMember.accountId) } + if let delegate = delegate as? TeamChatViewModelDelegate { - delegate.onTeamMemberUpdate(team: team) + delegate.onTeamMemberUpdate?(teamMembers) } } - - public func getTeamMember() { - teamMember = getTeamMember(userId: IMKitClient.instance.imAccid(), teamId: session.sessionId) - } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift index 6eb70637..af96b80d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift @@ -4,17 +4,17 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK @objcMembers open class TeamMemberSelectVM: NSObject { - public var chatRepo = ChatRepo.shared + public var teamRepo = TeamRepo.shared private let className = "TeamMemberSelectVM" open func fetchTeamMembers(sessionId: String, - _ completion: @escaping (Error?, ChatTeamInfoModel?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId: " + sessionId) - chatRepo.getTeamInfo(sessionId, completion) + _ completion: @escaping (Error?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId: " + sessionId) + teamRepo.getTeamWithMembers(sessionId, .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL, completion) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift index 2cf6919b..9ac80484 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift @@ -4,7 +4,7 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK protocol UserSettingViewModelDelegate: NSObjectProtocol { @@ -13,24 +13,65 @@ protocol UserSettingViewModelDelegate: NSObjectProtocol { } @objcMembers -open class UserSettingViewModel: NSObject { - var repo = ChatRepo.shared +open class UserSettingViewModel: NSObject, NEConversationListener { + var chatRepo = ChatRepo.shared + var contactRepo = ContactRepo.shared + var conversationRepo = ConversationRepo.shared + var settingRepo = SettingRepo.shared - var userInfo: NEKitUser? + var userInfo: NEUserWithFriend? var cellDatas = [UserSettingCellModel]() var delegate: UserSettingViewModelDelegate? + public var conversation: V2NIMConversation? + + override public init() { + super.init() + conversationRepo.addListener(self) + } + + deinit { + conversationRepo.removeListener(self) + } + private let className = "UserSettingViewModel" - func getUserSettingModel(_ userId: String) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + userId) - guard let user = repo.getUserInfo(userId: userId) else { - return + /// 回去单聊会话 + /// - Parameter userId: 用户id + /// - Parameter completion: 完成回调 + func getConversation(_ userId: String, _ completion: @escaping (NSError?) -> Void) { + if let cid = V2NIMConversationIdUtil.p2pConversationId(userId) { + weak var weakSelf = self + conversationRepo.getConversation(cid) { conversation, error in + if conversation != nil { + weakSelf?.conversation = conversation + } + completion(error) + } } - userInfo = user - weak var weakSelf = self + } + + /// 获取用户设置UI显示数据模型 + /// - Parameter userId: 用户id + /// - Parameter completion: 完成回调 + func getUserSettingModel(_ userId: String, _ completion: @escaping () -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + userId) + contactRepo.getFriendInfo(userId) { [weak self] user, error in + self?.userInfo = user + + self?.getSectionDatas() + + self?.delegate?.didNeedRefreshUI() + + completion() + } + } + + /// 拼装UI显示数据模型 + func getSectionDatas() { + cellDatas.removeAll() let mark = UserSettingCellModel() mark.cellName = chatLocalizable("operation_pin") @@ -39,13 +80,15 @@ open class UserSettingViewModel: NSObject { let remind = UserSettingCellModel() remind.cellName = chatLocalizable("message_remind") - if let isNotiMsg = user.imUser?.notifyForNewMsg() { - remind.switchOpen = isNotiMsg + if let userId = userInfo?.user?.accountId { + remind.switchOpen = settingRepo.getP2PMessageMuteMode(accountId: userId) == .NIM_P2P_MESSAGE_MUTE_MODE_OFF } + weak var weakSelf = self remind.swichChange = { isOpen in - if let uid = weakSelf?.userInfo?.userId { - weakSelf?.repo.setNotify(uid, isOpen) { error in + if let uid = weakSelf?.userInfo?.user?.accountId { + let muteMode: V2NIMP2PMessageMuteMode = isOpen ? .NIM_P2P_MESSAGE_MUTE_MODE_OFF : .NIM_P2P_MESSAGE_MUTE_MODE_ON + weakSelf?.settingRepo.setP2PMessageMuteMode(accountId: uid, muteMode: muteMode) { error in if let err = error { weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) @@ -60,41 +103,32 @@ open class UserSettingViewModel: NSObject { setTop.cellName = chatLocalizable("session_set_top") setTop.cornerType = .bottomRight.union(.bottomLeft) - if let uid = user.userId { - let session = NIMSession(uid, type: .P2P) - setTop.switchOpen = repo.isStickTop(session) + if let currentConversation = conversation { + setTop.switchOpen = currentConversation.stickTop } setTop.swichChange = { isOpen in - if let uid = weakSelf?.userInfo?.userId { - let session = NIMSession(uid, type: .P2P) + if let uid = weakSelf?.userInfo?.user?.accountId, let cid = V2NIMConversationIdUtil.p2pConversationId(uid) { if isOpen { - if weakSelf?.getRecenterSession() == nil { - weakSelf?.addRecentetSession() - } - let params = NIMAddStickTopSessionParams(session: session) - weakSelf?.repo.chatExtendProvider - .addStickTopSession(params: params) { error, info in - print("add stick : ", error as Any) - if let err = error { - weakSelf?.delegate?.didNeedRefreshUI() - weakSelf?.delegate?.didError(err) - } else { - setTop.switchOpen = false - } + weakSelf?.conversationRepo.addStickTop(cid) { error in + print("add stick : ", error as Any) + if let err = error { + weakSelf?.delegate?.didNeedRefreshUI() + weakSelf?.delegate?.didError(err) + } else { + setTop.switchOpen = false } + } + } else { - if let info = weakSelf?.repo.chatExtendProvider.getTopSessionInfo(session) { - weakSelf?.repo.chatExtendProvider - .removeStickTopSession(params: info) { error, info in - print("remote stick : ", error as Any) - if let err = error { - weakSelf?.delegate?.didNeedRefreshUI() - weakSelf?.delegate?.didError(err) - } else { - setTop.switchOpen = true - } - } + weakSelf?.conversationRepo.removeStickTop(cid) { error in + print("remote stick : ", error as Any) + if let err = error { + weakSelf?.delegate?.didNeedRefreshUI() + weakSelf?.delegate?.didError(err) + } else { + setTop.switchOpen = true + } } } } @@ -102,18 +136,16 @@ open class UserSettingViewModel: NSObject { cellDatas.append(contentsOf: [mark, remind, setTop]) } - public func addRecentetSession() { - if let uid = userInfo?.userId { - let currentSession = NIMSession(uid, type: .P2P) - repo.addRecentSession(currentSession) - } - } - - public func getRecenterSession() -> NIMRecentSession? { - if let uid = userInfo?.userId { - let currentSession = NIMSession(uid, type: .P2P) - return repo.getRecentSession(currentSession) + /// 会话变更回调 + /// - Parameter conversations: 会话列表 + public func onConversationChanged(_ conversations: [V2NIMConversation]) { + for changeConversation in conversations { + if let currentConversation = conversation, currentConversation.conversationId == changeConversation.conversationId { + conversation = changeConversation + getSectionDatas() + delegate?.didNeedRefreshUI() + continue + } } - return nil } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift b/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift index 44f6c34e..93f5260c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift @@ -14,7 +14,7 @@ import SDWebImageWebPCoder open class ChatRouter: NSObject { public static func setupInit() { NIMKitFileLocationHelper.setStaticAppkey(NIMSDK.shared().appKey()) - NIMKitFileLocationHelper.setStaticUserId(NIMSDK.shared().loginManager.currentAccount()) + NIMKitFileLocationHelper.setStaticUserId(IMKitClient.instance.account()) let webpCoder = SDImageWebPCoder() SDImageCodersManager.shared.addCoder(webpCoder) let svgCoder = SDImageSVGKCoder.shared diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift index d38eb966..c79789dd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift @@ -28,7 +28,7 @@ public let chat_min_h: CGFloat = 40.0 public let chat_reply_height: CGFloat = 16.0 // 气泡最大宽度 -public let chat_content_maxW: CGFloat = (kScreenWidth - 136) +public let chat_content_maxW: CGFloat = (kScreenWidth - 156) // 文本内容最大宽度 public let chat_text_maxW: CGFloat = chat_content_maxW - 2 * chat_content_margin diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift index 6a473b6c..75fad08d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift @@ -7,7 +7,7 @@ import Foundation import NEChatKit @_exported import NECommonKit @_exported import NECommonUIKit -@_exported import NECoreIMKit +@_exported import NECoreIM2Kit @_exported import NECoreKit let coreLoader = CoreLoader() diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift index 18a155ad..bb7f5fa7 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift @@ -43,12 +43,12 @@ open class NEChatUIKitClient: NSObject { /// 获取更多面板数据 /// - Returns: 返回更多操作数据 - open func getMoreActionData(sessionType: NIMSessionType) -> [NEMoreItemModel] { + open func getMoreActionData(sessionType: V2NIMConversationType) -> [NEMoreItemModel] { var more = [NEMoreItemModel]() - moreAction.forEach { model in + for model in moreAction { if model.type != .rtc { more.append(model) - } else if sessionType == .P2P { + } else if sessionType == .CONVERSATION_TYPE_P2P { more.append(model) } } @@ -62,7 +62,7 @@ open class NEChatUIKitClient: NSObject { /// 新增聊天页针对自定义消息的cell扩展,以及现有cell样式覆盖 open func regsiterCustomCell(_ registerDic: [String: UITableViewCell.Type]) { - registerDic.forEach { (key: String, value: UITableViewCell.Type) in + for (key, value) in registerDic { customRegisterDic[key] = value } } @@ -70,4 +70,18 @@ open class NEChatUIKitClient: NSObject { open func getRegisterCustomCell() -> [String: UITableViewCell.Type] { customRegisterDic } + + /// 获取图片资源 + /// - Parameter imageName 图片名称 + /// - Returns 图片资源 + open func getImageSource(imageName: String) -> UIImage? { + coreLoader.loadImage(imageName) + } + + /// 获取多语言 + /// - Parameter key 多语言key + /// - Returns 多语言 + open func getLanguage(key: String) -> String? { + coreLoader.localizable(key) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift b/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift index d27738a2..b7f528c5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import Foundation + extension UIAlertController { class func reconfimAlertView(title: String?, message: String?, confirm: @escaping () -> Void) -> UIAlertController { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatImageExtension.swift b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatImageExtension.swift index be05242b..1e0d0ee5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatImageExtension.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatImageExtension.swift @@ -6,6 +6,7 @@ import CoreGraphics import Foundation import UIKit + public extension UIImage { class func ne_imageNamed(name: String?) -> UIImage? { guard let imageName = name else { @@ -23,3 +24,67 @@ public extension UIImage { return UIImage() } } + +public extension UIImage { + /// 修复图片旋转 + func fixOrientation() -> UIImage { + // 默认方向无需旋转 + if imageOrientation == .up { + return self + } + + var transform = CGAffineTransform.identity + + switch imageOrientation { + // 默认方向旋转180度、镜像旋转180度 + case .down, .downMirrored: + transform = transform.translatedBy(x: size.width, y: size.height) + transform = transform.rotated(by: .pi) + + // 默认方向逆时针旋转90度、镜像逆时针旋转90度 + case .left, .leftMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.rotated(by: .pi / 2) + + // 默认方向顺时针旋转90度、镜像顺时针旋转90度 + case .right, .rightMirrored: + transform = transform.translatedBy(x: 0, y: size.height) + transform = transform.rotated(by: -.pi / 2) + + default: + break + } + + switch imageOrientation { + // 默认方向的竖线镜像、镜像旋转180度 + case .upMirrored, .downMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + + // 镜像逆时针旋转90度、镜像顺时针旋转90度 + case .leftMirrored, .rightMirrored: + transform = transform.translatedBy(x: size.height, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + + default: + break + } + + let ctx = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: cgImage!.bitsPerComponent, bytesPerRow: 0, space: cgImage!.colorSpace!, bitmapInfo: cgImage!.bitmapInfo.rawValue) + ctx?.concatenate(transform) + + // 重新绘制 + switch imageOrientation { + case .left, .leftMirrored, .right, .rightMirrored: + ctx?.draw(cgImage!, in: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.height), height: CGFloat(size.width))) + + default: + ctx?.draw(cgImage!, in: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width), height: CGFloat(size.height))) + } + + let cgimg: CGImage = (ctx?.makeImage())! + let img = UIImage(cgImage: cgimg) + + return img + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift index 1bca66c5..ea4ff441 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift @@ -5,28 +5,12 @@ import Foundation -// 缓存的用于计算高度的Label -var tempLabelForCalc: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - return label -}() - extension String { - /// 计算 string 的 size - static func getTextRectSize(_ text: String, font: UIFont, size: CGSize) -> CGSize { - let attributes = [NSAttributedString.Key.font: font] - let option = NSStringDrawingOptions.usesLineFragmentOrigin - let rect: CGRect = text.boundingRect(with: size, options: option, - attributes: attributes, context: nil) - return CGSize(width: ceil(rect.width), height: ceil(rect.height)) - } - /// 计算 string 的行数,使用 font 的 lineHeight static func calculateMaxLines(width: CGFloat, string: String?, font: UIFont) -> Int { let maxSize = CGSize(width: width, height: CGFloat(Float.infinity)) let charSize = font.lineHeight - let textSize = string?.finalSize(font, maxSize) ?? .zero + let textSize = String.getRealSize(string, font, maxSize) let lines = Int(textSize.height / charSize) return lines } @@ -35,7 +19,7 @@ extension String { static func calculateMaxLines(width: CGFloat, attributeString: NSAttributedString?, font: UIFont) -> Int { let maxSize = CGSize(width: width, height: CGFloat(Float.infinity)) let charSize = font.lineHeight - let textSize = attributeString?.finalSize(font, maxSize) ?? .zero + let textSize = NSAttributedString.getRealSize(attributeString, font, maxSize) let lines = Int(textSize.height / charSize) return lines } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Extension/NEErrorExtension.swift b/NEChatUIKit/NEChatUIKit/Classes/Extension/NEErrorExtension.swift index 1304ab31..1cc6d102 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Extension/NEErrorExtension.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Extension/NEErrorExtension.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import Foundation + extension NSError { class func paramError() -> NSError { NSError( diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift index f09afd29..722cffeb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift @@ -21,7 +21,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -32,6 +32,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco open func commonUILeft() { audioImageViewLeft.contentMode = .center audioImageViewLeft.translatesAutoresizingMaskIntoConstraints = false + audioImageViewLeft.accessibilityIdentifier = "id.animation" bubbleImageLeft.addSubview(audioImageViewLeft) NSLayoutConstraint.activate([ audioImageViewLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: 16), @@ -44,6 +45,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco timeLabelLeft.textColor = UIColor.ne_darkText timeLabelLeft.textAlignment = .left timeLabelLeft.translatesAutoresizingMaskIntoConstraints = false + timeLabelLeft.accessibilityIdentifier = "id.time" bubbleImageLeft.addSubview(timeLabelLeft) NSLayoutConstraint.activate([ timeLabelLeft.leftAnchor.constraint(equalTo: audioImageViewLeft.rightAnchor, constant: 12), @@ -62,6 +64,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco open func commonUIRight() { audioImageViewRight.contentMode = .center audioImageViewRight.translatesAutoresizingMaskIntoConstraints = false + audioImageViewRight.accessibilityIdentifier = "id.animation" bubbleImageRight.addSubview(audioImageViewRight) NSLayoutConstraint.activate([ audioImageViewRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -16), @@ -74,6 +77,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco timeLabelRight.textColor = UIColor.ne_darkText timeLabelRight.textAlignment = .right timeLabelRight.translatesAutoresizingMaskIntoConstraints = false + timeLabelRight.accessibilityIdentifier = "id.time" bubbleImageRight.addSubview(timeLabelRight) NSLayoutConstraint.activate([ timeLabelRight.rightAnchor.constraint(equalTo: audioImageViewRight.leftAnchor, constant: -12), @@ -90,13 +94,11 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco } open func startAnimation(byRight: Bool) { - if byRight { - if !audioImageViewRight.isAnimating { - audioImageViewRight.startAnimating() - } - } else if !audioImageViewLeft.isAnimating { - audioImageViewLeft.startAnimating() + let audioImageView = byRight ? audioImageViewRight : audioImageViewLeft + if !audioImageView.isAnimating { + audioImageView.startAnimating() } + if let m = contentModel as? MessageAudioModel { m.isPlaying = true isPlaying = true @@ -104,13 +106,11 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco } open func stopAnimation(byRight: Bool) { - if byRight { - if audioImageViewRight.isAnimating { - audioImageViewRight.stopAnimating() - } - } else if audioImageViewLeft.isAnimating { - audioImageViewLeft.stopAnimating() + let audioImageView = byRight ? audioImageViewRight : audioImageViewLeft + if audioImageView.isAnimating { + audioImageView.stopAnimating() } + if let m = contentModel as? MessageAudioModel { m.isPlaying = false isPlaying = false @@ -135,7 +135,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco timeLabelLeft.text = "\(m.duration)" + "″" } m.isPlaying ? startAnimation(byRight: isSend) : stopAnimation(byRight: isSend) - messageId = m.message?.messageId + messageId = m.message?.messageClientId } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift index f586fed8..13d784dd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift @@ -22,7 +22,7 @@ open class FunChatMessageBaseCell: NEBaseChatMessageCell { bubbleImageRight.image = image? .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) - seletedBtn.setImage(.ne_imageNamed(name: "fun_select"), for: .selected) + selectedButton.setImage(.ne_imageNamed(name: "fun_select"), for: .selected) } override open func baseCommonUI() { @@ -31,6 +31,9 @@ open class FunChatMessageBaseCell: NEBaseChatMessageCell { contentView.updateLayoutConstraint(firstItem: fullNameLabel, seconedItem: avatarImageLeft, attribute: .left, constant: 8 + funMargin) contentView.updateLayoutConstraint(firstItem: fullNameLabel, seconedItem: avatarImageLeft, attribute: .top, constant: -4) + + contentView.updateLayoutConstraint(firstItem: pinLabelLeft, seconedItem: bubbleImageLeft, attribute: .left, constant: 14 + funMargin) + contentView.updateLayoutConstraint(firstItem: pinLabelRight, seconedItem: bubbleImageRight, attribute: .right, constant: -funMargin) } override open func initSubviewsLayout() { diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift index ee526efc..46bbc2b2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift @@ -27,9 +27,10 @@ open class FunChatMessageCallCell: FunChatMessageBaseCell { contentLabelLeft.isEnabled = false contentLabelLeft.numberOfLines = 0 contentLabelLeft.isUserInteractionEnabled = false - contentLabelLeft.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + contentLabelLeft.font = messageTextFont contentLabelLeft.textAlignment = .center contentLabelLeft.backgroundColor = .clear + contentLabelLeft.accessibilityIdentifier = "id.chatMessageCallText" bubbleImageLeft.addSubview(contentLabelLeft) NSLayoutConstraint.activate([ contentLabelLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor, constant: -chat_content_margin), @@ -43,9 +44,10 @@ open class FunChatMessageCallCell: FunChatMessageBaseCell { contentLabelRight.isEnabled = false contentLabelRight.numberOfLines = 0 contentLabelRight.isUserInteractionEnabled = false - contentLabelRight.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + contentLabelRight.font = messageTextFont contentLabelRight.textAlignment = .center contentLabelRight.backgroundColor = .clear + contentLabelRight.accessibilityIdentifier = "id.chatMessageCallText" bubbleImageRight.addSubview(contentLabelRight) NSLayoutConstraint.activate([ contentLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -(chat_content_margin + funMargin)), diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift index 5dedf7ea..330c4222 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift @@ -11,19 +11,19 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { weak var weakModel: MessageFileModel? public lazy var imgViewLeft: UIImageView = { - let view_img = UIImageView() - view_img.translatesAutoresizingMaskIntoConstraints = false - view_img.backgroundColor = .clear - view_img.accessibilityIdentifier = "id.fileType" - return view_img + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = .clear + imageView.accessibilityIdentifier = "id.fileType" + return imageView }() public lazy var stateViewLeft: FileStateView = { - let state = FileStateView() - state.translatesAutoresizingMaskIntoConstraints = false - state.backgroundColor = .clear - state.accessibilityIdentifier = "id.fileStatus" - return state + let stateView = FileStateView() + stateView.translatesAutoresizingMaskIntoConstraints = false + stateView.backgroundColor = .clear + stateView.accessibilityIdentifier = "id.fileStatus" + return stateView }() public lazy var titleLabelLeft: UILabel = { @@ -34,6 +34,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { label.lineBreakMode = .byTruncatingMiddle label.font = DefaultTextFont(14) label.textAlignment = .left + label.accessibilityIdentifier = "id.displayName" return label }() @@ -43,6 +44,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { label.textColor = UIColor(hexString: "#999999") label.font = NEConstant.defaultTextFont(10.0) label.textAlignment = .left + label.accessibilityIdentifier = "id.displaySize" return label }() @@ -68,19 +70,19 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { }() public lazy var imgViewRight: UIImageView = { - let view_img = UIImageView() - view_img.translatesAutoresizingMaskIntoConstraints = false - view_img.backgroundColor = .clear - view_img.accessibilityIdentifier = "id.fileType" - return view_img + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = .clear + imageView.accessibilityIdentifier = "id.fileType" + return imageView }() public lazy var stateViewRight: FileStateView = { - let state = FileStateView() - state.translatesAutoresizingMaskIntoConstraints = false - state.backgroundColor = .clear - state.accessibilityIdentifier = "id.fileStatus" - return state + let stateView = FileStateView() + stateView.translatesAutoresizingMaskIntoConstraints = false + stateView.backgroundColor = .clear + stateView.accessibilityIdentifier = "id.fileStatus" + return stateView }() public lazy var titleLabelRight: UILabel = { @@ -91,6 +93,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { label.lineBreakMode = .byTruncatingMiddle label.font = DefaultTextFont(14) label.textAlignment = .left + label.accessibilityIdentifier = "id.displayName" return label }() @@ -100,6 +103,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { label.textColor = UIColor(hexString: "#999999") label.font = NEConstant.defaultTextFont(10.0) label.textAlignment = .left + label.accessibilityIdentifier = "id.displaySize" return label }() @@ -130,7 +134,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -223,56 +227,55 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { bubbleW?.constant = kScreenWidth <= 320 ? 222 : 242 // 适配小屏幕 - if let fileObject = model.message?.messageObject as? NIMFileObject { + if let fileObject = model.message?.attachment as? V2NIMMessageFileAttachment { if let fileModel = model as? MessageFileModel { weakModel?.cell = nil weakModel = fileModel fileModel.cell = self - fileModel.size = Float(fileObject.fileLength) + fileModel.size = Float(fileObject.size) if fileModel.state == .Success { stateView.state = .FileOpen } else { stateView.state = .FileDownload - stateView.setProgress(fileModel.progress) - if fileModel.progress >= 1 { + stateView.setProgress(Float(fileModel.progress)) + if fileModel.progress >= 100 { fileModel.state = .Success } } } var imageName = "file_unknown" - var displayName = "未知文件" - if let filePath = fileObject.path as? NSString { - displayName = filePath.lastPathComponent - switch filePath.pathExtension.lowercased() { - case file_doc_support: - imageName = "file_doc" - case file_xls_support: - imageName = "file_xls" - case file_img_support: - imageName = "file_img" - case file_ppt_support: - imageName = "file_ppt" - case file_txt_support: - imageName = "file_txt" - case file_audio_support: - imageName = "file_audio" - case file_vedio_support: - imageName = "file_vedio" - case file_zip_support: - imageName = "file_zip" - case file_pdf_support: - imageName = "file_pdf" - case file_html_support: - imageName = "file_html" - case "key", "keynote": - imageName = "file_keynote" - default: - imageName = "file_unknown" - } + let suffix = (fileObject.name as NSString).pathExtension.lowercased() + switch suffix { + case file_doc_support: + imageName = "file_doc" + case file_xls_support: + imageName = "file_xls" + case file_img_support: + imageName = "file_img" + case file_ppt_support: + imageName = "file_ppt" + case file_txt_support: + imageName = "file_txt" + case file_audio_support: + imageName = "file_audio" + case file_vedio_support: + imageName = "file_vedio" + case file_zip_support: + imageName = "file_zip" + case file_pdf_support: + imageName = "file_pdf" + case file_html_support: + imageName = "file_html" + case "key", "keynote": + imageName = "file_keynote" + default: + imageName = "file_unknown" } + imgView.image = UIImage.ne_imageNamed(name: imageName) - titleLabel.text = fileObject.displayName ?? displayName - let size_B = Double(fileObject.fileLength) + titleLabel.text = fileObject.name + + let size_B = Double(fileObject.size) var size_str = String(format: "%.1f B", size_B) if size_B > 1e3 { let size_KB = size_B / 1e3 @@ -290,8 +293,8 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { } } - override open func uploadProgress(byRight: Bool, _ progress: Float) { + override open func uploadProgress(byRight: Bool, _ progress: UInt) { let stateView = byRight ? stateViewRight : stateViewLeft - stateView.setProgress(progress) + stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift index 0c5e1790..3aab7a81 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift @@ -3,6 +3,7 @@ // found in the LICENSE file. import NIMSDK +import SDWebImage import UIKit @objcMembers @@ -15,7 +16,7 @@ open class FunChatMessageImageCell: FunChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -28,6 +29,7 @@ open class FunChatMessageImageCell: FunChatMessageBaseCell { contentImageViewLeft.contentMode = .scaleAspectFill contentImageViewLeft.clipsToBounds = true contentImageViewLeft.layer.cornerRadius = 4 + contentImageViewLeft.accessibilityIdentifier = "id.thumbnail" bubbleImageLeft.image = nil bubbleImageLeft.addSubview(contentImageViewLeft) NSLayoutConstraint.activate([ @@ -43,6 +45,7 @@ open class FunChatMessageImageCell: FunChatMessageBaseCell { contentImageViewRight.contentMode = .scaleAspectFill contentImageViewRight.clipsToBounds = true contentImageViewRight.layer.cornerRadius = 4 + contentImageViewRight.accessibilityIdentifier = "id.thumbnail" bubbleImageRight.image = nil bubbleImageRight.addSubview(contentImageViewRight) NSLayoutConstraint.activate([ @@ -63,12 +66,17 @@ open class FunChatMessageImageCell: FunChatMessageBaseCell { super.setModel(model, isSend) let contentImageView = isSend ? contentImageViewRight : contentImageViewLeft - if let m = model as? MessageImageModel, let imageUrl = m.imageUrl { + if let m = model as? MessageImageModel, let imageUrl = m.urlString { + var options: SDWebImageOptions = [.retryFailed] + if let imageObject = model.message?.attachment as? V2NIMMessageImageAttachment, imageObject.ext != ".gif" { + options = [.retryFailed, .progressiveLoad] + } + if imageUrl.hasPrefix("http") { contentImageView.sd_setImage( with: URL(string: imageUrl), placeholderImage: nil, - options: .retryFailed, + options: options, progress: nil, completed: nil ) diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift index 8e2c9509..a04c0157 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift @@ -12,6 +12,7 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_darkText label.font = UIFont.systemFont(ofSize: 16.0) + label.accessibilityIdentifier = "id.locationItemTitle" return label }() @@ -20,6 +21,7 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_lightText label.font = UIFont.systemFont(ofSize: 12.0) + label.accessibilityIdentifier = "id.locationItemAddress" return label }() @@ -34,6 +36,15 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { }() public var mapViewLeft: UIView? + + public var mapImageViewLeft: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + return imageView + }() + let backgroundViewLeft = UIView() // Right @@ -42,6 +53,7 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_darkText label.font = UIFont.systemFont(ofSize: 16.0) + label.accessibilityIdentifier = "id.locationItemTitle" return label }() @@ -50,6 +62,7 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_lightText label.font = UIFont.systemFont(ofSize: 12.0) + label.accessibilityIdentifier = "id.locationItemAddress" return label }() @@ -64,15 +77,38 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { }() public var mapViewRight: UIView? + + public var mapImageViewRight: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + return imageView + }() + let backgroundViewRight = UIView() + lazy var pointImageRight: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = coreLoader.loadImage("location_point") + return imageView + }() + + lazy var pointImageLeft: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = coreLoader.loadImage("location_point") + return imageView + }() + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonUI() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -122,33 +158,26 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { subTitleLabelLeft.topAnchor.constraint(equalTo: titleLabelLeft.bottomAnchor, constant: 4), ]) - if let map = NEChatKitClient.instance.delegate?.getCellMapView?() as? UIView { - mapViewLeft = map - backgroundViewLeft.addSubview(map) - map.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - map.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), - map.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor), - map.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), - map.topAnchor.constraint(equalTo: subTitleLabelLeft.bottomAnchor, constant: 4), - ]) - - let pointImage = UIImageView() - pointImage.translatesAutoresizingMaskIntoConstraints = false - pointImage.image = coreLoader.loadImage("location_point") - map.addSubview(pointImage) - NSLayoutConstraint.activate([ - pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), - pointImage.bottomAnchor.constraint(equalTo: map.bottomAnchor, constant: -30), - ]) - } else { - backgroundViewLeft.addSubview(emptyLabelLeft) - NSLayoutConstraint.activate([ - emptyLabelLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), - emptyLabelLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), - emptyLabelLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor, constant: -40), - ]) - } + backgroundViewLeft.addSubview(mapImageViewLeft) + NSLayoutConstraint.activate([ + mapImageViewLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), + mapImageViewLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor), + mapImageViewLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), + mapImageViewLeft.heightAnchor.constraint(equalToConstant: 86), + ]) + + mapImageViewLeft.addSubview(pointImageLeft) + NSLayoutConstraint.activate([ + pointImageLeft.centerXAnchor.constraint(equalTo: mapImageViewLeft.centerXAnchor), + pointImageLeft.bottomAnchor.constraint(equalTo: mapImageViewLeft.bottomAnchor, constant: -30), + ]) + + backgroundViewLeft.addSubview(emptyLabelLeft) + NSLayoutConstraint.activate([ + emptyLabelLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), + emptyLabelLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), + emptyLabelLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor, constant: -40), + ]) } open func commonUIRight() { @@ -192,33 +221,26 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { subTitleLabelRight.topAnchor.constraint(equalTo: titleLabelRight.bottomAnchor, constant: 4), ]) - if let map = NEChatKitClient.instance.delegate?.getCellMapView?() as? UIView { - mapViewRight = map - backgroundViewRight.addSubview(map) - map.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - map.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), - map.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor), - map.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), - map.topAnchor.constraint(equalTo: subTitleLabelRight.bottomAnchor, constant: 4), - ]) - - let pointImage = UIImageView() - pointImage.translatesAutoresizingMaskIntoConstraints = false - pointImage.image = coreLoader.loadImage("location_point") - map.addSubview(pointImage) - NSLayoutConstraint.activate([ - pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), - pointImage.bottomAnchor.constraint(equalTo: map.bottomAnchor, constant: -30), - ]) - } else { - backgroundViewRight.addSubview(emptyLabelRight) - NSLayoutConstraint.activate([ - emptyLabelRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), - emptyLabelRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), - emptyLabelRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor, constant: -40), - ]) - } + backgroundViewRight.addSubview(mapImageViewRight) + NSLayoutConstraint.activate([ + mapImageViewRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), + mapImageViewRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor), + mapImageViewRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), + mapImageViewRight.heightAnchor.constraint(equalToConstant: 86), + ]) + + mapImageViewRight.addSubview(pointImageRight) + NSLayoutConstraint.activate([ + pointImageRight.centerXAnchor.constraint(equalTo: mapImageViewRight.centerXAnchor), + pointImageRight.bottomAnchor.constraint(equalTo: mapImageViewRight.bottomAnchor, constant: -30), + ]) + + backgroundViewRight.addSubview(emptyLabelRight) + NSLayoutConstraint.activate([ + emptyLabelRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), + emptyLabelRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), + emptyLabelRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor, constant: -40), + ]) } override open func showLeftOrRight(showRight: Bool) { @@ -233,14 +255,24 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { let subTitleLabel = isSend ? subTitleLabelRight : subTitleLabelLeft let mapView = isSend ? mapViewRight : mapViewLeft let bubbleW = isSend ? bubbleWRight : bubbleWLeft - - bubbleW?.constant = kScreenWidth <= 320 ? 222 : 242 // 适配小屏幕 + let mapImageView = isSend ? mapImageViewRight : mapImageViewLeft + let emptyLabel = isSend ? emptyLabelRight : emptyLabelLeft + let pointImage = isSend ? pointImageRight : pointImageLeft if let m = model as? MessageLocationModel { titleLabel.text = m.title subTitleLabel.text = m.subTitle - if let lat = m.lat, let lng = m.lng, let map = mapView { - NEChatKitClient.instance.delegate?.setMapviewLocation?(lat: lat, lng: lng, mapview: map) + if let lat = m.lat, let lng = m.lng { + if let url = NEChatKitClient.instance.delegate?.getMapImageUrl?(lat: lat, lng: lng) { + NEALog.infoLog(className(), desc: #function + "location image url = \(url)") + mapImageView.sd_setImage(with: URL(string: url), placeholderImage: coreLoader.loadImage("chat_map_default")) + emptyLabel.isHidden = true + pointImage.isHidden = false + } else { + mapImageView.image = UIImage.ne_imageNamed(name: "map_placeholder_image") + emptyLabel.isHidden = false + pointImage.isHidden = true + } } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift index 15ba90b3..1cbe7aab 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift @@ -19,7 +19,7 @@ open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -175,7 +175,7 @@ open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { super.setModel(model, isSend) - guard let data = NECustomAttachment.dataOfCustomMessage(message: model.message) else { + guard let data = NECustomAttachment.dataOfCustomMessage(model.message?.attachment) else { return } @@ -214,9 +214,9 @@ open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { var contentText = "" if var senderNick = abstracts[i]["senderNick"] as? String { - if senderNick.count > 5 { - // 截取字符串 abcdefg -> ab...fg - let leftEndIndex = senderNick.index(senderNick.startIndex, offsetBy: 2) + if senderNick.count > 7 { + // 截取字符串 abcdefghi -> abcd...hi + let leftEndIndex = senderNick.index(senderNick.startIndex, offsetBy: 4) let rightStartIndex = senderNick.index(senderNick.endIndex, offsetBy: -2) senderNick = senderNick[senderNick.startIndex ..< leftEndIndex] + "..." + senderNick[rightStartIndex ..< senderNick.endIndex] } @@ -259,9 +259,9 @@ open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { // MARK: - lazy load public lazy var backViewLeft: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - return view + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView }() public lazy var titleLabelLeft1: UILabel = { @@ -321,9 +321,9 @@ open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { }() public lazy var backViewRight: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - return view + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView }() public lazy var titleLabelRight1: UILabel = { diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift index 57cd1925..7d5a0d5d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift @@ -12,6 +12,7 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyLabelLeft.textColor = .ne_greyText replyLabelLeft.translatesAutoresizingMaskIntoConstraints = false replyLabelLeft.font = UIFont.systemFont(ofSize: 13) + replyLabelLeft.accessibilityIdentifier = "id.messageReply" return replyLabelLeft }() @@ -32,6 +33,7 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyLabelRight.textColor = .ne_greyText replyLabelRight.translatesAutoresizingMaskIntoConstraints = false replyLabelRight.font = UIFont.systemFont(ofSize: 13) + replyLabelRight.accessibilityIdentifier = "id.messageReply" return replyLabelRight }() @@ -44,9 +46,6 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { return replyTextView }() - public var funPinLabelLeftTopAnchor: NSLayoutConstraint? // 左侧标记文案顶部布局约束 - public var funPinLabelRightTopAnchor: NSLayoutConstraint? // 右侧标记文案顶部布局约束 - override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonUI() @@ -54,7 +53,7 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func commonUI() { @@ -72,11 +71,6 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyTextViewLeft.widthAnchor.constraint(lessThanOrEqualToConstant: chat_content_maxW - funMargin), ]) - contentView.updateLayoutConstraint(firstItem: pinLabelLeft, seconedItem: bubbleImageLeft, attribute: .left, constant: 14 + funMargin) - pinLabelLeftTopAnchor?.isActive = false - funPinLabelLeftTopAnchor = pinLabelLeft.topAnchor.constraint(equalTo: replyTextViewLeft.bottomAnchor, constant: 4) - funPinLabelLeftTopAnchor?.isActive = true - replyTextViewLeft.addSubview(replyLabelLeft) NSLayoutConstraint.activate([ replyLabelLeft.topAnchor.constraint(equalTo: replyTextViewLeft.topAnchor, constant: 4), @@ -95,11 +89,6 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyTextViewRight.widthAnchor.constraint(lessThanOrEqualToConstant: chat_content_maxW - funMargin), ]) - contentView.updateLayoutConstraint(firstItem: pinLabelRight, seconedItem: bubbleImageRight, attribute: .right, constant: -funMargin) - pinLabelRightTopAnchor?.isActive = false - funPinLabelRightTopAnchor = pinLabelRight.topAnchor.constraint(equalTo: replyTextViewRight.bottomAnchor, constant: 4) - funPinLabelRightTopAnchor?.isActive = true - replyTextViewRight.addSubview(replyLabelRight) NSLayoutConstraint.activate([ replyLabelRight.topAnchor.constraint(equalTo: replyTextViewRight.topAnchor, constant: 4), @@ -112,7 +101,9 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { override open func showLeftOrRight(showRight: Bool) { super.showLeftOrRight(showRight: showRight) replyTextViewLeft.isHidden = showRight + replyLabelLeft.isHidden = showRight replyTextViewRight.isHidden = !showRight + replyLabelRight.isHidden = !showRight } open func addReplyGesture() { @@ -126,8 +117,7 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { } open func tapReplyView(tap: UITapGestureRecognizer) { - print(#function) - delegate?.didTapMessageView(self, contentModel) + delegate?.didTapMessageView(self, contentModel, contentModel?.replyedModel) } override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { @@ -139,6 +129,7 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, font: font, color: replyLabel.textColor) + replyLabel.accessibilityValue = text } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift index 33dd595e..e7b1546e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift @@ -17,7 +17,7 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -31,6 +31,7 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { revokeLabelLeft.textAlignment = .center revokeLabelLeft.lineBreakMode = .byTruncatingMiddle revokeLabelLeft.font = UIFont.systemFont(ofSize: 14.0) + revokeLabelLeft.accessibilityIdentifier = "id.messageText" contentView.addSubview(revokeLabelLeft) NSLayoutConstraint.activate([ revokeLabelLeft.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), @@ -45,6 +46,7 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { revokeLabelRight.textColor = UIColor.ne_greyText revokeLabelRight.textAlignment = .center revokeLabelRight.font = UIFont.systemFont(ofSize: 14.0) + revokeLabelRight.accessibilityIdentifier = "id.messageText" contentView.addSubview(revokeLabelRight) revokeLabelRightXAnchor = revokeLabelRight.centerXAnchor.constraint(equalTo: contentView.centerXAnchor, constant: 0) revokeLabelRightXAnchor?.isActive = true @@ -57,6 +59,7 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { reeditButton.translatesAutoresizingMaskIntoConstraints = false reeditButton.titleLabel?.font = UIFont.systemFont(ofSize: 14.0) reeditButton.setTitleColor(UIColor.ne_blueText, for: .normal) + reeditButton.accessibilityIdentifier = "id.reeditButton" contentView.addSubview(reeditButton) NSLayoutConstraint.activate([ @@ -83,14 +86,17 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { pinLabelRight.isHidden = true activityView.isHidden = true readView.isHidden = true - seletedBtn.isHidden = true + selectedButton.isHidden = true revokeLabelLeft.isHidden = showRight revokeLabelRight.isHidden = !showRight } override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { - if let time = model.message?.timestamp { + let isSend = IMKitClient.instance.isMe(model.message?.senderId) + let revokeLabel = isSend ? revokeLabelRight : revokeLabelLeft + + if let time = model.message?.createTime { let date = Date() let currentTime = date.timeIntervalSince1970 if currentTime - time >= 60 * 2 { @@ -98,8 +104,6 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { } } - let revokeLabel = isSend ? revokeLabelRight : revokeLabelLeft - model.contentSize = CGSize(width: kScreenWidth, height: 0) super.setModel(model, isSend) showLeftOrRight(showRight: isSend) @@ -111,7 +115,7 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { revokeLabel.text = (model.fullName ?? "") + " " + chatLocalizable("withdrew_message") } - if isSend, model.isRevokedText == true { + if isSend, model.isReedit == true { if model.timeOut == true { reeditButton.isHidden = true revokeLabelRightXAnchor?.constant = 0 @@ -127,7 +131,6 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { } func reeditEvent(button: UIButton) { - print(#function) delegate?.didTapReeditButton(self, contentModel) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift index 90300fee..32815a98 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift @@ -14,6 +14,7 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) label.backgroundColor = .clear + label.accessibilityIdentifier = "id.messageTitle" return label }() @@ -25,6 +26,7 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) label.backgroundColor = .clear + label.accessibilityIdentifier = "id.messageTitle" return label }() @@ -37,20 +39,22 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { /// left bubbleImageLeft.addSubview(titleLabelLeft) titleLabelLeftHeightAnchor = titleLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + titleLabelLeftHeightAnchor?.priority = .fittingSizeLevel + titleLabelLeftHeightAnchor?.isActive = true NSLayoutConstraint.activate([ titleLabelLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor, constant: -chat_content_margin), titleLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: chat_content_margin + funMargin), titleLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: chat_content_margin), - titleLabelLeftHeightAnchor!, ]) bubbleImageLeft.addSubview(contentLabelLeft) contentLabelLeftHeightAnchor = contentLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + contentLabelLeftHeightAnchor?.priority = .fittingSizeLevel + contentLabelLeftHeightAnchor?.isActive = true NSLayoutConstraint.activate([ contentLabelLeft.rightAnchor.constraint(equalTo: titleLabelLeft.rightAnchor, constant: -0), contentLabelLeft.leftAnchor.constraint(equalTo: titleLabelLeft.leftAnchor, constant: 0), contentLabelLeft.topAnchor.constraint(equalTo: titleLabelLeft.bottomAnchor, constant: chat_content_margin), - contentLabelLeftHeightAnchor!, ]) commonUILeft() @@ -58,20 +62,22 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { /// right bubbleImageRight.addSubview(titleLabelRight) titleLabelRightHeightAnchor = titleLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + titleLabelRightHeightAnchor?.priority = .fittingSizeLevel + titleLabelRightHeightAnchor?.isActive = true NSLayoutConstraint.activate([ titleLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -chat_content_margin - funMargin), titleLabelRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: chat_content_margin), titleLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: chat_content_margin), - titleLabelRightHeightAnchor!, ]) bubbleImageRight.addSubview(contentLabelRight) contentLabelRightHeightAnchor = contentLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + contentLabelRightHeightAnchor?.priority = .fittingSizeLevel + contentLabelRightHeightAnchor?.isActive = true NSLayoutConstraint.activate([ contentLabelRight.rightAnchor.constraint(equalTo: titleLabelRight.rightAnchor, constant: -0), contentLabelRight.leftAnchor.constraint(equalTo: titleLabelRight.leftAnchor, constant: 0), contentLabelRight.topAnchor.constraint(equalTo: titleLabelRight.bottomAnchor, constant: chat_content_margin), - contentLabelRightHeightAnchor!, ]) commonUIRight() @@ -89,17 +95,11 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { let titleLabel = isSend ? titleLabelRight : titleLabelLeft let titleLabelHeightAnchor = isSend ? titleLabelRightHeightAnchor : titleLabelLeftHeightAnchor let contentLabelHeightAnchor = isSend ? contentLabelRightHeightAnchor : contentLabelLeftHeightAnchor - let pinLabelTopAnchor = isSend ? pinLabelRightTopAnchor : pinLabelLeftTopAnchor - let funPinLabelTopAnchor = isSend ? funPinLabelRightTopAnchor : funPinLabelLeftTopAnchor if model.replyText == nil || model.replyText!.isEmpty { replyView.isHidden = true - funPinLabelTopAnchor?.isActive = false - pinLabelTopAnchor?.isActive = true } else { replyView.isHidden = false - funPinLabelTopAnchor?.isActive = true - pinLabelTopAnchor?.isActive = false } if let m = model as? MessageTextModel { @@ -109,9 +109,6 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { if let m = model as? MessageRichTextModel { titleLabel.attributedText = m.titleAttributeStr titleLabelHeightAnchor?.constant = m.titleTextHeight - if m.message?.text?.isEmpty == true { - titleLabelHeightAnchor?.constant = 26 - } } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift index 19656f6e..5daad0d6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift @@ -12,8 +12,9 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { label.isEnabled = false label.numberOfLines = 0 label.isUserInteractionEnabled = false - label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + label.font = messageTextFont label.backgroundColor = .clear + label.accessibilityIdentifier = "id.messageText" return label }() @@ -23,8 +24,9 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { label.isEnabled = false label.numberOfLines = 0 label.isUserInteractionEnabled = false - label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + label.font = messageTextFont label.backgroundColor = .clear + label.accessibilityIdentifier = "id.messageText" return label }() @@ -34,7 +36,7 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -45,7 +47,6 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { contentLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: chat_content_margin), contentLabelLeft.bottomAnchor.constraint(equalTo: bubbleImageLeft.bottomAnchor, constant: -chat_content_margin), ]) - contentView.updateLayoutConstraint(firstItem: pinLabelLeft, seconedItem: bubbleImageLeft, attribute: .left, constant: 14 + funMargin) bubbleImageRight.addSubview(contentLabelRight) NSLayoutConstraint.activate([ @@ -54,7 +55,6 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { contentLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: chat_content_margin), contentLabelRight.bottomAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: -chat_content_margin), ]) - contentView.updateLayoutConstraint(firstItem: pinLabelRight, seconedItem: bubbleImageRight, attribute: .right, constant: -funMargin) } override open func showLeftOrRight(showRight: Bool) { @@ -70,6 +70,7 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { if let m = model as? MessageTextModel { contentLabel.attributedText = m.attributeStr + contentLabel.accessibilityValue = m.message?.text } bubbleW?.constant += funMargin } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift index c5b900dc..b8255988 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift @@ -2,6 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECommonKit import NIMSDK import UIKit @@ -80,7 +81,7 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -127,24 +128,17 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { let timeLabel = isSend ? timeLabelRight : timeLabelLeft let stateView = isSend ? stateViewRight : stateViewLeft - if let videoObject = model.message?.messageObject as? NIMVideoObject { - if let path = videoObject.coverPath, FileManager.default.fileExists(atPath: path) { - contentImageView.sd_setImage( - with: URL(fileURLWithPath: path), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } else { - contentImageView.sd_setImage( - with: URL(string: videoObject.coverUrl ?? ""), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } + if let videoObject = model.message?.attachment as? V2NIMMessageVideoAttachment { + // 获取首帧 + let videoUrl = videoObject.url ?? "" + let thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + contentImageView.sd_setImage( + with: URL(string: thumbUrl), + placeholderImage: nil, + options: .retryFailed, + progress: nil, + completed: nil + ) if videoObject.duration > 0 { timeView.isHidden = false @@ -161,8 +155,8 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { stateView.state = .VideoPlay } else { stateView.state = .VideoDownload - stateView.setProgress(videoModel.progress) - if videoModel.progress >= 1 { + stateView.setProgress(Float(videoModel.progress)) + if videoModel.progress >= 100 { videoModel.state = .Success } } @@ -170,8 +164,8 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { } } - override open func uploadProgress(byRight: Bool, _ progress: Float) { + override open func uploadProgress(byRight: Bool, _ progress: UInt) { let stateView = byRight ? stateViewRight : stateViewLeft - stateView.setProgress(progress) + stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserSettingSelectCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserSettingSelectCell.swift index c0cb12da..84f773d9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserSettingSelectCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserSettingSelectCell.swift @@ -27,8 +27,8 @@ open class FunUserSettingSelectCell: NEBaseUserSettingSelectCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), ]) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserTableViewCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserTableViewCell.swift index 2e4876c7..a8eb189d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserTableViewCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserTableViewCell.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -10,17 +10,17 @@ open class FunUserTableViewCell: UserBaseTableViewCell { override open func baseCommonUI() { super.baseCommonUI() // avatar - avatarImage.layer.cornerRadius = 4 + avatarImageView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), - avatarImage.widthAnchor.constraint(equalToConstant: 40), - avatarImage.heightAnchor.constraint(equalToConstant: 40), - avatarImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), ]) titleLabel.textColor = .ne_darkText NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 11), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 11), titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -29), titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) @@ -30,7 +30,7 @@ open class FunUserTableViewCell: UserBaseTableViewCell { line.backgroundColor = .funChatLineBorderColor contentView.addSubview(line) NSLayoutConstraint.activate([ - line.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), + line.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), line.rightAnchor.constraint(equalTo: contentView.rightAnchor), line.heightAnchor.constraint(equalToConstant: 0.6), line.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift index a95e4c6c..331a7f3b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift @@ -8,11 +8,11 @@ import NIMSDK import UIKit @objcMembers -open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, NIMUserManagerDelegate, FunChatRecordViewDelegate { +open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, FunChatRecordViewDelegate { public weak var recordView: FunRecordAudioView? - override public init(session: NIMSession) { - super.init(session: session) + override public init(conversationId: String) { + super.init(conversationId: conversationId) cellRegisterDic = ChatMessageHelper.getChatCellRegisterDic(isFun: true) normalInputHeight = 90 @@ -21,16 +21,16 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funChatBackgroundColor // 换肤颜色提取 view.bringSubviewToFront(chatInputView) - brokenNetworkView.errorIcon.isHidden = false + brokenNetworkView.errorIconView.isHidden = false brokenNetworkView.backgroundColor = .funChatNetworkBrokenBackgroundColor - brokenNetworkView.content.textColor = .funChatNetworkBrokenTitleColor + brokenNetworkView.contentLabel.textColor = .funChatNetworkBrokenTitleColor getFunInputView()?.funDelegate = self } @@ -53,7 +53,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } override func getUserSelectVC() -> NEBaseSelectUserViewController { - FunSelectUserViewController(sessionId: viewmodel.session.sessionId, showSelf: false) + FunSelectUserViewController(sessionId: viewModel.sessionId, showSelf: false) } override func getTextViewController(title: String?, body: String?) -> TextViewController { @@ -87,7 +87,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } open func didHideReplyMode() { - viewmodel.isReplying = false + viewModel.isReplying = false if currentKeyboardHeight > 0 { normalOffset = 30 @@ -98,12 +98,20 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } open func didShowReplyMode() { - viewmodel.isReplying = true + viewModel.isReplying = true chatInputView.textView.becomeFirstResponder() } override open func expandMoreAction() { - var items = NEChatUIKitClient.instance.getMoreActionData(sessionType: viewmodel.session.sessionType) + var items = NEChatUIKitClient.instance.getMoreActionData(sessionType: V2NIMConversationIdUtil.conversationType(viewModel.conversationId)) + if NEChatKitClient.instance.delegate == nil { + items = items.filter { item in + if item.type == .location { + return false + } + return true + } + } let photo = NEMoreItemModel() photo.image = UIImage.ne_imageNamed(name: "fun_chat_photo") photo.title = chatLocalizable("chat_photo") @@ -115,37 +123,42 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } func openPhoto() { - NELog.infoLog(className(), desc: "open photo") + NEALog.infoLog(className(), desc: "open photo") willSelectItem(button: chatInputView.currentButton, index: showPhotoTag) } override open func showRtcCallAction() { - var param = [String: AnyObject]() - param["remoteUserAccid"] = viewmodel.session.sessionId as AnyObject - param["currentUserAccid"] = NIMSDK.shared().loginManager.currentAccount() as AnyObject - param["remoteShowName"] = titleContent as AnyObject - if let user = viewmodel.repo.getUserInfo(userId: viewmodel.session.sessionId), let avatar = user.userInfo?.avatarUrl { - param["remoteAvatar"] = avatar as AnyObject + let sessionId = viewModel.sessionId + + var param = [String: Any]() + param["remoteUserAccid"] = sessionId + param["currentUserAccid"] = IMKitClient.instance.account() + param["remoteShowName"] = titleContent + + if let user = viewModel.getShowName(sessionId).user { + param["remoteAvatar"] = user.user?.avatar } let videoCallAction = NECustomAlertAction(title: chatLocalizable("video_call")) { - param["type"] = NSNumber(integerLiteral: 2) as AnyObject + param["type"] = NSNumber(integerLiteral: 2) Router.shared.use(CallViewRouter, parameters: param) } + let audioCallAction = NECustomAlertAction(title: chatLocalizable("audio_call")) { - param["type"] = NSNumber(integerLiteral: 1) as AnyObject + param["type"] = NSNumber(integerLiteral: 1) Router.shared.use(CallViewRouter, parameters: param) } + showCustomActionSheet([videoCallAction, audioCallAction]) } override func getUserSettingViewController() -> NEBaseUserSettingViewController { - FunUserSettingViewController(userId: viewmodel.session.sessionId) + FunUserSettingViewController(userId: viewModel.sessionId) } override open func keyBoardWillShow(_ notification: Notification) { if chatInputView.chatInpuMode == .normal || chatInputView.chatInpuMode == .multipleSend { - if viewmodel.isReplying { + if viewModel.isReplying { normalOffset = -10 } else { normalOffset = 30 @@ -160,7 +173,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, override open func keyBoardWillHide(_ notification: Notification) { if chatInputView.chatInpuMode == .normal || chatInputView.chatInpuMode == .multipleSend { - if viewmodel.isReplying { + if viewModel.isReplying { normalOffset = -30 } else { normalOffset = 0 @@ -274,41 +287,41 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } override open func closeReply(button: UIButton?) { - viewmodel.isReplying = false + viewModel.isReplying = false getFunInputView()?.hideReplyMode() getFunInputView()?.replyLabel.attributedText = nil } override open func showReplyMessageView(isReEdit: Bool = false) { - viewmodel.isReplying = true + viewModel.isReplying = true guard let replyView = getFunInputView() else { return } replyView.showReplyMode() - if let message = viewmodel.operationModel?.message { + if let message = viewModel.operationModel?.message { if isReEdit { - replyView.replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: viewmodel.operationModel?.replyText ?? "", + replyView.replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: viewModel.operationModel?.replyText ?? "", font: .systemFont(ofSize: 13), color: .ne_greyText) - if let replyMessage = viewmodel.getReplyMessageWithoutThread(message: message) as? MessageContentModel { - viewmodel.operationModel = replyMessage + viewModel.getReplyMessageWithoutThread(message: message) { model in + if let replyMessage = model as? MessageContentModel { + self.viewModel.operationModel = replyMessage + } } } else { var text = chatLocalizable("msg_reply") - if let uid = message.from { - var showName = ChatUserCache.getShowName(userId: uid, teamId: viewmodel.session.sessionId, false) - if viewmodel.session.sessionType != .P2P, - !IMKitClient.instance.isMySelf(uid) { + if let uid = message.senderId { + var (showName, _) = ChatTeamCache.shared.getShowName(uid, false) + if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) != .CONVERSATION_TYPE_P2P, + !IMKitClient.instance.isMe(uid) { addToAtUsers(addText: "@" + showName + "", isReply: true, accid: uid) } - let user = viewmodel.getUserInfo(userId: uid) - if let alias = user?.alias, !alias.isEmpty { - showName = alias - } + + (showName, _) = ChatTeamCache.shared.getShowName(uid) text += " " + showName + text += ": \(ChatMessageHelper.contentOfMessage(message))" + getFunInputView()?.replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, + font: .systemFont(ofSize: 13), + color: .ne_greyText) } - text += ": \(ChatMessageHelper.contentOfMessage(message))" - getFunInputView()?.replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, - font: .systemFont(ofSize: 13), - color: .ne_greyText) } if chatInputView.textView.isFirstResponder { normalOffset = -10 @@ -319,19 +332,14 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } } - override open func getReadView(_ message: NIMMessage) -> NEBaseReadViewController { - FunReadViewController(message: message) + override open func getReadView(_ message: V2NIMMessage, _ teamId: String) -> NEBaseReadViewController { + FunReadViewController(message: message, teamId: teamId) } override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.messages[indexPath.row] + let model = viewModel.messages[indexPath.row] if let contentModel = model as? MessageContentModel { - if let tipModel = model as? MessageTipsModel { - tipModel.commonInit() - return tipModel.cellHeight() + chat_content_margin - } - if contentModel.type == .revoke { if let time = contentModel.timeContent, !time.isEmpty { return 28 + chat_timeCellH @@ -344,15 +352,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } open func getMessageModel(model: MessageModel) { - if model.type == .tip || - model.type == .notification || - model.type == .time { - if let tipModel = model as? MessageTipsModel { - tipModel.contentSize = String.getTextRectSize(tipModel.text ?? "", - font: .systemFont(ofSize: 14), - size: CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) - tipModel.height = max(tipModel.contentSize.height + chat_content_margin, 28) - } + if model.type == .tip || model.type == .notification { return } @@ -461,7 +461,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, normalInputHeight = 130 } - if viewmodel.isReplying { + if viewModel.isReplying { normalOffset = -30 } else { normalOffset = 0 diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift index 0d583f0d..36b91897 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift @@ -11,7 +11,7 @@ import UIKit open class FunForwardUserCell: NEBaseForwardUserCell { override func setupUI() { super.setupUI() - userHeader.layer.cornerRadius = 4 + userHeaderView.layer.cornerRadius = 4 } } @@ -19,10 +19,10 @@ open class FunForwardUserCell: NEBaseForwardUserCell { open class FunForwardAlertViewController: NEBaseForwardAlertViewController { override open func setupUI() { super.setupUI() - tip.font = .systemFont(ofSize: 16, weight: .semibold) - oneUserHead.layer.cornerRadius = 4.0 - sureBtn.setTitleColor(.funChatThemeColor, for: .normal) - userCollection.register( + tipLabel.font = .systemFont(ofSize: 16, weight: .semibold) + oneUserHeadView.layer.cornerRadius = 4.0 + sureButton.setTitleColor(.funChatThemeColor, for: .normal) + userCollectionView.register( FunForwardUserCell.self, forCellWithReuseIdentifier: "\(FunForwardUserCell.self)" ) diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift index 390085f0..a78acf2d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift @@ -17,15 +17,15 @@ open class FunMultiForwardViewController: MultiForwardViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funChatBackgroundColor // 换肤颜色提取 - brokenNetworkView.errorIcon.isHidden = false + brokenNetworkView.errorIconView.isHidden = false brokenNetworkView.backgroundColor = .funChatNetworkBrokenBackgroundColor - brokenNetworkView.content.textColor = .funChatNetworkBrokenTitleColor + brokenNetworkView.contentLabel.textColor = .funChatNetworkBrokenTitleColor navigationView.backgroundColor = .funChatBackgroundColor navigationView.titleBarBottomLine.backgroundColor = .funChatNavigationBottomLineColor } @@ -37,17 +37,13 @@ open class FunMultiForwardViewController: MultiForwardViewController { } override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - viewmodel.messages[indexPath.row].cellHeight() + viewModel.messages[indexPath.row].cellHeight() } open func getMessageModel(model: MessageModel) { - if model.type == .tip || - model.type == .notification || - model.type == .time { - if let tipModel = model as? MessageTipsModel { - tipModel.contentSize = String.getTextRectSize(tipModel.text ?? "", - font: .systemFont(ofSize: 14), - size: CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) + if model.type == .tip || model.type == .notification { + if let tipModel = model as? MessageTipsModel, let text = tipModel.text { + tipModel.contentSize = String.getRealSize(text, .systemFont(ofSize: 14), CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) tipModel.height = max(tipModel.contentSize.height + chat_content_margin, 28) } return diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift index 733572a6..31f9497f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift @@ -8,53 +8,88 @@ import UIKit @objcMembers open class FunP2PChatViewController: FunChatViewController { - public init(session: NIMSession, anchor: NIMMessage?) { - super.init(session: session) - viewmodel = ChatViewModel(session: session, anchor: anchor) + /// 重写父类的构造方法 + /// - Parameter conversationId: 会话id + override public init(conversationId: String) { + super.init(conversationId: conversationId) + viewModel = P2PChatViewModel(conversationId: conversationId, anchor: nil) } - override open func viewDidLoad() { - super.viewDidLoad() + /// 重写父类的构造方法 + /// - Parameter conversationId: 会话id + /// - Parameter anchor: 锚点消息 + public init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId) + viewModel = P2PChatViewModel(conversationId: conversationId, anchor: anchor) + } - // Do any additional setup after loading the view. + public required init?(coder: NSCoder) { + super.init(coder: coder) } - override open func getSessionInfo(session: NIMSession) { - var showName = session.sessionId - ChatUserCache.getUserInfo(session.sessionId) { [weak self] user, error in - if let name = user?.showName() { - showName = name + override open var title: String? { + didSet { + super.title = title + if let showName = title { + let text = chatLocalizable("fun_chat_input_placeholder") + let attribute = NSMutableAttributedString(string: text) + let style = NSMutableParagraphStyle() + style.lineBreakMode = .byTruncatingTail + style.alignment = .left + attribute.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: NSMakeRange(0, text.utf16.count)) + attribute.addAttribute(.foregroundColor, value: UIColor.funChatInputViewPlaceholderTextColor, range: NSMakeRange(0, text.utf16.count)) + attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.utf16.count)) + chatInputView.textView.attributedPlaceholder = attribute + chatInputView.textView.setNeedsLayout() } - - self?.title = showName - self?.titleContent = showName - let text = chatLocalizable("fun_chat_input_placeholder") - let attribute = NSMutableAttributedString(string: text) - let style = NSMutableParagraphStyle() - style.lineBreakMode = .byTruncatingTail - style.alignment = .left - attribute.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: NSMakeRange(0, text.utf16.count)) - attribute.addAttribute(.foregroundColor, value: UIColor.funChatInputViewPlaceholderTextColor, range: NSMakeRange(0, text.utf16.count)) - attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.utf16.count)) - self?.chatInputView.textView.attributedPlaceholder = attribute - self?.chatInputView.textView.setNeedsLayout() } } - /// 创建个人聊天页构造方法 - /// - Parameter sessionId: 会话id - public init(sessionId: String) { - let session = NIMSession(sessionId, type: .P2P) - super.init(session: session) + override open func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { + super.getSessionInfo(sessionId: sessionId) { [weak self] in + self?.viewModel.loadShowName([sessionId]) { + let name = self?.viewModel.getShowName(sessionId).name ?? sessionId + self?.title = name + self?.titleContent = name + } + completion() + } } - /// 重写父类的构造方法 - /// - Parameter session: sessionId - override public init(session: NIMSession) { - super.init(session: session) - } + /// 重写检查并发送正在输入状态 + /// - Parameter endEdit: 是否停止输入 + override open func checkAndSendTypingState(endEdit: Bool = false) { + guard let viewModel = viewModel as? P2PChatViewModel else { + return + } - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + if endEdit { + viewModel.sendInputTypingEndState() + return + } + + if chatInputView.chatInpuMode == .normal { + if let content = chatInputView.textView.text, content.count > 0 { + viewModel.sendInputTypingState() + } else { + viewModel.sendInputTypingEndState() + } + } else { + var title = "" + var content = "" + + if let titleText = chatInputView.titleField.text { + title = titleText + } + + if let contentText = chatInputView.textView.text { + content = contentText + } + if title.count <= 0, content.count <= 0 { + viewModel.sendInputTypingEndState() + } else { + viewModel.sendInputTypingState() + } + } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift index 1a942c49..d47e4c9e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift @@ -7,13 +7,13 @@ import UIKit @objcMembers open class FunPinMessageViewController: NEBasePinMessageViewController { - override public init(session: NIMSession) { - super.init(session: session) + override public init(conversationId: String) { + super.init(conversationId: conversationId) pin_content_maxW = (kScreenWidth - 32) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -26,7 +26,7 @@ open class FunPinMessageViewController: NEBasePinMessageViewController { ChatMessageHelper.getPinCellRegisterDic(isFun: true) } - override open func showAction(item: PinMessageModel) { + override open func showAction(item: NEPinMessageModel) { var actions = [NECustomAlertAction]() weak var weakSelf = self @@ -35,14 +35,14 @@ open class FunPinMessageViewController: NEBasePinMessageViewController { } actions.append(cancelPinAction) - if item.message.messageType == .text { + if item.message.messageType == .MESSAGE_TYPE_TEXT { let copyAction = NECustomAlertAction(title: chatLocalizable("operation_copy")) { weakSelf?.copyActionClicked(item: item) } actions.append(copyAction) } - if item.message.messageType != .audio { + if item.message.messageType != .MESSAGE_TYPE_AUDIO { let forwardAction = NECustomAlertAction(title: chatLocalizable("operation_forward")) { weakSelf?.forwardActionClicked(item: item) } @@ -56,7 +56,7 @@ open class FunPinMessageViewController: NEBasePinMessageViewController { FunForwardAlertViewController() } - override open func forwardMessage(_ message: NIMMessage) { + override open func forwardMessage(_ message: V2NIMMessage) { if IMKitClient.instance.getConfigCenter().teamEnable { let userAction = NECustomAlertAction(title: chatLocalizable("contact_user")) { [weak self] in self?.forwardMessageToUser(message) diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift index ccd2b5c8..56c0a7d7 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift index d7816365..d819c0fb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -15,7 +15,7 @@ open class FunSelectUserViewController: NEBaseSelectUserViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func commonUI() { diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunGroupChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunTeamChatViewController.swift similarity index 62% rename from NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunGroupChatViewController.swift rename to NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunTeamChatViewController.swift index 33b4726b..bd892636 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunGroupChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunTeamChatViewController.swift @@ -7,32 +7,33 @@ import NIMSDK import UIKit @objcMembers -open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelDelegate { +open class FunTeamChatViewController: FunChatViewController, TeamChatViewModelDelegate { private var isLeaveTeamByOther = false // 是否被移出群聊 private var isLeaveTeamBySelf = false // 是否多端登录另一端退出群聊 private var isdismissTeam = false // 群聊是否已解散 private var isdismissDiscuss = false // 讨论组是否已解散 private var onCurrentPage = false // 是否位于聊天详情页 - public init(session: NIMSession, anchor: NIMMessage?) { - super.init(session: session) - viewmodel = TeamChatViewModel(session: session, anchor: anchor) - viewmodel.delegate = self + public init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId) + viewModel = TeamChatViewModel(conversationId: conversationId, anchor: anchor) + viewModel.delegate = self } /// 创建群的构造方法 /// - Parameter sessionId: 会话id public init(sessionId: String) { - let session = NIMSession(sessionId, type: .team) - super.init(session: session) + let conversationId = V2NIMConversationIdUtil.teamConversationId(sessionId) ?? "" + super.init(conversationId: conversationId) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } deinit { NotificationCenter.default.removeObserver(self) + ChatTeamCache.shared.removeAllTeamInfo() } override open func viewWillAppear(_ animated: Bool) { @@ -71,11 +72,23 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD NotificationCenter.default.addObserver(self, selector: #selector(popGroupChatVC), name: NENotificationName.popGroupChatVC, object: nil) } - override open func getSessionInfo(session: NIMSession) { - if let vm = viewmodel as? TeamChatViewModel { - if let t = vm.getTeam(teamId: session.sessionId) { - updateTeamInfo(team: t) + override open func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { + chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("fun_chat_input_placeholder")) + super.getSessionInfo(sessionId: sessionId) { [weak self] in + + if let vm = self?.viewModel as? TeamChatViewModel { + vm.getTeamInfo(teamId: sessionId) { error, team in + if let team = team { + if team.isValidTeam == false { + self?.showSingleAlert(message: coreLoader.localizable("team_not_exist")) { + self?.popGroupChatVC() + } + } + self?.updateTeamInfo(team: team) + } + } } + completion() } } @@ -110,9 +123,30 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD return attribute } - open func updateTeamInfo(team: NIMTeam) { - title = team.getShowName() - if team.inAllMuteMode(), viewmodel.teamMember?.type != .manager, viewmodel.teamMember?.type != .owner { + open func updateTeamTitle(_ noti: Notification) { + if let tid = noti.userInfo?["teamId"] as? String, + tid == viewModel.sessionId, + let team = ChatTeamCache.shared.getTeamInfo() { + updateTeamInfo(team: team) + } + } + + /// 更新群聊信息(群聊名称、群禁言状态、缓存) + /// - Parameter team: 群聊信息 + open func updateTeamInfo(team: V2NIMTeam) { + title = team.name + ChatTeamCache.shared.updateTeamInfo(team) + setMute(team: team) + } + + /// 设置群禁言/取消群禁言状态 + /// - Parameter team: 群聊信息 + open func setMute(team: V2NIMTeam) { + guard let viewModel = viewModel as? TeamChatViewModel else { + return + } + + if team.chatBannedMode == .TEAM_CHAT_BANNED_MODE_BANNED_ALL || (team.chatBannedMode == .TEAM_CHAT_BANNED_MODE_BANNED_NORMAL && viewModel.teamMember?.memberRole == .TEAM_MEMBER_ROLE_NORMAL) { // 群禁言 isMute = true chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("team_mute")) @@ -138,20 +172,19 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD } } - override open func onRecvMessages(_ messages: [NIMMessage]) { + override open func onRecvMessages(_ messages: [V2NIMMessage]) { super.onRecvMessages(messages) for message in messages { - if let object = message.messageObject as? NIMNotificationObject, - let content = object.content as? NIMTeamNotificationContent { - if content.operationType == .leave, - IMKitClient.instance.isMySelf(content.sourceID) { + if let content = message.attachment as? V2NIMMessageNotificationAttachment { + if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE, + message.senderId == IMKitClient.instance.account() { isLeaveTeamBySelf = true if onCurrentPage { popGroupChatVC() } - } else if content.operationType == .kick, - let targetIDs = content.targetIDs, - targetIDs.contains(IMKitClient.instance.imAccid()) { + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_KICK, + let targetIDs = content.targetIds, + targetIDs.contains(IMKitClient.instance.account()) { // 被移出群聊 isLeaveTeamByOther = true if onCurrentPage { @@ -159,7 +192,7 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD self?.navigationController?.popViewController(animated: true) } } - } else if content.operationType == .dismiss { + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_DISMISS { if isdismissDiscuss { return } @@ -176,35 +209,19 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD } } - // MARK: TeamChatViewModelDelegate + // MARK: - TeamChatViewModelDelegate - open func onTeamRemoved(team: NIMTeam) { - // 多端登录另一端解散、退出讨论组 - if team.isDisscuss() == true { - isdismissDiscuss = true - if onCurrentPage { - popGroupChatVC() - } - return - } - } - - open func onTeamUpdate(team: NIMTeam) { - if team.teamId != viewmodel.session.sessionId { - return - } + /// 群聊更新回调 + /// - Parameter team: 群聊 + public func onTeamUpdate(team: V2NIMTeam) { updateTeamInfo(team: team) } - open func onTeamMemberUpdate(team: NIMTeam) { - didRefreshTable() - } - - override public func onTeamMemberChange(team: NIMTeam) { - if viewmodel.session.sessionId != team.teamId { - return + /// 群成员更新回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberUpdate(_ teamMembers: [V2NIMTeamMember]) { + if let team = ChatTeamCache.shared.getTeamInfo() { + setMute(team: team) } - (viewmodel as? TeamChatViewModel)?.getTeamMember() - updateTeamInfo(team: team) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift index e53c3c03..48deaef4 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift @@ -17,13 +17,13 @@ open class FunUserSettingViewController: NEBaseUserSettingViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funChatBackgroundColor - viewmodel.cellDatas.forEach { cellModel in + for cellModel in viewModel.cellDatas { cellModel.cornerType = .none } } @@ -33,87 +33,87 @@ open class FunUserSettingViewController: NEBaseUserSettingViewController { navigationController?.navigationBar.backgroundColor = .white navigationView.backgroundColor = .white navigationView.titleBarBottomLine.isHidden = false - userHeader.layer.cornerRadius = 4.0 - addBtn.setImage(coreLoader.loadImage("fun_setting_add"), for: .normal) + userHeaderView.layer.cornerRadius = 4.0 + addButton.setImage(coreLoader.loadImage("fun_setting_add"), for: .normal) contentTable.rowHeight = 56 } override open func headerView() -> UIView { - let header = UIView(frame: CGRect(x: 0, y: 0, width: view.width, height: 117)) - header.backgroundColor = .clear - let cornerBack = UIView() - cornerBack.backgroundColor = .white - cornerBack.translatesAutoresizingMaskIntoConstraints = false - header.addSubview(cornerBack) + let headerView = UIView(frame: CGRect(x: 0, y: 0, width: view.width, height: 117)) + headerView.backgroundColor = .clear + let cornerBackView = UIView() + cornerBackView.backgroundColor = .white + cornerBackView.translatesAutoresizingMaskIntoConstraints = false + headerView.addSubview(cornerBackView) NSLayoutConstraint.activate([ - cornerBack.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -8), - cornerBack.leftAnchor.constraint(equalTo: header.leftAnchor), - cornerBack.rightAnchor.constraint(equalTo: header.rightAnchor), - cornerBack.heightAnchor.constraint(equalToConstant: 109.0), + cornerBackView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -8), + cornerBackView.leftAnchor.constraint(equalTo: headerView.leftAnchor), + cornerBackView.rightAnchor.constraint(equalTo: headerView.rightAnchor), + cornerBackView.heightAnchor.constraint(equalToConstant: 109.0), ]) - cornerBack.addSubview(userHeader) + cornerBackView.addSubview(userHeaderView) let tap = UITapGestureRecognizer() - userHeader.addGestureRecognizer(tap) + userHeaderView.addGestureRecognizer(tap) tap.numberOfTapsRequired = 1 tap.numberOfTouchesRequired = 1 - if let url = viewmodel.userInfo?.userInfo?.avatarUrl, !url.isEmpty { - userHeader.sd_setImage(with: URL(string: url), completed: nil) - userHeader.setTitle("") - userHeader.backgroundColor = .clear - } else if let name = viewmodel.userInfo?.shortName(showAlias: false, count: 2) { - userHeader.sd_setImage(with: nil) - userHeader.setTitle(name) - userHeader.backgroundColor = UIColor.colorWithString(string: viewmodel.userInfo?.userId) + if let url = viewModel.userInfo?.user?.avatar, !url.isEmpty { + userHeaderView.sd_setImage(with: URL(string: url), completed: nil) + userHeaderView.setTitle("") + userHeaderView.backgroundColor = .clear + } else if let name = viewModel.userInfo?.shortName(showAlias: false, count: 2) { + userHeaderView.sd_setImage(with: nil) + userHeaderView.setTitle(name) + userHeaderView.backgroundColor = UIColor.colorWithString(string: viewModel.userInfo?.user?.accountId) } - nameLabel.text = viewmodel.userInfo?.showName() - cornerBack.addSubview(nameLabel) + nameLabel.text = viewModel.userInfo?.showName() + cornerBackView.addSubview(nameLabel) if IMKitClient.instance.getConfigCenter().teamEnable { NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: cornerBack.leftAnchor, constant: 22), - userHeader.topAnchor.constraint(equalTo: cornerBack.topAnchor, constant: 22), - userHeader.widthAnchor.constraint(equalToConstant: 50), - userHeader.heightAnchor.constraint(equalToConstant: 50), + userHeaderView.leftAnchor.constraint(equalTo: cornerBackView.leftAnchor, constant: 22), + userHeaderView.topAnchor.constraint(equalTo: cornerBackView.topAnchor, constant: 22), + userHeaderView.widthAnchor.constraint(equalToConstant: 50), + userHeaderView.heightAnchor.constraint(equalToConstant: 50), ]) nameLabel.font = NEConstant.defaultTextFont(12) nameLabel.textAlignment = .center NSLayoutConstraint.activate([ - nameLabel.topAnchor.constraint(equalTo: userHeader.bottomAnchor, constant: 3.0), - nameLabel.centerXAnchor.constraint(equalTo: userHeader.centerXAnchor), - nameLabel.widthAnchor.constraint(equalTo: userHeader.widthAnchor), + nameLabel.topAnchor.constraint(equalTo: userHeaderView.bottomAnchor, constant: 3.0), + nameLabel.centerXAnchor.constraint(equalTo: userHeaderView.centerXAnchor), + nameLabel.widthAnchor.constraint(equalTo: userHeaderView.widthAnchor), ]) - addBtn.addTarget(self, action: #selector(createDiscuss), for: .touchUpInside) - cornerBack.addSubview(addBtn) + addButton.addTarget(self, action: #selector(createDiscuss), for: .touchUpInside) + cornerBackView.addSubview(addButton) NSLayoutConstraint.activate([ - addBtn.leftAnchor.constraint(equalTo: userHeader.rightAnchor, constant: 20.0), - addBtn.topAnchor.constraint(equalTo: userHeader.topAnchor), - addBtn.widthAnchor.constraint(equalToConstant: 50.0), - addBtn.heightAnchor.constraint(equalToConstant: 50.0), + addButton.leftAnchor.constraint(equalTo: userHeaderView.rightAnchor, constant: 20.0), + addButton.topAnchor.constraint(equalTo: userHeaderView.topAnchor), + addButton.widthAnchor.constraint(equalToConstant: 50.0), + addButton.heightAnchor.constraint(equalToConstant: 50.0), ]) } else { NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: cornerBack.leftAnchor, constant: 16), - userHeader.centerYAnchor.constraint(equalTo: cornerBack.centerYAnchor), - userHeader.widthAnchor.constraint(equalToConstant: 60), - userHeader.heightAnchor.constraint(equalToConstant: 60), + userHeaderView.leftAnchor.constraint(equalTo: cornerBackView.leftAnchor, constant: 16), + userHeaderView.centerYAnchor.constraint(equalTo: cornerBackView.centerYAnchor), + userHeaderView.widthAnchor.constraint(equalToConstant: 60), + userHeaderView.heightAnchor.constraint(equalToConstant: 60), ]) nameLabel.font = NEConstant.defaultTextFont(16) nameLabel.textAlignment = .left NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: userHeader.rightAnchor, constant: 16.0), - nameLabel.rightAnchor.constraint(equalTo: cornerBack.rightAnchor), - nameLabel.centerYAnchor.constraint(equalTo: userHeader.centerYAnchor), + nameLabel.leftAnchor.constraint(equalTo: userHeaderView.rightAnchor, constant: 16.0), + nameLabel.rightAnchor.constraint(equalTo: cornerBackView.rightAnchor), + nameLabel.centerYAnchor.constraint(equalTo: userHeaderView.centerYAnchor), ]) } - return header + return headerView } override open func filterStackViewController() -> [UIViewController]? { @@ -126,7 +126,7 @@ open class FunUserSettingViewController: NEBaseUserSettingViewController { } } - override func getPinMessageViewController(session: NIMSession) -> NEBasePinMessageViewController { - FunPinMessageViewController(session: session) + override func getPinMessageViewController(conversationId: String) -> NEBasePinMessageViewController { + FunPinMessageViewController(conversationId: conversationId) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift index bae3339b..4d31f649 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift @@ -10,10 +10,10 @@ public extension ChatRouter { // pin Router.shared.register(PushPinMessageVCRouter) { param in let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let pin = FunPinMessageViewController(session: session) + let pin = FunPinMessageViewController(conversationId: conversationId) nav?.pushViewController(pin, animated: true) } @@ -21,11 +21,11 @@ public extension ChatRouter { Router.shared.register(PushP2pChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let p2pChatVC = FunP2PChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let p2pChatVC = FunP2PChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { @@ -46,12 +46,12 @@ public extension ChatRouter { Router.shared.register(PushTeamChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let groupVC = FunGroupChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let groupVC = FunTeamChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { nav?.viewControllers[i] = groupVC diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift index 38cc86cd..74a253de 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift @@ -18,14 +18,6 @@ public protocol FunChatInputViewDelegate: NSObjectProtocol { @objcMembers open class FunChatInputView: NEBaseChatInputView { - /* - // Only override draw() if you perform custom drawing. - // An empty implementation adversely affects performance during animation. - override func draw(_ rect: CGRect) { - // Drawing code - } - */ - var replyViewTopConstraint: NSLayoutConstraint? weak var funDelegate: FunChatInputViewDelegate? @@ -41,12 +33,12 @@ open class FunChatInputView: NEBaseChatInputView { // public var textViewHeight: NSLayoutConstraint? public var replyBackView: UIView = { - let back = UIView() - back.translatesAutoresizingMaskIntoConstraints = false - back.layer.cornerRadius = 8.0 - back.clipsToBounds = true - back.backgroundColor = UIColor.funChatInputReplyBg - return back + let backView = UIView() + backView.translatesAutoresizingMaskIntoConstraints = false + backView.layer.cornerRadius = 8.0 + backView.clipsToBounds = true + backView.backgroundColor = UIColor.funChatInputReplyBg + return backView }() public lazy var replyLabel: UILabel = { @@ -54,6 +46,7 @@ open class FunChatInputView: NEBaseChatInputView { label.translatesAutoresizingMaskIntoConstraints = false label.backgroundColor = UIColor.clear label.numberOfLines = 2 + label.accessibilityIdentifier = "id.messageReplyInput" return label }() @@ -62,6 +55,7 @@ open class FunChatInputView: NEBaseChatInputView { button.translatesAutoresizingMaskIntoConstraints = false button.backgroundColor = UIColor.clear button.setImage(coreLoader.loadImage("fun_chat_input_reply_clear"), for: .normal) + button.accessibilityIdentifier = "id.clear" return button }() @@ -71,6 +65,7 @@ open class FunChatInputView: NEBaseChatInputView { button.backgroundColor = UIColor.clear button.setImage(coreLoader.loadImage("fun_chat_input_change_record"), for: .normal) button.setImage(coreLoader.loadImage("fun_chat_input_keyboard"), for: .selected) + button.accessibilityIdentifier = "id.changeRecordMode" return button }() @@ -80,6 +75,7 @@ open class FunChatInputView: NEBaseChatInputView { button.backgroundColor = UIColor.clear button.tag = addMoreBtnTag button.setImage(coreLoader.loadImage("fun_chat_input_show_more"), for: .normal) + button.accessibilityIdentifier = "id.inputMore" return button }() @@ -89,6 +85,7 @@ open class FunChatInputView: NEBaseChatInputView { button.backgroundColor = UIColor.clear button.tag = addEmojBtnTag button.setImage(coreLoader.loadImage("fun_chat_input_show_emoj"), for: .normal) + button.accessibilityIdentifier = "id.inputEmoji" return button }() diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift index 8b298f69..623388c4 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift @@ -37,10 +37,10 @@ open class FunRecordAudioView: UIView { }() lazy var lottieContentView: UIView = { - let content = UIView() - content.translatesAutoresizingMaskIntoConstraints = false - content.backgroundColor = UIColor.clear - return content + let contentView = UIView() + contentView.translatesAutoresizingMaskIntoConstraints = false + contentView.backgroundColor = UIColor.clear + return contentView }() public var triangleView: UIView = { @@ -70,13 +70,13 @@ open class FunRecordAudioView: UIView { }() public let recordCloseImage: UIImageView = { - let close = UIImageView() - close.contentMode = .center - close.translatesAutoresizingMaskIntoConstraints = false - close.image = coreLoader.loadImage("fun_chat_record_close_dark") - close.highlightedImage = coreLoader.loadImage("fun_chat_record_close_light") - close.isHighlighted = false - return close + let imageView = UIImageView() + imageView.contentMode = .center + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = coreLoader.loadImage("fun_chat_record_close_dark") + imageView.highlightedImage = coreLoader.loadImage("fun_chat_record_close_light") + imageView.isHighlighted = false + return imageView }() public let releaseToSendLabel: UILabel = { diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift index 7bf3d4be..5e686a62 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift @@ -22,7 +22,7 @@ open class ChatMessageAudioCell: NormalChatMessageBaseCell, ChatAudioCellProtoco } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -95,13 +95,11 @@ open class ChatMessageAudioCell: NormalChatMessageBaseCell, ChatAudioCellProtoco } open func startAnimation(byRight: Bool) { - if byRight { - if !audioImageViewRight.isAnimating { - audioImageViewRight.startAnimating() - } - } else if !audioImageViewLeft.isAnimating { - audioImageViewLeft.startAnimating() + let audioImageView = byRight ? audioImageViewRight : audioImageViewLeft + if !audioImageView.isAnimating { + audioImageView.startAnimating() } + if let m = contentModel as? MessageAudioModel { m.isPlaying = true isPlaying = true @@ -109,13 +107,11 @@ open class ChatMessageAudioCell: NormalChatMessageBaseCell, ChatAudioCellProtoco } open func stopAnimation(byRight: Bool) { - if byRight { - if audioImageViewRight.isAnimating { - audioImageViewRight.stopAnimating() - } - } else if audioImageViewLeft.isAnimating { - audioImageViewLeft.stopAnimating() + let audioImageView = byRight ? audioImageViewRight : audioImageViewLeft + if audioImageView.isAnimating { + audioImageView.stopAnimating() } + if let m = contentModel as? MessageAudioModel { m.isPlaying = false isPlaying = false @@ -140,7 +136,7 @@ open class ChatMessageAudioCell: NormalChatMessageBaseCell, ChatAudioCellProtoco timeLabelLeft.text = "\(m.duration)" + "s" } m.isPlaying ? startAnimation(byRight: isSend) : stopAnimation(byRight: isSend) - messageId = m.message?.messageId + messageId = m.message?.messageClientId } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift index b77bd2ad..a3e5de21 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift @@ -27,7 +27,7 @@ open class ChatMessageCallCell: NormalChatMessageBaseCell { contentLabelLeft.isEnabled = false contentLabelLeft.numberOfLines = 0 contentLabelLeft.isUserInteractionEnabled = false - contentLabelLeft.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + contentLabelLeft.font = messageTextFont contentLabelLeft.textAlignment = .center contentLabelLeft.backgroundColor = .clear contentLabelLeft.accessibilityIdentifier = "id.chatMessageCallText" @@ -45,7 +45,7 @@ open class ChatMessageCallCell: NormalChatMessageBaseCell { contentLabelRight.isEnabled = false contentLabelRight.numberOfLines = 0 contentLabelRight.isUserInteractionEnabled = false - contentLabelRight.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + contentLabelRight.font = messageTextFont contentLabelRight.textAlignment = .center contentLabelRight.backgroundColor = .clear contentLabelRight.accessibilityIdentifier = "id.chatMessageCallText" diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift index 619af615..f250b6b2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift @@ -12,19 +12,19 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { weak var weakModel: MessageFileModel? public lazy var imgViewLeft: UIImageView = { - let view_img = UIImageView() - view_img.translatesAutoresizingMaskIntoConstraints = false - view_img.backgroundColor = .clear - view_img.accessibilityIdentifier = "id.fileType" - return view_img + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = .clear + imageView.accessibilityIdentifier = "id.fileType" + return imageView }() public lazy var stateViewLeft: FileStateView = { - let state = FileStateView() - state.translatesAutoresizingMaskIntoConstraints = false - state.backgroundColor = .clear - state.accessibilityIdentifier = "id.fileStatus" - return state + let stateView = FileStateView() + stateView.translatesAutoresizingMaskIntoConstraints = false + stateView.backgroundColor = .clear + stateView.accessibilityIdentifier = "id.fileStatus" + return stateView }() public lazy var titleLabelLeft: UILabel = { @@ -71,11 +71,11 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { }() public lazy var imgViewRight: UIImageView = { - let view_img = UIImageView() - view_img.translatesAutoresizingMaskIntoConstraints = false - view_img.backgroundColor = .clear - view_img.accessibilityIdentifier = "id.fileType" - return view_img + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = .clear + imageView.accessibilityIdentifier = "id.fileType" + return imageView }() public lazy var stateViewRight: FileStateView = { @@ -135,7 +135,7 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -228,56 +228,55 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { bubbleW?.constant = kScreenWidth <= 320 ? 222 : 242 // 适配小屏幕 - if let fileObject = model.message?.messageObject as? NIMFileObject { + if let fileObject = model.message?.attachment as? V2NIMMessageFileAttachment { if let fileModel = model as? MessageFileModel { weakModel?.cell = nil weakModel = fileModel fileModel.cell = self - fileModel.size = Float(fileObject.fileLength) + fileModel.size = Float(fileObject.size) if fileModel.state == .Success { stateView.state = .FileOpen } else { stateView.state = .FileDownload - stateView.setProgress(fileModel.progress) - if fileModel.progress >= 1 { + stateView.setProgress(Float(fileModel.progress)) + if fileModel.progress >= 100 { fileModel.state = .Success } } } var imageName = "file_unknown" - var displayName = "未知文件" - if let filePath = fileObject.path as? NSString { - displayName = filePath.lastPathComponent - switch filePath.pathExtension.lowercased() { - case file_doc_support: - imageName = "file_doc" - case file_xls_support: - imageName = "file_xls" - case file_img_support: - imageName = "file_img" - case file_ppt_support: - imageName = "file_ppt" - case file_txt_support: - imageName = "file_txt" - case file_audio_support: - imageName = "file_audio" - case file_vedio_support: - imageName = "file_vedio" - case file_zip_support: - imageName = "file_zip" - case file_pdf_support: - imageName = "file_pdf" - case file_html_support: - imageName = "file_html" - case "key", "keynote": - imageName = "file_keynote" - default: - imageName = "file_unknown" - } + let suffix = (fileObject.name as NSString).pathExtension.lowercased() + switch suffix { + case file_doc_support: + imageName = "file_doc" + case file_xls_support: + imageName = "file_xls" + case file_img_support: + imageName = "file_img" + case file_ppt_support: + imageName = "file_ppt" + case file_txt_support: + imageName = "file_txt" + case file_audio_support: + imageName = "file_audio" + case file_vedio_support: + imageName = "file_vedio" + case file_zip_support: + imageName = "file_zip" + case file_pdf_support: + imageName = "file_pdf" + case file_html_support: + imageName = "file_html" + case "key", "keynote": + imageName = "file_keynote" + default: + imageName = "file_unknown" } + imgView.image = UIImage.ne_imageNamed(name: imageName) - titleLabel.text = fileObject.displayName ?? displayName - let size_B = Double(fileObject.fileLength) + titleLabel.text = fileObject.name + + let size_B = Double(fileObject.size) var size_str = String(format: "%.1f B", size_B) if size_B > 1e3 { let size_KB = size_B / 1e3 @@ -295,8 +294,8 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { } } - override open func uploadProgress(byRight: Bool, _ progress: Float) { + override open func uploadProgress(byRight: Bool, _ progress: UInt) { let stateView = byRight ? stateViewRight : stateViewLeft - stateView.setProgress(progress) + stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift index f1d21e87..d08622bb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import NIMSDK +import SDWebImage import UIKit @objcMembers @@ -16,7 +17,7 @@ open class ChatMessageImageCell: NormalChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -79,12 +80,17 @@ open class ChatMessageImageCell: NormalChatMessageBaseCell { super.setModel(model, isSend) let contentImageView = isSend ? contentImageViewRight : contentImageViewLeft - if let m = model as? MessageImageModel, let imageUrl = m.imageUrl { + if let m = model as? MessageImageModel, let imageUrl = m.urlString { + var options: SDWebImageOptions = [.retryFailed] + if let imageObject = model.message?.attachment as? V2NIMMessageImageAttachment, imageObject.ext?.lowercased() != ".gif" { + options = [.retryFailed, .progressiveLoad] + } + if imageUrl.hasPrefix("http") { contentImageView.sd_setImage( with: URL(string: imageUrl), placeholderImage: nil, - options: .retryFailed, + options: options, progress: nil, completed: nil ) diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift index b7305f54..1bd20899 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift @@ -32,12 +32,20 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { label.text = chatLocalizable("no_map_plugin") label.textAlignment = .center label.textColor = UIColor.ne_greyText + label.isHidden = true return label }() public var mapViewLeft: UIView? let backgroundViewLeft = UIView() + public var mapImageViewLeft: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + return imageView + }() + // Right public lazy var titleLabelRight: UILabel = { let label = UILabel() @@ -64,19 +72,41 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { label.text = chatLocalizable("no_map_plugin") label.textAlignment = .center label.textColor = UIColor.ne_greyText + label.isHidden = true return label }() + lazy var pointImageLeft: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = coreLoader.loadImage("location_point") + return imageView + }() + + lazy var pointImageRight: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = coreLoader.loadImage("location_point") + return imageView + }() + public var mapViewRight: UIView? let backgroundViewRight = UIView() + public var mapImageViewRight: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + return imageView + }() + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonUI() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -126,33 +156,26 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { subTitleLabelLeft.topAnchor.constraint(equalTo: titleLabelLeft.bottomAnchor, constant: 4), ]) - if let map = NEChatKitClient.instance.delegate?.getCellMapView?() as? UIView { - mapViewLeft = map - backgroundViewLeft.addSubview(map) - map.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - map.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), - map.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor), - map.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), - map.topAnchor.constraint(equalTo: subTitleLabelLeft.bottomAnchor, constant: 4), - ]) - - let pointImage = UIImageView() - pointImage.translatesAutoresizingMaskIntoConstraints = false - pointImage.image = coreLoader.loadImage("location_point") - map.addSubview(pointImage) - NSLayoutConstraint.activate([ - pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), - pointImage.bottomAnchor.constraint(equalTo: map.bottomAnchor, constant: -30), - ]) - } else { - backgroundViewLeft.addSubview(emptyLabelLeft) - NSLayoutConstraint.activate([ - emptyLabelLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), - emptyLabelLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), - emptyLabelLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor, constant: -40), - ]) - } + backgroundViewLeft.addSubview(mapImageViewLeft) + NSLayoutConstraint.activate([ + mapImageViewLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), + mapImageViewLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor), + mapImageViewLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), + mapImageViewLeft.heightAnchor.constraint(equalToConstant: 86), + ]) + + mapImageViewLeft.addSubview(pointImageLeft) + NSLayoutConstraint.activate([ + pointImageLeft.centerXAnchor.constraint(equalTo: mapImageViewLeft.centerXAnchor), + pointImageLeft.bottomAnchor.constraint(equalTo: mapImageViewLeft.bottomAnchor, constant: -30), + ]) + + backgroundViewLeft.addSubview(emptyLabelLeft) + NSLayoutConstraint.activate([ + emptyLabelLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), + emptyLabelLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), + emptyLabelLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor, constant: -40), + ]) } open func commonUIRight() { @@ -196,33 +219,26 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { subTitleLabelRight.topAnchor.constraint(equalTo: titleLabelRight.bottomAnchor, constant: 4), ]) - if let map = NEChatKitClient.instance.delegate?.getCellMapView?() as? UIView { - mapViewRight = map - backgroundViewRight.addSubview(map) - map.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - map.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), - map.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor), - map.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), - map.topAnchor.constraint(equalTo: subTitleLabelRight.bottomAnchor, constant: 4), - ]) - - let pointImage = UIImageView() - pointImage.translatesAutoresizingMaskIntoConstraints = false - pointImage.image = coreLoader.loadImage("location_point") - map.addSubview(pointImage) - NSLayoutConstraint.activate([ - pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), - pointImage.bottomAnchor.constraint(equalTo: map.bottomAnchor, constant: -30), - ]) - } else { - backgroundViewRight.addSubview(emptyLabelRight) - NSLayoutConstraint.activate([ - emptyLabelRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), - emptyLabelRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), - emptyLabelRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor, constant: -40), - ]) - } + backgroundViewRight.addSubview(mapImageViewRight) + NSLayoutConstraint.activate([ + mapImageViewRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), + mapImageViewRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor), + mapImageViewRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), + mapImageViewRight.heightAnchor.constraint(equalToConstant: 86), + ]) + + mapImageViewRight.addSubview(pointImageRight) + NSLayoutConstraint.activate([ + pointImageRight.centerXAnchor.constraint(equalTo: mapImageViewRight.centerXAnchor), + pointImageRight.bottomAnchor.constraint(equalTo: mapImageViewRight.bottomAnchor, constant: -30), + ]) + + backgroundViewRight.addSubview(emptyLabelRight) + NSLayoutConstraint.activate([ + emptyLabelRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), + emptyLabelRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), + emptyLabelRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor, constant: -40), + ]) } override open func showLeftOrRight(showRight: Bool) { @@ -235,16 +251,25 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { super.setModel(model, isSend) let titleLabel = isSend ? titleLabelRight : titleLabelLeft let subTitleLabel = isSend ? subTitleLabelRight : subTitleLabelLeft - let mapView = isSend ? mapViewRight : mapViewLeft let bubbleW = isSend ? bubbleWRight : bubbleWLeft - - bubbleW?.constant = kScreenWidth <= 320 ? 222 : 242 // 适配小屏幕 + let mapImageView = isSend ? mapImageViewRight : mapImageViewLeft + let emptyLabel = isSend ? emptyLabelRight : emptyLabelLeft + let pointImage = isSend ? pointImageRight : pointImageLeft if let m = model as? MessageLocationModel { titleLabel.text = m.title subTitleLabel.text = m.subTitle - if let lat = m.lat, let lng = m.lng, let map = mapView { - NEChatKitClient.instance.delegate?.setMapviewLocation?(lat: lat, lng: lng, mapview: map) + if let lat = m.lat, let lng = m.lng { + if let url = NEChatKitClient.instance.delegate?.getMapImageUrl?(lat: lat, lng: lng) { + NEALog.infoLog(className(), desc: #function + "location image url = \(url)") + mapImageView.sd_setImage(with: URL(string: url)) + emptyLabel.isHidden = true + pointImage.isHidden = false + } else { + mapImageView.image = UIImage.ne_imageNamed(name: "map_placeholder_image") + emptyLabel.isHidden = false + pointImage.isHidden = true + } } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift index 587fd1ff..6780fb2c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift @@ -17,7 +17,7 @@ open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -171,7 +171,7 @@ open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { super.setModel(model, isSend) - guard let data = NECustomAttachment.dataOfCustomMessage(message: model.message) else { + guard let data = NECustomAttachment.dataOfCustomMessage(model.message?.attachment) else { return } @@ -210,9 +210,9 @@ open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { var contentText = "" if var senderNick = abstracts[i]["senderNick"] as? String { - if senderNick.count > 5 { - // 截取字符串 abcdefg -> ab...fg - let leftEndIndex = senderNick.index(senderNick.startIndex, offsetBy: 2) + if senderNick.count > 7 { + // 截取字符串 abcdefghi -> abcd...hi + let leftEndIndex = senderNick.index(senderNick.startIndex, offsetBy: 4) let rightStartIndex = senderNick.index(senderNick.endIndex, offsetBy: -2) senderNick = senderNick[senderNick.startIndex ..< leftEndIndex] + "..." + senderNick[rightStartIndex ..< senderNick.endIndex] } @@ -255,9 +255,9 @@ open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { // MARK: - lazy load public lazy var backViewLeft: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - return view + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView }() public lazy var titleLabelLeft1: UILabel = { @@ -318,9 +318,9 @@ open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { }() public lazy var backViewRight: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - return view + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView }() public lazy var titleLabelRight1: UILabel = { diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift index 7a38dfa1..f42d32f2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift @@ -31,7 +31,7 @@ open class ChatMessageReplyCell: ChatMessageTextCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func commonUI() { @@ -84,14 +84,21 @@ open class ChatMessageReplyCell: ChatMessageTextCell { override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { let replyLabel = isSend ? replyLabelRight : replyLabelLeft - if let text = model.replyText, + if var text = model.replyText, let font = replyLabel.font { - replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: "| " + text, + // 如果有回复的消息,需要在回复的消息前加上“| ” + if text != chatLocalizable("message_not_found") { + text = "| " + text + } + + replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, font: font, color: replyLabel.textColor) - if let attriText = replyLabel.attributedText { - let textSize = attriText.finalSize(font, CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) - model.contentSize.width = max(textSize.width + chat_content_margin * 2, model.contentSize.width) + replyLabel.accessibilityValue = text + + if let attriText = replyLabel.attributedText, let model = model as? MessageTextModel { + let textSize = NSAttributedString.getRealSize(attriText, font, CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) + model.contentSize.width = max(textSize.width, model.textWidght) + chat_content_margin * 2 } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift index 0ed2ed6d..05567eef 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift @@ -25,7 +25,7 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -70,9 +70,9 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { bubbleImageRight.addSubview(reeditButton) reeditButtonW = reeditButton.widthAnchor.constraint(equalToConstant: 86) + reeditButtonW?.isActive = true NSLayoutConstraint.activate([ reeditButton.leftAnchor.constraint(equalTo: revokeLabelRight.rightAnchor, constant: 8), - reeditButtonW!, reeditButton.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: 0), reeditButton.bottomAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: 0), ]) @@ -87,7 +87,7 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { activityView.isHidden = true readView.isHidden = true - seletedBtn.isHidden = true + selectedButton.isHidden = true pinLabelLeft.isHidden = true pinImageLeft.isHidden = true pinLabelRight.isHidden = true @@ -95,15 +95,17 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { } override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { - if let time = model.message?.timestamp { + let isSend = IMKitClient.instance.isMe(model.message?.senderId) + if let time = model.message?.createTime { let date = Date() let currentTime = date.timeIntervalSince1970 if currentTime - time >= 60 * 2 { model.timeOut = true } } + if isSend, - model.isRevokedText == true, + model.isReedit == true, model.timeOut == false { reeditButtonW?.constant = 86 reeditButton.isHidden = false @@ -121,7 +123,6 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { } func reeditEvent(button: UIButton) { - print(#function) delegate?.didTapReeditButton(self, contentModel) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift index b6c4956f..2c9b514f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift @@ -43,60 +43,60 @@ open class ChatMessageRichTextCell: ChatMessageReplyCell { override open func commonUI() { /// left bubbleImageLeft.addSubview(replyLabelLeft) - replyLabelLeftHeightAnchor = replyLabelLeft.heightAnchor.constraint(equalToConstant: 16.0) + replyLabelLeftHeightAnchor = replyLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + replyLabelLeftHeightAnchor?.isActive = true NSLayoutConstraint.activate([ replyLabelLeft.leadingAnchor.constraint(equalTo: bubbleImageLeft.leadingAnchor, constant: chat_content_margin), replyLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: chat_content_margin), - replyLabelLeftHeightAnchor!, replyLabelLeft.trailingAnchor.constraint(equalTo: bubbleImageLeft.trailingAnchor, constant: -chat_content_margin), ]) bubbleImageLeft.addSubview(titleLabelLeft) titleLabelLeftTopAnchor = titleLabelLeft.topAnchor.constraint(equalTo: replyLabelLeft.bottomAnchor, constant: chat_content_margin) + titleLabelLeftTopAnchor?.isActive = true titleLabelLeftHeightAnchor = titleLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + titleLabelLeftHeightAnchor?.isActive = true NSLayoutConstraint.activate([ titleLabelLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor, constant: -chat_content_margin), titleLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: chat_content_margin), - titleLabelLeftTopAnchor!, - titleLabelLeftHeightAnchor!, ]) bubbleImageLeft.addSubview(contentLabelLeft) contentLabelLeftHeightAnchor = contentLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + contentLabelLeftHeightAnchor?.isActive = true NSLayoutConstraint.activate([ contentLabelLeft.rightAnchor.constraint(equalTo: titleLabelLeft.rightAnchor, constant: 0), contentLabelLeft.leftAnchor.constraint(equalTo: titleLabelLeft.leftAnchor, constant: 0), contentLabelLeft.topAnchor.constraint(equalTo: titleLabelLeft.bottomAnchor, constant: chat_content_margin), - contentLabelLeftHeightAnchor!, ]) /// right bubbleImageRight.addSubview(replyLabelRight) - replyLabelRightHeightAnchor = replyLabelRight.heightAnchor.constraint(equalToConstant: 16.0) + replyLabelRightHeightAnchor = replyLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + replyLabelRightHeightAnchor?.isActive = true NSLayoutConstraint.activate([ replyLabelRight.leadingAnchor.constraint(equalTo: bubbleImageRight.leadingAnchor, constant: chat_content_margin), replyLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: chat_content_margin), - replyLabelRightHeightAnchor!, replyLabelRight.trailingAnchor.constraint(equalTo: bubbleImageRight.trailingAnchor, constant: -chat_content_margin), ]) bubbleImageRight.addSubview(titleLabelRight) titleLabelRightTopAnchor = titleLabelRight.topAnchor.constraint(equalTo: replyLabelRight.bottomAnchor, constant: chat_content_margin) + titleLabelRightTopAnchor?.isActive = true titleLabelRightHeightAnchor = titleLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + titleLabelRightHeightAnchor?.isActive = true NSLayoutConstraint.activate([ titleLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -chat_content_margin), titleLabelRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: chat_content_margin), - titleLabelRightTopAnchor!, - titleLabelRightHeightAnchor!, ]) bubbleImageRight.addSubview(contentLabelRight) contentLabelRightHeightAnchor = contentLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + contentLabelRightHeightAnchor?.isActive = true NSLayoutConstraint.activate([ contentLabelRight.rightAnchor.constraint(equalTo: titleLabelRight.rightAnchor, constant: -0), contentLabelRight.leftAnchor.constraint(equalTo: titleLabelRight.leftAnchor, constant: 0), contentLabelRight.topAnchor.constraint(equalTo: titleLabelRight.bottomAnchor, constant: chat_content_margin), - contentLabelRightHeightAnchor!, ]) } @@ -114,12 +114,12 @@ open class ChatMessageRichTextCell: ChatMessageReplyCell { let titleLabelHeightAnchor = isSend ? titleLabelRightHeightAnchor : titleLabelLeftHeightAnchor let contentLabelHeightAnchor = isSend ? contentLabelRightHeightAnchor : contentLabelLeftHeightAnchor - if let text = model.replyText { - replyLabelHeightAnchor?.constant = 16 - titleLabelTopAnchor?.constant = chat_content_margin - } else { + if model.replyText == nil || model.replyText!.isEmpty { replyLabelHeightAnchor?.constant = 0 titleLabelTopAnchor?.constant = 0 + } else { + replyLabelHeightAnchor?.constant = 16 + titleLabelTopAnchor?.constant = chat_content_margin } if let m = model as? MessageTextModel { diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift index 219e816a..a21ad783 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift @@ -13,7 +13,7 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { label.isEnabled = false label.numberOfLines = 0 label.isUserInteractionEnabled = false - label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + label.font = messageTextFont label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageText" return label @@ -25,7 +25,7 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { label.isEnabled = false label.numberOfLines = 0 label.isUserInteractionEnabled = false - label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + label.font = messageTextFont label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageText" return label @@ -37,7 +37,7 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -69,6 +69,7 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { let contentLabel = isSend ? contentLabelRight : contentLabelLeft if let m = model as? MessageTextModel { contentLabel.attributedText = m.attributeStr + contentLabel.accessibilityValue = m.message?.text } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift index 91a481b5..c9dc61a5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift @@ -3,6 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECommonKit import NIMSDK import UIKit @@ -81,7 +82,7 @@ open class ChatMessageVideoCell: ChatMessageImageCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -130,24 +131,17 @@ open class ChatMessageVideoCell: ChatMessageImageCell { let timeLabel = isSend ? timeLabelRight : timeLabelLeft let stateView = isSend ? stateViewRight : stateViewLeft - if let videoObject = model.message?.messageObject as? NIMVideoObject { - if let path = videoObject.coverPath, FileManager.default.fileExists(atPath: path) { - contentImageView.sd_setImage( - with: URL(fileURLWithPath: path), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } else { - contentImageView.sd_setImage( - with: URL(string: videoObject.coverUrl ?? ""), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } + if let videoObject = model.message?.attachment as? V2NIMMessageVideoAttachment { + // 获取首帧 + let videoUrl = videoObject.url ?? "" + let thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + contentImageView.sd_setImage( + with: URL(string: thumbUrl), + placeholderImage: nil, + options: .retryFailed, + progress: nil, + completed: nil + ) if videoObject.duration > 0 { timeView.isHidden = false @@ -164,8 +158,8 @@ open class ChatMessageVideoCell: ChatMessageImageCell { stateView.state = .VideoPlay } else { stateView.state = .VideoDownload - stateView.setProgress(videoModel.progress) - if videoModel.progress >= 1 { + stateView.setProgress(Float(videoModel.progress)) + if videoModel.progress >= 100 { videoModel.state = .Success } } @@ -173,8 +167,8 @@ open class ChatMessageVideoCell: ChatMessageImageCell { } } - override open func uploadProgress(byRight: Bool, _ progress: Float) { + override open func uploadProgress(byRight: Bool, _ progress: UInt) { let stateView = byRight ? stateViewRight : stateViewLeft - stateView.setProgress(progress) + stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserSettingSelectCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserSettingSelectCell.swift index 796a974a..8d7802f9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserSettingSelectCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserSettingSelectCell.swift @@ -23,8 +23,8 @@ open class UserSettingSelectCell: NEBaseUserSettingSelectCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + arrowImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), ]) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserTableViewCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserTableViewCell.swift index d9f5a332..9c62db07 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserTableViewCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserTableViewCell.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -11,12 +11,12 @@ open class UserTableViewCell: UserBaseTableViewCell { override open func baseCommonUI() { super.baseCommonUI() // avatar - avatarImage.layer.cornerRadius = 21 + avatarImageView.layer.cornerRadius = 21 NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), - avatarImage.widthAnchor.constraint(equalToConstant: 42), - avatarImage.heightAnchor.constraint(equalToConstant: 42), - avatarImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + avatarImageView.widthAnchor.constraint(equalToConstant: 42), + avatarImageView.heightAnchor.constraint(equalToConstant: 42), + avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), ]) titleLabel.font = UIFont.systemFont(ofSize: 16) @@ -27,7 +27,7 @@ open class UserTableViewCell: UserBaseTableViewCell { alpha: 1.0 ) NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 12), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 12), titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35), titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ForwardAlertViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ForwardAlertViewController.swift index 1df9eb9e..d0b8d1cf 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ForwardAlertViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ForwardAlertViewController.swift @@ -11,7 +11,7 @@ import UIKit open class ForwardUserCell: NEBaseForwardUserCell { override func setupUI() { super.setupUI() - userHeader.layer.cornerRadius = 16 + userHeaderView.layer.cornerRadius = 16 } } @@ -19,8 +19,8 @@ open class ForwardUserCell: NEBaseForwardUserCell { open class ForwardAlertViewController: NEBaseForwardAlertViewController { override open func setupUI() { super.setupUI() - oneUserHead.layer.cornerRadius = 16.0 - userCollection.register( + oneUserHeadView.layer.cornerRadius = 16.0 + userCollectionView.register( ForwardUserCell.self, forCellWithReuseIdentifier: "\(ForwardUserCell.self)" ) diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift index 1aa5b508..e645d565 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift @@ -7,15 +7,15 @@ import UIKit @objcMembers open class NormalChatViewController: ChatViewController { - override public init(session: NIMSession) { - super.init(session: session) + override public init(conversationId: String) { + super.init(conversationId: conversationId) navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white cellRegisterDic = ChatMessageHelper.getChatCellRegisterDic(isFun: false) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -39,7 +39,7 @@ open class NormalChatViewController: ChatViewController { } override func getUserSelectVC() -> NEBaseSelectUserViewController { - SelectUserViewController(sessionId: viewmodel.session.sessionId, showSelf: false) + SelectUserViewController(sessionId: viewModel.sessionId, showSelf: false) } open func getMessageModel(model: MessageModel) { @@ -72,7 +72,8 @@ open class NormalChatViewController: ChatViewController { } else { normalInputHeight = 150 } - bottomViewTopAnchor?.constant = -normalInputHeight + + layoutInputViewWithAnimation(offset: 0) checkAndRestoreReplyView() } @@ -87,7 +88,7 @@ open class NormalChatViewController: ChatViewController { // 切换到单行输入框如果有回复显示回复视图 func checkAndRestoreReplyView() { - if viewmodel.isReplying == true, replyView.superview == nil { + if viewModel.isReplying == true, replyView.superview == nil { view.addSubview(replyView) replyView.closeButton.addTarget(self, action: #selector(closeReply), for: .touchUpInside) replyView.translatesAutoresizingMaskIntoConstraints = false diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift index ff2087df..288909d5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift @@ -17,7 +17,7 @@ open class NormalMultiForwardViewController: MultiForwardViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func getMultiForwardViewController(_ messageAttachmentUrl: String?, diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift index eb1b6ad1..c4a1187c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift @@ -9,53 +9,92 @@ import UIKit @objcMembers open class P2PChatViewController: NormalChatViewController { - public init(session: NIMSession, anchor: NIMMessage?) { - super.init(session: session) - viewmodel = ChatViewModel(session: session, anchor: anchor) + /// 重写父类的构造方法 + /// - Parameter conversationId: 会话id + override public init(conversationId: String) { + super.init(conversationId: conversationId) + viewModel = P2PChatViewModel(conversationId: conversationId, anchor: nil) } - override open func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. + /// 重写父类的构造方法 + /// - Parameter conversationId: 会话id + /// - Parameter anchor: 锚点消息 + public init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId) + viewModel = P2PChatViewModel(conversationId: conversationId, anchor: anchor) } - override open func getSessionInfo(session: NIMSession) { - var showName = session.sessionId - ChatUserCache.getUserInfo(session.sessionId) { [weak self] user, error in - if let name = user?.showName() { - showName = name - } + public required init?(coder: NSCoder) { + super.init(coder: coder) + } - self?.title = showName - self?.titleContent = showName - let text = "\(chatLocalizable("send_to"))\(showName)" - let attribute = NSMutableAttributedString(string: text) - let style = NSMutableParagraphStyle() - style.lineBreakMode = .byTruncatingTail - style.alignment = .left - attribute.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: NSMakeRange(0, text.utf16.count)) - attribute.addAttribute(.foregroundColor, value: UIColor.gray, range: NSMakeRange(0, text.utf16.count)) - attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.utf16.count)) - self?.chatInputView.textView.attributedPlaceholder = attribute - self?.chatInputView.textView.setNeedsLayout() + override open var title: String? { + didSet { + super.title = title + let text = "\(chatLocalizable("send_to"))\(titleContent)" + let attribute = getPlaceHolder(text: text) + chatInputView.textView.attributedPlaceholder = attribute + chatInputView.textView.setNeedsLayout() } } - /// 创建个人聊天页构造方法 - /// - Parameter sessionId: 会话id - public init(sessionId: String) { - let session = NIMSession(sessionId, type: .P2P) - super.init(session: session) + private func getPlaceHolder(text: String) -> NSMutableAttributedString { + let attribute = NSMutableAttributedString(string: text) + let style = NSMutableParagraphStyle() + style.lineBreakMode = .byTruncatingTail + style.alignment = .left + attribute.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: NSMakeRange(0, text.utf16.count)) + attribute.addAttribute(.foregroundColor, value: UIColor.gray, range: NSMakeRange(0, text.utf16.count)) + attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.utf16.count)) + return attribute } - /// 重写父类的构造方法 - /// - Parameter session: sessionId - override public init(session: NIMSession) { - super.init(session: session) + override open func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { + chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("send_to")) + super.getSessionInfo(sessionId: sessionId) { [weak self] in + self?.viewModel.loadShowName([sessionId]) { + let name = self?.viewModel.getShowName(sessionId).name ?? sessionId + self?.titleContent = name + self?.title = name + } + completion() + } } - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + /// 重写检查并发送正在输入状态 + /// - Parameter endEdit: 是否停止输入 + override open func checkAndSendTypingState(endEdit: Bool = false) { + guard let viewModel = viewModel as? P2PChatViewModel else { + return + } + + if endEdit { + viewModel.sendInputTypingEndState() + return + } + + if chatInputView.chatInpuMode == .normal { + if let content = chatInputView.textView.text, content.count > 0 { + viewModel.sendInputTypingState() + } else { + viewModel.sendInputTypingEndState() + } + } else { + var title = "" + var content = "" + + if let titleText = chatInputView.titleField.text { + title = titleText + } + + if let contentText = chatInputView.textView.text { + content = contentText + } + if title.count <= 0, content.count <= 0 { + viewModel.sendInputTypingEndState() + } else { + viewModel.sendInputTypingState() + } + } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift index 06781a12..6c0760ea 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift @@ -4,20 +4,20 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @objcMembers open class ReadViewController: NEBaseReadViewController { - override init(message: NIMMessage) { - super.init(message: message) + override init(message: V2NIMMessage, teamId: String) { + super.init(message: message, teamId: teamId) navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func commonUI() { diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift index 9e9db17b..25a81491 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -15,7 +15,7 @@ open class SelectUserViewController: NEBaseSelectUserViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func commonUI() { diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/GroupChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/TeamChatViewController.swift similarity index 61% rename from NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/GroupChatViewController.swift rename to NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/TeamChatViewController.swift index e30091d5..91298d91 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/GroupChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/TeamChatViewController.swift @@ -4,38 +4,38 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @objcMembers -open class GroupChatViewController: NormalChatViewController, TeamChatViewModelDelegate { +open class TeamChatViewController: NormalChatViewController, TeamChatViewModelDelegate { private var isLeaveTeamByOther = false // 是否被移出群聊 private var isLeaveTeamBySelf = false // 是否多端登录另一端退出群聊 private var isdismissTeam = false // 群聊是否已解散 private var isdismissDiscuss = false // 讨论组是否已解散 private var onCurrentPage = false // 是否位于聊天详情页 - public init(session: NIMSession, anchor: NIMMessage?) { - // self.viewmodel = ChatViewModel(session: session) - super.init(session: session) - viewmodel = TeamChatViewModel(session: session, anchor: anchor) - viewmodel.delegate = self + public init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId) + viewModel = TeamChatViewModel(conversationId: conversationId, anchor: anchor) + viewModel.delegate = self } /// 创建群的构造方法 /// - Parameter sessionId: 会话id public init(sessionId: String) { - let session = NIMSession(sessionId, type: .team) - super.init(session: session) + let conversationId = V2NIMConversationIdUtil.teamConversationId(sessionId) ?? "" + super.init(conversationId: conversationId) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } deinit { NotificationCenter.default.removeObserver(self) + ChatTeamCache.shared.removeAllTeamInfo() } override open func viewWillAppear(_ animated: Bool) { @@ -74,11 +74,23 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD NotificationCenter.default.addObserver(self, selector: #selector(popGroupChatVC), name: NENotificationName.popGroupChatVC, object: nil) } - override open func getSessionInfo(session: NIMSession) { - if let vm = viewmodel as? TeamChatViewModel { - if let t = vm.getTeam(teamId: session.sessionId) { - updateTeamInfo(team: t) + override open func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { + chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("send_to")) + super.getSessionInfo(sessionId: sessionId) { [weak self] in + + if let vm = self?.viewModel as? TeamChatViewModel { + vm.getTeamInfo(teamId: sessionId) { error, team in + if let team = team { + if team.isValidTeam == false { + self?.showSingleAlert(message: coreLoader.localizable("team_not_exist")) { + self?.popGroupChatVC() + } + } + self?.updateTeamInfo(team: team) + } + } } + completion() } } @@ -113,10 +125,30 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD return attribute } - open func updateTeamInfo(team: NIMTeam) { - title = team.getShowName() + open func updateTeamTitle(_ noti: Notification) { + if let tid = noti.userInfo?["teamId"] as? String, + tid == viewModel.sessionId, + let team = ChatTeamCache.shared.getTeamInfo() { + updateTeamInfo(team: team) + } + } + + /// 更新群聊信息(群聊名称、群禁言状态、缓存) + /// - Parameter team: 群聊信息 + open func updateTeamInfo(team: V2NIMTeam) { + title = team.name + ChatTeamCache.shared.updateTeamInfo(team) + setMute(team: team) + } + + /// 设置群禁言/取消群禁言状态 + /// - Parameter team: 群聊信息 + open func setMute(team: V2NIMTeam) { + guard let viewModel = viewModel as? TeamChatViewModel else { + return + } - if team.inAllMuteMode(), viewmodel.teamMember?.type != .manager, viewmodel.teamMember?.type != .owner { + if team.chatBannedMode == .TEAM_CHAT_BANNED_MODE_BANNED_ALL || (team.chatBannedMode == .TEAM_CHAT_BANNED_MODE_BANNED_NORMAL && viewModel.teamMember?.memberRole == .TEAM_MEMBER_ROLE_NORMAL) { // 群禁言 isMute = true chatInputView.textView.isEditable = false @@ -134,7 +166,7 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD // 解除群禁言 isMute = false chatInputView.textView.isEditable = true - chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: "\(chatLocalizable("send_to"))\(team.getShowName())") + chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: "\(chatLocalizable("send_to"))\(team.name)") chatInputView.textView.backgroundColor = .white chatInputView.stackView.isUserInteractionEnabled = true @@ -142,20 +174,19 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD } } - override open func onRecvMessages(_ messages: [NIMMessage]) { + override open func onRecvMessages(_ messages: [V2NIMMessage]) { super.onRecvMessages(messages) for message in messages { - if let object = message.messageObject as? NIMNotificationObject, - let content = object.content as? NIMTeamNotificationContent { - if content.operationType == .leave, - IMKitClient.instance.isMySelf(content.sourceID) { + if let content = message.attachment as? V2NIMMessageNotificationAttachment { + if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE, + message.senderId == IMKitClient.instance.account() { isLeaveTeamBySelf = true if onCurrentPage { popGroupChatVC() } - } else if content.operationType == .kick, - let targetIDs = content.targetIDs, - targetIDs.contains(IMKitClient.instance.imAccid()) { + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_KICK, + let targetIDs = content.targetIds, + targetIDs.contains(IMKitClient.instance.account()) { // 被移出群聊 isLeaveTeamByOther = true if onCurrentPage { @@ -163,7 +194,7 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD self?.navigationController?.popViewController(animated: true) } } - } else if content.operationType == .dismiss { + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_DISMISS { if isdismissDiscuss { return } @@ -180,35 +211,19 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD } } - // MARK: TeamChatViewModelDelegate + // MARK: - TeamChatViewModelDelegate - open func onTeamRemoved(team: NIMTeam) { - // 多端登录另一端解散、退出讨论组 - if team.isDisscuss() == true { - isdismissDiscuss = true - if onCurrentPage { - popGroupChatVC() - } - return - } - } - - open func onTeamUpdate(team: NIMTeam) { - if team.teamId != viewmodel.session.sessionId { - return - } + /// 群聊更新回调 + /// - Parameter team: 群聊 + public func onTeamUpdate(team: V2NIMTeam) { updateTeamInfo(team: team) } - open func onTeamMemberUpdate(team: NIMTeam) { - didRefreshTable() - } - - override public func onTeamMemberChange(team: NIMTeam) { - if viewmodel.session.sessionId != team.teamId { - return + /// 群成员更新回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberUpdate(_ teamMembers: [V2NIMTeamMember]) { + if let team = ChatTeamCache.shared.getTeamInfo() { + setMute(team: team) } - (viewmodel as? TeamChatViewModel)?.getTeamMember() - updateTeamInfo(team: team) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift index c2b1c882..15afb122 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift @@ -20,15 +20,15 @@ open class UserSettingViewController: NEBaseUserSettingViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func setupUI() { super.setupUI() - userHeader.layer.cornerRadius = IMKitClient.instance.getConfigCenter().teamEnable ? 21.0 : 30.0 + userHeaderView.layer.cornerRadius = IMKitClient.instance.getConfigCenter().teamEnable ? 21.0 : 30.0 } - override func getPinMessageViewController(session: NIMSession) -> NEBasePinMessageViewController { - PinMessageViewController(session: session) + override func getPinMessageViewController(conversationId: String) -> NEBasePinMessageViewController { + PinMessageViewController(conversationId: conversationId) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift index 932a3f18..6dfe612d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift @@ -10,24 +10,30 @@ public extension ChatRouter { // pin Router.shared.register(PushPinMessageVCRouter) { param in let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let pin = PinMessageViewController(session: session) + let pin = PinMessageViewController(conversationId: conversationId) nav?.pushViewController(pin, animated: true) } // sendMessage Router.shared.register(ChatAddFriendRouter) { param in if let text = param["text"] as? String, - let sessionId = param["sessionId"] as? String, - let sessionType = param["sessionType"] as? NIMSessionType { - let msg = NIMMessage() + let sessionId = param["conversationId"] as? String { + let msg = V2NIMMessage() msg.text = text - let session = NIMSession(sessionId, type: sessionType) - NIMSDK.shared().chatManager.send(msg, to: session) { error in - if let err = error { - NELog.errorLog("ChatAddFriendRouter", desc: "send P2P message error:\(err.localizedDescription)") - } + + let config = V2NIMMessageConfig() + config.lastMessageUpdateEnabled = true + config.onlineSyncEnabled = true + config.unreadEnabled = true + let param = V2NIMSendMessageParams() + param.messageConfig = MessageUtils.messageConfig() + NIMSDK.shared().v2MessageService.send(msg, conversationId: sessionId, params: param) { result in + + } failure: { error in + NEALog.errorLog("ChatAddFriendRouter", desc: "send P2P message error:\(error.nserror.localizedDescription)") + } progress: { _ in } } } @@ -36,11 +42,11 @@ public extension ChatRouter { Router.shared.register(PushP2pChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let p2pChatVC = P2PChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let p2pChatVC = P2PChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { @@ -61,12 +67,12 @@ public extension ChatRouter { Router.shared.register(PushTeamChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let groupVC = GroupChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let groupVC = TeamChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { nav?.viewControllers[i] = groupVC diff --git a/NEContactUIKit/NEContactUIKit.podspec b/NEContactUIKit/NEContactUIKit.podspec index 39a52c66..412c6bcb 100644 --- a/NEContactUIKit/NEContactUIKit.podspec +++ b/NEContactUIKit/NEContactUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEContactUIKit' - s.version = '9.7.0' + s.version = '10.0.0-beta' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. @@ -34,5 +34,5 @@ Pod::Spec.new do |s| s.resource = 'NEContactUIKit/Assets/**/*' s.dependency 'NEChatKit' s.dependency 'NECommonUIKit' - + s.dependency 'MJRefresh' end diff --git a/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings b/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings index 52e6ae8a..6bc62318 100644 --- a/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings +++ b/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings @@ -4,7 +4,6 @@ // found in the LICENSE file. "contact"="Contact"; -"search"="Search"; "alert_tip"="tip"; "alert_sure"="ok"; "alert_cancel"="cancel"; @@ -30,6 +29,9 @@ "send_friend_apply"="Contact request sent"; "validation_message"="Verify message"; "no_validation_message"="No Validation Message"; +"add_request"="Friend request"; +"agreed_request"="Approved your apply"; +"refused_request"="Reject your apply"; "clear"="Clear"; "agreed"="Added"; "refused"="Rejected"; diff --git a/NEContactUIKit/NEContactUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NEContactUIKit/NEContactUIKit/Assets/zh-Hans.lproj/Localizable.strings index 0c42b4d0..cb04eded 100644 --- a/NEContactUIKit/NEContactUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEContactUIKit/NEContactUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -4,7 +4,6 @@ // found in the LICENSE file. "contact"="通讯录"; -"search"="搜索"; "alert_tip"="提示"; "alert_sure"="确定"; "alert_cancel"="取消"; @@ -30,6 +29,9 @@ "send_friend_apply"="好友申请已发送"; "validation_message"="验证消息"; "no_validation_message"="暂无验证消息"; +"add_request"="添加您为好友"; +"agreed_request"="同意了你的好友请求"; +"refused_request"="拒绝了你的好友请求"; "clear"="清空"; "agreed"="已同意"; "refused"="已拒绝"; diff --git a/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift index 88fc284c..0ee70357 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift @@ -7,21 +7,21 @@ import UIKit @objcMembers open class NEBaseContactViewCell: UITableViewCell { - public lazy var avatarImage: UIImageView = { - let avatar = UIImageView() - avatar.translatesAutoresizingMaskIntoConstraints = false - avatar.addSubview(nameLabel) - avatar.clipsToBounds = true - avatar.contentMode = .scaleAspectFill - avatar.backgroundColor = UIColor.colorWithNumber(number: 0) - avatar.accessibilityIdentifier = "id.avatar" + public lazy var avatarImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.addSubview(nameLabel) + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + imageView.backgroundColor = UIColor.colorWithNumber(number: 0) + imageView.accessibilityIdentifier = "id.avatar" NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: avatar.leftAnchor, constant: 1), - nameLabel.rightAnchor.constraint(equalTo: avatar.rightAnchor, constant: -1), - nameLabel.centerXAnchor.constraint(equalTo: avatar.centerXAnchor), - nameLabel.centerYAnchor.constraint(equalTo: avatar.centerYAnchor), + nameLabel.leftAnchor.constraint(equalTo: imageView.leftAnchor, constant: 1), + nameLabel.rightAnchor.constraint(equalTo: imageView.rightAnchor, constant: -1), + nameLabel.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + nameLabel.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), ]) - return avatar + return imageView }() public lazy var redAngleView: RedAngleLabel = { @@ -40,14 +40,14 @@ open class NEBaseContactViewCell: UITableViewCell { }() public lazy var nameLabel: UILabel = { - let name = UILabel() - name.translatesAutoresizingMaskIntoConstraints = false - name.textColor = .white - name.textAlignment = .center - name.font = UIFont.systemFont(ofSize: 14.0) - name.adjustsFontSizeToFitWidth = true - name.accessibilityIdentifier = "id.noAvatar" - return name + let nameLabel = UILabel() + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.textColor = .white + nameLabel.textAlignment = .center + nameLabel.font = UIFont.systemFont(ofSize: 14.0) + nameLabel.adjustsFontSizeToFitWidth = true + nameLabel.accessibilityIdentifier = "id.noAvatar" + return nameLabel }() public lazy var titleLabel: UILabel = { @@ -82,8 +82,8 @@ open class NEBaseContactViewCell: UITableViewCell { } open func setupCommonCircleHeader() { - contentView.addSubview(avatarImage) - leftConstraint = avatarImage.leftAnchor.constraint( + contentView.addSubview(avatarImageView) + leftConstraint = avatarImageView.leftAnchor.constraint( equalTo: contentView.leftAnchor, constant: 20 ) diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift index 7b9b28ab..3141fb73 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objc @@ -14,14 +14,22 @@ protocol BlackListCellDelegate: AnyObject { open class NEBaseBlackListCell: NEBaseTeamTableViewCell { weak var delegate: BlackListCellDelegate? var index = 0 - private var model: NEKitUser? + private var model: NEUserWithFriend? var button = UIButton() + + private lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.ne_greyLine + return view + }() + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func commonUI() { @@ -55,11 +63,11 @@ open class NEBaseBlackListCell: NEBaseTeamTableViewCell { } func buttonEvent(sender: UIButton) { - delegate?.removeUser(account: model?.userId, index: index) + delegate?.removeUser(account: model?.user?.accountId, index: index) } override open func setModel(_ model: Any) { - guard let user = model as? NEKitUser else { + guard let user = model as? NEUserWithFriend else { return } self.model = user @@ -68,21 +76,14 @@ open class NEBaseBlackListCell: NEBaseTeamTableViewCell { titleLabel.text = user.showName() // avatar - if let imageUrl = user.userInfo?.avatarUrl, !imageUrl.isEmpty { + if let imageUrl = user.user?.avatar, !imageUrl.isEmpty { nameLabel.text = "" - avatarImage.sd_setImage(with: URL(string: imageUrl), completed: nil) - avatarImage.backgroundColor = .clear + avatarImageView.sd_setImage(with: URL(string: imageUrl), completed: nil) + avatarImageView.backgroundColor = .clear } else { nameLabel.text = user.shortName(showAlias: false, count: 2) - avatarImage.image = nil - avatarImage.backgroundColor = UIColor.colorWithString(string: user.userId) + avatarImageView.image = nil + avatarImageView.backgroundColor = UIColor.colorWithString(string: user.user?.accountId) } } - - private lazy var bottomLine: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.ne_greyLine - return view - }() } diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift index ba573f1b..0dfddbfe 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift @@ -4,18 +4,32 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @objcMembers open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, - BlackListCellDelegate, UIGestureRecognizerDelegate { + BlackListCellDelegate, UIGestureRecognizerDelegate, BlackListViewModelDelegate { public let navigationView = NENavigationView() var tableView = UITableView(frame: .zero, style: .plain) var viewModel = BlackListViewModel() - public var blackList: [NEKitUser]? - var className = "BlackListBaseViewController" + + public lazy var headView: UIView = { + let headView = + UIView(frame: CGRect(x: 0, y: 0, width: Int(NEConstant.screenWidth), height: 40)) + return headView + }() + + public lazy var contentLabel: UILabel = { + let contentLabel = + UILabel(frame: CGRect(x: 20, y: 0, width: Int(NEConstant.screenWidth) - 20, height: 40)) + contentLabel.text = localizable("black_tip") + contentLabel.textColor = UIColor.ne_emptyTitleColor + contentLabel.font = UIFont.systemFont(ofSize: 14) + contentLabel.accessibilityIdentifier = "id.tips" + return contentLabel + }() override open func viewDidLoad() { super.viewDidLoad() @@ -32,6 +46,7 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, loadData() } + /// UI 初始化 func commonUI() { title = localizable("blacklist") navigationView.navTitle.text = title @@ -79,25 +94,17 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - let headView = - UIView(frame: CGRect(x: 0, y: 0, width: Int(NEConstant.screenWidth), height: 40)) - let contentLabel = - UILabel(frame: CGRect(x: 20, y: 0, width: Int(NEConstant.screenWidth) - 20, height: 40)) - contentLabel.text = localizable("black_tip") - contentLabel.textColor = UIColor.ne_emptyTitleColor - contentLabel.font = UIFont.systemFont(ofSize: 14) - contentLabel.accessibilityIdentifier = "id.tips" headView.addSubview(contentLabel) tableView.tableHeaderView = headView } func loadData() { - blackList = viewModel.getBlackList() + viewModel.getBlackList() tableView.reloadData() } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - blackList?.count ?? 0 + viewModel.blockList.count } open func tableView(_ tableView: UITableView, @@ -117,33 +124,20 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, let contactSelectVC = getContactSelectVC() navigationController?.pushViewController(contactSelectVC, animated: true) contactSelectVC.callBack = { [weak self] selectMemberarray in - var users = [NEKitUser]() - selectMemberarray.forEach { memberInfo in + var users = [NEUserWithFriend]() + for memberInfo in selectMemberarray { if let u = memberInfo.user { users.append(u) } } - return self?.addBlackUsers(users: users) + self?.addBlackUsers(users: users) } } - func addBlackUsers(users: [NEKitUser]) { - var num = users.count - var suc = [NEKitUser]() - for user in users { - viewModel.addBlackList(account: user.userId ?? "") { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.className ?? "BlackListViewController"), - desc: "CALLBACK addBlackList " + (error?.localizedDescription ?? "no error") - ) - if error == nil { - suc.append(user) - } - num -= 1 - if num == 0 { - print("add black finished") - self?.loadData() - } + func addBlackUsers(users: [NEUserWithFriend]) { + viewModel.addBlackList(users: users) { [weak self] error in + if let err = error { + self?.showToast(err.localizedDescription) } } } @@ -160,30 +154,36 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, guard let acc = account else { return } - viewModel.removeFromBlackList(account: acc) { error in - NELog.infoLog( - ModuleName + " " + self.className, - desc: "CALLBACK removeFromBlackList " + (error?.localizedDescription ?? "no error") - ) - // 1.当前页面刷新 - if error == nil { - self.blackList?.remove(at: index) - self.tableView.reloadData() - } else { - print("removeFromBlackList error:\(error!)") + + viewModel.removeFromBlackList(account: acc) { [weak self] error in + if let err = error { + self?.showToast(err.localizedDescription) } } } -} -// MARK: FriendProviderDelegate + // MARK: BlackListViewModelDelegate -extension NEBaseBlackListViewController: FriendProviderDelegate { - public func onFriendChanged(user: NECoreIMKit.NEKitUser) {} + /// 重新加载表格 + public func tableViewReload() { + tableView.reloadData() + } - public func onUserInfoChanged(user: NECoreIMKit.NEKitUser) {} + /// 重新加载单元格 + /// - Parameter indexs: 单元格位置 + public func tableViewReload(_ indexs: [IndexPath]) { + tableView.reloadData(indexs) + } - public func onBlackListChanged() { - loadData() + /// 删除单元格 + /// - Parameter indexs: 单元格位置 + public func tableViewDelete(_ indexs: [IndexPath]) { + tableView.deleteData(indexs) + } + + /// 插入单元格 + /// - Parameter indexs: 单元格位置 + public func tableViewInsert(_ indexs: [IndexPath]) { + tableView.insertData(indexs) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift index 93c45992..7304d596 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift @@ -4,60 +4,135 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit +public protocol BlackListViewModelDelegate: NSObjectProtocol { + func tableViewReload() + func tableViewReload(_ indexs: [IndexPath]) + func tableViewDelete(_ indexs: [IndexPath]) + func tableViewInsert(_ indexs: [IndexPath]) +} + @objcMembers -open class BlackListViewModel: NSObject, FriendProviderDelegate { +open class BlackListViewModel: NSObject, NEContactListener { var contactRepo = ContactRepo.shared - public weak var delegate: FriendProviderDelegate? - private let className = "BlackListViewModel" + public var blockList = [NEUserWithFriend]() + public weak var delegate: BlackListViewModelDelegate? override init() { - NELog.infoLog(ModuleName + " " + className, desc: #function) super.init() - contactRepo.addContactDelegate(delegate: self) + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + contactRepo.addContactListener(self) } - func getBlackList() -> [NEKitUser]? { - NELog.infoLog(ModuleName + " " + className, desc: #function) - return contactRepo.getBlackList() + /// 获取黑名单列表 + func getBlackList() { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + blockList = NEFriendUserCache.shared.getBlocklist().map(\.value) } + /// 移除黑名单 + /// - Parameters: + /// - account: 好友 Id + /// - index: 该用户在表格中的位置 + /// - completion: 回调 func removeFromBlackList(account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account:\(account)") - contactRepo.removeBlackList(account: account, completion) + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", account:\(account)") + contactRepo.removeBlockList(accountId: account) { error in + if let err = error as? NSError { + NEALog.errorLog(ModuleName + " " + BlackListViewModel.className(), desc: #function + ", error:\(err)") + completion(err) + } else { + NEFriendUserCache.shared.removeBlockAccount(account) + completion(nil) + } + } } - func addBlackList(account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account:\(account)") - contactRepo.addBlackList(account: account, completion) + /// 将好友添加到黑名单 + /// - Parameters: + /// - users: 好友列表 + /// - completion: 回调 + func addBlackList(users: [NEUserWithFriend], _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", users.count:\(users.count)") + + for (i, user) in users.enumerated() { + contactRepo.addBlockList(accountId: user.user?.accountId ?? "") { error in + if let err = error as? NSError { + NEALog.errorLog(ModuleName + " " + BlackListViewModel.className(), desc: #function + ", error:\(err)") + completion(err) + } else { + completion(nil) + } + } + } } - // MARK: callback + // MARK: - NEContactListener - public func onFriendChanged(user: NEKitUser) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:\(user.userId ?? "nil")") - delegate?.onFriendChanged(user: user) + /// 用户信息变更回调 + /// - Parameter users: 用户列表 + public func onUserProfileChanged(_ users: [V2NIMUser]) { + for user in users { + for (index, friendUser) in blockList.enumerated() { + if friendUser.user?.accountId == user.accountId { + friendUser.user = user + delegate?.tableViewReload([IndexPath(row: index, section: 0)]) + break + } + } + } } - public func onUserInfoChanged(user: NEKitUser) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:\(user.userId ?? "nil")") - delegate?.onUserInfoChanged(user: user) + /// 黑名单添加回调 + /// - Parameter user: 用户信息 + public func onBlockListAdded(_ user: V2NIMUser) { + guard let accountId = user.accountId else { return } + + // 黑名单中已存在 + if blockList.contains(where: { $0.user?.accountId == user.accountId }) { + return + } + + let blockUser = NEFriendUserCache.shared.getFriendInfo(accountId) ?? NEUserWithFriend(user: user) + let index = IndexPath(row: blockList.count, section: 0) + blockList.append(blockUser) + delegate?.tableViewInsert([index]) } - public func onBlackListChanged() { - NELog.infoLog(ModuleName + " " + className, desc: #function) - delegate?.onBlackListChanged() + /// 黑名单移除回调 + /// - Parameter accountId: 好友 Id + public func onBlockListRemoved(_ accountId: String) { + for (index, friendUser) in blockList.enumerated() { + if friendUser.user?.accountId == accountId { + blockList.remove(at: index) + delegate?.tableViewDelete([IndexPath(row: index, section: 0)]) + break + } + } } - public func onRecieveNotification(notification: NENotification) { - NELog.infoLog(ModuleName + " " + className, desc: #function) - print(#file + #function) + /// 删除好友通知 + /// 本端删除好友,多端同步 + /// - Parameters: + /// - accountId: 删除的好友账号ID + /// - deletionType: 好友删除的类型 + public func onFriendDeleted(_ accountId: String, deletionType: V2NIMFriendDeletionType) { + if NEFriendUserCache.shared.isBlockAccount(accountId) { + onBlockListRemoved(accountId) + } } - public func onNotificationUnreadCountChanged(count: Int) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", count:\(count)") - print(#file + #function) + /// 好友信息变更回调 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + for (index, friendUser) in blockList.enumerated() { + if friendUser.user?.accountId == friendInfo.accountId { + friendUser.friend = friendInfo + delegate?.tableViewReload([IndexPath(row: index, section: 0)]) + break + } + } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift b/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift index addd0380..5a16230e 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK diff --git a/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift b/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift index ef846f09..2619a5da 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import UIKit + /// 头像枚举类型 @objc public enum NEContactAvatarType: Int { case rectangle = 1 // 矩形 diff --git a/NEContactUIKit/NEContactUIKit/Classes/Extension/ContactImageExtension.swift b/NEContactUIKit/NEContactUIKit/Classes/Extension/ContactImageExtension.swift index 90494e69..77085ecb 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Extension/ContactImageExtension.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Extension/ContactImageExtension.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import Foundation + public extension UIImage { class func ne_imageNamed(name: String?) -> UIImage? { guard let imageName = name, !imageName.isEmpty else { diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunBlackListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunBlackListCell.swift index afc57415..974d723d 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunBlackListCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunBlackListCell.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -19,15 +19,15 @@ class FunBlackListCell: NEBaseBlackListCell { button.layer.borderColor = UIColor(hexString: "#D9D9D9").cgColor button.setTitleColor(.ne_darkText, for: .normal) - avatarImage.layer.cornerRadius = 4 - avatarImage.updateLayoutConstraint(firstItem: avatarImage, seconedItem: nil, attribute: .width, constant: 40) - avatarImage.updateLayoutConstraint(firstItem: avatarImage, seconedItem: nil, attribute: .height, constant: 40) + avatarImageView.layer.cornerRadius = 4 + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .width, constant: 40) + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .height, constant: 40) titleLabel.textColor = .ne_darkText contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 1), diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift index c55db64e..0ecc0589 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift @@ -10,18 +10,18 @@ open class FunContactSelectedCell: NEBaseContactSelectedCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 40), - avatarImage.heightAnchor.constraint(equalToConstant: 40), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } override open func commonUI() { super.commonUI() - sImage.highlightedImage = UIImage.ne_imageNamed(name: "fun_select") + sImageView.highlightedImage = UIImage.ne_imageNamed(name: "fun_select") NSLayoutConstraint.activate([ - sImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - sImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + sImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + sImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), ]) bottomLine.backgroundColor = .funContactLineBorderColor @@ -29,11 +29,11 @@ open class FunContactSelectedCell: NEBaseContactSelectedCell { override open func initSubviewsLayout() { if NEKitContactConfig.shared.ui.contactProperties.avatarType == .rectangle { - avatarImage.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius + avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius } else if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { - avatarImage.layer.cornerRadius = 20.0 + avatarImageView.layer.cornerRadius = 20.0 } else { - avatarImage.layer.cornerRadius = 4.0 // Fun UI + avatarImageView.layer.cornerRadius = 4.0 // Fun UI } } @@ -44,6 +44,6 @@ open class FunContactSelectedCell: NEBaseContactSelectedCell { override open func setModel(_ model: ContactInfo) { super.setModel(model) - arrow.isHidden = true + arrowImageView.isHidden = true } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift index bd6c2984..f4fc3abe 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -13,26 +13,26 @@ open class FunContactTableViewCell: NEBaseContactTableViewCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 40), - avatarImage.heightAnchor.constraint(equalToConstant: 40), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } override open func commonUI() { super.commonUI() bottomLine.backgroundColor = .funContactLineBorderColor - contentView.removeLayoutConstraint(firstItem: redAngleView, seconedItem: arrow, attribute: .right) + contentView.removeLayoutConstraint(firstItem: redAngleView, seconedItem: arrowImageView, attribute: .right) redAngleView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10).isActive = true } override open func initSubviewsLayout() { if NEKitContactConfig.shared.ui.contactProperties.avatarType == .rectangle { - avatarImage.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius + avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius } else if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { - avatarImage.layer.cornerRadius = 20.0 + avatarImageView.layer.cornerRadius = 20.0 } else { - avatarImage.layer.cornerRadius = 4.0 // Fun UI + avatarImageView.layer.cornerRadius = 4.0 // Fun UI } } @@ -43,6 +43,6 @@ open class FunContactTableViewCell: NEBaseContactTableViewCell { override open func setModel(_ model: ContactInfo) { super.setModel(model) - arrow.isHidden = true + arrowImageView.isHidden = true } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactUnCheckCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactUnCheckCell.swift index 2050b4eb..3da6a12e 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactUnCheckCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactUnCheckCell.swift @@ -9,12 +9,12 @@ import UIKit open class FunContactUnCheckCell: NEBaseContactUnCheckCell { override func setupUI() { super.setupUI() - avatarImage.layer.cornerRadius = 4 + avatarImageView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - avatarImage.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - avatarImage.widthAnchor.constraint(equalToConstant: 40), - avatarImage.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), ]) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSystemNotificationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSystemNotificationCell.swift index 78a23e22..58d80a40 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSystemNotificationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSystemNotificationCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -13,11 +13,11 @@ import UIKit open class FunSystemNotificationCell: NEBaseSystemNotificationCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() - avatarImage.layer.cornerRadius = 4 + avatarImageView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 40), - avatarImage.heightAnchor.constraint(equalToConstant: 40), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } @@ -26,8 +26,8 @@ open class FunSystemNotificationCell: NEBaseSystemNotificationCell { contentView.updateLayoutConstraint(firstItem: line, seconedItem: contentView, attribute: .right, constant: 0) line.backgroundColor = .funContactLineBorderColor - agreeBtn.backgroundColor = .funContactThemeColor - agreeBtn.setTitleColor(.white, for: .normal) - agreeBtn.layer.borderColor = UIColor.funContactThemeColor.cgColor + agreeButton.backgroundColor = .funContactThemeColor + agreeButton.setTitleColor(.white, for: .normal) + agreeButton.layer.borderColor = UIColor.funContactThemeColor.cgColor } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunTeamTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunTeamTableViewCell.swift index 5b940dfe..b09b6b17 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunTeamTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunTeamTableViewCell.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -16,15 +16,15 @@ open class FunTeamTableViewCell: NEBaseTeamTableViewCell { override func commonUI() { super.commonUI() - avatarImage.layer.cornerRadius = 4 - avatarImage.updateLayoutConstraint(firstItem: avatarImage, seconedItem: nil, attribute: .width, constant: 40) - avatarImage.updateLayoutConstraint(firstItem: avatarImage, seconedItem: nil, attribute: .height, constant: 40) + avatarImageView.layer.cornerRadius = 4 + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .width, constant: 40) + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .height, constant: 40) titleLabel.textColor = .ne_darkText contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 1), diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift index 508b2a8f..7c8171f8 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK @@ -32,10 +32,10 @@ public extension ContactRouter { Router.shared.register(ContactUserInfoPageRouter) { param in if let nav = param["nav"] as? UINavigationController { - if let user = param["user"] as? NEKitUser { + if let user = param["user"] as? NEUserWithFriend { let userInfoVC = FunContactUserViewController(user: user) nav.pushViewController(userInfoVC, animated: true) - } else if let nimUser = param["nim_user"] as? NEKitUser { + } else if let nimUser = param["nim_user"] as? NEUserWithFriend { let userInfoVC = FunContactUserViewController(user: nimUser) nav.pushViewController(userInfoVC, animated: true) } else if let uid = param["uid"] as? String { diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift index 3ec4e7df..b90837ed 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift @@ -2,14 +2,14 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers open class FunUserInfoHeaderView: NEBaseUserInfoHeaderView { override open func commonUI() { super.commonUI() - avatarImage.layer.cornerRadius = 4 + avatarImageView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ lineView.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), @@ -19,24 +19,24 @@ open class FunUserInfoHeaderView: NEBaseUserInfoHeaderView { ]) } - override open func setData(user: NEKitUser?) { + override open func setData(user: NEUserWithFriend?) { super.setData(user: user) - guard let u = user else { + guard let friendUser = user else { return } // title - if let alias = u.alias, !alias.isEmpty { + if let alias = friendUser.friend?.alias, !alias.isEmpty { commonUI(showDetail: true) titleLabel.text = alias - let uid = u.userId ?? "" - detailLabel.text = "\(localizable("nick")):\(u.userInfo?.nickName ?? uid)" + let uid = friendUser.user?.accountId ?? "" + detailLabel.text = "\(localizable("nick")):\(friendUser.user?.name ?? uid)" detailLabel2.text = "\(localizable("account")):\(uid)" } else { commonUI(showDetail: false) - titleLabel.text = u.showName() - detailLabel.text = "\(localizable("account")):\(u.userId ?? "")" + titleLabel.text = friendUser.showName() + detailLabel.text = "\(localizable("account")):\(friendUser.user?.accountId ?? "")" } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift index e33964a0..fc873334 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -12,11 +12,10 @@ import UIKit open class FunBlackListViewController: NEBaseBlackListViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - className = "FunBlackListViewController" } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func commonUI() { @@ -35,11 +34,15 @@ open class FunBlackListViewController: NEBaseBlackListViewController { ) as! FunBlackListCell cell.delegate = self cell.index = indexPath.row - cell.setModel(blackList?[indexPath.row] as Any) + cell.setModel(viewModel.blockList[indexPath.row] as Any) return cell } + /// 黑名单选择页面 + /// - Returns: 人员选择控制器 override open func getContactSelectVC() -> NEBaseContactsSelectedViewController { - FunContactsSelectedViewController() + var filterUsers = Set() + filterUsers.insert(IMKitClient.instance.account()) + return FunContactsSelectedViewController(filterUsers: filterUsers) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactRemakNameViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactRemakNameViewController.swift index 306847c9..b6f7f1eb 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactRemakNameViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactRemakNameViewController.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift index 13b2a39e..ff64c100 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -14,7 +14,7 @@ open class FunContactUserViewController: NEBaseContactUserViewController { headerView = FunUserInfoHeaderView() } - override public init(user: NEKitUser?) { + override public init(user: NEUserWithFriend?) { super.init(user: user) initFun() } @@ -25,7 +25,7 @@ open class FunContactUserViewController: NEBaseContactUserViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func commonUI() { @@ -60,7 +60,7 @@ open class FunContactUserViewController: NEBaseContactUserViewController { FunContactRemakNameViewController() } - override open func deleteFriend(user: NEKitUser?) { + override open func deleteFriend(user: NEUserWithFriend?) { let titleAction = NECustomAlertAction(title: String(format: localizable("delete_title"), user?.showName(true) ?? "")) {} titleAction.contentText.font = .systemFont(ofSize: 13) titleAction.contentText.textColor = UIColor(hexString: "#8F8F8F") diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsSelectedViewController.swift index bdc26ed0..27c1eace 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsSelectedViewController.swift @@ -15,7 +15,7 @@ open class FunContactsSelectedViewController: NEBaseContactsSelectedViewControll } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { @@ -23,7 +23,7 @@ open class FunContactsSelectedViewController: NEBaseContactsSelectedViewControll super.setupUI() emptyView.setEmptyImage(name: "fun_user_empty") collectionBackView.backgroundColor = .white - collection.register( + collectionView.register( FunContactUnCheckCell.self, forCellWithReuseIdentifier: "\(NSStringFromClass(FunContactUnCheckCell.self))" ) @@ -32,8 +32,10 @@ open class FunContactsSelectedViewController: NEBaseContactsSelectedViewControll override open func setupNavRightItem() { super.setupNavRightItem() + navigationView.setBackButtonTitle(localizable("close")) + navigationView.backButton.setTitleColor(.ne_darkText, for: .normal) navigationView.moreButton.backgroundColor = .funContactThemeColor - sureBtn.backgroundColor = .funContactThemeColor + sureButton.backgroundColor = .funContactThemeColor } override open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsViewController.swift index 7b212dcd..f81e1d32 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsViewController.swift @@ -2,7 +2,8 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECommonUIKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -12,7 +13,7 @@ open class FunContactsViewController: NEBaseContactsViewController { let view = FunSearchView() view.translatesAutoresizingMaskIntoConstraints = false view.searchBotton.setImage(UIImage.ne_imageNamed(name: "funSearch"), for: .normal) - view.searchBotton.setTitle(localizable("search"), for: .normal) + view.searchBotton.setTitle(commonLocalizable("search"), for: .normal) return view }() @@ -61,7 +62,7 @@ open class FunContactsViewController: NEBaseContactsViewController { deinit { if let searchViewGestures = searchView.gestureRecognizers { - searchViewGestures.forEach { gesture in + for gesture in searchViewGestures { searchView.removeGestureRecognizer(gesture) } } @@ -90,7 +91,7 @@ open class FunContactsViewController: NEBaseContactsViewController { forHeaderFooterViewReuseIdentifier: "\(NSStringFromClass(ContactSectionView.self))" ) - cellRegisterDic.forEach { (key: Int, value: NEBaseContactTableViewCell.Type) in + for (key, value) in cellRegisterDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } @@ -100,7 +101,7 @@ open class FunContactsViewController: NEBaseContactsViewController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let info = viewModel.contacts[indexPath.section].contacts[indexPath.row] - var reusedId = "\(info.contactCellType)" + let reusedId = "\(info.contactCellType)" let cell = tableView.dequeueReusableCell(withIdentifier: reusedId, for: indexPath) if let c = cell as? FunContactTableViewCell { diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift index 384eaf2d..4e32dc84 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -17,37 +17,37 @@ open class FunFindFriendViewController: NEBaseFindFriendViewController { override open func setupUI() { view.backgroundColor = UIColor(hexString: "0xEDEDED") - let searchBack = UIView() - view.addSubview(searchBack) - searchBack.backgroundColor = UIColor.white - searchBack.translatesAutoresizingMaskIntoConstraints = false - searchBack.clipsToBounds = true - searchBack.layer.cornerRadius = 4.0 + let searchBackView = UIView() + view.addSubview(searchBackView) + searchBackView.backgroundColor = UIColor.white + searchBackView.translatesAutoresizingMaskIntoConstraints = false + searchBackView.clipsToBounds = true + searchBackView.layer.cornerRadius = 4.0 NSLayoutConstraint.activate([ - searchBack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - searchBack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - searchBack.topAnchor.constraint(equalTo: view.topAnchor, constant: 10 + topConstant), - searchBack.heightAnchor.constraint(equalToConstant: 36), + searchBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + searchBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + searchBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10 + topConstant), + searchBackView.heightAnchor.constraint(equalToConstant: 36), ]) - let searchImage = UIImageView() - searchBack.addSubview(searchImage) - searchImage.image = UIImage.ne_imageNamed(name: "search") - searchImage.translatesAutoresizingMaskIntoConstraints = false + let searchImageView = UIImageView() + searchBackView.addSubview(searchImageView) + searchImageView.image = UIImage.ne_imageNamed(name: "search") + searchImageView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - searchImage.centerYAnchor.constraint(equalTo: searchBack.centerYAnchor), - searchImage.leftAnchor.constraint(equalTo: searchBack.leftAnchor, constant: 18), - searchImage.widthAnchor.constraint(equalToConstant: 13), - searchImage.heightAnchor.constraint(equalToConstant: 13), + searchImageView.centerYAnchor.constraint(equalTo: searchBackView.centerYAnchor), + searchImageView.leftAnchor.constraint(equalTo: searchBackView.leftAnchor, constant: 18), + searchImageView.widthAnchor.constraint(equalToConstant: 13), + searchImageView.heightAnchor.constraint(equalToConstant: 13), ]) - searchBack.addSubview(searchInput) + searchBackView.addSubview(searchInput) searchInput.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - searchInput.leftAnchor.constraint(equalTo: searchImage.rightAnchor, constant: 5), - searchInput.rightAnchor.constraint(equalTo: searchBack.rightAnchor, constant: -18), - searchInput.topAnchor.constraint(equalTo: searchBack.topAnchor), - searchInput.bottomAnchor.constraint(equalTo: searchBack.bottomAnchor), + searchInput.leftAnchor.constraint(equalTo: searchImageView.rightAnchor, constant: 5), + searchInput.rightAnchor.constraint(equalTo: searchBackView.rightAnchor, constant: -18), + searchInput.topAnchor.constraint(equalTo: searchBackView.topAnchor), + searchInput.bottomAnchor.constraint(equalTo: searchBackView.bottomAnchor), ]) searchInput.textColor = UIColor(hexString: "0x333333") searchInput.placeholder = localizable("input_userId") @@ -55,6 +55,10 @@ open class FunFindFriendViewController: NEBaseFindFriendViewController { searchInput.returnKeyType = .search searchInput.delegate = self searchInput.clearButtonMode = .always + searchInput.accessibilityIdentifier = "id.addFriendAccount" + if let clearButton = searchInput.value(forKey: "_clearButton") as? UIButton { + clearButton.accessibilityIdentifier = "id.clear" + } NotificationCenter.default.addObserver( self, @@ -67,6 +71,8 @@ open class FunFindFriendViewController: NEBaseFindFriendViewController { NSLayoutConstraint.activate([ emptyView.centerXAnchor.constraint(equalTo: view.centerXAnchor), emptyView.topAnchor.constraint(equalTo: searchInput.bottomAnchor, constant: 74), + emptyView.widthAnchor.constraint(equalToConstant: 200), + emptyView.heightAnchor.constraint(equalToConstant: 200), ]) emptyView.setEmptyImage(name: "fun_user_empty") diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift index baa49432..088e8370 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -11,11 +11,10 @@ import UIKit open class FunValidationMessageViewController: NEBaseValidationMessageViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - tag = "FunValidationMessageViewController" } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift index 9ffbfeee..91333698 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -19,7 +19,7 @@ open class ContactInfo: NSObject { nil } - public var user: NEKitUser? + public var user: NEUserWithFriend? public var contactCellType = ContactCellType.ContactPerson.rawValue public var router = ContactPersonRouter public var isSelected = false diff --git a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift index d2eee358..46fe8c81 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift @@ -5,7 +5,7 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit /* 通讯录 section 数据模型 diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/BlackListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/BlackListCell.swift index 073bd43e..52fb15a8 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/BlackListCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/BlackListCell.swift @@ -2,14 +2,21 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers open class BlackListCell: NEBaseBlackListCell { + private lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.ne_greyLine + return view + }() + override func commonUI() { super.commonUI() - avatarImage.layer.cornerRadius = 21 + avatarImageView.layer.cornerRadius = 21 titleLabel.font = UIFont.systemFont(ofSize: 16) titleLabel.textColor = UIColor( @@ -31,11 +38,4 @@ open class BlackListCell: NEBaseBlackListCell { bottomLine.heightAnchor.constraint(equalToConstant: 1), ]) } - - private lazy var bottomLine: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.ne_greyLine - return view - }() } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactSelectedCell.swift index bfc10d15..4bfc8cb7 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactSelectedCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactSelectedCell.swift @@ -4,14 +4,15 @@ // found in the LICENSE file. import UIKit + @objcMembers open class ContactSelectedCell: NEBaseContactSelectedCell { override open func commonUI() { super.commonUI() - sImage.highlightedImage = UIImage.ne_imageNamed(name: "select") + sImageView.highlightedImage = UIImage.ne_imageNamed(name: "select") NSLayoutConstraint.activate([ - sImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - sImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + sImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + sImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), ]) } @@ -23,9 +24,9 @@ open class ContactSelectedCell: NEBaseContactSelectedCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 36), - avatarImage.heightAnchor.constraint(equalToConstant: 36), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 36), + avatarImageView.heightAnchor.constraint(equalToConstant: 36), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactTableViewCell.swift index ba12e5db..04dac9d2 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactTableViewCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -23,9 +23,9 @@ open class ContactTableViewCell: NEBaseContactTableViewCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 36), - avatarImage.heightAnchor.constraint(equalToConstant: 36), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 36), + avatarImageView.heightAnchor.constraint(equalToConstant: 36), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactUnCheckCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactUnCheckCell.swift index f7950ca4..1ba32db3 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactUnCheckCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactUnCheckCell.swift @@ -9,12 +9,12 @@ import UIKit open class ContactUnCheckCell: NEBaseContactUnCheckCell { override func setupUI() { super.setupUI() - avatarImage.layer.cornerRadius = 18 + avatarImageView.layer.cornerRadius = 18 NSLayoutConstraint.activate([ - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - avatarImage.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - avatarImage.widthAnchor.constraint(equalToConstant: 36), - avatarImage.heightAnchor.constraint(equalToConstant: 36), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: 36), + avatarImageView.heightAnchor.constraint(equalToConstant: 36), ]) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SystemNotificationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SystemNotificationCell.swift index e01d5f87..245430af 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SystemNotificationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SystemNotificationCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -13,11 +13,11 @@ import UIKit open class SystemNotificationCell: NEBaseSystemNotificationCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() - avatarImage.layer.cornerRadius = 18 + avatarImageView.layer.cornerRadius = 18 NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 36), - avatarImage.heightAnchor.constraint(equalToConstant: 36), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 36), + avatarImageView.heightAnchor.constraint(equalToConstant: 36), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/TeamTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/TeamTableViewCell.swift index 1157c9d2..02a4e9c1 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/TeamTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/TeamTableViewCell.swift @@ -2,14 +2,14 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers open class TeamTableViewCell: NEBaseTeamTableViewCell { override func commonUI() { super.commonUI() - avatarImage.layer.cornerRadius = 21 + avatarImageView.layer.cornerRadius = 21 titleLabel.font = UIFont.systemFont(ofSize: 16) titleLabel.textColor = UIColor( diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift index 20486958..dc67bf3a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK @@ -32,10 +32,10 @@ public extension ContactRouter { Router.shared.register(ContactUserInfoPageRouter) { param in if let nav = param["nav"] as? UINavigationController { - if let user = param["user"] as? NEKitUser { + if let user = param["user"] as? NEUserWithFriend { let userInfoVC = ContactUserViewController(user: user) nav.pushViewController(userInfoVC, animated: true) - } else if let nimUser = param["nim_user"] as? NEKitUser { + } else if let nimUser = param["nim_user"] as? NEUserWithFriend { let userInfoVC = ContactUserViewController(user: nimUser) nav.pushViewController(userInfoVC, animated: true) } else if let uid = param["uid"] as? String { diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift index c390bcb2..ee3309ca 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift @@ -3,14 +3,14 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers open class UserInfoHeaderView: NEBaseUserInfoHeaderView { override open func commonUI() { super.commonUI() - avatarImage.layer.cornerRadius = 30 + avatarImageView.layer.cornerRadius = 30 NSLayoutConstraint.activate([ lineView.leftAnchor.constraint(equalTo: leftAnchor), @@ -20,23 +20,23 @@ open class UserInfoHeaderView: NEBaseUserInfoHeaderView { ]) } - override open func setData(user: NEKitUser?) { + override open func setData(user: NEUserWithFriend?) { super.setData(user: user) - guard let u = user else { + guard let userFriend = user else { return } // title - if let alias = u.alias, !alias.isEmpty { + if let alias = userFriend.friend?.alias, !alias.isEmpty { commonUI(showDetail: true) titleLabel.text = alias - let uid = u.userId ?? "" - detailLabel.text = "\(localizable("nick")):\(u.userInfo?.nickName ?? uid)" + let uid = userFriend.user?.accountId ?? "" + detailLabel.text = "\(localizable("nick")):\(userFriend.user?.name ?? uid)" detailLabel2.text = "\(localizable("account")):\(uid)" } else { commonUI(showDetail: false) - titleLabel.text = u.showName() - detailLabel.text = "\(localizable("account")):\(u.userId ?? "")" + titleLabel.text = userFriend.showName() + detailLabel.text = "\(localizable("account")):\(userFriend.user?.accountId ?? "")" } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift index 093dcd78..54ecef40 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -12,13 +12,12 @@ import UIKit open class BlackListViewController: NEBaseBlackListViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - className = "BlackListViewController" navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func commonUI() { @@ -30,8 +29,12 @@ open class BlackListViewController: NEBaseBlackListViewController { tableView.rowHeight = 62 } + /// 黑名单选择页面 + /// - Returns: 人员选择控制器 override open func getContactSelectVC() -> NEBaseContactsSelectedViewController { - ContactsSelectedViewController() + var filterUsers = Set() + filterUsers.insert(IMKitClient.instance.account()) + return ContactsSelectedViewController(filterUsers: filterUsers) } override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -41,7 +44,7 @@ open class BlackListViewController: NEBaseBlackListViewController { ) as! BlackListCell cell.delegate = self cell.index = indexPath.row - cell.setModel(blackList?[indexPath.row] as Any) + cell.setModel(viewModel.blockList[indexPath.row] as Any) return cell } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactRemakNameViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactRemakNameViewController.swift index b40957d5..f2cbc56a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactRemakNameViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactRemakNameViewController.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift index 52cd8e37..deca97b0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -15,7 +15,7 @@ open class ContactUserViewController: NEBaseContactUserViewController { headerView = UserInfoHeaderView() } - override public init(user: NEKitUser?) { + override public init(user: NEUserWithFriend?) { super.init(user: user) initNormal() } @@ -26,7 +26,7 @@ open class ContactUserViewController: NEBaseContactUserViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func commonUI() { diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsSelectedViewController.swift index db8c3f96..14396dda 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsSelectedViewController.swift @@ -17,13 +17,13 @@ open class ContactsSelectedViewController: NEBaseContactsSelectedViewController } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { super.setupUI() - collection.register( + collectionView.register( ContactUnCheckCell.self, forCellWithReuseIdentifier: "\(NSStringFromClass(ContactUnCheckCell.self))" ) @@ -34,8 +34,8 @@ open class ContactsSelectedViewController: NEBaseContactsSelectedViewController super.setupNavRightItem() navigationView.moreButton.backgroundColor = .white navigationView.moreButton.setTitleColor(UIColor(hexString: "337EFF"), for: .normal) - sureBtn.backgroundColor = .white - sureBtn.setTitleColor(UIColor(hexString: "337EFF"), for: .normal) + sureButton.backgroundColor = .white + sureButton.setTitleColor(UIColor(hexString: "337EFF"), for: .normal) } override open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsViewController.swift index e93b878c..9b1dd3cd 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsViewController.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -59,7 +59,7 @@ open class ContactsViewController: NEBaseContactsViewController { forHeaderFooterViewReuseIdentifier: "\(NSStringFromClass(ContactSectionView.self))" ) - cellRegisterDic.forEach { (key: Int, value: NEBaseContactTableViewCell.Type) in + for (key, value) in cellRegisterDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } @@ -71,7 +71,7 @@ open class ContactsViewController: NEBaseContactsViewController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let info = viewModel.contacts[indexPath.section].contacts[indexPath.row] - var reusedId = "\(info.contactCellType)" + let reusedId = "\(info.contactCellType)" let cell = tableView.dequeueReusableCell(withIdentifier: reusedId, for: indexPath) if let c = cell as? ContactTableViewCell { diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift index a5a6f29e..1b27b893 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift @@ -13,6 +13,6 @@ open class FindFriendViewController: NEBaseFindFriendViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift index 22b9487d..a0942e91 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -11,13 +11,12 @@ import UIKit open class ValidationMessageViewController: NEBaseValidationMessageViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - tag = "ValidationMessageViewController" navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift index 7798afdf..9b9dc61b 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift @@ -2,12 +2,12 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers open class NEBaseTeamTableViewCell: UITableViewCell { - public var avatarImage = UIImageView() + public var avatarImageView = UIImageView() public var nameLabel = UILabel() public var titleLabel = UILabel() // public var arrow = UIImageView(image:UIImage.ne_imageNamed(name: "arrowRight")) @@ -18,21 +18,21 @@ open class NEBaseTeamTableViewCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { selectionStyle = .none - avatarImage.translatesAutoresizingMaskIntoConstraints = false - avatarImage.clipsToBounds = true - avatarImage.contentMode = .scaleAspectFill - avatarImage.accessibilityIdentifier = "id.avatar" - contentView.addSubview(avatarImage) + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + avatarImageView.clipsToBounds = true + avatarImageView.contentMode = .scaleAspectFill + avatarImageView.accessibilityIdentifier = "id.avatar" + contentView.addSubview(avatarImageView) NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), - avatarImage.widthAnchor.constraint(equalToConstant: 42), - avatarImage.heightAnchor.constraint(equalToConstant: 42), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + avatarImageView.widthAnchor.constraint(equalToConstant: 42), + avatarImageView.heightAnchor.constraint(equalToConstant: 42), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) nameLabel.textAlignment = .center @@ -41,17 +41,17 @@ open class NEBaseTeamTableViewCell: UITableViewCell { nameLabel.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(nameLabel) NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), - nameLabel.rightAnchor.constraint(equalTo: avatarImage.rightAnchor), - nameLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor), - nameLabel.bottomAnchor.constraint(equalTo: avatarImage.bottomAnchor), + nameLabel.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), + nameLabel.rightAnchor.constraint(equalTo: avatarImageView.rightAnchor), + nameLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor), + nameLabel.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), ]) titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.accessibilityIdentifier = "id.name" contentView.addSubview(titleLabel) NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 12), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 12), titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35), titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), @@ -78,13 +78,13 @@ open class NEBaseTeamTableViewCell: UITableViewCell { } titleLabel.text = name // self.nameLabel.text = name.count > 2 ? String(name[name.index(name.endIndex, offsetBy: -2)...]) : name - if let url = team.thumbAvatarUrl, !url.isEmpty { - avatarImage.sd_setImage(with: URL(string: url), completed: nil) - avatarImage.backgroundColor = .clear + if let url = team.avatarUrl, !url.isEmpty { + avatarImageView.sd_setImage(with: URL(string: url), completed: nil) + avatarImageView.backgroundColor = .clear } else { // random avatar // avatarImage.image = randomAvatar(teamId: team.teamId) - avatarImage.backgroundColor = UIColor.colorWithString(string: team.teamId) + avatarImageView.backgroundColor = UIColor.colorWithString(string: team.teamId) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift index 672b98eb..f6b1598f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift @@ -72,8 +72,13 @@ open class NEBaseTeamListViewController: UIViewController, UITableViewDelegate, } func loadData() { - viewModel.getTeamList() - tableView.reloadData() + viewModel.getTeamList { [weak self] _, error in + if let err = error { + print("getTeamList error: \(err)") + } else { + self?.tableView.reloadData() + } + } } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -90,17 +95,17 @@ open class NEBaseTeamListViewController: UIViewController, UITableViewDelegate, if isClickCallBack == true { Router.shared.use( ContactTeamDataRouter, - parameters: ["team": model.nimTeam as Any], + parameters: ["team": model.v2Team as Any], closure: nil ) navigationController?.popViewController(animated: true) return } if let teamid = model.teamId { - let session = NIMSession(teamid, type: .team) + let conversationId = V2NIMConversationIdUtil.teamConversationId(teamid) Router.shared.use( PushTeamChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any], + parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any], closure: nil ) } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift index dc08705b..231fcce9 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift @@ -4,52 +4,57 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit @objcMembers -open class TeamListViewModel: NSObject, NIMTeamManagerDelegate { - var contactRepo = ContactRepo.shared +open class TeamListViewModel: NSObject, NETeamListener { + var teamRepo = TeamRepo.shared var refresh: () -> Void = {} public var teamList = [NETeam]() - private let className = "TeamListViewModel" override public init() { super.init() - contactRepo.addTeamDelegate(delegate: self) + teamRepo.addTeamListener(self) } deinit { - contactRepo.removeTeamDelegate(delegate: self) + teamRepo.removeTeamListener(self) } - func getTeamList() -> [NETeam]? { - NELog.infoLog(ModuleName + " " + className, desc: #function) - teamList = contactRepo.getTeamList() - teamList.sort(by: { team1, team2 in - (team1.createTime ?? 0) > (team2.createTime ?? 0) - }) - return teamList + func getTeamList(_ completion: @escaping ([NETeam]?, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + teamRepo.getTeamList { [weak self] teams, error in + if let error = error { + NEALog.errorLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + ", error: " + error.localizedDescription) + } else if let teams = teams { + self?.teamList = teams + self?.teamList.sort(by: { team1, team2 in + (team1.createTime ?? 0) > (team2.createTime ?? 0) + }) + completion(teams, nil) + } + } } // MARK: NIMTeamManagerDelegate - public func onTeamAdded(_ team: NIMTeam) { - teamList.insert(NETeam(teamInfo: team), at: 0) + public func onTeamAdded(_ team: V2NIMTeam) { + teamList.insert(NETeam(v2teamInfo: team), at: 0) refresh() } - public func onTeamUpdated(_ team: NIMTeam) { + public func onTeamUpdated(_ team: V2NIMTeam) { for (i, t) in teamList.enumerated() { if t.teamId == team.teamId { - teamList[i] = NETeam(teamInfo: team) + teamList[i] = NETeam(v2teamInfo: team) refresh() break } } } - public func onTeamRemoved(_ team: NIMTeam) { + public func onTeamRemoved(_ team: V2NIMTeam) { for (i, t) in teamList.enumerated() { if t.teamId == team.teamId { teamList.remove(at: i) @@ -58,4 +63,26 @@ open class TeamListViewModel: NSObject, NIMTeamManagerDelegate { } } } + + // MARK: - V2NIMTeamListener + + public func onTeamCreated(_ team: V2NIMTeam) { + onTeamAdded(team) + } + + public func onTeamJoined(_ team: V2NIMTeam) { + onTeamAdded(team) + } + + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + onTeamUpdated(team) + } + + public func onTeamLeft(_ team: V2NIMTeam, isKicked: Bool) { + onTeamRemoved(team) + } + + public func onTeamDismissed(_ team: V2NIMTeam) { + onTeamRemoved(team) + } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/CenterTextCell.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/CenterTextCell.swift index 06edabf2..be8f59a0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/CenterTextCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/CenterTextCell.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import UIKit + @objcMembers open class CenterTextCell: UITableViewCell { public var titleLabel: UILabel = .init() @@ -36,6 +37,6 @@ open class CenterTextCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/ContactBaseTextCell.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/ContactBaseTextCell.swift index 0362b53e..5587825a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/ContactBaseTextCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/ContactBaseTextCell.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import UIKit + @objcMembers open class ContactBaseTextCell: UITableViewCell { public var titleLabel: UILabel = .init() @@ -37,7 +38,7 @@ open class ContactBaseTextCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setModel(model: UserItem) { diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithDetailTextCell.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithDetailTextCell.swift index 1dbd77e4..2843416e 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithDetailTextCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithDetailTextCell.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import UIKit + @objcMembers open class TextWithDetailTextCell: ContactBaseTextCell { public var detailTitleLabel = UILabel() @@ -28,6 +29,6 @@ open class TextWithDetailTextCell: ContactBaseTextCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithRightArrowCell.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithRightArrowCell.swift index 09aadb5c..c05fb79b 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithRightArrowCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithRightArrowCell.swift @@ -4,25 +4,26 @@ // found in the LICENSE file. import UIKit + @objcMembers open class TextWithRightArrowCell: ContactBaseTextCell { - public var arrowImage = UIImageView(image: UIImage.ne_imageNamed(name: "arrowRight")) + public var arrowImageView = UIImageView(image: UIImage.ne_imageNamed(name: "arrowRight")) override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - arrowImage.translatesAutoresizingMaskIntoConstraints = false - arrowImage.contentMode = .center - contentView.addSubview(arrowImage) + arrowImageView.translatesAutoresizingMaskIntoConstraints = false + arrowImageView.contentMode = .center + contentView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - arrowImage.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - arrowImage.widthAnchor.constraint(equalToConstant: 20), - arrowImage.heightAnchor.constraint(equalToConstant: 20), - arrowImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowImageView.widthAnchor.constraint(equalToConstant: 20), + arrowImageView.heightAnchor.constraint(equalToConstant: 20), + arrowImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func setModel(model: UserItem) { diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithSwitchCell.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithSwitchCell.swift index 65b51add..e82df8fe 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithSwitchCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithSwitchCell.swift @@ -27,7 +27,7 @@ open class TextWithSwitchCell: ContactBaseTextCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func valueChanged(switchBtn: UISwitch) { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift index 02b3820c..fb1134aa 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift @@ -2,15 +2,14 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import MJRefresh +import NECoreIM2Kit import NECoreKit import UIKit @objcMembers open class NEBaseValidationMessageViewController: NEBaseContactViewController { public let viewModel = ValidationMessageViewModel() - public let tableView = UITableView() - var tag = "ValidationMessageViewController" override open func viewDidLoad() { super.viewDidLoad() @@ -26,26 +25,97 @@ open class NEBaseValidationMessageViewController: NEBaseContactViewController { NotificationCenter.default.addObserver(self, selector: #selector(appEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) } - // 返回上一级页面 + override open func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewModel.setAddApplicationRead(nil) + } + + /// 返回上一级页面 override open func backToPrevious() { super.backToPrevious() - viewModel.clearNotiUnreadCount() + viewModel.setAddApplicationRead(nil) } + /// 进入后台,清空未读 func appEnterBackground() { - viewModel.clearNotiUnreadCount() - loadData() + viewModel.setAddApplicationRead { [weak self] success, error in + if success { + self?.loadData() + } + } + } + + public lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.showsVerticalScrollIndicator = false + tableView.delegate = self + tableView.dataSource = self + tableView.backgroundColor = .clear + tableView.keyboardDismissMode = .onDrag + + tableView.mj_header = MJRefreshNormalHeader( + refreshingTarget: self, + refreshingAction: #selector(loadData) + ) + return tableView + }() + + /// 表格添加底部 loading + func addBottomLoadMore() { + let footer = MJRefreshAutoFooter( + refreshingTarget: self, + refreshingAction: #selector(loadMoreData) + ) + footer.triggerAutomaticallyRefreshPercent = -10 + tableView.mj_footer = footer + } + + /// 表格移除底部 loading + func removeBottomLoadMore() { + tableView.mj_footer?.endRefreshingWithNoMoreData() + tableView.mj_footer = nil } + /// 加载数据 func loadData() { - weak var weakSelf = self - viewModel.getValidationMessage { - NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "✅ getValidationMessage SUCCESS") - weakSelf?.emptyView.isHidden = (weakSelf?.viewModel.datas.count ?? 0) > 0 - weakSelf?.tableView.reloadData() + viewModel.loadApplicationList(true) { [weak self] finished, error in + if let err = error { + NEALog.errorLog(ModuleName + " " + NEBaseValidationMessageViewController.className(), desc: "loadApplicationList CALLBACK error: \(err.localizedDescription)") + } else { + if finished { + self?.removeBottomLoadMore() + } else { + self?.addBottomLoadMore() + } + + self?.tableView.mj_header?.endRefreshing() + self?.emptyView.isHidden = (self?.viewModel.datas.count ?? 0) > 0 + self?.tableView.reloadData() + } } } + /// 加载更多 + func loadMoreData() { + viewModel.loadApplicationList(false) { [weak self] finished, error in + if let err = error { + NEALog.errorLog(ModuleName + " " + NEBaseValidationMessageViewController.className(), desc: "loadMoreApplicationList CALLBACK error: \(err.localizedDescription)") + } else { + if finished { + self?.removeBottomLoadMore() + } else { + self?.addBottomLoadMore() + } + + self?.tableView.mj_footer?.endRefreshing() + self?.tableView.reloadData() + } + } + } + + /// 控件初始化 open func setupUI() { let clearItem = UIBarButtonItem( title: localizable("clear"), @@ -66,11 +136,7 @@ open class NEBaseValidationMessageViewController: NEBaseContactViewController { navigationView.moreButton.setTitleColor(.ne_darkText, for: .normal) navigationView.addMoreButtonTarget(target: self, selector: #selector(clearMessage)) - tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) - tableView.dataSource = self - tableView.delegate = self - tableView.separatorStyle = .none NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), @@ -89,17 +155,14 @@ open class NEBaseValidationMessageViewController: NEBaseContactViewController { ]) } + /// 清空好友申请 func clearMessage() { - viewModel.clearAllNoti { - NELog.infoLog(ModuleName + " " + self.tag, desc: "✅ clearAllNoti SUCCESS") - tableView.reloadData() - emptyView.isHidden = false - } + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + viewModel.clearNotification() } open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - viewModel.clearNotiUnreadCount() - return true + true } } @@ -120,114 +183,74 @@ extension NEBaseValidationMessageViewController: UITableViewDelegate, UITableVie } extension NEBaseValidationMessageViewController: SystemNotificationCellDelegate { - open func changeValidationStatus(notifiModel: NENotification, notiStatus: NEHandleStatus) { - var notifiModels = [NENotification]() - if let msgList = notifiModel.msgList, - msgList.count > 0 { - for msg in msgList { - notifiModels.append(msg) - } + /// 处理好友申请 + /// - Parameters: + /// - notifiModel: 申请模型 + /// - notiStatus: 处理状态 + public func changeValidationStatus(notifiModel: NENotification, notiStatus: NEHandleStatus) { + notifiModel.handleStatus = notiStatus + notifiModel.unReadCount = 0 + for msg in notifiModel.msgList ?? [] { + msg.handleStatus = notiStatus } - notifiModel.handleStatus = notiStatus - notifiModel.imNotification?.handleStatus = notiStatus.rawValue - viewModel.clearSingleNotifyUnreadCount(notification: notifiModel.imNotification!) - for noti in notifiModels { - noti.handleStatus = notiStatus - noti.imNotification?.handleStatus = notiStatus.rawValue - viewModel.clearSingleNotifyUnreadCount(notification: noti.imNotification!) + DispatchQueue.main.async { + self.tableView.reloadData() } - notifiModel.unReadCount = 0 - loadData() } + /// 同意好友申请 + /// - Parameter notifiModel: 申请模型 open func onAccept(_ notifiModel: NENotification) { weak var weakSelf = self - guard let teamId = notifiModel.targetID, let invitorId = notifiModel.sourceID else { + guard let info = notifiModel.v2Notification else { return } - if notifiModel.type == .teamInvite { - viewModel.acceptInviteWithTeam(teamId, invitorId) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), - desc: "CALLBACK acceptInviteWithTeam " + (error?.localizedDescription ?? "no error") - ) - if let err = error as? NSError { - NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "❌CALLBACK acceptInviteWithTeam failed,error = \(error!.localizedDescription)") - if err.code == 807 || err.code == 809 { - weakSelf?.showToast(localizable("validate_processed")) - } else if err.code == teamNotExistCode { - weakSelf?.showToast(localizable("team_not_exist")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) - } - } - } else if notifiModel.type == .addFriendRequest { - viewModel.agreeRequest(invitorId) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), - desc: "CALLBACK agreeRequest " + (error?.localizedDescription ?? "no error") - ) - if let err = error { - NELog.infoLog(ModuleName + " " + self.tag, desc: "❌CALLBACK agreeRequest failed,error = \(error!)") + viewModel.agreeRequest(application: info) { error in + if let err = error as? NSError, err.code != friendAlreadyExist { + NEALog.errorLog(ModuleName + " " + NEBaseValidationMessageViewController.className(), desc: "CALLBACK agreeRequest failed,error = \(err.localizedDescription)") + switch err.code { + case protocolSendFailed: + weakSelf?.showToast(commonLocalizable("network_error")) + default: weakSelf?.showToast(localizable("failed_operation")) - } else { - weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) + } + } else { + weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) + weakSelf?.viewModel.setAddApplicationRead(nil) + if let accid = info.operatorAccountId, let conversationId = V2NIMConversationIdUtil.p2pConversationId(accid) { Router.shared.use(ChatAddFriendRouter, parameters: ["text": localizable("let_us_chat"), - "sessionId": invitorId, - "sessionType": NIMSessionType.P2P]) + "conversationId": conversationId as Any]) } } } } + /// 拒绝好友申请 + /// - Parameter notifiModel: 申请模型 open func onRefuse(_ notifiModel: NENotification) { weak var weakSelf = self - guard let teamId = notifiModel.targetID, let invitorId = notifiModel.sourceID else { + guard let info = notifiModel.v2Notification else { return } - if notifiModel.type == .teamInvite { - weakSelf?.viewModel.rejectInviteWithTeam(teamId, invitorId) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), - desc: "CALLBACK rejectInviteWithTeam " + (error?.localizedDescription ?? "no error") - ) - if let err = error as? NSError { - NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "❌CALLBACK rejectInviteWithTeam failed,error = \(error!.localizedDescription)") - if err.code == 807 || err.code == 809 { - weakSelf?.showToast(localizable("validate_processed")) - } else if err.code == teamNotExistCode { - weakSelf?.showToast(localizable("team_not_exist")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeNo) - } - } - } else if notifiModel.type == .addFriendRequest { - viewModel.refuseRequest(invitorId) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), - desc: "CALLBACK refuseRequest " + (error?.localizedDescription ?? "no error") - ) - if let err = error { - NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "❌CALLBACK agreeRequest failed,error = \(err.localizedDescription)") - if err.code == 509 { - weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) - weakSelf?.showToast(localizable("validate_processed")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeNo) + viewModel.refuseRequest(application: info) { error in + if let err = error as? NSError { + NEALog.errorLog(ModuleName + " " + NEBaseValidationMessageViewController.className(), desc: "CALLBACK agreeRequest failed,error = \(err.localizedDescription)") + switch err.code { + case protocolSendFailed: + weakSelf?.showToast(commonLocalizable("network_error")) + case friendAlreadyExist: + weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) + weakSelf?.showToast(localizable("validate_processed")) + default: + weakSelf?.showToast(localizable("failed_operation")) } + } else { + weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeNo) + weakSelf?.viewModel.setAddApplicationRead(nil) } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift index 4c6265dd..da6832f0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift @@ -4,58 +4,105 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit @objcMembers -open class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate { - typealias DataRefresh = () -> Void - - var dataRefresh: DataRefresh? - private let className = "ValidationMessageViewModel" +open class ValidationMessageViewModel: NSObject, NEContactListener { let contactRepo = ContactRepo.shared var datas = [NENotification]() + var dataRefresh: (() -> Void)? + var offset: UInt = 0 // 查询的偏移量 + var pageMaxLimit: UInt = 100 // 查询的每页数量 override init() { - NELog.infoLog(ModuleName + " " + className, desc: #function) super.init() - contactRepo.notiDelegate = self + contactRepo.addContactListener(self) + } + + deinit { + contactRepo.removeContactListener(self) } - open func onNotificationUnreadCountChanged(_ count: Int) {} - - // 内容待完善 -// open func onRecieveNotification(_ notification: XNotification) { -// NELog.infoLog(className, desc: #function) -// var isInsert = true -// for notify in datas { -// if notify.sourceID == notification.sourceID, notify.type == notification.type { -// isInsert = false -// break -// } -// } -// if isInsert { -// datas.insert(notification, at: 0) -// } -// contactRepo.clearNotificationUnreadCount() -// if let block = dataRefresh { -// block() -// } -// } - // 内容待完善 -// func getValidationMessage(_ completin: @escaping () -> Void) { -// NELog.infoLog(className, desc: #function) -// weak var weakSelf = self -// contactRepo.getNotificationList(limit: 500) { notifications in -// weakSelf?.datas = notifications -// if let count = weakSelf?.datas.count, count > 0 { -// completin() -// } else { -// NELog.warn(weakSelf?.className ?? "ValidationMessageViewModel", desc: "⚠️NotificationList is empty") -// } -// } -// } + /// 加载(更多)好友申请消息 + /// - Parameter firstLoad: 是否是首次加载 + /// - Parameter completin: 完成回调,(是否还有数据,错误信息) + func loadApplicationList(_ firstLoad: Bool, _ completin: @escaping (Bool, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + let offset = firstLoad ? 0 : offset + if firstLoad { + datas.removeAll() + } + getValidationMessage(offset) { [weak self] offset, finished, error in + if let err = error { + completin(finished, err) + } else { + self?.offset = offset + completin(finished, nil) + } + } + } + + /// 分页查询好友验证消息 + /// - Parameters: + /// - offset: 偏移量 + /// - completin: 完成回调(验证消息列表,下一次偏移量,是否还有数据,错误信息) + func getValidationMessage(_ offset: UInt, _ completin: @escaping (UInt, Bool, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + contactRepo.getNotificationList(offset: offset, limit: pageMaxLimit) { [weak self] result, error in + if let err = error { + completin(0, false, err) + } else if let result = result, let infos = result.infos { + let dateNow = Date().timeIntervalSince1970 + let group = DispatchGroup() + + for info in infos { + var noti = NENotification(info: info) + + // 过期事件:7天(604800s) + if noti.handleStatus == .HandleTypePending, + dateNow - (noti.timestamp ?? 0) > 604_800 { + noti.handleStatus = .HandleTypeOutOfDate + } + + // 查询用户信息 + var uid: String? + // 自己申请添加别人,则存储操作者的信息 + if noti.applicantAccid == IMKitClient.instance.account() { + uid = noti.operatorAccid + } else { + // 别人申请添加自己,则存储申请者的信息 + uid = noti.applicantAccid + } + + if let uid = uid { + group.enter() + self?.contactRepo.getFriendInfo(uid) { user, error in + noti.userInfo = user + if var datas = self?.datas, self?.isExist(xNoti: ¬i, list: &datas) == false { + self?.datas.append(noti) + } + group.leave() + } + } + } + + group.notify(queue: .main) { [weak self] in + self?.datas.sort { xNoti1, xNoti2 in + (xNoti1.timestamp ?? 0) > (xNoti2.timestamp ?? 0) + } + completin(result.offset, result.finished, nil) + } + } + } + } + + /// 判断该条申请是否已存在(是否可以聚合) + /// - Parameters: + /// - xNoti: 申请 + /// - list: 聚合列表 + /// - Returns: 是否已存在 func isExist(xNoti: inout NENotification, list: inout [NENotification]) -> Bool { for loopList in list { if xNoti.isEqualTo(noti: loopList) { @@ -83,89 +130,81 @@ open class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate { return false } - open func onRecieveNotification(_ notification: NENotification) { - NELog.infoLog(ModuleName + " " + className, desc: #function) - var noti = notification - if !isExist(xNoti: ¬i, list: &datas) { - datas.insert(notification, at: 0) - } - datas.sort { xNoti1, xNoti2 in - (xNoti1.timestamp ?? 0) > (xNoti2.timestamp ?? 0) - } - if let block = dataRefresh { - block() - } - } - - func getValidationMessage(_ completin: @escaping () -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) - contactRepo.getNotificationList(limit: 500) { [weak self] xNotiList in - var data = [NENotification]() - let dateNow = Date().timeIntervalSince1970 - for xNoti in xNotiList { - var noti = xNoti - - // 过期事件:7天(604800s) - if noti.handleStatus == .HandleTypePending, - dateNow - (noti.timestamp ?? 0) > 604_800 { - noti.handleStatus = .HandleTypeOutOfDate - } - - if !self!.isExist(xNoti: ¬i, list: &data) { - data.append(xNoti) - } - } - self!.datas = data.sorted(by: { xNoti1, xNoti2 in - (xNoti1.timestamp ?? 0) > (xNoti2.timestamp ?? 0) - }) - if self!.datas.count <= 0 { - NELog.warn(ModuleName + " " + self!.className, desc: "⚠️NotificationList is empty") + /// 设置所有好友申请已读 + /// - Parameter completion: 完成回调 + func setAddApplicationRead(_ completion: ((Bool, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + contactRepo.setAddApplicationRead { success, error in + completion?(success, error as? NSError) + DispatchQueue.main.async { + NotificationCenter.default.post(name: NENotificationName.clearValidationUnreadCount, object: nil) } - completin() } } - func clearNotiUnreadCount() { - contactRepo.clearNotificationUnreadCount() + /// 同意好友申请 + /// - Parameters: + /// - application: 好友申请 + /// - completion: 完成回调 + func agreeRequest(application: V2NIMFriendAddApplication, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", operatorAccountId:\(String(describing: application.operatorAccountId))") + contactRepo.acceptAddApplication(application: application, completion) } - func clearSingleNotifyUnreadCount(notification: NIMSystemNotification) { - contactRepo.clearSingleNotifyUnreadCount(notification: notification) + /// 拒绝好友申请 + /// - Parameters: + /// - application: 好友申请 + /// - completion: 完成回调 + func refuseRequest(application: V2NIMFriendAddApplication, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", operatorAccountId:\(String(describing: application.operatorAccountId))") + contactRepo.rejectAddApplication(application: application, completion) } - func clearAllNoti(_ completion: () -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) + /// 清空好友申请通知 + func clearNotification() { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) contactRepo.clearNotification() datas.removeAll() - completion() - } - - open func acceptInviteWithTeam(_ teamId: String, _ invitorId: String, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") - contactRepo.acceptTeamInvite(teamId, invitorId, completion) + dataRefresh?() } - open func rejectInviteWithTeam(_ teamId: String, _ invitorId: String, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") - contactRepo.rejectTeamInvite(teamId, invitorId, completion) - } + // MARK: - NEContactListener + + /// 收到好友添加申请回调 + /// - Parameter application: 申请添加好友信息 + public func onFriendAddApplication(_ application: V2NIMFriendAddApplication) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + var noti = NENotification(info: application) + let group = DispatchGroup() + + // 查询用户信息 + var uid: String? + // 自己申请添加别人,则存储操作者的信息 + if noti.applicantAccid == IMKitClient.instance.account() { + uid = noti.operatorAccid + } else { + // 别人申请添加自己,则存储申请者的信息 + uid = noti.applicantAccid + } - func agreeRequest(_ account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account:\(account)") - let request = NEAddFriendRequest() - request.account = account - request.operationType = .verify - contactRepo.addFriend(request: request, completion) - } + if let uid = uid { + group.enter() + contactRepo.getFriendInfo(uid) { [self] user, error in + noti.userInfo = user + if !isExist(xNoti: ¬i, list: &datas) { + datas.insert(noti, at: 0) + } + datas.sort { xNoti1, xNoti2 in + (xNoti1.timestamp ?? 0) > (xNoti2.timestamp ?? 0) + } + group.leave() + } + } - func refuseRequest(_ account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account:\(account)") - print("account : ", account) - let request = NEAddFriendRequest() - request.account = account - request.operationType = .reject - contactRepo.addFriend(request: request, completion) + group.notify(queue: .main) { [weak self] in + self?.dataRefresh?() + } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift index 09ef5d3d..1973160f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -14,6 +14,52 @@ open class NEBaseSystemNotificationCell: NEBaseValidationCell { private var notifModel: NENotification? public weak var delegate: SystemNotificationCellDelegate? + public var rejectButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(localizable("refuse"), for: .normal) + button.setTitleColor(UIColor(hexString: "333333"), for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 14.0) + button.clipsToBounds = false + button.layer.cornerRadius = 4 + button.layer.borderColor = UIColor(hexString: "D9D9D9").cgColor + button.layer.borderWidth = 1 + button.accessibilityIdentifier = "id.reject" + return button + }() + + public var agreeButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(localizable("agree"), for: .normal) + let blue = UIColor(hexString: "337EFF") + button.setTitleColor(blue, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 14) + button.clipsToBounds = true + button.layer.cornerRadius = 4 + button.layer.borderWidth = 1 + button.layer.borderColor = blue.cgColor + button.accessibilityIdentifier = "id.accept" + return button + }() + + public lazy var resultImage: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = UIImage.ne_imageNamed(name: "finishFlag") + return imageView + }() + + public lazy var resultLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor(hexString: "B3B7BC") + label.font = UIFont.systemFont(ofSize: 14.0) + label.textAlignment = .right + label.accessibilityIdentifier = "id.verifyResult" + return label + }() + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } @@ -24,26 +70,26 @@ open class NEBaseSystemNotificationCell: NEBaseValidationCell { override open func setupUI() { super.setupUI() - contentView.addSubview(agreeBtn) - contentView.addSubview(rejectBtn) + contentView.addSubview(agreeButton) + contentView.addSubview(rejectButton) contentView.addSubview(resultImage) contentView.addSubview(resultLabel) resultLabel.text = "" NSLayoutConstraint.activate([ - agreeBtn.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - agreeBtn.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - agreeBtn.widthAnchor.constraint(equalToConstant: 60), - agreeBtn.heightAnchor.constraint(equalToConstant: 32), + agreeButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + agreeButton.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + agreeButton.widthAnchor.constraint(equalToConstant: 60), + agreeButton.heightAnchor.constraint(equalToConstant: 32), ]) - agreeBtn.addTarget(self, action: #selector(agreeClick(_:)), for: .touchUpInside) + agreeButton.addTarget(self, action: #selector(agreeClick(_:)), for: .touchUpInside) NSLayoutConstraint.activate([ - rejectBtn.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - rejectBtn.rightAnchor.constraint(equalTo: agreeBtn.leftAnchor, constant: -16), - rejectBtn.widthAnchor.constraint(equalToConstant: 60), - rejectBtn.heightAnchor.constraint(equalToConstant: 32), + rejectButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + rejectButton.rightAnchor.constraint(equalTo: agreeButton.leftAnchor, constant: -16), + rejectButton.widthAnchor.constraint(equalToConstant: 60), + rejectButton.heightAnchor.constraint(equalToConstant: 32), ]) - rejectBtn.addTarget(self, action: #selector(rejectClick(_:)), for: .touchUpInside) + rejectButton.addTarget(self, action: #selector(rejectClick(_:)), for: .touchUpInside) NSLayoutConstraint.activate([ resultLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), @@ -61,116 +107,43 @@ open class NEBaseSystemNotificationCell: NEBaseValidationCell { override open func confige(_ model: NENotification) { super.confige(model) notifModel = model - let hideActionButton = shouldHideActionButton() - agreeBtn.isHidden = hideActionButton - rejectBtn.isHidden = hideActionButton - - if hideActionButton { - let hidden = shouldHideResultStatus() - - resultLabel.isHidden = hidden - resultImage.isHidden = hidden - - switch notifModel?.handleStatus { - case .HandleTypeOk: - resultLabel.text = localizable("agreed") - resultImage.image = UIImage.ne_imageNamed(name: "finishFlag") - case .HandleTypeNo: - resultLabel.text = localizable("refused") - resultImage.image = UIImage.ne_imageNamed(name: "refused") - case .HandleTypeOutOfDate: - resultLabel.text = localizable("expired") - resultImage.image = UIImage.ne_imageNamed(name: "refused") - default: - resultLabel.text = "" + + if model.handleStatus != .HandleTypePending { + agreeButton.isHidden = true + rejectButton.isHidden = true + titleLabelRightMargin?.constant = -90 + + if model.applicantAccid == IMKitClient.instance.account() { + // 自己申请的,不展示结果 + resultLabel.isHidden = true + resultImage.isHidden = true + } else { + resultLabel.isHidden = false + resultImage.isHidden = false + + switch model.handleStatus { + case .HandleTypeOk: + resultLabel.text = localizable("agreed") + resultImage.image = UIImage.ne_imageNamed(name: "finishFlag") + case .HandleTypeNo: + resultLabel.text = localizable("refused") + resultImage.image = UIImage.ne_imageNamed(name: "refused") + case .HandleTypeOutOfDate: + resultLabel.text = localizable("expired") + resultImage.image = UIImage.ne_imageNamed(name: "refused") + default: + resultLabel.text = "" + } } - titleLabelRightMargin?.constant = hidden ? -20 : -90 } else { + agreeButton.isHidden = false + rejectButton.isHidden = false resultLabel.isHidden = true resultImage.isHidden = true titleLabelRightMargin?.constant = -180 } } - func shouldHideActionButton() -> Bool { - let type = notifModel?.type - let handled = notifModel?.handleStatus != .HandleTypePending - var needHandel = false - if type == .teamApply || - type == .teamInvite || - type == .superTeamInvite || - type == .superTeamApply { - needHandel = true - } - - if type == .addFriendRequest { - if let obj = notifModel?.attachment { - if obj.isKind(of: NIMUserAddAttachment.self) { - let operation = (obj as NIMUserAddAttachment).operationType - needHandel = operation == .request - } - } - } - return !(!handled && needHandel) - } - - func shouldHideResultStatus() -> Bool { - let type = notifModel?.type - if type == .addFriendVerify || - type == .addFriendReject || - type == .teamInviteReject { - return true - } else { - return false - } - } - - public var rejectBtn: ExpandButton = { - let button = ExpandButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(localizable("refuse"), for: .normal) - button.setTitleColor(UIColor(hexString: "333333"), for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 14.0) - button.clipsToBounds = false - button.layer.cornerRadius = 4 - button.layer.borderColor = UIColor(hexString: "D9D9D9").cgColor - button.layer.borderWidth = 1 - button.accessibilityIdentifier = "id.reject" - return button - }() - - public var agreeBtn: ExpandButton = { - let button = ExpandButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(localizable("agree"), for: .normal) - let blue = UIColor(hexString: "337EFF") - button.setTitleColor(blue, for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 14) - button.clipsToBounds = true - button.layer.cornerRadius = 4 - button.layer.borderWidth = 1 - button.layer.borderColor = blue.cgColor - button.accessibilityIdentifier = "id.accept" - return button - }() - - public lazy var resultImage: UIImageView = { - let rightImage = UIImageView() - rightImage.translatesAutoresizingMaskIntoConstraints = false - rightImage.image = UIImage.ne_imageNamed(name: "finishFlag") - return rightImage - }() - - public lazy var resultLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = UIColor(hexString: "B3B7BC") - label.font = UIFont.systemFont(ofSize: 14.0) - label.textAlignment = .right - label.accessibilityIdentifier = "id.verifyResult" - return label - }() - open func rejectClick(_ sender: UIButton) { if let model = notifModel { delegate?.onRefuse(model) diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift index d8dab8b7..3c7db85b 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -38,15 +38,15 @@ open class NEBaseValidationCell: NEBaseContactViewCell { contentView.addSubview(redAngleView) NSLayoutConstraint.activate([ - redAngleView.centerXAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: -8), - redAngleView.centerYAnchor.constraint(equalTo: avatarImage.topAnchor, constant: 8), + redAngleView.centerXAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: -8), + redAngleView.centerYAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: 8), redAngleView.heightAnchor.constraint(equalToConstant: 18), ]) addSubview(titleLabel) NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 10), - titleLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 10), + titleLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor), ]) titleLabelRightMargin = titleLabel.rightAnchor.constraint( equalTo: contentView.rightAnchor, @@ -56,7 +56,7 @@ open class NEBaseValidationCell: NEBaseContactViewCell { addSubview(optionLabel) NSLayoutConstraint.activate([ - optionLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 10), + optionLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 10), optionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor), optionLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -180), ]) @@ -74,46 +74,21 @@ open class NEBaseValidationCell: NEBaseContactViewCell { open func confige(_ model: NENotification) { var optionLabelContent = "" - var nickName = "" - var teamName = "" - // 设置操作者名称 - - if let alias = model.userInfo?.alias { - nickName = alias - } else if let nick = model.userInfo?.userInfo?.nickName { - nickName = nick - } else if let source = model.sourceName { - nickName = source - } - if model.userInfo == nil, let uid = model.sourceID { - let user = NIMSDK.shared().userManager.userInfo(uid) - if let alias = user?.alias, !alias.isEmpty { - nickName = alias - } else if let nick = user?.userInfo?.nickName, !nick.isEmpty { - nickName = nick - } - } // 设置头像 - if let headerUrl = model.userInfo?.userInfo?.avatarUrl, !headerUrl.isEmpty { - avatarImage.sd_setImage(with: URL(string: headerUrl), completed: nil) + if let headerUrl = model.userInfo?.user?.avatar, !headerUrl.isEmpty { + avatarImageView.sd_setImage(with: URL(string: headerUrl), completed: nil) nameLabel.text = "" - avatarImage.backgroundColor = .clear + avatarImageView.backgroundColor = .clear } else if let teamUrl = model.teamInfo?.avatarUrl, !teamUrl.isEmpty { - avatarImage.sd_setImage(with: URL(string: teamUrl), completed: nil) + avatarImageView.sd_setImage(with: URL(string: teamUrl), completed: nil) nameLabel.text = "" - avatarImage.backgroundColor = .clear + avatarImageView.backgroundColor = .clear } else { // 无头像设置其name - if !nickName.isEmpty { - showNameOnCircleHeader(nickName) - } else { - if let id = model.sourceID { - showNameOnCircleHeader(id) - } - } - avatarImage.sd_setImage(with: URL(string: ""), completed: nil) - avatarImage.backgroundColor = UIColor.colorWithString(string: model.userInfo?.userId) + showNameOnCircleHeader(model.userInfo?.showName(false) ?? "") + avatarImageView.sd_setImage(with: URL(string: ""), completed: nil) + avatarImageView.backgroundColor = UIColor.colorWithString(string: model.userInfo?.user?.accountId) } // 设置未读状态(未读数角标+底色) @@ -127,43 +102,17 @@ open class NEBaseValidationCell: NEBaseContactViewCell { } } - if let t = model.targetName { - teamName = t - } - if let type = model.type { - switch type { - case .teamApply: - optionLabelContent = "申请加入群聊 \"\(teamName)\"" - case .teamApplyReject: - optionLabelContent = "拒绝入群邀请 \"\(teamName)\"" - case .teamInvite: - optionLabelContent = "邀请您加入群聊 \"\(teamName)\"" - case .teamInviteReject: - optionLabelContent = "拒绝入群邀请 \"\(teamName)\"" - - case .superTeamApply: - optionLabelContent = "申请加入超大群" - case .superTeamApplyReject: - optionLabelContent = "拒绝加入超大群" - - case .superTeamInvite: - optionLabelContent = "邀请您加入群聊 \"\(teamName)\"" - case .superTeamInviteReject: - optionLabelContent = "拒绝入群邀请 \"\(teamName)\"" - case .addFriendDirectly: - optionLabelContent = "添加您为好友" - case .addFriendRequest: - optionLabelContent = "好友申请" - case .addFriendVerify: - optionLabelContent = "同意了你的好友请求" - case .addFriendReject: - optionLabelContent = "拒绝了你的好友请求" - @unknown default: - optionLabelContent = "未知操作" + if model.applicantAccid != IMKitClient.instance.account() { + optionLabelContent = localizable("add_request") + } else { + if model.handleStatus == .HandleTypeNo { + optionLabelContent = localizable("refused_request") + } else if model.handleStatus == .HandleTypeOk { + optionLabelContent = localizable("agreed_request") } } - titleLabel.text = nickName + titleLabel.text = model.userInfo?.showName() optionLabel.text = optionLabelContent } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift index ad7bd690..7e40b54f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift @@ -5,7 +5,7 @@ import CoreMedia import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit @objcMembers @@ -13,51 +13,38 @@ open class ContactUserViewModel: NSObject { let contactRepo = ContactRepo.shared private let className = "ContactUserViewModel" - func addFriend(_ account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) - let request = NEAddFriendRequest() - request.account = account - request.operationType = .addRequest - contactRepo.addFriend(request: request, completion) + func addFriend(_ account: String, _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) + contactRepo.addFriend(accountId: account, completion) } - open func deleteFriend(account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) + open func deleteFriend(account: String, _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) contactRepo.deleteFriend(account: account, completion) } - open func isFriend(account: String) -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) - return contactRepo.isFriend(account: account) + open func isFriend(account: String, _ completion: @escaping (Bool) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) + contactRepo.isFriend(accountId: account, completion) } - open func isBlack(account: String) -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) - return contactRepo.isBlackList(account: account) + open func isBlack(account: String, _ completion: @escaping (Bool) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) + contactRepo.isBlockList(accountId: account, completion) } - open func removeBlackList(account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) - return contactRepo.removeBlackList(account: account, completion) + open func removeBlackList(account: String, _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) + contactRepo.removeBlockList(accountId: account, completion) } - open func update(_ user: NEKitUser, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + (user.userId ?? "nil")) + open func update(_ user: NEUserWithFriend, _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + (user.user?.accountId ?? "nil")) contactRepo.updateUser(user, completion) } - open func getUserInfo(_ uid: String, _ completion: @escaping (Error?, NEKitUser?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", uid: " + uid) - contactRepo.getUserInfo(uid) { error, users in - completion(error, users?.first) - } - } - - open func fetchUserInfo(accountList: [String], - _ completion: @escaping ([NEKitUser]?, NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", uid: \(accountList)") - contactRepo.fetchUserInfo(accountList: accountList) { users, error in - completion(users, error) - } + open func getUserInfo(_ uid: String, _ completion: @escaping (NEUserWithFriend?, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", uid: " + uid) + contactRepo.getFriendInfo(uid, completion) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift index 579710d7..e2806467 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift @@ -4,189 +4,198 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit +public protocol ContactViewModelDelegate: NSObjectProtocol { + func reloadTableView() + func reloadTableView(_ index: IndexPath) +} + @objcMembers -open class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { +open class ContactViewModel: NSObject, NEContactListener { typealias RefreshBlock = () -> Void public var contacts: [ContactSection] = [] public var indexs: [String]? - private var contactHeaders: [ContactHeadItem]? + private var contactHeaders: ContactSection? public var contactRepo = ContactRepo.shared private var initalDict = [String: [ContactInfo]]() - private let className = "ContactViewModel" + public weak var delegate: ContactViewModelDelegate? - var unreadCount = 0 + var unreadCount = 0 { + didSet { + refresh?() + } + } var refresh: RefreshBlock? init(contactHeaders: [ContactHeadItem]?) { super.init() - NELog.infoLog( - ModuleName + " " + className, + NEALog.infoLog( + ModuleName + " " + className(), desc: #function + ", contactHeaders.count: \(contactHeaders?.count ?? 0)" ) - contactRepo.notiDelegate = self - unreadCount = contactRepo.getNotificationUnreadCount() - self.contactHeaders = contactHeaders - } - open func onNotificationUnreadCountChanged(_ count: Int) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", count: \(count)") - print("onNotificationUnreadCountChanged : ", count) - unreadCount = count - if let block = refresh { - block() + contactRepo.addContactListener(self) + + if let headSection = headerSection(headerItem: contactHeaders) { + self.contactHeaders = headSection + contacts.append(headSection) } } - open func onRecieveNotification(_ notification: NENotification) {} - - func loadData(fetch: Bool = false, _ filters: Set? = nil, completion: @escaping (NSError?, Int) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) + func loadData(_ filters: Set? = nil, completion: @escaping (NSError?, Int) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) weak var weakSelf = self - getContactList(fetch, filters) { contacts, error in + getContactList(filters) { contacts, error in if let users = contacts { - NELog.infoLog("contact loadData", desc: "contact data:\(users)") - weakSelf?.contacts = users - weakSelf?.indexs = self.getIndexs(contactSections: users) - if let headSection = weakSelf?.headerSection(headerItem: weakSelf?.contactHeaders) { - weakSelf?.contacts.insert(headSection, at: 0) + NEALog.infoLog("contact loadData", desc: "contact data:\(users)") + weakSelf?.contacts.removeAll() + if let contactHeaders = weakSelf?.contactHeaders { + weakSelf?.contacts.append(contactHeaders) } + weakSelf?.contacts.append(contentsOf: users) + weakSelf?.indexs = self.getIndexs(contactSections: users) completion(nil, users.count) + } else { + completion(nil, 0) } } } - func getContactList(_ fetch: Bool = false, _ filters: Set? = nil, _ completion: @escaping ([ContactSection]?, NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", filters.count: \(filters?.count ?? 0)") + func getContactList(_ filters: Set? = nil, _ completion: @escaping ([ContactSection]?, NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", filters.count: \(filters?.count ?? 0)") + + // 优选从缓存中取 + if !NEFriendUserCache.shared.isEmpty() { + let friends = NEFriendUserCache.shared.getFriendListNotInBlocklist().map(\.value) + let contactList = formatData(friends, filters) + completion(contactList, nil) + return + } + + // 缓存中没有则远端查询 + contactRepo.getContactList { [weak self] friends, error in + NEALog.infoLog("contact bar getFriendList", desc: "friend count:\(String(describing: friends?.count))") + let contactList = self?.formatData(friends, filters) + completion(contactList, error as? NSError) + } + } + + /// 数据格式化 + /// - Parameters: + /// - friends: 好友列表 + /// - filters: 过滤列表 + /// - Returns: 格式化后的好友列表 + func formatData(_ friends: [NEUserWithFriend]?, _ filters: Set? = nil) -> [ContactSection] { var contactList: [ContactSection] = [] - weak var weakSelf = self - var local = false - if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { - local = true - } - contactRepo.getFriendList(fetch, local: local) { friends, error in - if var users = friends { - NELog.infoLog("contact bar getFriendList", desc: "friend count:\(users.count)") - weakSelf?.initalDict = [String: [ContactInfo]]() - if let filterUsers = filters { - users = users.filter { user in - if let uid = user.userId, filterUsers.contains(uid) { - return false - } - return true + if var users = friends { + initalDict = [String: [ContactInfo]]() + if let filterUsers = filters { + users = users.filter { userFriend in + if let uid = userFriend.user?.accountId, filterUsers.contains(uid) { + return false } + return true } + } - if users.isEmpty { - completion(contactList, nil) - return - } + if users.isEmpty { + return contactList + } - let digitRegular = NSPredicate(format: "SELF MATCHES %@", "[0-9]") - let azRegular = NSPredicate(format: "SELF MATCHES %@", "[A-Z]") - var digitList = [ContactInfo]() - var specialCharList = [ContactInfo]() - for contact: NEKitUser in users { - // get inital of name - var name = contact.alias?.isEmpty == false ? contact.alias : contact.userInfo?.nickName - if name == nil { - name = contact.userId - } - let inital = name?.initalLetter() ?? "#" - let contactInfo = ContactInfo() - contactInfo.user = contact - contactInfo.headerBackColor = UIColor.colorWithString(string: contact.userId ?? "") - - if digitRegular.evaluate(with: inital) { // [0-9] - digitList.append(contactInfo) - } else if !azRegular.evaluate(with: inital) { // [#] - specialCharList.append(contactInfo) - } else { // [A-Z] - if weakSelf?.initalDict[inital] != nil { - weakSelf?.initalDict[inital]?.append(contactInfo) - } else { - weakSelf?.initalDict[inital] = [contactInfo] - } - } + let digitRegular = NSPredicate(format: "SELF MATCHES %@", "[0-9]") + let azRegular = NSPredicate(format: "SELF MATCHES %@", "[A-Z]") + var digitList = [ContactInfo]() + var specialCharList = [ContactInfo]() + for userFriend: NEUserWithFriend in users { + // get inital of name + var name = userFriend.user?.name ?? userFriend.user?.accountId + if let alias = userFriend.friend?.alias, !alias.isEmpty { + name = alias } - digitList.sort { s1, s2 in - s1.user!.showName()! < s2.user!.showName()! - } - specialCharList.sort { s1, s2 in - s1.user!.showName()! < s2.user!.showName()! - } + let inital = name?.initalLetter() ?? "#" + let contactInfo = ContactInfo() + contactInfo.user = userFriend + contactInfo.headerBackColor = UIColor.colorWithString(string: userFriend.user?.accountId ?? "") - guard let initalDict = weakSelf?.initalDict else { - return + if digitRegular.evaluate(with: inital) { // [0-9] + digitList.append(contactInfo) + } else if !azRegular.evaluate(with: inital) { // [#] + specialCharList.append(contactInfo) + } else { // [A-Z] + if initalDict[inital] != nil { + initalDict[inital]?.append(contactInfo) + } else { + initalDict[inital] = [contactInfo] + } } + } + + digitList.sort { s1, s2 in + s1.user!.showName()! < s2.user!.showName()! + } + specialCharList.sort { s1, s2 in + s1.user!.showName()! < s2.user!.showName()! + } - for key in initalDict.keys { - if var value = weakSelf?.initalDict[key] { - value.sort { s1, s2 in - s1.user!.showName()! < s2.user!.showName()! - } - contactList.append(ContactSection(initial: key, contacts: value)) + for key in initalDict.keys { + if var value = initalDict[key] { + value.sort { s1, s2 in + s1.user!.showName()! < s2.user!.showName()! } + contactList.append(ContactSection(initial: key, contacts: value)) } + } - var result = contactList.sorted { s1, s2 in - s1.initial < s2.initial - } + var result = contactList.sorted { s1, s2 in + s1.initial < s2.initial + } - let specialList = digitList + specialCharList - if specialList.count > 0 { - result.append(ContactSection(initial: "#", contacts: specialList)) - } - completion(result, nil) + let specialList = digitList + specialCharList + if specialList.count > 0 { + result.append(ContactSection(initial: "#", contacts: specialList)) } + return result + } + return contactList + } + + /// 返回好友列表 + /// - Returns: 不包含顶部预设数据(验证消息、黑名单、我的群聊)的好友列表 + func getFriendSections() -> [ContactSection] { + let friendSections = contacts.filter { $0.initial != "" } + return friendSections + } + + func getAddApplicationUnreadCount(_ completion: ((Int, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + contactRepo.getUnreadApplicationCount { [weak self] count, error in + self?.unreadCount = count + completion?(count, error as? NSError) } } func headerSection(headerItem: [ContactHeadItem]?) -> ContactSection? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", headerItem.count: \(headerItem?.count ?? 0)") + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", headerItem.count: \(headerItem?.count ?? 0)") guard let header = headerItem else { return nil } var infos: [ContactInfo] = [] for item in header { - let user = NEKitUser() - user.alias = item.name - let userInfo = NEKitUserInfo(nickName: "", avatar: item.imageName) - user.userInfo = userInfo - let info = ContactInfo() - info.user = user + info.user = NEUserWithFriend(alias: item.name, avatar: item.imageName) info.contactCellType = ContactCellType.ContactOthers.rawValue info.router = item.router info.headerBackColor = item.color - if let _ = user.userId { - info.headerBackColor = UIColor.colorWithString(string: user.userId) - } infos.append(info) } return ContactSection(initial: "", contacts: infos) } func getIndexs(contactSections: [ContactSection]?) -> [String]? { - // 根据用户列表获取导航标签 -// NELog.infoLog( -// ModuleName + " " + className, -// desc: #function + ", contactSections.count: \(contactSections?.count ?? 0)" -// ) -// guard let sections = contactSections else { -// return nil -// } -// var indexs: [String] = [] -// for section in sections { -// if section.initial.count > 0 { -// indexs.append(section.initial) -// } -// } - // ["A"..."Z", "#"] let idx = UnicodeScalar("A").value ... UnicodeScalar("Z").value var indexs = (idx.map { String(UnicodeScalar($0)!) }) @@ -194,4 +203,97 @@ open class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { return indexs } + + // MARK: - NEContactListener + + /// 从通讯录中移除 + /// - Parameter accountId: 好友 Id + func removeFromContacts(_ accountId: String) { + for (title, section) in contacts.enumerated() { + for (i, contact) in section.contacts.enumerated() { + if contact.user?.user?.accountId == accountId { + section.contacts.remove(at: i) + + // 该分组无好友后要删除该分组 + if section.contacts.isEmpty { + contacts.remove(at: title) + } + delegate?.reloadTableView() + return + } + } + } + } + + /// 好友添加回调 + /// - Parameter friendInfo: 好友信息 + public func onFriendAdded(_ friendInfo: V2NIMFriend) { + loadData { [weak self] _, _ in + self?.delegate?.reloadTableView() + } + } + + /// 删除好友通知 + /// 本端删除好友,多端同步 + /// - Parameters: + /// - accountId: 删除的好友账号ID + /// - deletionType: 好友删除的类型 + public func onFriendDeleted(_ accountId: String, deletionType: V2NIMFriendDeletionType) { + if NEFriendUserCache.shared.isBlockAccount(accountId) { + return + } + + removeFromContacts(accountId) + } + + /// 收到好友添加申请回调 + /// - Parameter application: 申请添加好友信息 + public func onFriendAddApplication(_ application: V2NIMFriendAddApplication) { + getAddApplicationUnreadCount(nil) + } + + /// 好友添加申请被拒绝回调 + /// - Parameter rejectionInfo: 申请添加好友拒绝信息 + public func onFriendAddRejected(_ rejectionInfo: V2NIMFriendAddApplication) { + getAddApplicationUnreadCount(nil) + } + + /// 好友信息变更回调 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + guard let accountId = friendInfo.accountId else { return } + + if NEFriendUserCache.shared.isBlockAccount(accountId) { + return + } + + loadData { [weak self] _, _ in + self?.delegate?.reloadTableView() + } + } + + /// 用户信息变更回调 + /// - Parameter users: 用户信息 + public func onUserProfileChanged(_ users: [V2NIMUser]) { + loadData { [weak self] _, _ in + self?.delegate?.reloadTableView() + } + } + + /// 黑名单添加回调 + /// - Parameter user: 用户信息 + public func onBlockListAdded(_ user: V2NIMUser) { + guard let accountId = user.accountId else { return } + removeFromContacts(accountId) + } + + /// 黑名单移除回调 + /// - Parameter accountId: 用户 Id + public func onBlockListRemoved(_ accountId: String) { + if NEFriendUserCache.shared.isFriend(accountId) { + loadData { [weak self] _, _ in + self?.delegate?.reloadTableView() + } + } + } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift index 3bc5bcf8..6818f94e 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift @@ -4,7 +4,7 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit @objcMembers @@ -12,8 +12,16 @@ open class FindFriendViewModel: NSObject { let contactRepo = ContactRepo.shared private let className = "FindFriendViewModel" - func searchFriend(_ text: String, _ completion: @escaping ([NEKitUser]?, NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", text: \(text.count)") - contactRepo.fetchUserInfo(accountList: [text], completion) + func searchFriend(_ text: String, _ completion: @escaping (NEUserWithFriend?, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", text: \(text.count)") + + // 优先去缓存中取 + if let userFriend = NEFriendUserCache.shared.getFriendInfo(text) { + completion(userFriend, nil) + return + } + + // 缓存中没有则去远端查询 + contactRepo.getFriendInfo(text, completion) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift index c61d8f08..ec84b024 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift @@ -4,37 +4,38 @@ // found in the LICENSE file. import UIKit + @objcMembers open class NEBaseContactSelectedCell: NEBaseContactTableViewCell { - let sImage = UIImageView() + let sImageView = UIImageView() var sModel: ContactInfo? override open func commonUI() { super.commonUI() leftConstraint?.constant = 50 - contentView.addSubview(sImage) - sImage.image = UIImage.ne_imageNamed(name: "unselect") - sImage.translatesAutoresizingMaskIntoConstraints = false - sImage.accessibilityIdentifier = "id.selector" + contentView.addSubview(sImageView) + sImageView.image = UIImage.ne_imageNamed(name: "unselect") + sImageView.translatesAutoresizingMaskIntoConstraints = false + sImageView.accessibilityIdentifier = "id.selector" } override open func setModel(_ model: ContactInfo) { super.setModel(model) if model.isSelected == false { - sImage.isHighlighted = false + sImageView.isHighlighted = false } else { - sImage.isHighlighted = true + sImageView.isHighlighted = true } } func setSelect() { sModel?.isSelected = true - sImage.isHighlighted = true + sImageView.isHighlighted = true } func setUnselect() { sModel?.isSelected = false - sImage.isHighlighted = false + sImageView.isHighlighted = false } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift index 7830549b..424ca988 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift @@ -5,13 +5,13 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @objcMembers open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataProtrol { - public lazy var arrow: UIImageView = { + public lazy var arrowImageView: UIImageView = { let imageView = UIImageView(image: UIImage.ne_imageNamed(name: "arrowRight")) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .center @@ -32,7 +32,7 @@ open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataPro } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -40,23 +40,23 @@ open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataPro contentView.addSubview(titleLabel) NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 12), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 12), titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35), titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) - contentView.addSubview(arrow) + contentView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - arrow.widthAnchor.constraint(equalToConstant: 15), - arrow.topAnchor.constraint(equalTo: contentView.topAnchor), - arrow.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowImageView.widthAnchor.constraint(equalToConstant: 15), + arrowImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + arrowImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 1), @@ -65,18 +65,18 @@ open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataPro contentView.addSubview(redAngleView) NSLayoutConstraint.activate([ redAngleView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - redAngleView.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -10), + redAngleView.rightAnchor.constraint(equalTo: arrowImageView.leftAnchor, constant: -10), redAngleView.heightAnchor.constraint(equalToConstant: 18), ]) } open func initSubviewsLayout() { if NEKitContactConfig.shared.ui.contactProperties.avatarType == .rectangle { - avatarImage.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius + avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius } else if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { - avatarImage.layer.cornerRadius = 18.0 + avatarImageView.layer.cornerRadius = 18.0 } else { - avatarImage.layer.cornerRadius = 18.0 // Normal UI + avatarImageView.layer.cornerRadius = 18.0 // Normal UI } } @@ -92,33 +92,34 @@ open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataPro } setConfig() - if let userId = user.userId, let u = ChatUserCache.getUserInfo(userId) { + // 更新用户信息 + if let userId = user.user?.accountId, let u = NEFriendUserCache.shared.getFriendInfo(userId) { user = u } if model.contactCellType == 1 { - NELog.infoLog("contact other cell configData", desc: "\(user.alias), image name:\(user.userInfo?.avatarUrl)") + NEALog.infoLog("contact other cell configData", desc: "\(user.friend?.alias), image name:\(user.user?.avatar)") nameLabel.text = "" - titleLabel.text = user.alias - avatarImage.image = UIImage.ne_imageNamed(name: user.userInfo?.avatarUrl) - avatarImage.backgroundColor = model.headerBackColor - arrow.isHidden = false + titleLabel.text = user.friend?.alias + avatarImageView.image = UIImage.ne_imageNamed(name: user.user?.avatar) + avatarImageView.backgroundColor = model.headerBackColor + arrowImageView.isHidden = false } else { // person、custom titleLabel.text = user.showName() nameLabel.text = user.shortName(showAlias: false, count: 2) - if let imageUrl = user.userInfo?.avatarUrl, !imageUrl.isEmpty { - NELog.infoLog("contact p2p cell configData", desc: "imageName:\(imageUrl)") + if let imageUrl = user.user?.avatar, !imageUrl.isEmpty { + NEALog.infoLog("contact p2p cell configData", desc: "imageName:\(imageUrl)") nameLabel.isHidden = true - avatarImage.sd_setImage(with: URL(string: imageUrl), completed: nil) + avatarImageView.sd_setImage(with: URL(string: imageUrl), completed: nil) } else { - NELog.infoLog("contact p2p cell configData", desc: "imageName is nil") + NEALog.infoLog("contact p2p cell configData", desc: "imageName is nil") nameLabel.isHidden = false - avatarImage.sd_setImage(with: nil) - avatarImage.backgroundColor = model.headerBackColor + avatarImageView.sd_setImage(with: nil) + avatarImageView.backgroundColor = model.headerBackColor } - arrow.isHidden = true + arrowImageView.isHidden = true } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactUnCheckCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactUnCheckCell.swift index 8f60c979..0292322b 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactUnCheckCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactUnCheckCell.swift @@ -17,10 +17,10 @@ open class NEBaseContactUnCheckCell: UICollectionViewCell { } func setupUI() { - contentView.addSubview(avatarImage) + contentView.addSubview(avatarImageView) } - lazy var avatarImage: NEUserHeaderView = { + lazy var avatarImageView: NEUserHeaderView = { let view = NEUserHeaderView(frame: .zero) view.titleLabel.font = UIFont.systemFont(ofSize: 16.0) view.clipsToBounds = true @@ -29,10 +29,10 @@ open class NEBaseContactUnCheckCell: UICollectionViewCell { }() func configure(_ model: ContactInfo) { - avatarImage.configHeadData( - headUrl: model.user?.userInfo?.avatarUrl, + avatarImageView.configHeadData( + headUrl: model.user?.user?.avatar, name: model.user?.showName() ?? "", - uid: model.user?.userId ?? "" + uid: model.user?.user?.accountId ?? "" ) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/ContactSectionView.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/ContactSectionView.swift index 0a5d6376..fceebfdb 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/ContactSectionView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/ContactSectionView.swift @@ -16,7 +16,7 @@ open class ContactSectionView: UITableViewHeaderFooterView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift index bb6bd6e7..264e8068 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift @@ -4,16 +4,16 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @objcMembers open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UITextFieldDelegate { - typealias ModifyBlock = (_ user: NEKitUser) -> Void + typealias ModifyBlock = (_ user: NEUserWithFriend) -> Void var completion: ModifyBlock? - var user: NEKitUser? + var user: NEUserWithFriend? let viewmodel = ContactUserViewModel() let textLimit = 15 lazy var aliasInput: UITextField = { @@ -59,7 +59,7 @@ open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UI view.addSubview(aliasInput) aliasInput.placeholder = localizable("input_noteName") - if let alias = user?.alias, !alias.isEmpty { + if let alias = user?.friend?.alias, !alias.isEmpty { aliasInput.text = alias } } @@ -83,15 +83,12 @@ open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UI return } - if user?.alias != aliasInput.text { - user?.alias = aliasInput.text - NotificationCenter.default.post(name: NENotificationName.updateFriendInfo, object: user) - } + user?.friend?.alias = aliasInput.text if let u = user { view.makeToastActivity(.center) viewmodel.update(u) { error in - NELog.infoLog( + NEALog.infoLog( "ContactRemakNameViewController", desc: "CALLBACK update " + (error?.localizedDescription ?? "no error") ) diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift index 3c3afa2d..501ef666 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -12,7 +12,7 @@ import UIKit @objcMembers open class NEBaseContactUserViewController: NEBaseContactViewController, UITableViewDelegate, UITableViewDataSource { - var user: NEKitUser? + var user: NEUserWithFriend? var uid: String? public var isBlack: Bool = false var className = "ContactUserViewController" @@ -22,10 +22,10 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable var data = [[UserItem]]() public var headerView = NEBaseUserInfoHeaderView() - public init(user: NEKitUser?) { + public init(user: NEUserWithFriend?) { super.init(nibName: nil, bundle: nil) self.user = user - uid = user?.userId + uid = user?.user?.accountId } public init(uid: String) { @@ -34,7 +34,7 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -47,17 +47,16 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable weakSelf?.showToast(commonLocalizable("network_error"), .bottom) } view.makeToastActivity(.center) - viewModel.fetchUserInfo(accountList: [userId]) { users, error in + viewModel.getUserInfo(userId) { user, error in weakSelf?.view.hideToastActivity() - NELog.infoLog( + NEALog.infoLog( weakSelf?.className ?? "ContactUserViewController", desc: "CALLBACK getUserInfo " + (error?.localizedDescription ?? "no error") ) if let err = error { weakSelf?.showToast(err.localizedDescription) - } else if let u = users?.first { + } else if let u = user { weakSelf?.user = u - ChatUserCache.updateUserInfo(u) weakSelf?.loadData() } } @@ -108,77 +107,80 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable } func loadData() { - let isFriend = viewModel.contactRepo.isFriend(account: user?.userId ?? "") - isBlack = viewModel.contactRepo.isBlackList(account: user?.userId ?? "") - - if isFriend { - data = [ - [ - UserItem(title: localizable("noteName"), - detailTitle: user?.alias, - value: false, - textColor: UIColor.darkText, - cellClass: TextWithRightArrowCell.self), - ], - [ - UserItem(title: localizable("birthday"), - detailTitle: user?.userInfo?.birth, - value: false, - textColor: UIColor.darkText, - cellClass: TextWithDetailTextCell.self), - UserItem(title: localizable("phone"), - detailTitle: user?.userInfo?.mobile, - value: false, - textColor: UIColor.darkText, - cellClass: TextWithDetailTextCell.self), - UserItem(title: localizable("email"), - detailTitle: user?.userInfo?.email, - value: false, - textColor: UIColor.darkText, - cellClass: TextWithDetailTextCell.self), - UserItem(title: localizable("sign"), - detailTitle: user?.userInfo?.sign, - value: false, - textColor: UIColor.darkText, - cellClass: TextWithDetailTextCell.self), - ], - - [ - UserItem(title: localizable("add_blackList"), - detailTitle: "", - value: isBlack, - textColor: UIColor.darkText, - cellClass: TextWithSwitchCell.self), - ], - [ - UserItem(title: localizable("chat"), - detailTitle: "", - value: false, - textColor: UIColor(hexString: "#337EFF"), - cellClass: CenterTextCell.self), - UserItem(title: localizable("delete_friend"), - detailTitle: "", - value: false, - textColor: UIColor.red, - cellClass: CenterTextCell.self), - ], - ] - } else { - data = [ - [ - UserItem(title: localizable("add_friend"), - detailTitle: user?.alias, - value: false, - textColor: UIColor(hexString: "#337EFF"), - cellClass: CenterTextCell.self), - ], - ] - } + guard let uid = user?.user?.accountId else { return } + viewModel.contactRepo.isFriend(accountId: uid) { [weak self] isFriend in + self?.viewModel.contactRepo.isBlockList(accountId: uid) { isBlack in + self?.isBlack = isBlack + if isFriend { + self?.data = [ + [ + UserItem(title: localizable("noteName"), + detailTitle: self?.user?.friend?.alias, + value: false, + textColor: UIColor.darkText, + cellClass: TextWithRightArrowCell.self), + ], + [ + UserItem(title: localizable("birthday"), + detailTitle: self?.user?.user?.birthday, + value: false, + textColor: UIColor.darkText, + cellClass: TextWithDetailTextCell.self), + UserItem(title: localizable("phone"), + detailTitle: self?.user?.user?.mobile, + value: false, + textColor: UIColor.darkText, + cellClass: TextWithDetailTextCell.self), + UserItem(title: localizable("email"), + detailTitle: self?.user?.user?.email, + value: false, + textColor: UIColor.darkText, + cellClass: TextWithDetailTextCell.self), + UserItem(title: localizable("sign"), + detailTitle: self?.user?.user?.sign, + value: false, + textColor: UIColor.darkText, + cellClass: TextWithDetailTextCell.self), + ], + + [ + UserItem(title: localizable("add_blackList"), + detailTitle: "", + value: isBlack, + textColor: UIColor.darkText, + cellClass: TextWithSwitchCell.self), + ], + [ + UserItem(title: localizable("chat"), + detailTitle: "", + value: false, + textColor: UIColor(hexString: "#337EFF"), + cellClass: CenterTextCell.self), + UserItem(title: localizable("delete_friend"), + detailTitle: "", + value: false, + textColor: UIColor.red, + cellClass: CenterTextCell.self), + ], + ] + } else { + self?.data = [ + [ + UserItem(title: localizable("add_friend"), + detailTitle: self?.user?.friend?.alias, + value: false, + textColor: UIColor(hexString: "#337EFF"), + cellClass: CenterTextCell.self), + ], + ] + } - headerView.setData(user: user) - tableView.tableHeaderView = tableView.tableHeaderView - tableView.tableHeaderView?.layoutIfNeeded() - tableView.reloadData() + self?.headerView.setData(user: self?.user) + self?.tableView.tableHeaderView = self?.tableView.tableHeaderView + self?.tableView.tableHeaderView?.layoutIfNeeded() + self?.tableView.reloadData() + } + } } open func numberOfSections(in tableView: UITableView) -> Int { @@ -242,9 +244,9 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable } open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = UIColor.clear - return header + let headerView = UIView() + headerView.backgroundColor = UIColor.clear + return headerView } open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { @@ -273,11 +275,14 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable deleteFriend(user: user) } if item.title == localizable("add_friend") { - if let uId = user?.userId, - viewModel.isFriend(account: uId) { - loadData() - } else { - addFriend() + if let uId = user?.user?.accountId { + viewModel.isFriend(account: uId) { isFriend in + if isFriend { + self.loadData() + } else { + self.addFriend() + } + } } } } @@ -292,7 +297,6 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable remark.completion = { [weak self] u in self?.user = u self?.headerView.setData(user: u) - ChatUserCache.updateUserInfo(u) } navigationController?.pushViewController(remark, animated: true) @@ -311,12 +315,12 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable return } - guard let userId = user?.userId else { + guard let userId = user?.user?.accountId else { return } if isBlack { // add - viewModel.contactRepo.addBlackList(account: userId) { [weak self] error in + viewModel.contactRepo.addBlockList(accountId: userId) { [weak self] error in if error != nil { self?.view.makeToast(error?.localizedDescription) } else { @@ -328,7 +332,7 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable } else { // remove - viewModel.contactRepo.removeBlackList(account: userId) { [weak self] error in + viewModel.contactRepo.removeBlockList(accountId: userId) { [weak self] error in if error != nil { self?.view.makeToast(error?.localizedDescription) } else { @@ -340,42 +344,42 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable } } - func chat(user: NEKitUser?) { - guard let accid = self.user?.userId else { + func chat(user: NEUserWithFriend?) { + guard let accid = self.user?.user?.accountId else { return } - let session = NIMSession(accid, type: .P2P) + let conversationId = V2NIMConversationIdUtil.p2pConversationId(accid) Router.shared.use( PushP2pChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session, "removeUserVC": true], + parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any, "removeUserVC": true], closure: nil ) } - func deleteFriendAction(user: NEKitUser?) { + func deleteFriendAction(user: NEUserWithFriend?) { weak var weakSelf = self if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { weakSelf?.showToast(commonLocalizable("network_error")) return } - if let userId = user?.userId { + if let userId = user?.user?.accountId { viewModel.deleteFriend(account: userId) { error in - NELog.infoLog( + NEALog.infoLog( self.className, desc: "CALLBACK deleteFriend " + (error?.localizedDescription ?? "no error") ) if error != nil { self.showToast(error?.localizedDescription ?? "") } else { - ChatUserCache.removeUserInfo(userId) + NEFriendUserCache.shared.removeFriendInfo(userId) self.navigationController?.popViewController(animated: true) } } } } - open func deleteFriend(user: NEKitUser?) { + open func deleteFriend(user: NEUserWithFriend?) { let alertTitle = String(format: localizable("delete_title"), user?.showName(true) ?? "") let alertController = UIAlertController( title: alertTitle, @@ -411,23 +415,26 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable weakSelf?.showToast(commonLocalizable("network_error")) return } - if let account = user?.userId { + if let account = user?.user?.accountId { viewModel.addFriend(account) { error in - NELog.infoLog( + NEALog.infoLog( self.className, desc: "CALLBACK addFriend " + (error?.localizedDescription ?? "no error") ) if let err = error { - NELog.errorLog("ContactUserViewController", desc: "❌add friend failed :\(err)") + NEALog.errorLog("ContactUserViewController", desc: "❌add friend failed :\(err)") } else { weakSelf?.showToast(localizable("send_friend_apply")) - if let model = weakSelf?.viewModel, - model.isBlack(account: account) { - weakSelf?.viewModel.removeBlackList(account: account) { err in - NELog.infoLog( - self.className, - desc: #function + "CALLBACK " + (err?.localizedDescription ?? "no error") - ) + if let model = weakSelf?.viewModel { + model.isBlack(account: account) { isBlack in + if isBlack { + weakSelf?.viewModel.removeBlackList(account: account) { err in + NEALog.infoLog( + self.className, + desc: #function + "CALLBACK " + (err?.localizedDescription ?? "no error") + ) + } + } } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsSelectedViewController.swift index c29e1b79..30ccfa7c 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsSelectedViewController.swift @@ -2,6 +2,8 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -19,6 +21,7 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI public var selectArray = [ContactInfo]() public let selectDic = [String: ContactInfo]() + public var isCreating = false // 是否正在创建群组 public lazy var collectionBackView: UIView = { let view = UIView() @@ -29,18 +32,18 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI return view }() - public lazy var collection: UICollectionView = { + public lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumLineSpacing = 0 layout.minimumInteritemSpacing = 0 - let collect = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) - collect.contentInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) - collect.accessibilityIdentifier = "id.selected" - return collect + let collectView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) + collectView.contentInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) + collectView.accessibilityIdentifier = "id.selected" + return collectView }() - public var sureBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 76, height: 32)) + public var sureButton = UIButton(frame: CGRect(x: 0, y: 0, width: 76, height: 32)) var collectionBackViewTopMargin: CGFloat = 0 var collectionBackViewHeight: CGFloat = 52 @@ -50,19 +53,19 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI public let viewModel = ContactViewModel(contactHeaders: nil) public lazy var tableView: UITableView = { - let table = UITableView(frame: .zero, style: .plain) - table.backgroundColor = .clear - table.sectionIndexColor = .ne_greyText - table.delegate = self - table.dataSource = self - table.translatesAutoresizingMaskIntoConstraints = false - table.separatorStyle = .none - table.contentInset = .init(top: -10, left: 0, bottom: 0, right: 0) + let tableView = UITableView(frame: .zero, style: .plain) + tableView.backgroundColor = .clear + tableView.sectionIndexColor = .ne_greyText + tableView.delegate = self + tableView.dataSource = self + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.contentInset = .init(top: -10, left: 0, bottom: 0, right: 0) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0 + tableView.sectionHeaderTopPadding = 0 } - return table + return tableView }() var tableViewTopAnchor: NSLayoutConstraint? @@ -73,7 +76,7 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -100,27 +103,27 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI collectionBackView.heightAnchor.constraint(equalToConstant: collectionBackViewHeight), ]) - collection.backgroundColor = .clear - collection.delegate = self - collection.dataSource = self - collection.allowsMultipleSelection = false - collection.translatesAutoresizingMaskIntoConstraints = false - collectionBackView.addSubview(collection) + collectionView.backgroundColor = .clear + collectionView.delegate = self + collectionView.dataSource = self + collectionView.allowsMultipleSelection = false + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionBackView.addSubview(collectionView) NSLayoutConstraint.activate([ - collection.centerYAnchor.constraint(equalTo: collectionBackView.centerYAnchor), - collection.leftAnchor.constraint(equalTo: collectionBackView.leftAnchor), - collection.rightAnchor.constraint(equalTo: collectionBackView.rightAnchor), - collection.heightAnchor.constraint(equalToConstant: collectionBackViewHeight), + collectionView.centerYAnchor.constraint(equalTo: collectionBackView.centerYAnchor), + collectionView.leftAnchor.constraint(equalTo: collectionBackView.leftAnchor), + collectionView.rightAnchor.constraint(equalTo: collectionBackView.rightAnchor), + collectionView.heightAnchor.constraint(equalToConstant: collectionBackViewHeight), ]) view.addSubview(tableView) tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + tableViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - tableViewTopAnchor!, ]) tableView.register( @@ -128,9 +131,9 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI forHeaderFooterViewReuseIdentifier: "\(NSStringFromClass(ContactSectionView.self))" ) - customCells.forEach { (key: Int, value: AnyClass) in + for (key, value) in customCells { if value is ContactCellDataProtrol.Type { - self.tableView.register( + tableView.register( value, forCellReuseIdentifier: "\(NSStringFromClass(value))" ) @@ -148,27 +151,30 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI open func setupNavRightItem() { if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { - let rightItem = UIBarButtonItem(customView: sureBtn) + let rightItem = UIBarButtonItem(customView: sureButton) navigationItem.rightBarButtonItem = rightItem - sureBtn.addTarget(self, action: #selector(sureClick(_:)), for: .touchUpInside) - sureBtn.setTitle(localizable("alert_sure"), for: .normal) - sureBtn.setTitleColor(.white, for: .normal) - sureBtn.layer.cornerRadius = 4 - sureBtn.contentHorizontalAlignment = .center - sureBtn.titleLabel?.font = UIFont.systemFont(ofSize: 16.0) + sureButton.addTarget(self, action: #selector(sureClick(_:)), for: .touchUpInside) + sureButton.setTitle(localizable("alert_sure"), for: .normal) + sureButton.setTitleColor(.white, for: .normal) + sureButton.layer.cornerRadius = 4 + sureButton.contentHorizontalAlignment = .center + sureButton.titleLabel?.font = UIFont.systemFont(ofSize: 16.0) } else { navigationView.setMoreButtonTitle(localizable("alert_sure")) navigationView.moreButton.setTitleColor(.white, for: .normal) navigationView.moreButton.layer.cornerRadius = 4 navigationView.moreButton.contentHorizontalAlignment = .center navigationView.addMoreButtonTarget(target: self, selector: #selector(sureClick(_:))) - navigationView.setBackButtonTitle(localizable("close")) - navigationView.backButton.setTitleColor(.ne_darkText, for: .normal) - sureBtn = navigationView.moreButton + sureButton = navigationView.moreButton } } open func sureClick(_ sender: UIButton) { + // 防止多次点击确定按钮会多次创建群聊 + if isCreating { + return + } + if selectArray.count <= 0 { showToast(localizable("select_contact")) return @@ -185,35 +191,51 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI return } + isCreating = true var accids = [String]() var names = [String]() + let group = DispatchGroup() + var mine: NEUserWithFriend? - names.append(viewModel.contactRepo.getUserName()) - - var users = [NIMUser]() - for c in selectArray { - accids.append(c.user?.userId ?? "") - if let name = c.user?.userInfo?.nickName { - names.append(name) - } else if let accid = c.user?.userId { - names.append(accid) - } - if let user = c.user?.imUser { - users.append(user) + if let mineInfo = NEFriendUserCache.shared.getFriendInfo(IMKitClient.instance.account()) { + mine = mineInfo + } else { + group.enter() + ContactRepo.shared.getMyUserInfo { mineInfo in + mine = mineInfo + group.leave() } } - if let uid = userId { - accids.append(uid) + group.notify(queue: .main) { [weak self] in + let myName = mine?.showName() ?? IMKitClient.instance.account() + names.append(myName) + var users = [V2NIMUser]() + for c in self?.selectArray ?? [] { + accids.append(c.user?.user?.accountId ?? "") + if let name = c.user?.user?.name { + names.append(name) + } else if let accid = c.user?.user?.accountId { + names.append(accid) + } + if let user = c.user?.user { + users.append(user) + } + } + + if let uid = self?.userId { + accids.append(uid) + } + let nameString = names.joined(separator: "、") + print("name string : ", nameString) + Router.shared.use( + ContactSelectedUsersRouter, + parameters: ["accids": accids, "names": nameString, "im_user": users], + closure: nil + ) + self?.navigationController?.popViewController(animated: true) + self?.isCreating = false } - let nameString = names.joined(separator: "、") - print("name string : ", nameString) - Router.shared.use( - ContactSelectedUsersRouter, - parameters: ["accids": accids, "names": nameString, "im_user": users], - closure: nil - ) - navigationController?.popViewController(animated: true) } // MARK: - Table View DataSource And Delegate @@ -320,7 +342,7 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI tableViewTopAnchor?.constant += collectionBackViewHeight + collectionBackViewTopMargin * 2 } } - collection.reloadData() + collectionView.reloadData() tableView.reloadData() refreshSelectCount() } @@ -334,16 +356,16 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI collectionBackView.isHidden = true tableViewTopAnchor?.constant -= collectionBackViewHeight + collectionBackViewTopMargin * 2 } - collection.reloadData() + collectionView.reloadData() tableView.reloadData() refreshSelectCount() } func refreshSelectCount() { if selectArray.count > 0 { - sureBtn.setTitle("确定(\(selectArray.count))", for: .normal) + sureButton.setTitle("确定(\(selectArray.count))", for: .normal) } else { - sureBtn.setTitle(localizable("alert_sure"), for: .normal) + sureButton.setTitle(localizable("alert_sure"), for: .normal) } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsViewController.swift index 4f418728..20392e98 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsViewController.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -13,8 +13,7 @@ public protocol NEBaseContactsViewControllerDelegate { } @objcMembers -open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, - SystemMessageProviderDelegate, FriendProviderDelegate, TabNavigationViewDelegate, UIGestureRecognizerDelegate { +open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate, TabNavigationViewDelegate, ContactViewModelDelegate { public var delegate: NEBaseContactsViewControllerDelegate? // custom ui cell @@ -41,10 +40,97 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, private var bodyTopViewHeightAnchor: NSLayoutConstraint? private var bodyBottomViewHeightAnchor: NSLayoutConstraint? + public lazy var navigationView: TabNavigationView = { + let nav = TabNavigationView(frame: CGRect.zero) + nav.translatesAutoresizingMaskIntoConstraints = false + nav.delegate = self + + if let addImg = NEKitContactConfig.shared.ui.titleBarRightRes { + nav.addBtn.setImage(addImg, for: .normal) + } + if let searchImg = NEKitContactConfig.shared.ui.titleBarRight2Res { + nav.searchBtn.setImage(searchImg, for: .normal) + } + return nav + }() + + public lazy var bodyTopView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + return view + }() + + public lazy var bodyView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + view.addSubview(contentView) + + NSLayoutConstraint.activate([ + contentView.topAnchor.constraint(equalTo: view.topAnchor), + contentView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + return view + }() + + public lazy var contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + view.addSubview(tableView) + view.addSubview(emptyView) + + NSLayoutConstraint.activate([ + tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.rightAnchor.constraint(equalTo: view.rightAnchor), + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + NSLayoutConstraint.activate([ + emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), + emptyView.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 100), + emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), + ]) + + return view + }() + + public lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .grouped) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + tableView.backgroundColor = UIColor.ne_backgroundColor + tableView.sectionFooterHeight = 0 + tableView.sectionIndexColor = .ne_greyText + + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + return tableView + }() + + lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView(imageName: "user_empty", content: localizable("no_friend"), frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + view.isHidden = true + return view + }() + + public lazy var bodyBottomView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + return view + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nil, bundle: nil) - viewModel.contactRepo.addNotificationDelegate(delegate: self) - viewModel.contactRepo.addContactDelegate(delegate: self) } public required init?(coder: NSCoder) { @@ -53,13 +139,6 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - - // 通讯录异步进行远端加载 - if IMKitConfigCenter.shared.contactAsyncLoadEnable { - DispatchQueue.main.async { - self.loadData(fetch: true) - } - } if navigationController?.viewControllers.count ?? 0 > 0 { if let root = navigationController?.viewControllers[0] as? UIViewController { if root.isKind(of: NEBaseContactsViewController.self) { @@ -67,24 +146,28 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, } } } + loadData() + viewModel.getAddApplicationUnreadCount(nil) } override open func viewDidLoad() { super.viewDidLoad() showTitleBar() commonUI() - viewModel.refresh = { [weak self] in - self?.didRefreshTable() - } - loadData(fetch: true) + weak var weakSelf = self + viewModel.delegate = self + viewModel.refresh = { + weakSelf?.didRefreshTable() + } - NotificationCenter.default.addObserver(self, selector: #selector(didRefreshTable), name: NENotificationName.updateFriendInfo, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(clearValidationUnreadCount), name: NENotificationName.clearValidationUnreadCount, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(clearValidationUnreadCount), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(loadData), name: NENotificationName.friendCacheInit, object: nil) } - deinit { - viewModel.contactRepo.removeNotificationDelegate(delegate: self) - viewModel.contactRepo.removeContactDelegate(delegate: self) + func clearValidationUnreadCount() { + viewModel.unreadCount = 0 } open func showTitleBar() { @@ -167,100 +250,8 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, return true } - // MARK: lazy load - - public lazy var navigationView: TabNavigationView = { - let nav = TabNavigationView(frame: CGRect.zero) - nav.translatesAutoresizingMaskIntoConstraints = false - nav.delegate = self - - if let addImg = NEKitContactConfig.shared.ui.titleBarRightRes { - nav.addBtn.setImage(addImg, for: .normal) - } - if let searchImg = NEKitContactConfig.shared.ui.titleBarRight2Res { - nav.searchBtn.setImage(searchImg, for: .normal) - } - return nav - }() - - public lazy var bodyTopView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - return view - }() - - public lazy var bodyView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - view.addSubview(contentView) - - NSLayoutConstraint.activate([ - contentView.topAnchor.constraint(equalTo: view.topAnchor), - contentView.leftAnchor.constraint(equalTo: view.leftAnchor), - contentView.rightAnchor.constraint(equalTo: view.rightAnchor), - contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - return view - }() - - public lazy var contentView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - view.addSubview(tableView) - view.addSubview(emptyView) - - NSLayoutConstraint.activate([ - tableView.leftAnchor.constraint(equalTo: view.leftAnchor), - tableView.rightAnchor.constraint(equalTo: view.rightAnchor), - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - NSLayoutConstraint.activate([ - emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), - emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), - emptyView.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 100), - emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), - ]) - - return view - }() - - public lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .grouped) - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.backgroundColor = UIColor.ne_backgroundColor - tableView.sectionFooterHeight = 0 - tableView.sectionIndexColor = .ne_greyText - - tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) - return tableView - }() - - lazy var emptyView: NEEmptyDataView = { - let view = NEEmptyDataView(imageName: "user_empty", content: localizable("no_friend"), frame: .zero) - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - view.isHidden = true - return view - }() - - public lazy var bodyBottomView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - return view - }() - - open func loadData(fetch: Bool = false) { - viewModel.loadData(fetch: fetch) { [weak self] error, userSectionCount in - self?.emptyView.isHidden = userSectionCount > 0 + open func loadData() { + viewModel.loadData { [weak self] error, userSectionCount in if error == nil { self?.delegate?.onDataLoaded() self?.didRefreshTable() @@ -274,7 +265,7 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - NELog.infoLog(ModuleName + " " + className(), desc: "contact section: \(section), count:\(viewModel.contacts[section].contacts.count)") + NEALog.infoLog(ModuleName + " " + className(), desc: "contact section: \(section), count:\(viewModel.contacts[section].contacts.count)") return viewModel.contacts[section].contacts.count } @@ -384,41 +375,16 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, func didRefreshTable() { tableView.reloadData() + emptyView.isHidden = viewModel.getFriendSections().count > 0 } -// MARK: SystemMessageProviderDelegate - - open func onRecieveNotification(notification: NENotification) { - print("onRecieveNotification type:\(notification.type)") - if notification.type == .addFriendDirectly { - loadData() - } - } - - open func onNotificationUnreadCountChanged(count: Int) { - print("unread count:\(count)") - viewModel.unreadCount = count + public func reloadTableView() { didRefreshTable() } -// MARK: FriendProviderDelegate - - open func onFriendChanged(user: NEKitUser) { - print("onFriendChanged:\(user.userId)") - loadData() - } - - open func onBlackListChanged() { - print("onBlackListChanged") - loadData() - } - - open func onUserInfoChanged(user: NEKitUser) { - print("onUserInfoChanged:\(user.userId)") - loadData() + public func reloadTableView(_ index: IndexPath) { + tableView.reloadData([index]) } - - open func onReceive(_ notification: NIMCustomSystemNotification) {} } extension NEBaseContactsViewController { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift index 9ce375e1..e4207f7a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift @@ -3,7 +3,8 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECommonKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -11,7 +12,39 @@ import UIKit open class NEBaseFindFriendViewController: NEBaseContactViewController, UITextFieldDelegate { public let viewModel = FindFriendViewModel() public let hasRequest = false - public let searchInput = UITextField() + + /// 搜索输入框 + public let searchInput: UITextField = { + let searchInput = UITextField() + searchInput.translatesAutoresizingMaskIntoConstraints = false + searchInput.textColor = UIColor(hexString: "333333") + searchInput.placeholder = localizable("input_userId") + searchInput.font = UIFont.systemFont(ofSize: 14.0) + searchInput.returnKeyType = .search + searchInput.clearButtonMode = .always + searchInput.accessibilityIdentifier = "id.addFriendAccount" + return searchInput + }() + + public var isRequesting = false + + /// 搜索背景 + public lazy var searchBackView: UIView = { + let searchBackView = UIView() + searchBackView.backgroundColor = UIColor(hexString: "F2F4F5") + searchBackView.translatesAutoresizingMaskIntoConstraints = false + searchBackView.clipsToBounds = true + searchBackView.layer.cornerRadius = 4.0 + return searchBackView + }() + + /// 搜索图片 + public lazy var searchImageView: UIImageView = { + let searchImageView = UIImageView() + searchImageView.image = UIImage.ne_imageNamed(name: "search") + searchImageView.translatesAutoresizingMaskIntoConstraints = false + return searchImageView + }() override open func viewDidLoad() { super.viewDidLoad() @@ -25,49 +58,37 @@ open class NEBaseFindFriendViewController: NEBaseContactViewController, UITextFi })) } + /// UI 初始化 open func setupUI() { - let searchBack = UIView() - view.addSubview(searchBack) - searchBack.backgroundColor = UIColor(hexString: "F2F4F5") - searchBack.translatesAutoresizingMaskIntoConstraints = false - searchBack.clipsToBounds = true - searchBack.layer.cornerRadius = 4.0 + view.addSubview(searchBackView) NSLayoutConstraint.activate([ - searchBack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - searchBack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - searchBack.topAnchor.constraint(equalTo: view.topAnchor, constant: 20 + topConstant), - searchBack.heightAnchor.constraint(equalToConstant: 32), + searchBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + searchBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + searchBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20 + topConstant), + searchBackView.heightAnchor.constraint(equalToConstant: 32), ]) - let searchImage = UIImageView() - searchBack.addSubview(searchImage) - searchImage.image = UIImage.ne_imageNamed(name: "search") - searchImage.translatesAutoresizingMaskIntoConstraints = false + searchBackView.addSubview(searchImageView) NSLayoutConstraint.activate([ - searchImage.centerYAnchor.constraint(equalTo: searchBack.centerYAnchor), - searchImage.leftAnchor.constraint(equalTo: searchBack.leftAnchor, constant: 18), - searchImage.widthAnchor.constraint(equalToConstant: 13), - searchImage.heightAnchor.constraint(equalToConstant: 13), + searchImageView.centerYAnchor.constraint(equalTo: searchBackView.centerYAnchor), + searchImageView.leftAnchor.constraint(equalTo: searchBackView.leftAnchor, constant: 18), + searchImageView.widthAnchor.constraint(equalToConstant: 13), + searchImageView.heightAnchor.constraint(equalToConstant: 13), ]) - searchBack.addSubview(searchInput) - searchInput.translatesAutoresizingMaskIntoConstraints = false + searchBackView.addSubview(searchInput) + searchInput.delegate = self + NSLayoutConstraint.activate([ - searchInput.leftAnchor.constraint(equalTo: searchImage.rightAnchor, constant: 5), - searchInput.rightAnchor.constraint(equalTo: searchBack.rightAnchor, constant: -18), - searchInput.topAnchor.constraint(equalTo: searchBack.topAnchor), - searchInput.bottomAnchor.constraint(equalTo: searchBack.bottomAnchor), + searchInput.leftAnchor.constraint(equalTo: searchImageView.rightAnchor, constant: 5), + searchInput.rightAnchor.constraint(equalTo: searchBackView.rightAnchor, constant: -18), + searchInput.topAnchor.constraint(equalTo: searchBackView.topAnchor), + searchInput.bottomAnchor.constraint(equalTo: searchBackView.bottomAnchor), ]) - searchInput.textColor = UIColor(hexString: "333333") - searchInput.placeholder = localizable("input_userId") - searchInput.font = UIFont.systemFont(ofSize: 14.0) - searchInput.returnKeyType = .search - searchInput.delegate = self - searchInput.clearButtonMode = .always + if let clearButton = searchInput.value(forKey: "_clearButton") as? UIButton { clearButton.accessibilityIdentifier = "id.clear" } - searchInput.accessibilityIdentifier = "id.addFriendAccount" NotificationCenter.default.addObserver( self, @@ -105,7 +126,12 @@ open class NEBaseFindFriendViewController: NEBaseContactViewController, UITextFi } open func startSearch(_ text: String) { - if IMKitClient.instance.isMySelf(text) { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + if IMKitClient.instance.isMe(text) { Router.shared.use( MeSettingRouter, parameters: ["nav": navigationController as Any], @@ -114,14 +140,19 @@ open class NEBaseFindFriendViewController: NEBaseContactViewController, UITextFi return } + if isRequesting == true { + return + } + isRequesting = true weak var weakSelf = self - viewModel.searchFriend(text) { users, error in - NELog.infoLog( + viewModel.searchFriend(text) { user, error in + weakSelf?.isRequesting = false + NEALog.infoLog( "NEBaseFindFriendViewController", desc: "CALLBACK searchFriend " + (error?.localizedDescription ?? "no error") ) if error == nil { - if let user = users?.first { + if let user = user, user.user != nil || user.friend != nil { // go to detail Router.shared.use( ContactUserInfoPageRouter, diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift index 6878dfae..9a03b0d1 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift @@ -3,20 +3,20 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers open class NEBaseUserInfoHeaderView: UIView { public var labelConstraints = [NSLayoutConstraint]() - public lazy var avatarImage: UIImageView = { - let avatarImage = UIImageView() - avatarImage.backgroundColor = UIColor(hexString: "#537FF4") - avatarImage.translatesAutoresizingMaskIntoConstraints = false - avatarImage.contentMode = .scaleAspectFill - avatarImage.clipsToBounds = true - avatarImage.accessibilityIdentifier = "id.avatar" - return avatarImage + public lazy var avatarImageView: UIImageView = { + let imageView = UIImageView() + imageView.backgroundColor = UIColor(hexString: "#537FF4") + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + imageView.accessibilityIdentifier = "id.avatar" + return imageView }() public lazy var nameLabel: UILabel = { @@ -68,29 +68,29 @@ open class NEBaseUserInfoHeaderView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { backgroundColor = .white - addSubview(avatarImage) + addSubview(avatarImageView) addSubview(nameLabel) addSubview(titleLabel) addSubview(detailLabel) addSubview(lineView) NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: leftAnchor, constant: 20), - avatarImage.widthAnchor.constraint(equalToConstant: 60), - avatarImage.heightAnchor.constraint(equalToConstant: 60), - avatarImage.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 0), + avatarImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: 20), + avatarImageView.widthAnchor.constraint(equalToConstant: 60), + avatarImageView.heightAnchor.constraint(equalToConstant: 60), + avatarImageView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 0), ]) NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), - nameLabel.rightAnchor.constraint(equalTo: avatarImage.rightAnchor), - nameLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor), - nameLabel.bottomAnchor.constraint(equalTo: avatarImage.bottomAnchor), + nameLabel.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), + nameLabel.rightAnchor.constraint(equalTo: avatarImageView.rightAnchor), + nameLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor), + nameLabel.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), ]) commonUI(showDetail: false) @@ -103,9 +103,9 @@ open class NEBaseUserInfoHeaderView: UIView { var detail2Constraint = [NSLayoutConstraint]() if showDetail { titleConstraint = [ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 20), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 20), titleLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -35), - titleLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor, constant: -2), + titleLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: -2), titleLabel.heightAnchor.constraint(equalToConstant: 22), ] @@ -125,9 +125,9 @@ open class NEBaseUserInfoHeaderView: UIView { ] } else { titleConstraint = [ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 16), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 16), titleLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -20), - titleLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor, constant: 7), + titleLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: 7), titleLabel.heightAnchor.constraint(equalToConstant: 22), ] @@ -146,20 +146,20 @@ open class NEBaseUserInfoHeaderView: UIView { updateConstraintsIfNeeded() } - open func setData(user: NEKitUser?) { - guard let user = user else { + open func setData(user: NEUserWithFriend?) { + guard let userFriend = user else { return } // avatar - if let imageUrl = user.userInfo?.avatarUrl, !imageUrl.isEmpty { - avatarImage.sd_setImage(with: URL(string: imageUrl), completed: nil) - avatarImage.backgroundColor = .clear + if let imageUrl = userFriend.user?.avatar, !imageUrl.isEmpty { + avatarImageView.sd_setImage(with: URL(string: imageUrl), completed: nil) + avatarImageView.backgroundColor = .clear nameLabel.isHidden = true } else { - avatarImage.sd_setImage(with: nil) - avatarImage.backgroundColor = UIColor.colorWithString(string: user.userId) - nameLabel.text = user.shortName(showAlias: false, count: 2) + avatarImageView.sd_setImage(with: nil) + avatarImageView.backgroundColor = UIColor.colorWithString(string: userFriend.user?.accountId) + nameLabel.text = userFriend.shortName(showAlias: false, count: 2) nameLabel.isHidden = false } } diff --git a/NEConversationUIKit/NEConversationUIKit.podspec b/NEConversationUIKit/NEConversationUIKit.podspec index ee0ea46a..c4821700 100644 --- a/NEConversationUIKit/NEConversationUIKit.podspec +++ b/NEConversationUIKit/NEConversationUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEConversationUIKit' - s.version = '9.7.0' + s.version = '10.0.0-beta' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. @@ -40,4 +40,5 @@ TODO: Add long description of the pod here. s.dependency 'NECommonUIKit' s.dependency 'NEChatKit' + s.dependency 'MJRefresh' end diff --git a/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings b/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings index 0fbc65a4..846b1d14 100644 --- a/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings +++ b/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings @@ -15,7 +15,6 @@ "group"="Group"; "discussion_group"="Temp Group"; "senior_group"="Group"; -"search"="Search"; "cancel"="Cancel"; "search_keyword"="Enter the key words"; "user_not_exist"="Not Exist"; diff --git a/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings index 6369c851..687ba504 100644 --- a/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -15,7 +15,6 @@ "group"="群聊"; "discussion_group"="讨论组"; "senior_group"="高级群"; -"search"="搜索"; "cancel"="取消"; "search_keyword"="请输入你要搜索的关键字"; "user_not_exist"="该用户不存在"; diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift index 3350b588..12a37b1a 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift @@ -3,9 +3,11 @@ // found in the LICENSE file. import Foundation +import NECoreIM2Kit import NIMSDK + @objcMembers -public class ConversationDeduplicationHelper: NSObject, NIMLoginManagerDelegate { +public class ConversationDeduplicationHelper: NSObject, NEIMKitClientListener { // 单例变量 static let instance = ConversationDeduplicationHelper() // 最多缓存数量,可外部修改 @@ -15,20 +17,20 @@ public class ConversationDeduplicationHelper: NSObject, NIMLoginManagerDelegate override private init() { super.init() - NIMSDK.shared().loginManager.add(self) + IMKitClient.instance.addLoginListener(self) } deinit { - NIMSDK.shared().loginManager.remove(self) + IMKitClient.instance.removeLoginListener(self) } - public func onLogin(_ step: NIMLoginStep) { - if step == .logout { + public func onLoginStatus(_ status: V2NIMLoginStatus) { + if status == .LOGIN_STATUS_LOGOUT { clearCache() } } - public func onKickout(_ result: NIMLoginKickoutResult) { + public func onKickedOffline(_ detail: V2NIMKickedOfflineDetail) { clearCache() } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationUI.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationUI.swift index 2b66118b..e16e831f 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationUI.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationUI.swift @@ -8,6 +8,6 @@ import Foundation @_exported import NEChatKit @_exported import NECommonKit @_exported import NECommonUIKit -@_exported import NECoreIMKit +@_exported import NECoreIM2Kit @_exported import NECoreKit @_exported import NIMSDK diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift index 0cd25e6b..73435a53 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift @@ -8,9 +8,8 @@ import UIKit @objcMembers open class NEBaseConversationListCell: UITableViewCell { -// private var viewModel = ConversationViewModel() public var topStickInfos = [NIMSession: NIMStickTopSessionInfo]() - private let repo = ConversationRepo.shared + private var timeWidth: NSLayoutConstraint? override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -20,7 +19,7 @@ open class NEBaseConversationListCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupSubviews() { @@ -29,16 +28,16 @@ open class NEBaseConversationListCell: UITableViewCell { backgroundColor = bgColor } - contentView.addSubview(headImge) + contentView.addSubview(headImageView) contentView.addSubview(redAngleView) - contentView.addSubview(title) - contentView.addSubview(subTitle) + contentView.addSubview(titleLabel) + contentView.addSubview(subTitleLabel) contentView.addSubview(timeLabel) - contentView.addSubview(notifyMsg) + contentView.addSubview(notifyMsgView) NSLayoutConstraint.activate([ - redAngleView.centerXAnchor.constraint(equalTo: headImge.rightAnchor, constant: -8), - redAngleView.centerYAnchor.constraint(equalTo: headImge.topAnchor, constant: 8), + redAngleView.centerXAnchor.constraint(equalTo: headImageView.rightAnchor, constant: -8), + redAngleView.centerYAnchor.constraint(equalTo: headImageView.topAnchor, constant: 8), redAngleView.heightAnchor.constraint(equalToConstant: 18), ]) timeWidth = timeLabel.widthAnchor.constraint(equalToConstant: 0) @@ -52,69 +51,89 @@ open class NEBaseConversationListCell: UITableViewCell { ]) NSLayoutConstraint.activate([ - subTitle.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - subTitle.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), - subTitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), + subTitleLabel.leftAnchor.constraint(equalTo: headImageView.rightAnchor, constant: 12), + subTitleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), + subTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), ]) } func initSubviewsLayout() {} - open func configData(sessionModel: ConversationListModel?) { + /// 数据绑定UI + /// - Parameter sessionModel: 会话数据 + open func configureData(_ sessionModel: NEConversationListModel?) { guard let conversationModel = sessionModel else { return } - - if let userId = conversationModel.userInfo?.userId, - let user = ChatUserCache.getUserInfo(userId) { - conversationModel.userInfo = user - } - - if conversationModel.recentSession?.session?.sessionType == .P2P { + if conversationModel.conversation?.type == .CONVERSATION_TYPE_P2P { // p2p head image - if let imageName = conversationModel.userInfo?.userInfo?.avatarUrl, !imageName.isEmpty { - headImge.setTitle("") - headImge.sd_setImage(with: URL(string: imageName), completed: nil) - headImge.backgroundColor = .clear + if let imageName = conversationModel.conversation?.avatar, !imageName.isEmpty { + headImageView.setTitle("") + headImageView.sd_setImage(with: URL(string: imageName), completed: nil) + headImageView.backgroundColor = .clear } else { - headImge.setTitle(conversationModel.userInfo?.shortName(showAlias: false, count: 2) ?? "") - headImge.sd_setImage(with: nil, completed: nil) - headImge.backgroundColor = UIColor - .colorWithString(string: conversationModel.userInfo?.userId) + if let name = conversationModel.conversation?.shortName(count: 2) { + headImageView.setTitle(name) + } else if let conversationId = conversationModel.conversation?.conversationId { + // 截断长度 + let count = 2 + let showId = conversationId + .count > count ? String(conversationId[conversationId.index(conversationId.endIndex, offsetBy: -count)...]) : conversationId + headImageView.setTitle(showId) + } + headImageView.sd_setImage(with: nil, completed: nil) + if let cid = conversationModel.conversation?.conversationId, let uid = V2NIMConversationIdUtil.conversationTargetId(cid) { + headImageView.backgroundColor = UIColor + .colorWithString(string: uid) + } } // p2p nickName - title.text = conversationModel.userInfo?.showName() - - // notifyForNewMsg -// notifyMsg.isHidden = viewModel -// .notifyForNewMsg(userId: conversationModel.userInfo?.userId) - notifyMsg.isHidden = repo.isNeedNotify(userId: conversationModel.userInfo?.userId) + if let name = conversationModel.conversation?.name, name.count > 0 { + titleLabel.text = conversationModel.conversation?.name + } else if let conversationId = conversationModel.conversation?.conversationId, let accountId = V2NIMConversationIdUtil.conversationTargetId(conversationId) { + titleLabel.text = accountId + } - } else if conversationModel.recentSession?.session?.sessionType == .team { + } else if conversationModel.conversation?.type == .CONVERSATION_TYPE_TEAM { // team head image - if let imageName = conversationModel.teamInfo?.avatarUrl, !imageName.isEmpty { - headImge.setTitle("") - headImge.sd_setImage(with: URL(string: imageName), completed: nil) - headImge.backgroundColor = .clear + if let imageName = conversationModel.conversation?.avatar, !imageName.isEmpty { + headImageView.setTitle("") + headImageView.sd_setImage(with: URL(string: imageName), completed: nil) + headImageView.backgroundColor = .clear } else { - headImge.setTitle(conversationModel.teamInfo?.getShowName() ?? "") - headImge.sd_setImage(with: nil, completed: nil) - headImge.backgroundColor = UIColor - .colorWithString(string: conversationModel.teamInfo?.teamId) + headImageView.setTitle(conversationModel.conversation?.name ?? "") + headImageView.sd_setImage(with: nil, completed: nil) + if let name = conversationModel.conversation?.shortName(count: 2) { + headImageView.setTitle(name) + } else if let conversationId = conversationModel.conversation?.conversationId { + // 截断长度 + let count = 2 + let showId = conversationId + .count > count ? String(conversationId[conversationId.index(conversationId.endIndex, offsetBy: -count)...]) : conversationId + headImageView.setTitle(showId) + } + if let cid = conversationModel.conversation?.conversationId, let uid = V2NIMConversationIdUtil.conversationTargetId(cid) { + headImageView.backgroundColor = UIColor + .colorWithString(string: uid) + } + } + titleLabel.text = conversationModel.conversation?.name + if let name = conversationModel.conversation?.name { + titleLabel.text = name + } else if let conversationId = conversationModel.conversation?.conversationId, let teamId = V2NIMConversationIdUtil.conversationTargetId(conversationId) { + titleLabel.text = teamId } - title.text = conversationModel.teamInfo?.getShowName() + } - // notifyForNewMsg -// let teamNotifyState = viewModel -// .notifyStateForNewMsg(teamId: conversationModel.teamInfo?.teamId) - let teamNotifyState = repo.isNeedNotifyForTeam(teamId: conversationModel.teamInfo?.teamId) - notifyMsg.isHidden = teamNotifyState == .none ? false : true + // notifyForNewMsg + if let mute = conversationModel.conversation?.mute { + notifyMsgView.isHidden = !mute } // last message - if let lastMessage = conversationModel.recentSession?.lastMessage { - let text = contentForRecentSession(message: lastMessage) + if let lastMessage = conversationModel.conversation?.lastMessage { + let text = contentForConversation(lastMessage: lastMessage) let mutaAttri = NSMutableAttributedString(string: text) - if let sessionId = sessionModel?.recentSession?.session?.sessionId { + if let sessionId = conversationModel.conversation?.conversationId { let isAtMessage = NEAtMessageManager.instance?.isAtCurrentUser(sessionId: sessionId) if isAtMessage == true { let atStr = localizable("you_were_mentioned") @@ -123,17 +142,17 @@ open class NEBaseConversationListCell: UITableViewCell { mutaAttri.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: NEKitConversationConfig.shared.ui.conversationProperties.itemContentSize > 0 ? NEKitConversationConfig.shared.ui.conversationProperties.itemContentSize : 13), range: NSMakeRange(0, mutaAttri.length)) } } - subTitle.attributedText = mutaAttri // contentForRecentSession(message: lastMessage) + subTitleLabel.attributedText = mutaAttri } else { - subTitle.attributedText = nil + subTitleLabel.attributedText = nil } // unRead message count - if let unReadCount = conversationModel.recentSession?.unreadCount { + if let unReadCount = conversationModel.conversation?.unreadCount { if unReadCount <= 0 { redAngleView.isHidden = true } else { - redAngleView.isHidden = notifyMsg.isHidden ? false : true + redAngleView.isHidden = notifyMsgView.isHidden ? false : true if unReadCount <= 99 { redAngleView.text = "\(unReadCount)" } else { @@ -143,10 +162,10 @@ open class NEBaseConversationListCell: UITableViewCell { } // time - if let rencentSession = conversationModel.recentSession { + if let time = conversationModel.conversation?.lastMessage?.messageRefer.createTime { timeLabel .text = - dealTime(time: timestampDescriptionForRecentSession(recentSession: rencentSession)) + dealTime(time: time) if let text = timeLabel.text { let maxSize = CGSize(width: UIScreen.main.bounds.width, height: 0) let attibutes = [NSAttributedString.Key.font: timeLabel.font] @@ -188,14 +207,14 @@ open class NEBaseConversationListCell: UITableViewCell { } } - open func contentForRecentSession(message: NIMMessage) -> String { - let text = NEMessageUtil.messageContent(message: message) + open func contentForConversation(lastMessage: V2NIMLastMessage) -> String { + let text = NEMessageUtil.messageContent(lastMessage.messageType, lastMessage.text, lastMessage.attachment) return text } // MARK: lazy Method - public lazy var headImge: NEUserHeaderView = { + public lazy var headImageView: NEUserHeaderView = { let headView = NEUserHeaderView(frame: .zero) headView.titleLabel.textColor = .white headView.titleLabel.font = NEConstant.defaultTextFont(14) @@ -221,7 +240,7 @@ open class NEBaseConversationListCell: UITableViewCell { }() // 会话列表会话名称 - public lazy var title: UILabel = { + public lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = NEKitConversationConfig.shared.ui.conversationProperties.itemTitleColor @@ -232,7 +251,7 @@ open class NEBaseConversationListCell: UITableViewCell { }() // 会话列表外露消息 - public lazy var subTitle: UILabel = { + public lazy var subTitleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = NEKitConversationConfig.shared.ui.conversationProperties.itemContentColor @@ -253,12 +272,12 @@ open class NEBaseConversationListCell: UITableViewCell { }() // 免打扰icon - public lazy var notifyMsg: UIImageView = { - let notify = UIImageView() - notify.translatesAutoresizingMaskIntoConstraints = false - notify.image = UIImage.ne_imageNamed(name: "noNeed_notify") - notify.isHidden = true - notify.accessibilityIdentifier = "id.mute" - return notify + public lazy var notifyMsgView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = UIImage.ne_imageNamed(name: "noNeed_notify") + imageView.isHidden = true + imageView.accessibilityIdentifier = "id.mute" + return imageView }() } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift index 10be5e15..7e584792 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift @@ -12,37 +12,37 @@ open class NEBaseConversationSearchCell: TextBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } public var searchModel: ConversationSearchListModel? { didSet { if let _ = searchModel { - if let userInfo = searchModel?.userInfo { - titleLabel.text = userInfo.showName() - subTitleLabel.text = userInfo.userId + if let userFriend = searchModel?.userInfo { + titleLabel.text = userFriend.showName() + subTitleLabel.text = userFriend.user?.accountId - if let imageName = userInfo.userInfo?.avatarUrl, !imageName.isEmpty { - headImge.setTitle("") - headImge.sd_setImage(with: URL(string: imageName), completed: nil) - headImge.backgroundColor = .clear + if let imageName = userFriend.user?.avatar, !imageName.isEmpty { + headImageView.setTitle("") + headImageView.sd_setImage(with: URL(string: imageName), completed: nil) + headImageView.backgroundColor = .clear } else { - headImge.setTitle(userInfo.showName() ?? "") - headImge.sd_setImage(with: nil, completed: nil) - headImge.backgroundColor = UIColor.colorWithString(string: userInfo.userId) + headImageView.setTitle(userFriend.showName() ?? "") + headImageView.sd_setImage(with: nil, completed: nil) + headImageView.backgroundColor = UIColor.colorWithString(string: userFriend.user?.accountId) } } - if let teamInfo = searchModel?.teamInfo { + if let teamInfo = searchModel?.team { titleLabel.text = teamInfo.getShowName() subTitleLabel.text = nil - if let imageName = teamInfo.avatarUrl, !imageName.isEmpty { - headImge.setTitle("") - headImge.sd_setImage(with: URL(string: imageName), completed: nil) - headImge.backgroundColor = .clear + if let imageName = teamInfo.avatar, !imageName.isEmpty { + headImageView.setTitle("") + headImageView.sd_setImage(with: URL(string: imageName), completed: nil) + headImageView.backgroundColor = .clear } else { - headImge.setTitle(teamInfo.getShowName()) - headImge.sd_setImage(with: nil, completed: nil) - headImge.backgroundColor = UIColor.colorWithString(string: teamInfo.teamId) + headImageView.setTitle(teamInfo.getShowName()) + headImageView.sd_setImage(with: nil, completed: nil) + headImageView.backgroundColor = UIColor.colorWithString(string: teamInfo.teamId) } } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift index 708b4de6..5b3dc055 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift @@ -3,10 +3,10 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECommonUIKit -import NECoreKit +import MJRefresh +import NECommonKit +import NECoreIM2Kit import NIMSDK -import UIKit @objc public protocol NEBaseConversationControllerDelegate { @@ -14,7 +14,7 @@ public protocol NEBaseConversationControllerDelegate { } @objcMembers -open class NEBaseConversationController: UIViewController, NIMChatManagerDelegate, UIGestureRecognizerDelegate { +open class NEBaseConversationController: UIViewController, UIGestureRecognizerDelegate { var className = "NEBaseConversationController" public var deleteBottonBackgroundColor: UIColor = NEConstant.hexRGB(0xA8ABB6) @@ -27,6 +27,9 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat public var delegate: NEBaseConversationControllerDelegate? + /// 是否取过数据 + public var isRequestedData = false + public var bodyTopViewHeight: CGFloat = 0 { didSet { bodyTopViewHeightAnchor?.constant = bodyTopViewHeight @@ -44,6 +47,137 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat public var cellRegisterDic = [0: NEBaseConversationListCell.self] public let viewModel = ConversationViewModel() + public lazy var navigationView: TabNavigationView = { + let nav = TabNavigationView(frame: CGRect.zero) + nav.translatesAutoresizingMaskIntoConstraints = false + nav.delegate = self + + nav.brandBtn.addTarget(self, action: #selector(brandBtnClick), for: .touchUpInside) + + if let brandTitle = NEKitConversationConfig.shared.ui.titleBarTitle { + nav.brandBtn.setTitle(brandTitle, for: .normal) + } + if let brandTitleColor = NEKitConversationConfig.shared.ui.titleBarTitleColor { + nav.brandBtn.setTitleColor(brandTitleColor, for: .normal) + } + if !NEKitConversationConfig.shared.ui.showTitleBarLeftIcon { + nav.brandBtn.setImage(nil, for: .normal) + // 如果左侧图标为空,则左侧文案左对齐 + nav.brandBtn.layoutButtonImage(style: .left, space: 0) + } + if let brandImg = NEKitConversationConfig.shared.ui.titleBarLeftRes { + nav.brandBtn.setImage(brandImg, for: .normal) + if brandImg.size.width == 0, brandImg.size.height == 0 { + // 如果左侧图标为空,则左侧文案左对齐 + nav.brandBtn.layoutButtonImage(style: .left, space: 0) + } + } + if let rightImg = NEKitConversationConfig.shared.ui.titleBarRightRes { + nav.addBtn.setImage(rightImg, for: .normal) + } + if let right2Img = NEKitConversationConfig.shared.ui.titleBarRight2Res { + nav.searchBtn.setImage(right2Img, for: .normal) + } + return nav + }() + + public lazy var bodyTopView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + return view + }() + + public lazy var bodyView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + + view.addSubview(brokenNetworkView) + view.addSubview(contentView) + + NSLayoutConstraint.activate([ + brokenNetworkView.topAnchor.constraint(equalTo: view.topAnchor), + brokenNetworkView.leftAnchor.constraint(equalTo: view.leftAnchor), + brokenNetworkView.rightAnchor.constraint(equalTo: view.rightAnchor), + brokenNetworkView.heightAnchor.constraint(equalToConstant: brokenNetworkViewHeight), + ]) + + contentViewTopAnchor = contentView.topAnchor.constraint(equalTo: view.topAnchor) + contentViewTopAnchor?.isActive = true + NSLayoutConstraint.activate([ + contentView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + return view + }() + + public lazy var brokenNetworkView: NEBrokenNetworkView = { + let view = NEBrokenNetworkView() + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + return view + }() + + public lazy var contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + view.addSubview(tableView) + view.addSubview(emptyView) + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) + + NSLayoutConstraint.activate([ + emptyView.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 100), + emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), + emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), + ]) + + return view + }() + + public lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView( + imageName: "user_empty", + content: localizable("session_empty"), + frame: CGRect.zero + ) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + view.backgroundColor = .clear + return view + }() + + public lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + tableView.mj_footer = MJRefreshBackNormalFooter( + refreshingTarget: self, + refreshingAction: #selector(loadMoreData) + ) + return tableView + }() + + public lazy var bodyBottomView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + return view + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nil, bundle: nil) } @@ -55,16 +189,10 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) showTitleBar() - viewModel.loadStickTopSessionInfos { [weak self] error, sessionInfos in - NELog.infoLog( - ModuleName + " " + (self?.className ?? "NEBaseConversationController"), - desc: "CALLBACK loadStickTopSessionInfos " + (error?.localizedDescription ?? "no error") - ) - if let infos = sessionInfos { - self?.viewModel.stickTopInfos = infos - self?.reloadTableView() - self?.delegate?.onDataLoaded() - } + + // 是否取过数据,如果取过数据再刷新页面 + if isRequestedData == true { + reloadTableView() } NEChatDetectNetworkTool.shareInstance.netWorkReachability { [weak self] status in @@ -92,18 +220,18 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat setupSubviews() requestData() initialConfig() - NIMSDK.shared().chatManager.add(self) - NotificationCenter.default.addObserver(self, selector: #selector(didRefreshTable), name: NENotificationName.updateFriendInfo, object: nil) + + // 拉取好友信息 + DispatchQueue.global().async { + ContactRepo.shared.getMyUserInfo(nil) + ContactRepo.shared.getContactList { _, _ in } + } } override open func viewWillDisappear(_ animated: Bool) { popListView.removeSelf() } - deinit { - NIMSDK.shared().chatManager.remove(self) - } - open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let navigationController = navigationController, navigationController.responds(to: #selector(getter: UINavigationController.interactivePopGestureRecognizer)), @@ -193,7 +321,7 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat bodyBottomViewHeightAnchor = bodyBottomView.heightAnchor.constraint(equalToConstant: bodyBottomViewHeight) bodyBottomViewHeightAnchor?.isActive = true - cellRegisterDic.forEach { (key: Int, value: NEBaseConversationListCell.Type) in + for (key, value) in cellRegisterDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } @@ -206,164 +334,52 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat viewModel.delegate = self } - func requestData() { - let params = NIMFetchServerSessionOption() - params.minTimestamp = 0 - params.maxTimestamp = Date().timeIntervalSince1970 * 1000 - params.limit = 50 - weak var weakSelf = self - viewModel.fetchServerSessions(option: params) { error, recentSessions in - if error == nil { - NELog.infoLog(ModuleName + " " + self.className, desc: "✅CALLBACK fetchServerSessions SUCCESS") - if let recentList = recentSessions { - NELog.infoLog(ModuleName + " " + self.className, desc: "✅CALLBACK fetchServerSessions SUCCESS count : \(recentList.count)") - if recentList.count > 0 { - weakSelf?.emptyView.isHidden = true - weakSelf?.reloadTableView() - weakSelf?.delegate?.onDataLoaded() - } else { - weakSelf?.emptyView.isHidden = false - } + func loadMoreData() { + viewModel.getConversationListByPage { [weak self] error, finishied in + self?.isRequestedData = true + if let end = finishied, end == true { + self?.tableView.mj_footer?.endRefreshingWithNoMoreData() + DispatchQueue.main.async { + self?.tableView.mj_footer = nil } - } else { - NELog.errorLog( - ModuleName + " " + self.className, - desc: "❌CALLBACK fetchServerSessions failed,error = \(error!)" - ) - weakSelf?.emptyView.isHidden = false + self?.tableView.mj_footer?.endRefreshing() } + self?.delegate?.onDataLoaded() + self?.reloadTableView() } } - // MARK: lazyMethod - - public lazy var navigationView: TabNavigationView = { - let nav = TabNavigationView(frame: CGRect.zero) - nav.translatesAutoresizingMaskIntoConstraints = false - nav.delegate = self - - nav.brandBtn.addTarget(self, action: #selector(brandBtnClick), for: .touchUpInside) - - if let brandTitle = NEKitConversationConfig.shared.ui.titleBarTitle { - nav.brandBtn.setTitle(brandTitle, for: .normal) - } - if let brandTitleColor = NEKitConversationConfig.shared.ui.titleBarTitleColor { - nav.brandBtn.setTitleColor(brandTitleColor, for: .normal) - } - if !NEKitConversationConfig.shared.ui.showTitleBarLeftIcon { - nav.brandBtn.setImage(nil, for: .normal) - // 如果左侧图标为空,则左侧文案左对齐 - nav.brandBtn.layoutButtonImage(style: .left, space: 0) - } - if let brandImg = NEKitConversationConfig.shared.ui.titleBarLeftRes { - nav.brandBtn.setImage(brandImg, for: .normal) - if brandImg.size.width == 0, brandImg.size.height == 0 { - // 如果左侧图标为空,则左侧文案左对齐 - nav.brandBtn.layoutButtonImage(style: .left, space: 0) + func requestData() { + viewModel.getConversationListByPage { [weak self] error, finished in + + if let err = error { + self?.view.ne_makeToast(err.localizedDescription) + self?.emptyView.isHidden = false + NEALog.errorLog( + ModuleName + " " + (self?.className ?? ""), + desc: "❌CALLBACK requestData failed,error = \(error!)" + ) + } else { + if let end = finished, end == true { + DispatchQueue.main.async { + self?.tableView.mj_footer = nil + } + } + if let topDats = self?.viewModel.stickTopConversations, let normalDatas = self?.viewModel.conversationListData { + if topDats.count <= 0, normalDatas.count <= 0 { + self?.emptyView.isHidden = false + } else { + self?.emptyView.isHidden = true + self?.reloadTableView() + self?.delegate?.onDataLoaded() + } + } } } - if let rightImg = NEKitConversationConfig.shared.ui.titleBarRightRes { - nav.addBtn.setImage(rightImg, for: .normal) - } - if let right2Img = NEKitConversationConfig.shared.ui.titleBarRight2Res { - nav.searchBtn.setImage(right2Img, for: .normal) - } - return nav - }() - - public lazy var bodyTopView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - return view - }() - - public lazy var bodyView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - - view.addSubview(brokenNetworkView) - view.addSubview(contentView) - - NSLayoutConstraint.activate([ - brokenNetworkView.topAnchor.constraint(equalTo: view.topAnchor), - brokenNetworkView.leftAnchor.constraint(equalTo: view.leftAnchor), - brokenNetworkView.rightAnchor.constraint(equalTo: view.rightAnchor), - brokenNetworkView.heightAnchor.constraint(equalToConstant: brokenNetworkViewHeight), - ]) - - contentViewTopAnchor = contentView.topAnchor.constraint(equalTo: view.topAnchor) - contentViewTopAnchor?.isActive = true - NSLayoutConstraint.activate([ - contentView.leftAnchor.constraint(equalTo: view.leftAnchor), - contentView.rightAnchor.constraint(equalTo: view.rightAnchor), - contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - return view - }() - - public lazy var brokenNetworkView: NEBrokenNetworkView = { - let view = NEBrokenNetworkView() - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - return view - }() - - public lazy var contentView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - view.addSubview(tableView) - view.addSubview(emptyView) - - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - tableView.leftAnchor.constraint(equalTo: view.leftAnchor), - tableView.rightAnchor.constraint(equalTo: view.rightAnchor), - ]) - - NSLayoutConstraint.activate([ - emptyView.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 100), - emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), - emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), - emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), - ]) - - return view - }() - - public lazy var emptyView: NEEmptyDataView = { - let view = NEEmptyDataView( - imageName: "user_empty", - content: localizable("session_empty"), - frame: CGRect.zero - ) - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - view.backgroundColor = .clear - return view - }() - - public lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .plain) - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) - return tableView - }() + } - public lazy var bodyBottomView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - return view - }() + // MARK: lazyMethod } extension NEBaseConversationController: TabNavigationViewDelegate { @@ -372,6 +388,7 @@ extension NEBaseConversationController: TabNavigationViewDelegate { NEKitConversationConfig.shared.ui.titleBarLeftClick?() } + /// 点击搜索会话 open func searchAction() { if let searchBlock = NEKitConversationConfig.shared.ui.titleBarRight2Click { searchBlock() @@ -443,14 +460,22 @@ extension NEBaseConversationController: TabNavigationViewDelegate { } } + /// 创建讨论组 open func createDiscussGroup() { Router.shared.register(ContactSelectedUsersRouter) { param in print("user setting accids : ", param) Router.shared.use(TeamCreateDisuss, parameters: param, closure: nil) } + + // 创建讨论组-人员选择页面不包含自己 + var filters = Set() + filters.insert(IMKitClient.instance.account()) + Router.shared.use( ContactUserSelectRouter, - parameters: ["nav": navigationController as Any, "limit": inviteNumberLimit], + parameters: ["nav": navigationController as Any, + "limit": inviteNumberLimit, + "filters": filters], closure: nil ) weak var weakSelf = self @@ -458,26 +483,34 @@ extension NEBaseConversationController: TabNavigationViewDelegate { print("create discuss ", param) if let code = param["code"] as? Int, let teamid = param["teamId"] as? String, code == 0 { - let session = weakSelf?.viewModel.repo.createTeamSession(teamid) - Router.shared.use( - PushTeamChatVCRouter, - parameters: ["nav": weakSelf?.navigationController as Any, - "session": session as Any], - closure: nil - ) + if let conversationId = V2NIMConversationIdUtil.teamConversationId(teamid) { + var params = [String: Any]() + params["nav"] = weakSelf?.navigationController as Any + params["conversationId"] = conversationId as Any + + Router.shared.use(PushTeamChatVCRouter, parameters: params, closure: nil) + } } else if let msg = param["msg"] as? String { weakSelf?.showToast(msg) } } } + /// 创建高级群 open func createSeniorGroup() { Router.shared.register(ContactSelectedUsersRouter) { param in Router.shared.use(TeamCreateSenior, parameters: param, closure: nil) } + + // 创建高级群-人员选择页面不包含自己 + var filters = Set() + filters.insert(IMKitClient.instance.account()) + Router.shared.use( ContactUserSelectRouter, - parameters: ["nav": navigationController as Any, "limit": 200], + parameters: ["nav": navigationController as Any, + "limit": 200, + "filters": filters], closure: nil ) weak var weakSelf = self @@ -485,109 +518,88 @@ extension NEBaseConversationController: TabNavigationViewDelegate { print("create senior : ", param) if let code = param["code"] as? Int, let teamid = param["teamId"] as? String, code == 0 { - let session = weakSelf?.viewModel.repo.createTeamSession(teamid) - Router.shared.use( - PushTeamChatVCRouter, - parameters: ["nav": weakSelf?.navigationController as Any, - "session": session as Any], - closure: nil - ) + if let conversationId = V2NIMConversationIdUtil.teamConversationId(teamid) { + var params = [String: Any]() + params["nav"] = weakSelf?.navigationController as Any + params["conversationId"] = conversationId as Any + + Router.shared.use(PushTeamChatVCRouter, parameters: params, closure: nil) + } } else if let msg = param["msg"] as? String { weakSelf?.showToast(msg) } } } +} - // MARK: =========================NIMChatManagerDelegate======================== - - open func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { - guard let msg = notification.message else { - return - } - - if ConversationDeduplicationHelper.instance.isRevokeMessageSaved(messageId: msg.messageId) { - return - } - saveRevokeMessage(msg) { [weak self] error in - } +extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSource { + public func numberOfSections(in tableView: UITableView) -> Int { + 2 } - open func saveRevokeMessage(_ message: NIMMessage, _ completion: @escaping (Error?) -> Void) { - let messageNew = NIMMessage() - messageNew.text = localizable("message_recalled") - var muta = [String: Any]() - muta[revokeLocalMessage] = true -// if message.messageType == .text { -// muta[revokeLocalMessageContent] = message.text -// } - messageNew.timestamp = message.timestamp - messageNew.from = message.from - messageNew.localExt = muta - let setting = NIMMessageSetting() - setting.shouldBeCounted = false - setting.isSessionUpdate = false - messageNew.setting = setting - if let session = message.session { - viewModel.repo.saveMessageToDB(messageNew, session, completion) + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0 { + return viewModel.stickTopConversations.count } - } -} -// MARK: - UITableViewDelegate, UITableViewDataSource + if section == 1 { + let conversationCount = viewModel.conversationListData.count + return conversationCount + } -extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSource { - open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let count = viewModel.conversationListArray?.count ?? 0 - NELog.infoLog(ModuleName + " " + "ConversationController", - desc: "numberOfRowsInSection count : \(count)") - return count + return 0 } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewModel.conversationListArray?[indexPath.row] + var model: NEConversationListModel? + + if indexPath.section == 0 { + model = viewModel.stickTopConversations[indexPath.row] + } else if indexPath.section == 1 { + model = viewModel.conversationListData[indexPath.row] + } + let reusedId = "\(model?.customType ?? 0)" let cell = tableView.dequeueReusableCell(withIdentifier: reusedId, for: indexPath) - if let c = cell as? NEBaseConversationListCell { - c.topStickInfos = viewModel.stickTopInfos - c.configData(sessionModel: model) + if let c = cell as? NEBaseConversationListCell, let m = model { + c.configureData(m) } return cell } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let conversationModel = viewModel.conversationListArray?[indexPath.row] + var conversationModel: NEConversationListModel? + if indexPath.section == 0 { + conversationModel = viewModel.stickTopConversations[indexPath.row] + } else if indexPath.section == 1 { + conversationModel = viewModel.conversationListData[indexPath.row] + } - if let didClick = NEKitConversationConfig.shared.ui.itemClick { - didClick(conversationModel, indexPath) + if let didClick = NEKitConversationConfig.shared.ui.itemClick, let model = conversationModel { + didClick(model, indexPath) return } - let sid = conversationModel?.recentSession?.session?.sessionId ?? "" - let sessionType = conversationModel?.recentSession?.session?.sessionType ?? .P2P - onselectedTableRow(sessionType: sessionType, sessionId: sid, indexPath: indexPath) + if let conversation = conversationModel?.conversation { + onselectedTableRow(conversation: conversation) + } } open func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { weak var weakSelf = self - var rowActions = [UITableViewRowAction]() - - let conversationModel = weakSelf?.viewModel.conversationListArray?[indexPath.row] - guard let recentSession = conversationModel?.recentSession, - let session = recentSession.session else { - return rowActions - } + var rowActions = [UITableViewRowAction]() let deleteAction = UITableViewRowAction(style: .destructive, title: NEKitConversationConfig.shared.ui.deleteBottonTitle) { action, indexPath in weakSelf?.deleteActionHandler(action: action, indexPath: indexPath) } // 置顶和取消置顶 - let isTop = viewModel.stickTopInfos[session] != nil + let isTop = indexPath.section == 0 ? true : false // viewModel.stickTopInfos[session] != nil let topAction = UITableViewRowAction(style: .destructive, title: isTop ? NEKitConversationConfig.shared.ui.stickTopBottonCancelTitle : NEKitConversationConfig.shared.ui.stickTopBottonTitle) { action, indexPath in @@ -601,83 +613,71 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour return rowActions } - /* - @available(iOS 11.0, *) - open func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - - var rowActions = [UIContextualAction]() - - let deleteAction = UIContextualAction(style: .normal, title: "删除") { (action, sourceView, completionHandler) in - - // self.dataSource.remove(at: indexPath.row) - // tableView.deleteRows(at: [indexPath], with: .automatic) - // 需要返回true,否则没有反应 - completionHandler(true) - } - deleteAction.backgroundColor = NEConstant.hexRGB(0xA8ABB6) - rowActions.append(deleteAction) - - let topAction = UIContextualAction(style: .normal, title: "置顶") { (action, sourceView, completionHandler) in - - // self.dataSource.remove(at: indexPath.row) - // tableView.deleteRows(at: [indexPath], with: .automatic) - // 需要返回true,否则没有反应 - completionHandler(true) - } - topAction.backgroundColor = NEConstant.hexRGB(0x337EFF) - rowActions.append(topAction) + /// 删除会话 + open func deleteActionHandler(action: UITableViewRowAction?, indexPath: IndexPath) { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } - let actionConfig = UISwipeActionsConfiguration.init(actions: rowActions) - actionConfig.performsFirstActionWithFullSwipe = false + var conversationModel: NEConversationListModel? - return actionConfig - } - */ + if indexPath.section == 0 { + conversationModel = viewModel.stickTopConversations[indexPath.row] - open func deleteActionHandler(action: UITableViewRowAction?, indexPath: IndexPath) { - let conversationModel = viewModel.conversationListArray?[indexPath.row] + } else if indexPath.section == 1 { + conversationModel = viewModel.conversationListData[indexPath.row] + } if let deleteBottonClick = NEKitConversationConfig.shared.ui.deleteBottonClick { deleteBottonClick(conversationModel, indexPath) return } - if let recentSession = conversationModel?.recentSession { - viewModel.deleteRecentSession(recentSession: recentSession) - didDeleteConversationCell( - model: conversationModel ?? ConversationListModel(), - indexPath: indexPath - ) + if let conversation = conversationModel?.conversation { + viewModel.deleteConversation(conversation) { [weak self] error in + if let err = error { + self?.view.ne_makeToast(err.localizedDescription) + } + self?.reloadTableView() + } } } + /// 点击会话 open func topActionHandler(action: UITableViewRowAction?, indexPath: IndexPath, isTop: Bool) { if !NEChatDetectNetworkTool.shareInstance.isNetworkRecahability() { showToast(localizable("network_error")) return } - let conversationModel = viewModel.conversationListArray?[indexPath.row] + var conversationModel: NEConversationListModel? + if indexPath.section == 0 { + conversationModel = viewModel.stickTopConversations[indexPath.row] + } else { + conversationModel = viewModel.conversationListData[indexPath.row] + } if let stickTopBottonClick = NEKitConversationConfig.shared.ui.stickTopBottonClick { stickTopBottonClick(conversationModel, indexPath) return } - if let recentSession = conversationModel?.recentSession { - onTopRecentAtIndexPath( - rencent: recentSession, - indexPath: indexPath, - isTop: isTop - ) { [weak self] error, sessionInfo in - if error == nil { + if let conversation = conversationModel?.conversation { + onTopRecentAtIndexPath(conversation: conversation, + indexPath: indexPath, + isTop: isTop) { [weak self] error in + + if let err = error { + self?.view.ne_makeToast(err.localizedDescription) + } else { if isTop { self?.didRemoveStickTopSession( - model: conversationModel ?? ConversationListModel(), + model: conversationModel ?? NEConversationListModel(), indexPath: indexPath ) } else { self?.didAddStickTopSession( - model: conversationModel ?? ConversationListModel(), + model: conversationModel ?? NEConversationListModel(), indexPath: indexPath ) } @@ -686,55 +686,81 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour } } - private func onTopRecentAtIndexPath(rencent: NIMRecentSession, indexPath: IndexPath, - isTop: Bool, - _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) - -> Void) { - guard let session = rencent.session else { - NELog.errorLog(ModuleName + " " + className, desc: "❌session is nil") - return + /// 非置顶变为置顶 + /// - Parameter conversation: 会话 + private func moveNormalConversationToTop(conversation: V2NIMConversation) { + var addModel: NEConversationListModel? + viewModel.conversationListData.removeAll(where: { model in + if model.conversation?.conversationId == conversation.conversationId { + addModel = model + return true + } + return false + }) + if let model = addModel { + viewModel.stickTopConversations.append(model) } - weak var weakSelf = self - if isTop { - guard let params = viewModel.stickTopInfos[session] else { - return + } + + /// 置顶变为非置顶 + /// - Parameter conversation: 会话 + private func moveTopToNormalConversation(conversation: V2NIMConversation) { + var addModel: NEConversationListModel? + viewModel.stickTopConversations.removeAll(where: { model in + if model.conversation?.conversationId == conversation.conversationId { + addModel = model + return true } + return false + }) + if let model = addModel { + viewModel.conversationListData.append(model) + } + } - viewModel.removeStickTopSession(params: params) { error, topSessionInfo in + /// 点击回调 + /// - Parameter conversation: 会话 + /// - Parameter indexPath: 索引 + /// - Parameter isTop: 置顶 + /// - Parameter completion: 完成回调 + func onTopRecentAtIndexPath(conversation: V2NIMConversation, indexPath: IndexPath, + isTop: Bool, + _ completion: @escaping (NSError?) + -> Void) { + weak var weakSelf = self + if indexPath.section == 0 { + viewModel.removeStickTop(conversation: conversation) { error in if let err = error { - NELog.errorLog( - ModuleName + " " + (weakSelf?.className ?? "ConversationController"), - desc: "❌CALLBACK removeStickTopSession failed,error = \(err)" - ) - completion(error as NSError?, nil) + NEALog.errorLog(ModuleName + " " + (weakSelf?.className ?? "ConversationController"), desc: "❌CALLBACK removeStickTopSession failed,error = \(err)") + completion(error) return } else { - NELog.infoLog( - ModuleName + " " + (weakSelf?.className ?? "ConversationController"), - desc: "✅CALLBACK removeStickTopSession SUCCESS" + NEALog.infoLog( + ModuleName + " " + (weakSelf?.className ?? "ConversationController"), desc: "✅CALLBACK removeStickTopSession SUCCESS" ) - weakSelf?.viewModel.stickTopInfos[session] = nil + weakSelf?.moveTopToNormalConversation(conversation: conversation) + weakSelf?.reloadTableView() - completion(nil, topSessionInfo) + completion(nil) } } } else { - viewModel.addStickTopSession(session: session) { error, newInfo in + viewModel.addStickTop(conversation: conversation) { error in if let err = error { - NELog.errorLog( + NEALog.errorLog( ModuleName + " " + (weakSelf?.className ?? "ConversationController"), desc: "❌CALLBACK addStickTopSession failed,error = \(err)" ) - completion(error as NSError?, nil) + completion(error) return } else { - NELog.infoLog(ModuleName + " " + (weakSelf?.className ?? "ConversationController"), - desc: "✅CALLBACK addStickTopSession callback SUCCESS") - weakSelf?.viewModel.stickTopInfos[session] = newInfo + NEALog.infoLog(ModuleName + " " + (weakSelf?.className ?? "ConversationController"), + desc: "✅CALLBACK addStickTopSession callback SUCCESS") + weakSelf?.moveNormalConversationToTop(conversation: conversation) weakSelf?.reloadTableView() - completion(nil, newInfo) + completion(nil) } } } @@ -745,53 +771,46 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour extension NEBaseConversationController { /// cell点击事件,可重写该事件处理自己的逻辑业务,例如跳转到指定的会话页面 - /// - Parameters: - /// - sessionType: 会话类型 - /// - sessionId: 会话id - /// - indexPath: indexpath - open func onselectedTableRow(sessionType: NIMSessionType, sessionId: String, - indexPath: IndexPath) { - if sessionType == .P2P { - let session = NIMSession(sessionId, type: .P2P) + /// - Parameter conversation: 会话 + open func onselectedTableRow(conversation: V2NIMConversation) { + if conversation.type == .CONVERSATION_TYPE_P2P { + let conversationId = V2NIMConversationIdUtil.p2pConversationId(conversation.getUid()) Router.shared.use( PushP2pChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any], + parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any], closure: nil ) - } else if sessionType == .team { - let session = NIMSession(sessionId, type: .team) + } else if conversation.type == .CONVERSATION_TYPE_TEAM { + let conversationId = V2NIMConversationIdUtil.teamConversationId(conversation.getTeamId()) Router.shared.use( PushTeamChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any], + parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any], closure: nil ) } } /// 删除会话 - /// - Parameters: - /// - model: 会话模型 - /// - indexPath: indexpath - open func didDeleteConversationCell(model: ConversationListModel, indexPath: IndexPath) {} + /// - parameter model: 会话模型 + /// - parameter indexpath: 索引 + open func didDeleteConversationCell(model: NEConversationListModel, indexPath: IndexPath) {} /// 删除一条置顶记录 - /// - Parameters: - /// - model: 会话模型 - /// - indexPath: indexpath - open func didRemoveStickTopSession(model: ConversationListModel, indexPath: IndexPath) {} + /// - parameter model: 会话模型 + /// - parameter indexpath + open func didRemoveStickTopSession(model: NEConversationListModel, indexPath: IndexPath) {} /// 添加一条置顶记录 - /// - Parameters: - /// - model: 会话模型 - /// - indexPath: indexpath - open func didAddStickTopSession(model: ConversationListModel, indexPath: IndexPath) {} + /// - Parameter model: 会话模型 + /// - Parameter indexpath: 索引 + open func didAddStickTopSession(model: NEConversationListModel, indexPath: IndexPath) {} } // MARK: ================= ConversationViewModelDelegate=================== extension NEBaseConversationController: ConversationViewModelDelegate { open func didAddRecentSession() { - NELog.infoLog("ConversationController", desc: "didAddRecentSession") + NEALog.infoLog("ConversationController", desc: "didAddRecentSession") reloadTableView() } @@ -805,12 +824,18 @@ extension NEBaseConversationController: ConversationViewModelDelegate { } open func didRefreshTable() { - tableView.reloadData() + reloadTableView() } + /// 带排序的刷新 open func reloadTableView() { - emptyView.isHidden = (viewModel.conversationListArray?.count ?? 0) > 0 - viewModel.sortRecentSession() - didRefreshTable() + if viewModel.stickTopConversations.count <= 0, viewModel.conversationListData.count <= 0 { + emptyView.isHidden = false + } else { + emptyView.isHidden = true + } + viewModel.conversationListData.sort() + viewModel.stickTopConversations.sort() + tableView.reloadData() } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift index 66b23264..1615b3cd 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift @@ -3,6 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NECommonUIKit import NIMSDK import UIKit @@ -14,6 +15,57 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr var searchStr = "" var headTitleArr = [localizable("friend")] + public lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.keyboardDismissMode = .onDrag + tableView.delegate = self + tableView.dataSource = self + tableView.rowHeight = 60 + tableView.backgroundColor = .white + tableView.sectionHeaderHeight = 30 + tableView.sectionFooterHeight = 0 + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + return tableView + }() + + public lazy var searchTextField: SearchTextField = { + let textField = SearchTextField() + let leftImageView = UIImageView(image: UIImage + .ne_imageNamed(name: "conversation_search_icon")) + textField.contentMode = .center + textField.leftView = leftImageView + textField.leftViewMode = .always + textField.placeholder = commonLocalizable("search") + textField.font = UIFont.systemFont(ofSize: 14) + textField.textColor = UIColor.ne_greyText + textField.translatesAutoresizingMaskIntoConstraints = false + textField.layer.cornerRadius = 8 + textField.backgroundColor = .ne_lightBackgroundColor + textField.clearButtonMode = .always + textField.returnKeyType = .search + textField.addTarget(self, action: #selector(searchTextFieldChange), for: .editingChanged) + + if let clearButton = textField.value(forKey: "_clearButton") as? UIButton { + clearButton.accessibilityIdentifier = "id.clear" + } + textField.accessibilityIdentifier = "id.search" + return textField + }() + + public lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView( + imageName: "user_empty", + content: localizable("user_not_exist"), + frame: CGRect.zero + ) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + view.backgroundColor = .clear + return view + }() + override open func viewDidLoad() { super.viewDidLoad() initialConfig() @@ -25,7 +77,7 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr } open func initialConfig() { - title = localizable("search") + title = commonLocalizable("search") // 可在此处选择是否展示群聊结果 headTitleArr.append(contentsOf: [localizable("discussion_group"), @@ -53,9 +105,9 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr } if searchText.count <= 0 { emptyView.isHidden = true - viewModel.searchResult?.friend = [ConversationSearchListModel]() - viewModel.searchResult?.contactGroup = [ConversationSearchListModel]() - viewModel.searchResult?.seniorGroup = [ConversationSearchListModel]() + viewModel.friendDatas.removeAll() + viewModel.discussionDatas.removeAll() + viewModel.seniorDatas.removeAll() tableView.reloadData() return } @@ -64,76 +116,17 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr if textRange == nil || ((textRange?.isEmpty) == nil) { weak var weakSelf = self searchStr = searchText - viewModel.doSearch(searchStr: searchText) { error, tupleInfo in - if let err = error { - NELog.errorLog(ModuleName + " " + self.tag, desc: "❌CALLBACK doSearch failed,error = \(err)") + viewModel.doSearch(searchText) { + if weakSelf?.viewModel.friendDatas.count == 0, weakSelf?.viewModel.discussionDatas.count == 0, weakSelf?.viewModel.seniorDatas.count == 0 { + weakSelf?.emptyView.isHidden = false } else { - NELog.infoLog(ModuleName + " " + self.tag, desc: "✅CALLBACK doSearch SUCCESS") - if tupleInfo?.friend.count == 0, tupleInfo?.contactGroup.count == 0, - tupleInfo?.seniorGroup.count == 0 { - weakSelf?.emptyView.isHidden = false - } else { - weakSelf?.emptyView.isHidden = true - } - weakSelf?.tableView.reloadData() + weakSelf?.emptyView.isHidden = true } + weakSelf?.tableView.reloadData() } } } - // MARK: lazy method - - public lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .plain) - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.separatorStyle = .none - tableView.keyboardDismissMode = .onDrag - tableView.delegate = self - tableView.dataSource = self - tableView.rowHeight = 60 - tableView.backgroundColor = .white - tableView.sectionHeaderHeight = 30 - tableView.sectionFooterHeight = 0 - tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) - return tableView - }() - - public lazy var searchTextField: SearchTextField = { - let textField = SearchTextField() - let leftImageView = UIImageView(image: UIImage - .ne_imageNamed(name: "conversation_search_icon")) - textField.contentMode = .center - textField.leftView = leftImageView - textField.leftViewMode = .always - textField.placeholder = localizable("search") - textField.font = UIFont.systemFont(ofSize: 14) - textField.textColor = UIColor.ne_greyText - textField.translatesAutoresizingMaskIntoConstraints = false - textField.layer.cornerRadius = 8 - textField.backgroundColor = .ne_lightBackgroundColor - textField.clearButtonMode = .always - textField.returnKeyType = .search - textField.addTarget(self, action: #selector(searchTextFieldChange), for: .editingChanged) - - if let clearButton = textField.value(forKey: "_clearButton") as? UIButton { - clearButton.accessibilityIdentifier = "id.clear" - } - textField.accessibilityIdentifier = "id.search" - return textField - }() - - public lazy var emptyView: NEEmptyDataView = { - let view = NEEmptyDataView( - imageName: "user_empty", - content: localizable("user_not_exist"), - frame: CGRect.zero - ) - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - view.backgroundColor = .clear - return view - }() - // MARK: UITableViewDelegate, UITableViewDataSource open func numberOfSections(in tableView: UITableView) -> Int { @@ -141,15 +134,14 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if let friend = viewModel.searchResult?.friend, section == 0 { - return friend.count - } else if let contactGroup = viewModel.searchResult?.contactGroup, section == 1 { - return contactGroup.count - } else if let seniorGroup = viewModel.searchResult?.seniorGroup, section == 2 { - return seniorGroup.count - } else { - return 0 + if section == 0 { + return viewModel.friendDatas.count + } else if section == 1 { + return viewModel.discussionDatas.count + } else if section == 2 { + return viewModel.seniorDatas.count } + return 0 } open func tableView(_ tableView: UITableView, @@ -159,11 +151,11 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr for: indexPath ) as? NEBaseConversationSearchCell { if indexPath.section == 0 { - cell.searchModel = viewModel.searchResult?.friend[indexPath.row] + cell.searchModel = viewModel.friendDatas[indexPath.row] } else if indexPath.section == 1 { - cell.searchModel = viewModel.searchResult?.contactGroup[indexPath.row] + cell.searchModel = viewModel.discussionDatas[indexPath.row] } else { - cell.searchModel = viewModel.searchResult?.seniorGroup[indexPath.row] + cell.searchModel = viewModel.seniorDatas[indexPath.row] } cell.searchText = searchStr return cell @@ -174,53 +166,61 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { weak var weakSelf = self if indexPath.section == 0 { - let searchModel = viewModel.searchResult?.friend[indexPath.row] - if let userId = searchModel?.userInfo?.userId { - let session = NIMSession(userId, type: .P2P) + let searchModel = viewModel.friendDatas[indexPath.row] + if let userId = searchModel.userInfo?.user?.accountId { + let conversationId = V2NIMConversationIdUtil.p2pConversationId(userId) Router.shared.use( PushP2pChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any], + parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any], closure: nil ) } } else if indexPath.section == 1 { - let searchModel = viewModel.searchResult?.contactGroup[indexPath.row] - if let teamId = searchModel?.teamInfo?.teamId { - TeamRepo.shared.fetchTeamInfo(teamId) { error, teamInfo in - if let err = error as? NSError { - if err.code == noNetworkCode { + let searchModel = viewModel.discussionDatas[indexPath.row] + if let teamId = searchModel.team?.teamId { + TeamRepo.shared.getTeamInfo(teamId) { team, error in + if let err = error { + if err.code == protocolSendFailed { weakSelf?.showToast(commonLocalizable("network_error")) } else { weakSelf?.showSingleAlert(title: localizable("leave_team"), message: localizable("leave_team_desc")) {} } } else { - let session = NIMSession(teamId, type: .team) + if team?.isValidTeam == false { + weakSelf?.showSingleAlert(title: localizable("leave_team"), message: localizable("leave_team_desc")) {} + return + } + let conversationId = V2NIMConversationIdUtil.teamConversationId(teamId) Router.shared.use( PushTeamChatVCRouter, parameters: ["nav": weakSelf?.navigationController as Any, - "session": session as Any], + "conversationId": conversationId as Any], closure: nil ) } } } } else { - let searchModel = viewModel.searchResult?.seniorGroup[indexPath.row] - if let teamId = searchModel?.teamInfo?.teamId { - TeamRepo.shared.fetchTeamInfo(teamId) { error, teamInfo in - if let err = error as? NSError { - if err.code == noNetworkCode { + let searchModel = viewModel.seniorDatas[indexPath.row] + if let teamId = searchModel.team?.teamId { + TeamRepo.shared.getTeamInfo(teamId) { team, error in + if let err = error { + if err.code == protocolSendFailed { weakSelf?.showToast(commonLocalizable("network_error")) } else { weakSelf?.showSingleAlert(title: localizable("leave_team"), message: localizable("leave_team_desc")) {} } } else { - let session = NIMSession(teamId, type: .team) + if team?.isValidTeam == false { + weakSelf?.showSingleAlert(title: localizable("leave_team"), message: localizable("leave_team_desc")) {} + return + } + let conversationId = V2NIMConversationIdUtil.teamConversationId(teamId) Router.shared.use( PushTeamChatVCRouter, parameters: ["nav": weakSelf?.navigationController as Any, - "session": session as Any], + "conversationId": conversationId as Any], closure: nil ) } @@ -243,14 +243,11 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - if let friend = viewModel.searchResult?.friend, friend.count > 0, section == 0 { + if section == 0, viewModel.friendDatas.count > 0 { return 30 - - } else if let contactGroup = viewModel.searchResult?.contactGroup, contactGroup.count > 0, - section == 1 { + } else if section == 1, viewModel.discussionDatas.count > 0 { return 30 - } else if let seniorGroup = viewModel.searchResult?.seniorGroup, seniorGroup.count > 0, - section == 2 { + } else if section == 2, viewModel.seniorDatas.count > 0 { return 30 } else { return 0 diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListView.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListView.swift index a956c346..d2bcafee 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListView.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListView.swift @@ -21,10 +21,11 @@ open class PopListItem: NSObject { @objcMembers open class NEBasePopListView: UIView { public let shadowView = UIView() + public let popView = UIView() + public var buttonHeight: CGFloat = 32.0 - let popView = UIView() public var popViewWidth: CGFloat = 122.0 - var popViewHeight: CGFloat = 0 + public var popViewHeight: CGFloat = 0 public var popViewRadius: CGFloat = 8.0 public var topConstant: CGFloat = 0 @@ -45,7 +46,7 @@ open class NEBasePopListView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setupUI() { diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift index 15469be7..51376a38 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift @@ -2,41 +2,190 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NIMSDK import UIKit @objcMembers -open class ConversationSearchViewModel: NSObject { - let repo = ConversationRepo.shared - public var searchResult: ( - friend: [ConversationSearchListModel], - contactGroup: [ConversationSearchListModel], - seniorGroup: [ConversationSearchListModel] - )? +open class ConversationSearchViewModel: NSObject, NETeamListener, NEContactListener { + let conversationRepo = ConversationRepo.shared + + /// 群数据缓存 + var teamDic = [String: ConversationSearchListModel]() + /// 好友数据缓存 + var friendDic = [String: ConversationSearchListModel]() + /// 好友搜索结果 + var friendDatas = [ConversationSearchListModel]() + /// 讨论组搜索结果 + var discussionDatas = [ConversationSearchListModel]() + /// 高级群搜索结果 + var seniorDatas = [ConversationSearchListModel]() + private let className = "ConversationSearchViewModel" override public init() { super.init() + ContactRepo.shared.addContactListener(self) + TeamRepo.shared.addTeamListener(self) + + weak var weakSelf = self + getSearchData { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "get data finish") + print("get data finish") + } } - /// 请求接口 - /// - Parameters: - /// - searchStr: 搜索的内容 - /// - completion: 回调结果 - open func doSearch(searchStr: String, - _ completion: @escaping (NSError?, ( - friend: [ConversationSearchListModel], - contactGroup: [ConversationSearchListModel], - seniorGroup: [ConversationSearchListModel] - )?) -> Void) { - NELog.infoLog( + deinit { + ContactRepo.shared.removeContactListener(self) + TeamRepo.shared.removeTeamListener(self) + } + + /// 搜索 + /// - Parameter searchText: 搜索文案 + /// - Parameter completion: 完成回调 + open func doSearch(_ searchText: String?, _ completion: @escaping () -> Void) { + NEALog.infoLog( ModuleName + " " + className, - desc: #function + ", searchStr.count:\(searchStr.count)" + desc: #function + ", searchTexty: \(searchText ?? "")" ) + + friendDatas.removeAll() + discussionDatas.removeAll() + seniorDatas.removeAll() + + guard let search = searchText else { + completion() + return + } + for (_, value) in friendDic { + if let user = value.userInfo { + if user.showName()?.contains(search) == true { + friendDatas.append(value) + } else if user.user?.accountId?.contains(search) == true { + friendDatas.append(value) + } + } + } + for (_, value) in teamDic { + if let showName = value.team?.getShowName() { + if showName.contains(search) == true { + if let serverExtension = value.team?.serverExtension, serverExtension.contains(discussTeamKey) == true { + discussionDatas.append(value) + } else { + seniorDatas.append(value) + } + } + } + } + friendDatas.sort() + discussionDatas.sort() + seniorDatas.sort() + completion() + } + + /// 获取所有数据 + /// - Parameter completion: 完成回调 + func getSearchData(_ completion: @escaping () -> Void) { + let workingGroup = DispatchGroup() + let workingQueue = DispatchQueue(label: "get_search_data_queue") weak var weakSelf = self - repo.searchContact(searchStr: searchStr) { error, searchResult in - weakSelf?.searchResult = searchResult - completion(error, searchResult) + workingGroup.enter() + workingQueue.async { + let userFriends = NEFriendUserCache.shared.getFriendListNotInBlocklist() + for (uid, userFriend) in userFriends { + let model = ConversationSearchListModel() + model.userInfo = userFriend + weakSelf?.friendDic[uid] = model + } + } + + workingGroup.enter() + workingQueue.async { + TeamRepo.shared.getTeamList { teams, error in + teams?.forEach { team in + if let tid = team.v2Team?.teamId { + let model = ConversationSearchListModel() + model.team = team.v2Team + weakSelf?.teamDic[tid] = model + } + } + workingGroup.leave() + } + } + + workingGroup.notify(queue: workingQueue) { + DispatchQueue.main.async { + completion() + } } } + + // MARK: - NEContactListener + + /// 好友信息更新 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + if let uid = friendInfo.accountId { + ContactRepo.shared.getFriendInfoList(accountIds: [uid]) { [weak self] u, error in + if let user = u?.first { + let model = ConversationSearchListModel() + model.userInfo = user + self?.friendDic[uid] = model + } + } + } + } + + // MARK: - V2NIMTeamListener + + /// 群信息更新回调 + /// - Parameter team: 群 + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + if let model = teamDic[team.teamId] { + model.team = team + } else { + addTeam(team) + } + } + + /// 加入群回调 + /// - Parameters: + /// - team: 群 + public func onTeamJoined(_ team: V2NIMTeam) { + addTeam(team) + } + + /// 创建群回调 + /// - Parameter team: 群 + public func onTeamCreated(_ team: V2NIMTeam) { + addTeam(team) + } + + /// 群解散回调 + /// - Parameter team: 群 + public func onTeamDismissed(_ team: V2NIMTeam) { + removeTeam(team) + } + + /// 退出群回调 + /// - Parameters: + /// - team: 群 + /// - isKicked: 是否被踢 + public func onTeamLeft(_ team: V2NIMTeam, isKicked: Bool) { + removeTeam(team) + } + + /// 移除群 + /// - Parameter team: 群 + private func removeTeam(_ team: V2NIMTeam) { + teamDic.removeValue(forKey: team.teamId) + } + + /// 添加群 + /// - Parameter team: 群 + private func addTeam(_ team: V2NIMTeam) { + let model = ConversationSearchListModel() + model.team = team + teamDic[team.teamId] = model + } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift index 9225ca5a..fba8bab1 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift @@ -4,12 +4,9 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK -let revokeLocalMessage = "revoke_message_local" -let revokeLocalMessageContent = "revoke_message_local_content" - @objc public protocol ConversationViewModelDelegate: NSObjectProtocol { func didAddRecentSession() @@ -18,614 +15,460 @@ public protocol ConversationViewModelDelegate: NSObjectProtocol { func reloadTableView() } +public typealias ConversationCallBack = (NSError?, Bool?) -> Void + @objcMembers -open class ConversationViewModel: NSObject, ConversationRepoDelegate, - NIMConversationManagerDelegate, NIMTeamManagerDelegate, NIMUserManagerDelegate, NIMChatManagerDelegate { - public var conversationListArray: [ConversationListModel]? - public var stickTopInfos = [NIMSession: NIMStickTopSessionInfo]() +open class ConversationViewModel: NSObject, NEConversationListener, NETeamListener, NEChatListener, NEContactListener, NEIMKitClientListener { public weak var delegate: ConversationViewModelDelegate? private let className = "ConversationViewModel" - public let repo = ConversationRepo.shared - - var cacheUpdateSessionDic = [String: NIMRecentSession]() - var cacheAddSessionDic = [String: ConversationListModel]() - override public init() { - NELog.infoLog(ModuleName + " " + className, desc: #function) - super.init() - repo.delegate = self - repo.addSessionDelegate(delegate: self) - repo.addChatDelegate(delegate: self) - repo.addTeamDelegate(delegate: self) - stickTopInfos = repo.getStickTopInfos() - NIMSDK.shared().userManager.add(self) - NotificationCenter.default.addObserver(self, selector: #selector(atMessageChange), name: Notification.Name(AtMessageChangeNoti), object: nil) - } + /// 会话API单例 + public let conversationRepo = ConversationRepo.shared - deinit { - NELog.infoLog(ModuleName + className(), desc: #function) - repo.removeSessionDelegate(delegate: self) - repo.removeChatDelegate(delegate: self) - repo.removeTeamDelegate(delegate: self) - NIMSDK.shared().userManager.remove(self) - NotificationCenter.default.removeObserver(self) - } + /// 会话列表起始索引 + public var offset: Int64 = 0 - func atMessageChange() { - NELog.infoLog(className(), desc: "atMessageChange") - delegate?.reloadTableView() - } + /// 会话列表分页大小 + public var page = 20 - open func fetchServerSessions(option: NIMFetchServerSessionOption, - _ completion: @escaping (NSError?, [ConversationListModel]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) - weak var weakSelf = self - repo.getSessionList { error, conversaitonList in - DispatchQueue.main.async { - weakSelf?.conversationListArray = conversaitonList - NELog.infoLog(ModuleName, desc: "get session list : \(conversaitonList?.count ?? 0)") - var set = Set() - conversaitonList?.forEach { model in - NELog.infoLog(ModuleName, desc: "get session sid : \(model.recentSession?.session?.sessionId ?? "nil")") - if let recentSession = model.recentSession, let sid = recentSession.session?.sessionId { - set.insert(sid) - if let recent = weakSelf?.cacheUpdateSessionDic[sid] { - NELog.infoLog(ModuleName, desc: "cacheUpdateSessionDic fitler sid: \(recent.session?.sessionId ?? "nil")") - if let time1 = recentSession.lastMessage?.timestamp, let time2 = recent.lastMessage?.timestamp, time1 < time2 { - model.recentSession = recent - } - } + /// 非置顶会话数据 + public var conversationListData = [NEConversationListModel]() - if let recent = weakSelf?.cacheAddSessionDic[sid]?.recentSession { - NELog.infoLog(ModuleName, desc: "cacheAddSessionDic fitler sid: \(recent.session?.sessionId ?? "nil")") - if let time1 = recentSession.lastMessage?.timestamp, let time2 = recent.lastMessage?.timestamp, time1 < time2 { - model.recentSession = recent - } - } - } - } - NELog.infoLog(ModuleName, desc: "cacheAddSessionDic count: \(weakSelf?.cacheAddSessionDic.count ?? 0)") - weakSelf?.cacheAddSessionDic.forEach { (key: String, value: ConversationListModel) in - NELog.infoLog(ModuleName, desc: "cacheAddSessionDic key: \(key)") - if set.contains(key) == false { - if let recent = weakSelf?.cacheUpdateSessionDic[key] { - if let time1 = value.recentSession?.lastMessage?.timestamp, let time2 = recent.lastMessage?.timestamp, time1 < time2 { - value.recentSession = recent - } - } - NELog.infoLog(ModuleName, desc: "cacheAddSessionDic : \(key)") - weakSelf?.conversationListArray?.append(value) - } - } - weakSelf?.cacheAddSessionDic.removeAll() + /// 置顶会话数据 + public var stickTopConversations = [NEConversationListModel]() - // 可在此处对会话列表进行过滤 + /// 所有会话数据记录 + public var conversationDic = [String: NEConversationListModel]() - NELog.infoLog(ModuleName, desc: "conversationListArray count : \(weakSelf?.conversationListArray?.count ?? 0)") - completion(error, weakSelf?.conversationListArray) + /// 当前是否在请求会话列表 + private var isRequesting = false - // 拉取好友信息 - ChatUserCache.removeAllUserInfo() - ContactRepo.shared.getFriendList(true, local: false) { friends, error in - if let friends = friends { - friends.forEach { user in - ChatUserCache.updateUserInfo(user) - } - } - } - } - } - } - - open func deleteRecentSession(recentSession: NIMRecentSession) { - NELog.infoLog( - ModuleName + " " + className, - desc: #function + ", sessionId:" + (recentSession.session?.sessionId ?? "nil") - ) - weak var weakSelf = self - let option = NIMDeleteRecentSessionOption() - option.isDeleteRoamMessage = true - option.shouldMarkAllMessagesReadInSessions = true - repo.deleteRecentConversation(recentSession, option) { error in - weakSelf?.repo.deleteLocalSession(recentSession: recentSession) + /// 是否同步完成 + public var syncFinished = false { + didSet { + print("syncFinished ", syncFinished) } } - open func stickTopInfoForSession(session: NIMSession) -> NIMStickTopSessionInfo? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + session.sessionId) - return repo.getStickTopSessionInfo(session: session) - } + /// 回调 + public var callBack: ConversationCallBack? - open func addStickTopSession(session: NIMSession, - _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + session.sessionId) - let params = NIMAddStickTopSessionParams(session: session) - repo.addStickTop(params: params) { error, stickTopSessionInfo in - completion(error as NSError?, stickTopSessionInfo) - } - } - - open func removeStickTopSession(params: NIMStickTopSessionInfo, - _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + params.session.sessionId) - repo.removeStickTop(params: params) { error, stickTopSessionInfo in - completion(error as NSError?, stickTopSessionInfo) - } - } - - open func loadStickTopSessionInfos(_ completion: - @escaping (NSError?, [NIMSession: NIMStickTopSessionInfo]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) - repo.getStickTopSessionList(completion) + override public init() { + NEALog.infoLog(ModuleName + " " + className, desc: #function) + super.init() + NotificationCenter.default.addObserver(self, selector: #selector(atMessageChange), name: Notification.Name(AtMessageChangeNoti), object: nil) + conversationRepo.addListener(self) + ChatRepo.shared.addChatListener(self) + TeamRepo.shared.addTeamListener(self) + ContactRepo.shared.addContactListener(self) + IMKitClient.instance.addLoginListener(self) } - open func notifyForNewMsg(userId: String?) -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:" + (userId ?? "nil")) - return repo.isNeedNotify(userId: userId) + deinit { + NEALog.infoLog(ModuleName + className(), desc: #function) + NotificationCenter.default.removeObserver(self) + conversationRepo.removeListener(self) + ChatRepo.shared.removeChatListener(self) + TeamRepo.shared.removeTeamListener(self) + ContactRepo.shared.removeContactListener(self) + IMKitClient.instance.removeLoginListener(self) } - open func notifyStateForNewMsg(teamId: String?) -> NIMTeamNotifyState { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (teamId ?? "nil")) - return repo.isNeedNotifyForTeam(teamId: teamId) + func atMessageChange() { + NEALog.infoLog(className(), desc: "atMessageChange") + delegate?.reloadTableView() } - // MARK: ======================== private method ============================== - - open func sortRecentSession() { - NELog.infoLog(ModuleName + " " + className, desc: #function) - var tempArr = [NIMRecentSession]() - var dic = [String: ConversationListModel]() - conversationListArray?.forEach { listModel in - if let session = listModel.recentSession { - tempArr.append(session) - if let sessionId = session.session?.sessionId { - dic[sessionId] = listModel - } - } + /// 分页获取会话列表 + open func getConversationListByPage(_ completion: @escaping (NSError?, Bool?) -> Void) { + if syncFinished == false { + callBack = completion + return } - let resultArr = repo.sortSessionList(recentSessions: tempArr, stickTopInfo: stickTopInfos) - var sortResultArr = [ConversationListModel]() - resultArr.forEach { recentSession in - let listModel = ConversationListModel() - listModel.recentSession = recentSession - if recentSession.session?.sessionType == .P2P { - if let sessionId = recentSession.session?.sessionId, - let userInfo = dic[sessionId]?.userInfo { - listModel.userInfo = userInfo + if isRequesting == true { + // 防止多次请求造成数据混乱,等上次请求成功后进行下一次 + completion(nil, false) + return + } + isRequesting = true + print("did getConversationList") + conversationRepo.getConversationList(offset, page) { [weak self] conversations, offset, finished, error in + if error == nil { + if let set = offset { + // 更新索引 + self?.offset = set } - - } else if recentSession.session?.sessionType == .team { - if let sessionId = recentSession.session?.sessionId, - let teamInfo = dic[sessionId]?.teamInfo { - listModel.teamInfo = teamInfo + self?.isRequesting = false + // 区分置顶消息和非置顶消息 + conversations?.forEach { conversation in + self?.addOrUpdateConversationData(conversation) } } - sortResultArr.append(listModel) + completion(error, finished) } - conversationListArray = sortResultArr } - // 本地排序 在didUpdate的时候如有需要在打开 - func findInsertPlace(recentSession: NIMRecentSession) -> NSInteger { - NELog.infoLog( - ModuleName + " " + className, - desc: #function + ", sessionId:" + (recentSession.session?.sessionId ?? "nil") - ) - var matchIndex = 0 - var find = false - if let conversationArr = conversationListArray { - for (i, listModel) in conversationArr.enumerated() { - if let enumTime = listModel.recentSession?.lastMessage?.timestamp, - let targetTime = recentSession.lastMessage?.timestamp { - if enumTime <= targetTime { - find = true - matchIndex = i - break - } + /// 添加或者更新会话 + /// - Parameter conversation 会话对象 + open func addOrUpdateConversationData(_ conversation: V2NIMConversation) { + if let cacheModel = conversationDic[conversation.conversationId] { + if let lastMsg = conversation.lastMessage, let cacheLastMsg = cacheModel.conversation?.lastMessage { + let createTime = lastMsg.messageRefer.createTime + let cacheCreateTime = cacheLastMsg.messageRefer.createTime + if cacheCreateTime < createTime { + cacheModel.conversation = conversation } } - } - - if find { - return matchIndex } else { - return conversationListArray?.count ?? 0 + let model = NEConversationListModel() + model.conversation = conversation + conversationDic[conversation.conversationId] = model + if conversation.stickTop == true { + stickTopConversations.insert(model, at: 0) + } else { + conversationListData.insert(model, at: 0) + } } } - // MARK: ==================== NIMChatManagerDelegate ========================== - - open func onRecvMessageReceipts(_ receipts: [NIMMessageReceipt]) { - receipts.forEach { receipt in - if receipt.session?.sessionType == .P2P { - if let listArr = conversationListArray { - for (i, listModel) in listArr.enumerated() { - if listModel.recentSession?.session?.sessionType == .P2P, - receipt.session?.sessionId == listModel.recentSession?.session?.sessionId { - delegate?.didUpdateRecentSession(index: i) - } - } - } + /// 删除会话 + /// - Parameter conversation 会话对象 + /// - Parameter completion 完成回调 + open func deleteConversation(_ conversation: V2NIMConversation, _ completion: @escaping (NSError?) -> Void) { + conversationRepo.deleteConversation(conversation.conversationId) { error in + if let err = error { + completion(err) + } else { + // 通知界面刷新 + completion(nil) } } } - // MARK: ==================== ConversationRepoDelegate ========================== + /// 添加置顶 + /// - Parameter conversation 会话对象 + /// - Parameter completion 完成回调 + open func addStickTop(conversation: V2NIMConversation, + _ completion: @escaping (NSError?) + -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + conversation.conversationId) + conversationRepo.addStickTop(conversation.conversationId) { error in + completion(error) + } + } - open func onNotifyAddStickTopSession(_ newInfo: NIMStickTopSessionInfo) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ",onNotifyAddStickTopSession sessionId:" + newInfo.session.sessionId) - stickTopInfos[newInfo.session] = newInfo - if repo.getRecentSession(newInfo.session) == nil { - repo.addRecentSession(newInfo.session) + /// 取消置顶 + /// - Parameter conversation 会话对象 + /// - Parameter completion 完成回调 + open func removeStickTop(conversation: V2NIMConversation, + _ completion: @escaping (NSError?) + -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + conversation.conversationId) + conversationRepo.removeStickTop(conversation.conversationId) { error in + completion(error) } - delegate?.reloadTableView() } - open func onNotifyRemoveStickTopSession(_ removedInfo: NIMStickTopSessionInfo) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ",onNotifyRemoveStickTopSession sessionId:" + removedInfo.session.sessionId) - stickTopInfos[removedInfo.session] = nil + open func onMuteListChanged() { delegate?.reloadTableView() } - open func onNotifySyncStickTopSessions(_ response: NIMSyncStickTopSessionResponse) { - loadStickTopSessionInfos { [weak self] error, sessionInfos in - NELog.infoLog( - ModuleName + " " + (self?.className ?? "ConversationViewModel"), - desc: "CALLBACK loadStickTopSessionInfos " + (error?.localizedDescription ?? "no error") - ) - if error != nil { - if let infos = self?.repo.getStickTopInfos() { - self?.stickTopInfos = infos - } - } else if let infos = sessionInfos { - self?.stickTopInfos = infos - } - self?.delegate?.reloadTableView() - self?.delegate?.reloadData() + // 创建会话回调 + public func onConversationCreated(_ conversation: V2NIMConversation) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", did add session targetId:" + conversation.conversationId) + if checkDismissTeamNoti(conversation) { + return } - } - open func didServerSessionUpdated(_ recentSession: NIMRecentSession?) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ",didServerSessionUpdated:" + (recentSession?.session?.sessionId ?? "")) + addOrUpdateConversationData(conversation) + delegate?.reloadTableView() } - // MARK: ====================NIMConversationManagerDelegate===================== + /// 会话变更 + /// - Parameter conversations 会话列表 + public func onConversationChanged(_ conversations: [V2NIMConversation]) { + // 置顶逻辑处理 + filterStickTopData(conversations) - open func didAdd(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { - guard let targetId = recentSession.session?.sessionId else { - NELog.errorLog(ModuleName + " " + className, desc: "❌sessionId is nil") - return + for conversation in conversations { + if checkDismissTeamNoti(conversation) { + continue + } + addOrUpdateConversationData(conversation) } - NELog.infoLog(ModuleName + " " + className, desc: #function + ", did add session targetId:" + targetId) - // 解散、退出群聊 - if let object = recentSession.lastMessage?.messageObject as? NIMNotificationObject, object.notificationType == .team { - if let content = object.content as? NIMTeamNotificationContent { - if content.operationType == .dismiss || - (content.operationType == .kick && - content.targetIDs?.contains(IMKitClient.instance.imAccid()) == true) || - (content.operationType == .leave && - IMKitClient.instance.isMySelf(content.sourceID)) { - // 群聊被解散 - // 被踢出群聊 - // 主动退出群聊 - NELog.infoLog( - ModuleName + " " + className, - desc: #function + "didAdd team dismiss or leave noti" + (recentSession.session?.sessionId ?? "nil") - ) - repo.deleteLocalSession(recentSession: recentSession) + delegate?.reloadTableView() + } - // 移除置顶 - if let session = recentSession.session { - if let param = stickTopInfos[session] { - removeStickTopSession(params: param) { error, _ in - if let err = error { - NELog.errorLog( - ModuleName + " ConversationViewModel", - desc: "CALLBACK removeStickTopSession failed,error = \(err)" - ) - } - } - } - stickTopInfos.removeValue(forKey: session) - } - return - } - } - } + public func updateUserInfo(_ model: NEConversationListModel, _ user: NEUserWithFriend, _ conversation: V2NIMConversation) { + model.conversation = conversation + } - weak var weakSelf = self - var listModel = ConversationListModel() - if let sid = recentSession.session?.sessionId { - print("session session id : ", sid) - if let model = cacheAddSessionDic[sid] { - listModel = model - NELog.infoLog( - ModuleName + " " + className, - desc: #function + "didAdd team has added" + (recentSession.session?.sessionId ?? "nil") - ) - } - cacheAddSessionDic[sid] = listModel - } - listModel.recentSession = recentSession - - if recentSession.session?.sessionType == .P2P { - repo.fetchUserInfo(accountList: [targetId]) { users, error in - if error == nil { - listModel.userInfo = users?.first - if let model = weakSelf?.sessionIsExist(listModel) { - model.userInfo = users?.first + public func updateTeamInfo(_ model: NEConversationListModel, _ team: V2NIMTeam, _ conversation: V2NIMConversation) { + model.conversation = conversation + } + + /// 处理置顶变更逻辑 + public func filterStickTopData(_ conversations: [V2NIMConversation]) { + var changeTostickTopSet = Set() + var changeToUnStickTopDic = Set() + for conversation in conversations { + if let model = conversationDic[conversation.conversationId] { + if model.conversation?.stickTop != conversation.stickTop { + if conversation.stickTop == true { + changeTostickTopSet.insert(conversation.conversationId) } else { - weakSelf?.conversationListArray?.append(listModel) + changeToUnStickTopDic.insert(conversation.conversationId) } - weakSelf?.delegate?.didAddRecentSession() } + model.conversation = conversation } + } - } else if recentSession.session?.sessionType == .team { - if !repo.isMyTeam(teamId: targetId) { - // 自己不在群内,不新增该群聊会话 - return + conversationListData.removeAll { model in + if let cid = model.conversation?.conversationId { + if changeTostickTopSet.contains(cid) { + stickTopConversations.insert(model, at: 0) + return true + } } + return false + } - repo.getTeamInfo(teamId: targetId) { error, teamInfo in - listModel.teamInfo = teamInfo - if let model = weakSelf?.sessionIsExist(listModel) { - model.teamInfo = teamInfo - weakSelf?.delegate?.didAddRecentSession() - } else { - if listModel.teamInfo != nil { - // 会话列表新增一项 - weakSelf?.conversationListArray?.append(listModel) - weakSelf?.delegate?.didAddRecentSession() - } + stickTopConversations.removeAll { model in + if let cid = model.conversation?.conversationId { + if changeToUnStickTopDic.contains(cid) { + conversationListData.append(model) + return true } } + return false } } - open func didUpdate(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { - guard let targetId = recentSession.session?.sessionId else { - NELog.errorLog(ModuleName + " " + className, desc: "❌sessionId is nil") - return - } - - NELog.infoLog( - ModuleName + " " + className, - desc: #function + "recentSession, didUpdate sessionId: \(targetId), unread count : \(totalUnreadCount)" - ) - - if recentSession.unreadCount <= 0 { - if NEAtMessageManager.instance?.isAtCurrentUser(sessionId: targetId) == true { - NEAtMessageManager.instance?.clearAtRecord(targetId) + /// 会话删除 + /// - Parameter conversationIds: 会话id列表 + public func onConversationDeleted(_ conversationIds: [String]) { + var removeFlagSet = Set() + for id in conversationIds { + removeFlagSet.insert(id) + conversationDic.removeValue(forKey: id) + } + stickTopConversations.removeAll(where: { + if let sid = $0.conversation?.conversationId, removeFlagSet.contains(sid) { + return true } - } + return false + }) + conversationListData.removeAll(where: { + if let sid = $0.conversation?.conversationId, removeFlagSet.contains(sid) { + return true + } + return false + }) + delegate?.reloadTableView() + } - if let object = recentSession.lastMessage?.messageObject as? NIMNotificationObject, object.notificationType == .team { - if let content = object.content as? NIMTeamNotificationContent { - if content.operationType == .dismiss || - (content.operationType == .kick && - content.targetIDs?.contains(IMKitClient.instance.imAccid()) == true) || - (content.operationType == .leave && - IMKitClient.instance.isMySelf(content.sourceID)) { + /// 检查会话是否包含解散通知的变更 + /// - Parameter conversation: 会话 + public func checkDismissTeamNoti(_ conversation: V2NIMConversation) -> Bool { + // 解散、退出群聊 + let targetId = conversation.conversationId + if conversation.type == V2NIMConversationType.CONVERSATION_TYPE_TEAM, conversation.lastMessage?.messageType == V2NIMMessageType.MESSAGE_TYPE_NOTIFICATION { + if let content = conversation.lastMessage?.attachment as? V2NIMMessageNotificationAttachment { + if content.type == V2NIMMessageNotificationType.MESSAGE_NOTIFICATION_TYPE_TEAM_DISMISS || + (content.type == V2NIMMessageNotificationType.MESSAGE_NOTIFICATION_TYPE_TEAM_KICK && + content.targetIds?.contains(IMKitClient.instance.account()) == true) || + (content.type == V2NIMMessageNotificationType.MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE && + IMKitClient.instance.isMe(conversation.lastMessage?.messageRefer.senderId)) { // 群聊被解散 // 被踢出群聊 // 主动退出群聊 - NELog.infoLog( + NEALog.infoLog( ModuleName + " " + className, - desc: #function + "didUpdate team dismiss or leave noti: \(targetId)" + desc: #function + "didAdd team dismiss or leave noti " + targetId ) - repo.deleteLocalSession(recentSession: recentSession) + conversationRepo.deleteConversation(targetId) { error in + } // 移除置顶 - if let session = recentSession.session { - if let param = stickTopInfos[session] { - removeStickTopSession(params: param) { error, _ in - if let err = error { - NELog.errorLog( - ModuleName + " ConversationViewModel", - desc: "CALLBACK removeStickTopSession failed,error = \(err)" - ) - } - } + conversationDic.removeValue(forKey: targetId) + stickTopConversations.removeAll { model in + if model.conversation?.conversationId == targetId { + return true } - stickTopInfos.removeValue(forKey: session) + return false } - return + delegate?.reloadTableView() + return true } } } + return false + } - cacheUpdateSessionDic[targetId] = recentSession - if let model = cacheAddSessionDic[targetId], let recent = model.recentSession { - if let time1 = recentSession.lastMessage?.timestamp, let time2 = recent.lastMessage?.timestamp, time1 > time2 { - model.recentSession = recentSession - } + /// 保存撤回消息 + /// - Parameter conversationId: 会话id + /// - Parameter createTime: 撤回时间 + /// - Parameter revokeAccountId: 撤回人id + /// - Parameter extention: 扩展信息 + /// - Parameter completion: 完成回调 + open func saveRevokeMessage(_ messageRevoke: V2NIMMessageRevokeNotification, + _ completion: @escaping (NSError?) -> Void) { + let messageNew = V2NIMMessageCreator.createTextMessage(localizable("message_recalled")) + messageNew.messageConfig?.unreadEnabled = true + + if let ext = messageRevoke.serverExtension { + messageNew.localExtension = ext + } else { + var muta = [String: Any]() + muta[revokeLocalMessage] = true + messageNew.localExtension = NECommonUtil.getJSONStringFromDictionary(muta) } - weak var weakSelf = self - if let _ = conversationListArray { - for i in 0 ..< conversationListArray!.count { - let listModel = conversationListArray![i] - NELog.infoLog( - ModuleName + " " + className, - desc: #function + "update session id : " + (listModel.recentSession?.session?.sessionId ?? "nil") - ) - if targetId == listModel.recentSession?.session?.sessionId { - listModel.recentSession = recentSession - if recentSession.session?.sessionType == .P2P { - repo.fetchUserInfo(accountList: [targetId]) { users, error in - if error == nil { - listModel.userInfo = users?.first - } - } - } else if recentSession.session?.sessionType == .team { - repo.getTeamInfo(teamId: targetId) { error, teamInfo in - listModel.teamInfo = teamInfo - } - } - weakSelf?.delegate?.reloadTableView() - return - } - } - - // 会话列表中没有该回话则添加 - didAdd(recentSession, totalUnreadCount: totalUnreadCount) + ChatRepo.shared.saveMessageToDB(message: messageNew, + conversationId: messageRevoke.messageRefer?.conversationId ?? "", + senderId: messageRevoke.revokeAccountId, + createTime: messageRevoke.messageRefer?.createTime) { _, error in + completion(error) } } - open func didRemove(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { - guard let targetId = recentSession.session?.sessionId else { - NELog.errorLog(ModuleName + " " + className, desc: "❌sessionId is nil") - return - } - - NELog.infoLog( - ModuleName + " " + className, - desc: #function + ",didRemove recentSession sessionId: \(targetId)" - ) + /// 撤回通知监听 + /// - Parameter revokeNotifications: 撤回通知列表 + public func onMessageRevokeNotifications(_ revokeNotifications: [V2NIMMessageRevokeNotification]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "onMessageRevokeNotifications ids: \(revokeNotifications.map { $0.messageRefer?.messageServerId })") - cacheUpdateSessionDic.removeValue(forKey: targetId) + for messageRevoke in revokeNotifications { + guard let msgServerId = messageRevoke.messageRefer?.messageServerId else { + return + } - if let conversationArr = conversationListArray { - for i in 0 ..< conversationArr.count { - if conversationArr[i].recentSession?.session?.sessionId.count ?? 0 <= 0 { - NELog.infoLog( - ModuleName + " " + className, - desc: #function + ",didRemove recentSession sessionId is empty user: \(conversationArr[i].userInfo?.userId ?? "") team: \(conversationArr[i].teamInfo?.teamId ?? "")" - ) - } - if conversationArr[i].recentSession?.session?.sessionId == recentSession.session? - .sessionId { - NELog.infoLog( - ModuleName + " " + className, - desc: #function + ",remove session list at index : \(i) sessionid : \(targetId)" - ) + // 防止重复插入本地撤回消息 + if ConversationDeduplicationHelper.instance.isRevokeMessageSaved(messageId: msgServerId) { + return + } - conversationListArray?.remove(at: i) - break + saveRevokeMessage(messageRevoke) { error in + if let err = error { + NEALog.infoLog(ModuleName + " " + ConversationViewModel.className(), desc: "saveRevokeMessage error \(err)") } } } - delegate?.reloadTableView() } - // 收到多端登录单向删除通知,手动更新会话最后一条消息 - open func onRecvMessagesDeleted(_ messages: [NIMMessage], exts: [String: String]?) { - for message in messages { - guard let session = message.session else { return } - - for listModel in conversationListArray ?? [] { - if session.sessionId == listModel.recentSession?.session?.sessionId { - NELog.infoLog(ModuleName + " " + className, desc: #function + "onRecvMessagesDeleted sessionid: \(session.sessionId) message text: \(message.text ?? "")") - - // 手动查询最后一条消息 - let param = NIMGetMessagesDynamicallyParam() - param.session = session - param.limit = 1 - ChatRepo.shared.getMessagesDynamically(param) { [weak self] _, _, messages in - listModel.recentSession?.lastMessage = messages?.first - self?.delegate?.reloadTableView() - } + /// 收到点对点已读回执 + /// - Parameter readReceipts: 已读回执 + public func onReceiveP2PMessageReadReceipts(_ readReceipts: [V2NIMP2PMessageReadReceipt]) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + "onReceive p2p readReceipts count: \(readReceipts.count)") + for receipt in readReceipts { + if let cid = receipt.conversationId { + if conversationDic[cid] != nil { + delegate?.reloadTableView() break } } } } - // MARK: ========================NIMUserManagerDelegate========================= - - open func onFriendChanged(_ user: NIMUser) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:" + (user.userId ?? "nil")) - if let listArr = conversationListArray { - for (i, listModel) in listArr.enumerated() { - if listModel.recentSession?.session?.sessionType == .P2P { - if listModel.userInfo?.userId == user.userId { - listModel.userInfo = NEKitUser(user: user) - delegate?.didUpdateRecentSession(index: i) - break - } - } - } - } - } + /// 加入群回调 + /// - Parameter team: 群信息 + public func onTeamJoined(_ team: V2NIMTeam) {} - // MARK: =========================NIMTeamManagerDelegate======================== + /// 建群回调 + /// - Parameter team: 群信息 + public func onTeamCreated(_ team: V2NIMTeam) {} - open func onTeamUpdated(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (team.teamId ?? "nil")) - guard let conversationArr = conversationListArray else { - return - } - for (i, listModel) in conversationArr.enumerated() { - if listModel.recentSession?.session?.sessionId == team.teamId { - listModel.teamInfo = team - delegate?.didUpdateRecentSession(index: i) - break - } + public func onTeamLeft(_ team: V2NIMTeam, isKicked: Bool) { + NEALog.infoLog(className(), desc: "conversation onTeamLeft team id: \(team.teamId) team name : \(team.name) isKicked : \(isKicked)") + if let cid = V2NIMConversationIdUtil.teamConversationId(team.teamId) { + didDeleteConversation(cid) } } - open func onTeamAdded(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + "onTeamAdded, teamId:" + (team.teamId ?? "nil")) - guard let tid = team.teamId else { - return + /// 群解散回调 + /// - Parameter team: 群信息 + public func onTeamDismissed(_ team: V2NIMTeam) { + NEALog.infoLog(className(), desc: "onTeamDismissed team id : \(team.teamId) team name: \(team.name)") + if let cid = V2NIMConversationIdUtil.teamConversationId(team.teamId) { + didDeleteConversation(cid) } - let _ = repo.createTeamSession(tid) - delegate?.didAddRecentSession() } - open func onTeamRemoved(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (team.teamId ?? "nil")) - // 做删除会话操作(自己退出群聊会触发) - guard let conversationArr = conversationListArray else { - return - } - - DispatchQueue.main.async { - for listModel in conversationArr { - if let teamInfo = listModel.teamInfo, teamInfo.teamId == team.teamId { - if let recentSession = listModel.recentSession { - self.deleteRecentSession(recentSession: recentSession) - break + private func didDeleteConversation(_ cid: String) { + conversationRepo.deleteConversation(cid) { [weak self] error in + if let err = error { + NEALog.infoLog(self?.className() ?? " ", desc: "onTeamDismissed delete conversation error : \(err.localizedDescription)") + } else { + self?.conversationDic.removeValue(forKey: cid) + self?.stickTopConversations.removeAll { model in + if model.conversation?.conversationId == cid { + return true } + return false } + self?.conversationListData.removeAll { model in + if model.conversation?.conversationId == cid { + return true + } + return false + } + self?.delegate?.reloadTableView() } } } - open func onTeamMemberChanged(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (team.teamId ?? "nil")) - guard let conversationArr = conversationListArray else { - return - } - for (i, listModel) in conversationArr.enumerated() { - if listModel.recentSession?.session?.sessionId == team.teamId { - listModel.teamInfo = team - delegate?.didUpdateRecentSession(index: i) - break + public func createConversation(_ team: V2NIMTeam) { + if let cid = V2NIMConversationIdUtil.teamConversationId(team.teamId) { + conversationRepo.createConversation(cid) { [weak self] conversation, error in + if let conv = conversation { + let model = NEConversationListModel() + model.conversation = conv + self?.conversationDic[cid] = model + self?.conversationListData.append(model) + self?.delegate?.reloadTableView() + } else { + NEALog.infoLog(self?.className() ?? " ", desc: "createConversation error : \(error?.localizedDescription ?? "")") + } } } } - private func sessionIsExist(_ model: ConversationListModel) -> ConversationListModel? { - if let array = conversationListArray { - for index in 0 ..< array.count { - let m = array[index] - if m.recentSession?.session?.sessionId == model.recentSession?.session?.sessionId { - return m - } + public func onConversationSyncFinished() { + NEALog.infoLog(className(), desc: "onConversationSyncFinished") + delegate?.reloadTableView() + } + + public func onDataSync(_ type: V2NIMDataSyncType, state: V2NIMDataSyncState, error: V2NIMError?) { + if type == .DATA_SYNC_TYPE_MAIN, state == .DATA_SYNC_STATE_COMPLETED { + /// 设置同步完成标识 + syncFinished = true + + if let completion = callBack { + NEALog.infoLog(className(), desc: "onConversationSyncFinished getConversationListByPage") + /// 取数据 + getConversationListByPage(completion) + /// 回调置空 + callBack = nil } } - return nil } - open func onMuteListChanged() { + public func onFriendDeleted(_ accountId: String, deletionType: V2NIMFriendDeletionType) { + delegate?.reloadTableView() + } + + public func onTeamSyncFinished() { + delegate?.reloadTableView() + } + + public func onConversationSyncFailed(_ error: V2NIMError) { + NEALog.infoLog(className(), desc: "onConversationSyncFailed : \(error.desc)") + } + + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + NEALog.infoLog(className(), desc: "onFriendInfoChanged : \(friendInfo.accountId ?? "")") delegate?.reloadTableView() } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift b/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift index 67d155a8..cca6c845 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift @@ -51,14 +51,14 @@ public class ConversationUIConfig: NSObject { /// 会话列表 cell 左划置顶按钮背景颜色 public var stickTopBottonBackgroundColor: UIColor? /// 会话列表 cell 左划置顶按钮点击事件 - public var stickTopBottonClick: ((ConversationListModel?, IndexPath) -> Void)? + public var stickTopBottonClick: ((NEConversationListModel?, IndexPath) -> Void)? /// 会话列表 cell 左划删除按钮文案内容 public var deleteBottonTitle = localizable("delete") /// 会话列表 cell 左划删除按钮背景颜色 public var deleteBottonBackgroundColor: UIColor? /// 会话列表 cell 左划删除按钮点击事件 - public var deleteBottonClick: ((ConversationListModel?, IndexPath) -> Void)? + public var deleteBottonClick: ((NEConversationListModel?, IndexPath) -> Void)? /// 标题栏左侧按钮点击事件 public var titleBarLeftClick: (() -> Void)? @@ -70,7 +70,7 @@ public class ConversationUIConfig: NSObject { public var titleBarRight2Click: (() -> Void)? /// 会话列表点击事件 - public var itemClick: ((ConversationListModel?, IndexPath) -> Void)? + public var itemClick: ((NEConversationListModel?, IndexPath) -> Void)? /// 会话列表的视图控制器回调,回调中会返回会话列表的视图控制器 public var customController: ((NEBaseConversationController) -> Void)? diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift index 2079eea4..9dda6639 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift @@ -7,68 +7,69 @@ import UIKit @objcMembers open class FunConversationListCell: NEBaseConversationListCell { - var contentModel: ConversationListModel? + var contentModel: NEConversationListModel? + /// 分割线视图 + public lazy var bottomLine: UIView = { + let bottomLine = UIView() + bottomLine.translatesAutoresizingMaskIntoConstraints = false + bottomLine.backgroundColor = .funConversationListLineBorderColor + return bottomLine + }() + + /// UI 初始化 override open func setupSubviews() { super.setupSubviews() NSLayoutConstraint.activate([ - headImge.leftAnchor.constraint( + headImageView.leftAnchor.constraint( equalTo: contentView.leftAnchor, constant: NEConstant.screenInterval ), - headImge.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - headImge.widthAnchor.constraint(equalToConstant: 48), - headImge.heightAnchor.constraint(equalToConstant: 48), + headImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + headImageView.widthAnchor.constraint(equalToConstant: 48), + headImageView.heightAnchor.constraint(equalToConstant: 48), ]) - title.font = .systemFont(ofSize: NEKitConversationConfig.shared.ui.conversationProperties.itemTitleSize > 0 ? NEKitConversationConfig.shared.ui.conversationProperties.itemTitleSize : 17) + titleLabel.font = .systemFont(ofSize: NEKitConversationConfig.shared.ui.conversationProperties.itemTitleSize > 0 ? NEKitConversationConfig.shared.ui.conversationProperties.itemTitleSize : 17) NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - title.rightAnchor.constraint(equalTo: timeLabel.leftAnchor, constant: -5), - title.topAnchor.constraint(equalTo: headImge.topAnchor, constant: 4), + titleLabel.leftAnchor.constraint(equalTo: headImageView.rightAnchor, constant: 12), + titleLabel.rightAnchor.constraint(equalTo: timeLabel.leftAnchor, constant: -5), + titleLabel.topAnchor.constraint(equalTo: headImageView.topAnchor, constant: 4), ]) - let bottomLine = UIView() - bottomLine.translatesAutoresizingMaskIntoConstraints = false - bottomLine.backgroundColor = .funConversationListLineBorderColor contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: title.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 0.5), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) NSLayoutConstraint.activate([ - notifyMsg.rightAnchor.constraint(equalTo: timeLabel.rightAnchor, constant: -2), - notifyMsg.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 10), - notifyMsg.widthAnchor.constraint(equalToConstant: 14), - notifyMsg.heightAnchor.constraint(equalToConstant: 14), + notifyMsgView.rightAnchor.constraint(equalTo: timeLabel.rightAnchor, constant: -2), + notifyMsgView.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 10), + notifyMsgView.widthAnchor.constraint(equalToConstant: 14), + notifyMsgView.heightAnchor.constraint(equalToConstant: 14), ]) } override func initSubviewsLayout() { if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .rectangle { - headImge.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius + headImageView.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius } else if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .cycle { - headImge.layer.cornerRadius = 24.0 + headImageView.layer.cornerRadius = 24.0 } else { - headImge.layer.cornerRadius = 4.0 + headImageView.layer.cornerRadius = 4.0 } } - override open func configData(sessionModel: ConversationListModel?) { - super.configData(sessionModel: sessionModel) + override open func configureData(_ sessionModel: NEConversationListModel?) { + super.configureData(sessionModel) contentModel = sessionModel - - // backgroundColor - if let session = sessionModel?.recentSession?.session { - let isTop = topStickInfos[session] != nil - if isTop { - contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemStickTopBackground ?? .funConversationBackgroundColor - } else { - contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemBackground ?? .white - } + if sessionModel?.conversation?.stickTop == true { + contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemStickTopBackground ?? .funConversationBackgroundColor + } else { + contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemBackground ?? .white } } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift index a1618580..d8b33c86 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift @@ -7,19 +7,25 @@ import UIKit @objcMembers open class FunConversationSearchCell: NEBaseConversationSearchCell { + /// 分割线视图 + lazy var bottomLine: UIView = { + let bottomLine = UIView() + bottomLine.translatesAutoresizingMaskIntoConstraints = false + bottomLine.backgroundColor = .funConversationLineBorderColor + return bottomLine + }() + + /// UI 初始化 override open func setupSubviews() { super.setupSubviews() - headImge.layer.cornerRadius = 4.0 + headImageView.layer.cornerRadius = 4.0 - headImge.updateLayoutConstraint(firstItem: headImge, seconedItem: nil, attribute: .width, constant: 40) - headImge.updateLayoutConstraint(firstItem: headImge, seconedItem: nil, attribute: .height, constant: 40) + headImageView.updateLayoutConstraint(firstItem: headImageView, seconedItem: nil, attribute: .width, constant: 40) + headImageView.updateLayoutConstraint(firstItem: headImageView, seconedItem: nil, attribute: .height, constant: 40) - let bottomLine = UIView() - bottomLine.translatesAutoresizingMaskIntoConstraints = false - bottomLine.backgroundColor = .funConversationLineBorderColor contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: headImge.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: headImageView.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 1), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift index 968b50f9..0814d1c0 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift @@ -9,30 +9,32 @@ import UIKit @objcMembers open class FunConversationController: NEBaseConversationController { + /// 搜索视图 + public lazy var searchView: FunSearchView = { + let view = FunSearchView() + view.translatesAutoresizingMaskIntoConstraints = false + view.searchBotton.setImage(UIImage.ne_imageNamed(name: "funSearch"), for: .normal) + view.searchBotton.setTitle(commonLocalizable("search"), for: .normal) + view.searchBotton.accessibilityIdentifier = "id.titleBarSearchImg" + return view + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) className = "FunConversationController" deleteBottonBackgroundColor = .funConversationdeleteActionColor cellRegisterDic = [0: FunConversationListCell.self] brokenNetworkViewHeight = 48 - brokenNetworkView.errorIcon.isHidden = false + brokenNetworkView.errorIconView.isHidden = false brokenNetworkView.backgroundColor = .funConversationNetworkBrokenBackgroundColor - brokenNetworkView.content.textColor = .funConversationNetworkBrokenTitleColor + brokenNetworkView.contentLabel.textColor = .funConversationNetworkBrokenTitleColor emptyView.setEmptyImage(name: "fun_user_empty") } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } - public lazy var searchView: FunSearchView = { - let view = FunSearchView() - view.translatesAutoresizingMaskIntoConstraints = false - view.searchBotton.setImage(UIImage.ne_imageNamed(name: "funSearch"), for: .normal) - view.searchBotton.setTitle(localizable("search"), for: .normal) - return view - }() - override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funConversationBackgroundColor @@ -42,7 +44,7 @@ open class FunConversationController: NEBaseConversationController { deinit { if let searchViewGestures = searchView.gestureRecognizers { - searchViewGestures.forEach { gesture in + for gesture in searchViewGestures { searchView.removeGestureRecognizer(gesture) } } @@ -51,6 +53,7 @@ open class FunConversationController: NEBaseConversationController { override func initSystemNav() { super.initSystemNav() let addBarButton = UIButton() + addBarButton.accessibilityIdentifier = "id.titleBarMoreImg" addBarButton.setImage(UIImage.ne_imageNamed(name: "chat_add"), for: .normal) addBarButton.addTarget(self, action: #selector(didClickAddBtn), for: .touchUpInside) let addBarItem = UIBarButtonItem(customView: addBarButton) diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift index 55aa8002..f5305c33 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift @@ -7,13 +7,26 @@ import UIKit @objcMembers open class FunConversationSearchController: NEBaseConversationSearchController { + /// 取消按钮 + lazy var cancelButton: UIButton = { + let cancelButton = UIButton() + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.setTitle(localizable("cancel"), for: .normal) + cancelButton.setTitleColor(.ne_greyText, for: .normal) + cancelButton.addTarget(self, action: #selector(backEvent), for: .touchUpInside) + cancelButton.titleLabel?.adjustsFontSizeToFitWidth = true + cancelButton.contentHorizontalAlignment = .center + cancelButton.accessibilityIdentifier = "id.cancelBtn" + return cancelButton + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) tag = "FunConversationSearchController" } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -24,6 +37,7 @@ open class FunConversationSearchController: NEBaseConversationSearchController { emptyView.setEmptyImage(name: "fun_user_empty") } + /// 初始化子视图 override open func setupSubviews() { super.setupSubviews() let leftImageView = UIImageView(image: UIImage @@ -40,13 +54,6 @@ open class FunConversationSearchController: NEBaseConversationSearchController { searchTextField.heightAnchor.constraint(equalToConstant: 36), ]) - let cancelButton = UIButton() - cancelButton.translatesAutoresizingMaskIntoConstraints = false - cancelButton.setTitle(localizable("cancel"), for: .normal) - cancelButton.setTitleColor(.ne_greyText, for: .normal) - cancelButton.addTarget(self, action: #selector(backEvent), for: .touchUpInside) - cancelButton.titleLabel?.adjustsFontSizeToFitWidth = true - cancelButton.contentHorizontalAlignment = .center view.addSubview(cancelButton) NSLayoutConstraint.activate([ cancelButton.centerYAnchor.constraint(equalTo: searchTextField.centerYAnchor), @@ -87,7 +94,7 @@ open class FunConversationSearchController: NEBaseConversationSearchController { .dequeueReusableHeaderFooterView( withIdentifier: "\(NSStringFromClass(SearchSessionBaseView.self))" ) as! FunSearchSessionHeaderView - sectionView.title.textColor = .funConversationSearchHeaderViewTitleColor + sectionView.titleLabel.textColor = .funConversationSearchHeaderViewTitleColor sectionView.bottomLine.backgroundColor = .funConversationLineBorderColor sectionView.setUpTitle(title: headTitleArr[section]) return sectionView diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift index a592f4dd..d5f1cdde 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift @@ -3,6 +3,8 @@ // found in the LICENSE file. import Foundation +import NEChatKit +import NECoreIM2Kit import NIMSDK public let atAllKey = "ait_all" @@ -23,7 +25,7 @@ open class AtMEMessageRecord: NSObject { } @objcMembers -open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManagerDelegate { +open class NEAtMessageManager: NSObject, NEIMKitClientListener, NEChatListener { public static var instance: NEAtMessageManager? private let workQueue = DispatchQueue(label: "AtMessageWorkQueue") private let lock = NSLock() @@ -32,47 +34,52 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager override private init() { super.init() - NIMSDK.shared().chatManager.add(self) - NIMSDK.shared().loginManager.add(self) + ChatRepo.shared.addChatListener(self) + IMKitClient.instance.addLoginListener(self) } deinit { - NIMSDK.shared().chatManager.remove(self) - NIMSDK.shared().loginManager.remove(self) + ChatRepo.shared.removeChatListener(self) + IMKitClient.instance.removeLoginListener(self) } + /// 初始化 public static func setupInstance() { NEAtMessageManager.instance = NEAtMessageManager() } - open func onLogin(_ step: NIMLoginStep) { - if step == .loginOK { - NELog.infoLog(className(), desc: "login ok") - currentAccid = NIMSDK.shared().loginManager.currentAccount() + /// 登录状态变更 + /// - Parameter status: 登录状态 + public func onLoginStatus(_ status: V2NIMLoginStatus) { + if status == .LOGIN_STATUS_LOGINED { + NEALog.infoLog(className(), desc: "login ok") + currentAccid = IMKitClient.instance.account() weak var weakSelf = self let newDic = [String: AtMEMessageRecord]() setMessageDic(newDic) workQueue.async { weakSelf?.loadCacheFromDocument() } - } else if step == .syncing { - NELog.infoLog(className(), desc: "roaming messages start") - } else if step == .syncOK { - NELog.infoLog(className(), desc: "roaming messages finish") - if currentAccid.count <= 0 { - currentAccid = NIMSDK.shared().loginManager.currentAccount() - } - startFilterRoamingMessagesTask() } } - open func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { - guard let msg = notification.message else { - return + /// 数据同步回调 + /// - Parameter type: 同步类型 + /// - Parameter state: 同步状态 + /// - Parameter error: 错误信息 + public func onDataSync(_ type: V2NIMDataSyncType, state: V2NIMDataSyncState, error: V2NIMError?) { + if state == .DATA_SYNC_STATE_COMPLETED { + if currentAccid.count <= 0 { + currentAccid = IMKitClient.instance.account() + } + } else if state == .DATA_SYNC_STATE_SYNCING { + NEALog.infoLog(className(), desc: "roaming messages start") + } else if state == .DATA_SYNC_STATE_WAITING { + NEALog.infoLog(className(), desc: "roaming messages waitting") } - removeRevokeAtMessage(messages: [msg]) } + /// 获取当前at消息内存缓存 private func getMessageDic() -> [String: AtMEMessageRecord] { lock.lock() let result = atMessageDic @@ -80,29 +87,39 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager return result } + /// 设置at消息缓存 + /// - Parameter dic: at消息缓存 private func setMessageDic(_ dic: [String: AtMEMessageRecord]) { lock.lock() atMessageDic = dic lock.unlock() } + /// 判断是否是当前用户 + /// - Parameter sessionId: 会话id + /// - Returns: 是否是当前用户 open func isAtCurrentUser(sessionId: String) -> Bool { let dic = getMessageDic() - NELog.infoLog(className(), desc: "session id : \(sessionId)") - NELog.infoLog(className(), desc: "dic : \(dic)") + NEALog.infoLog(className(), desc: "session id : \(sessionId)") + NEALog.infoLog(className(), desc: "dic : \(dic)") + if let model = dic[sessionId], model.isRead == false { + NEALog.infoLog(className(), desc: "read == false") return true } return false } - open func clearAtRecord(_ sessionId: String) { + /// 清理at消息记录 + /// - Parameter conversationId: 会话id + open func clearAtRecord(_ conversationId: String) { + NEALog.infoLog(className(), desc: "clearAtRecord session id : \(conversationId)") weak var weakSelf = self workQueue.async { guard let dic = weakSelf?.getMessageDic() else { return } - if let model = dic[sessionId] { + if let model = dic[conversationId] { model.isRead = true model.atMessages.removeAll() weakSelf?.setMessageDic(dic) @@ -111,8 +128,10 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } } - open func filterAtMessage(messages: [NIMMessage]) { - NELog.infoLog(className(), desc: "at manager filterAtMessage : \(messages.count)") + /// 过滤 at 消息 + /// - Parameter messages: 消息列表 + open func filterAtMessage(messages: [V2NIMMessage]) { + NEALog.infoLog(className(), desc: "at manager filterAtMessage : \(messages.count)") weak var weakSelf = self workQueue.async { if let result = weakSelf?.filterAtMessageInWorkqueue(messages: messages), result == true { @@ -124,13 +143,7 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } } - open func removeRevokeAtMessage(messages: [NIMMessage]) { - weak var weakSelf = self - workQueue.async { - weakSelf?.removeRevokeAtMessageInWorkqueue(messages: messages) - } - } - + /// 开启遍历漫游消息任务 open func startFilterRoamingMessagesTask() { weak var weakSelf = self workQueue.async { @@ -138,52 +151,24 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } } - private func removeRevokeAtMessageInWorkqueue(messages: [NIMMessage]) { - let currentAccid = NIMSDK.shared().loginManager.currentAccount() - weak var weakSelf = self - var isAtMessageChange = false - var temDic = getMessageDic() - messages.forEach { message in - if message.status == .read { - return - } - if let remoteExt = message.remoteExt, let dic = remoteExt[yxAitMsg] as? [String: AnyObject] { - if dic[atAllKey] != nil, message.from != currentAccid { - isAtMessageChange = weakSelf?.removeRecord(message: message, record: &temDic) ?? false - return - } - if dic[currentAccid] != nil { - isAtMessageChange = weakSelf?.removeRecord(message: message, record: &temDic) ?? false - return - } - } - } - if isAtMessageChange == true { - atMessageChangeNoti() - } - } - @discardableResult - private func filterAtMessageInWorkqueue(messages: [NIMMessage]) -> Bool { - let currentAccid = NIMSDK.shared().loginManager.currentAccount() + private func filterAtMessageInWorkqueue(messages: [V2NIMMessage]) -> Bool { + let currentAccid = IMKitClient.instance.account() weak var weakSelf = self var isExistAtMessage = false var temDic = getMessageDic() - messages.forEach { message in - if message.status == .read { - return - } - if let remoteExt = message.remoteExt, let dic = remoteExt[yxAitMsg] as? [String: AnyObject] { - if dic[atAllKey] != nil, message.from != currentAccid { + for message in messages { + if let serverExtension = message.serverExtension, let remoteExt = NECommonUtil.getDictionaryFromJSONString(serverExtension), let dic = remoteExt[yxAitMsg] as? [String: AnyObject] { + if dic[atAllKey] != nil, message.senderId != currentAccid { weakSelf?.addAtRecord(message: message, record: &temDic) isExistAtMessage = true - return + continue } if dic[currentAccid] != nil { weakSelf?.addAtRecord(message: message, record: &temDic) isExistAtMessage = true - return + continue } } } @@ -192,51 +177,69 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } private func startFilterRoamingMessagesTaskInWorkqueue() { - let sessions = NIMSDK.shared().conversationManager.allRecentSessions() - NELog.infoLog(className(), desc: "startFilterRoamingMessagesTaskInWorkqueue session count : \(sessions?.count ?? 0)") - var temDic = getMessageDic() + var conversations = [V2NIMConversation]() weak var weakSelf = self - var isExistAtMessage = false - print("recent session filter at message") - sessions?.forEach { recentSession in - if recentSession.unreadCount <= 0 { + + getAllConversation(&conversations) { error in + + let workingGroup = DispatchGroup() + let workingQueue = DispatchQueue(label: "at_message_queue") + guard var temDic = weakSelf?.getMessageDic() else { return } - if let session = recentSession.session { - let messages = NIMSDK.shared().conversationManager.messages(in: session, message: nil, limit: 100) - messages?.forEach { message in - if message.status == .read { - return - } - if let remoteExt = message.remoteExt, let dic = remoteExt[yxAitMsg] as? [String: AnyObject] { - if dic[atAllKey] != nil, message.from != currentAccid { - weakSelf?.addAtRecord(message: message, record: &temDic) - isExistAtMessage = true - return - } - if dic[currentAccid] != nil { - weakSelf?.addAtRecord(message: message, record: &temDic) - isExistAtMessage = true - return + guard let accid = weakSelf?.currentAccid else { + return + } + var isExistAtMessage = false + for conversation in conversations { + if conversation.type != .CONVERSATION_TYPE_TEAM { + break + } + weak var weakSelf = self + workingGroup.enter() + workingQueue.async { + let option = V2NIMMessageListOption() + option.limit = 100 + option.conversationId = conversation.conversationId + option.strictMode = false + ChatProvider.shared.getMessageList(option: option) { messages, v2Error in + messages?.forEach { message in + if let serverExtension = message.serverExtension, let remoteExt = NECommonUtil.getDictionaryFromJSONString(serverExtension), let dic = remoteExt[yxAitMsg] as? [String: AnyObject] { + if dic[atAllKey] != nil, message.isSelf == false { + weakSelf?.addAtRecord(message: message, record: &temDic) + isExistAtMessage = true + return + } + if dic[accid] != nil { + weakSelf?.addAtRecord(message: message, record: &temDic) + isExistAtMessage = true + return + } + } } + workingGroup.leave() } } } - } - if isExistAtMessage == true { - writeCacheToDocument(dictionary: temDic) - setMessageDic(temDic) - atMessageChangeNoti() + + workingGroup.notify(queue: workingQueue) { + if isExistAtMessage == true { + weakSelf?.writeCacheToDocument(dictionary: temDic) + weakSelf?.setMessageDic(temDic) + weakSelf?.atMessageChangeNoti() + } + } } } + /// at 消息记录写文件缓存 private func writeCacheToDocument(dictionary: [String: AtMEMessageRecord]) { if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(currentAccid)_at_message.plist") - NELog.infoLog(className(), desc: "writeCacheToDocument path : \(filePath)") + NEALog.infoLog(className(), desc: "writeCacheToDocument path : \(filePath)") do { var jsonObject = [String: Any]() - dictionary.forEach { (key: String, value: AtMEMessageRecord) in + for (key, value) in dictionary { if let jsonValue = value.yx_modelToJSONObject() { jsonObject[key] = jsonValue } @@ -245,13 +248,14 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager let jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: []) try jsonData.write(to: filePath) } catch { - NELog.infoLog(className(), desc: "write cache error : \(error.localizedDescription)") + NEALog.infoLog(className(), desc: "write cache error : \(error.localizedDescription)") } } } + /// 加载本地缓存文件 private func loadCacheFromDocument() { - NELog.infoLog(className(), desc: "loadCacheFromDocument") + NEALog.infoLog(className(), desc: "loadCacheFromDocument") if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { weak var weakSelf = self let documentDir = documentsDirectory.appendingPathComponent("NEIMUIKit/") @@ -259,19 +263,19 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager do { try FileManager.default.createDirectory(at: documentDir, withIntermediateDirectories: false) } catch { - NELog.infoLog(className(), desc: "create dir error : \(error.localizedDescription)") + NEALog.infoLog(className(), desc: "create dir error : \(error.localizedDescription)") } } let filePath = documentDir.appendingPathComponent("\(currentAccid)_at_message.plist") if FileManager.default.fileExists(atPath: filePath.path) == false { let success = FileManager.default.createFile(atPath: filePath.absoluteString, contents: nil) - NELog.infoLog(className(), desc: "create file success: \(success) path: \(filePath.absoluteString)") + NEALog.infoLog(className(), desc: "create file success: \(success) path: \(filePath.absoluteString)") } else { do { let data = try Data(contentsOf: filePath) if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: [String: Any]] { var temdDic = weakSelf?.getMessageDic() - jsonObject.forEach { (key: String, value: [String: Any]) in + for (key, value) in jsonObject { if let model = AtMEMessageRecord.yx_model(with: value) { temdDic?[key] = model if let dic = jsonObject[key], let isRead = dic[#keyPath(AtMEMessageRecord.isRead)] as? Bool { @@ -289,17 +293,20 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } } } catch { - NELog.infoLog(className(), desc: "convert to message data to json object error : \(error.localizedDescription)") + NEALog.infoLog(className(), desc: "convert to message data to json object error : \(error.localizedDescription)") } } } } - private func removeRecord(message: NIMMessage, record: inout [String: AtMEMessageRecord]) -> Bool { + /// 移除at记录 + /// - Parameter message: 消息 + /// - Parameter record: at 消息记录缓存 + private func removeRecord(message: V2NIMMessage, record: inout [String: AtMEMessageRecord]) -> Bool { var didRemove = false - if let atMeRecord = record[message.session?.sessionId ?? ""] { - if atMeRecord.atMessages[message.messageId] != nil { - atMeRecord.atMessages.removeValue(forKey: message.messageId) + if let atMeRecord = record[message.conversationId ?? ""] { + if atMeRecord.atMessages[message.messageClientId ?? ""] != nil { + atMeRecord.atMessages.removeValue(forKey: message.messageClientId ?? "") if atMeRecord.atMessages.count <= 0 { atMeRecord.isRead = true didRemove = true @@ -309,34 +316,39 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager return didRemove } - private func addAtRecord(message: NIMMessage, record: inout [String: AtMEMessageRecord]) { - if let atMeRecord = record[message.session?.sessionId ?? ""] { + /// 添加at消息记录 + /// - Parameter message: 消息 + /// - Parameter record: at 消息记录缓存 + private func addAtRecord(message: V2NIMMessage, record: inout [String: AtMEMessageRecord]) { + if let atMeRecord = record[message.conversationId ?? ""] { let lastTime = atMeRecord.lastTime?.doubleValue ?? 0 - if lastTime < message.timestamp { + if lastTime < message.createTime { let atMessage = AtMessageModel() - atMessage.messageId = message.messageId - atMessage.messageTime = NSNumber(value: message.timestamp) - atMeRecord.lastTime = NSNumber(value: message.timestamp) - atMeRecord.atMessages[message.messageId] = NSNumber(value: message.timestamp) + atMessage.messageId = message.messageClientId + atMessage.messageTime = NSNumber(value: message.createTime) + atMeRecord.lastTime = NSNumber(value: message.createTime) + atMeRecord.atMessages[message.messageClientId ?? ""] = NSNumber(value: message.createTime) atMeRecord.isRead = false - if let sessionId = message.session?.sessionId { - record[sessionId] = atMeRecord + if let conversationId = message.conversationId { + record[conversationId] = atMeRecord } } } else { let atMeRecord = AtMEMessageRecord() let atMessage = AtMessageModel() - atMessage.messageId = message.messageId - atMessage.messageTime = NSNumber(value: message.timestamp) - atMeRecord.lastTime = NSNumber(value: message.timestamp) - atMeRecord.atMessages[message.messageId] = NSNumber(value: message.timestamp) + atMessage.messageId = message.messageClientId + atMessage.messageTime = NSNumber(value: message.createTime) + atMeRecord.lastTime = NSNumber(value: message.createTime) + atMeRecord.atMessages[message.messageClientId ?? ""] = NSNumber(value: message.createTime) atMeRecord.isRead = false - if let sessionId = message.session?.sessionId { - record[sessionId] = atMeRecord + if let conversationId = message.conversationId { + record[conversationId] = atMeRecord } } } + /// 获取at消息变更通知 + /// - Parameter isCurrentThread: 是否在当前线程发送 private func atMessageChangeNoti(_ isCurrentThread: Bool = false) { if isCurrentThread == false { DispatchQueue.main.async { @@ -347,7 +359,95 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } } - open func onRecvMessages(_ messages: [NIMMessage]) { + /// 获取所有会话 + /// - Parameter conversations: 会话列表 + /// - Parameter offset: 偏移量 + /// - Parameter completion: 完成回调 + private func getAllConversation(_ conversations: inout [V2NIMConversation], _ offset: Int64 = 0, _ completion: @escaping (NSError?) -> Void) { + let limit = 20 + var temConversations = conversations + ConversationProvider.shared.getConversationList(offset, limit) { [weak self] result, error in + if let err = error { + completion(err) + } else { + if let datas = result?.conversationList { + temConversations.append(contentsOf: datas) + } + if result?.finished == false, let nextToken = result?.offset { + self?.getAllConversation(&temConversations, nextToken, completion) + } else { + completion(nil) + } + } + } + } + + /// 撤回消息回调 + /// - Parameter revokeNotifications: 撤回通知 + public func onMessageRevokeNotifications(_ revokeNotifications: [V2NIMMessageRevokeNotification]) { + var messageRefers = [V2NIMMessageRefer]() + for notification in revokeNotifications { + if let messageRefer = notification.messageRefer { + messageRefers.append(messageRefer) + } + } + if messageRefers.count > 0 { + removeRevokeAtMessage(messages: messageRefers) + } + } + + /// 移除at消息记录 + /// - Parameter messages: 消息列表 + open func removeRevokeAtMessage(messages: [V2NIMMessageRefer]) { + weak var weakSelf = self + workQueue.async { + weakSelf?.removeRevokeAtMessageInWorkqueue(messageRefers: messages) + } + } + + /// 遍历所有撤回消息判断是否要清除at消息标识(会发送通知通知会话列表) + /// - Parameter messageRefers: 消息索引列表 + private func removeRevokeAtMessageInWorkqueue(messageRefers: [V2NIMMessageRefer]) { + let currentAccid = IMKitClient.instance.account() + weak var weakSelf = self + var isAtMessageChange = false + var temDic = getMessageDic() + for messageRefer in messageRefers { + if messageRefer.senderId != currentAccid { + let removeRetResult = weakSelf?.removeRecordWithMessageref(messageRefer: messageRefer, record: &temDic) + if removeRetResult == true { + isAtMessageChange = true + } + } + } + if isAtMessageChange == true { + atMessageChangeNoti() + DispatchQueue.main.async { + weakSelf?.writeCacheToDocument(dictionary: temDic) + } + } + } + + /// 移除at记录(根据消息指针类,因为撤回的时候拿不到消息对象) + /// - Parameter messageRefer: 消息索引 + /// - Parameter record: at 消息记录缓存 + private func removeRecordWithMessageref(messageRefer: V2NIMMessageRefer, record: inout [String: AtMEMessageRecord]) -> Bool { + var didRemove = false + if let atMeRecord = record[messageRefer.conversationId ?? ""] { + if atMeRecord.atMessages[messageRefer.messageClientId ?? ""] != nil { + atMeRecord.atMessages.removeValue(forKey: messageRefer.messageClientId ?? "") + if atMeRecord.atMessages.count <= 0 { + atMeRecord.isRead = true + didRemove = true + } + } + } + return didRemove + } + + /// 收到消息回调 + /// - Parameter messages: 消息列表 + public func onReceiveMessages(_ messages: [V2NIMMessage]) { filterAtMessage(messages: messages) } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift index d772dd7c..54a514fa 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift @@ -12,50 +12,45 @@ open class ConversationListCell: NEBaseConversationListCell { super.setupSubviews() NSLayoutConstraint.activate([ - headImge.leftAnchor.constraint( + headImageView.leftAnchor.constraint( equalTo: contentView.leftAnchor, constant: NEConstant.screenInterval ), - headImge.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - headImge.widthAnchor.constraint(equalToConstant: 42), - headImge.heightAnchor.constraint(equalToConstant: 42), + headImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + headImageView.widthAnchor.constraint(equalToConstant: 42), + headImageView.heightAnchor.constraint(equalToConstant: 42), ]) NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - title.rightAnchor.constraint(equalTo: timeLabel.leftAnchor, constant: -5), - title.topAnchor.constraint(equalTo: headImge.topAnchor), + titleLabel.leftAnchor.constraint(equalTo: headImageView.rightAnchor, constant: 12), + titleLabel.rightAnchor.constraint(equalTo: timeLabel.leftAnchor, constant: -5), + titleLabel.topAnchor.constraint(equalTo: headImageView.topAnchor), ]) NSLayoutConstraint.activate([ - notifyMsg.rightAnchor.constraint(equalTo: timeLabel.rightAnchor), - notifyMsg.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 5), - notifyMsg.widthAnchor.constraint(equalToConstant: 13), - notifyMsg.heightAnchor.constraint(equalToConstant: 13), + notifyMsgView.rightAnchor.constraint(equalTo: timeLabel.rightAnchor), + notifyMsgView.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 5), + notifyMsgView.widthAnchor.constraint(equalToConstant: 13), + notifyMsgView.heightAnchor.constraint(equalToConstant: 13), ]) } override func initSubviewsLayout() { if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .rectangle { - headImge.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius + headImageView.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius } else if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .cycle { - headImge.layer.cornerRadius = 21.0 + headImageView.layer.cornerRadius = 21.0 } else { - headImge.layer.cornerRadius = 21.0 + headImageView.layer.cornerRadius = 21.0 } } - override open func configData(sessionModel: ConversationListModel?) { - super.configData(sessionModel: sessionModel) - - // backgroundColor - if let session = sessionModel?.recentSession?.session { - let isTop = topStickInfos[session] != nil - if isTop { - contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemStickTopBackground ?? UIColor(hexString: "0xF3F5F7") - } else { - contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemBackground ?? .white - } + override open func configureData(_ sessionModel: NEConversationListModel?) { + super.configureData(sessionModel) + if sessionModel?.conversation?.stickTop == true { + contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemStickTopBackground ?? UIColor(hexString: "0xF3F5F7") + } else { + contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemBackground ?? .white } } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift index 889f9713..0f072b91 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift @@ -10,6 +10,24 @@ import UIKit @objcMembers open class ConversationController: NEBaseConversationController { + /// 搜索按钮 + public lazy var searchBarButton: UIButton = { + let searchBarButton = UIButton() + searchBarButton.accessibilityIdentifier = "id.titleBarSearchImg" + searchBarButton.setImage(UIImage.ne_imageNamed(name: "chat_search"), for: .normal) + searchBarButton.addTarget(self, action: #selector(searchAction), for: .touchUpInside) + return searchBarButton + }() + + /// 添加按钮 + public lazy var addBarButton: UIButton = { + let addBarButton = UIButton() + addBarButton.accessibilityIdentifier = "id.titleBarMoreImg" + addBarButton.setImage(UIImage.ne_imageNamed(name: "chat_add"), for: .normal) + addBarButton.addTarget(self, action: #selector(didClickAddBtn), for: .touchUpInside) + return addBarButton + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nil, bundle: nil) className = "ConversationController" @@ -17,7 +35,7 @@ open class ConversationController: NEBaseConversationController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -27,18 +45,9 @@ open class ConversationController: NEBaseConversationController { override func initSystemNav() { super.initSystemNav() - let searchBarButton = UIButton() - searchBarButton.accessibilityIdentifier = "id.titleBarSearchImg" - searchBarButton.setImage(UIImage.ne_imageNamed(name: "chat_search"), for: .normal) - searchBarButton.addTarget(self, action: #selector(searchAction), for: .touchUpInside) - let searchBarItem = UIBarButtonItem(customView: searchBarButton) - let addBarButton = UIButton() - addBarButton.accessibilityIdentifier = "id.titleBarMoreImg" - addBarButton.setImage(UIImage.ne_imageNamed(name: "chat_add"), for: .normal) - addBarButton.addTarget(self, action: #selector(didClickAddBtn), for: .touchUpInside) + let searchBarItem = UIBarButtonItem(customView: searchBarButton) let addBarItem = UIBarButtonItem(customView: addBarButton) - let spaceBarItem = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) spaceBarItem.width = NEConstant.screenInterval diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift index 76d9c89a..7ec2bd04 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift @@ -11,13 +11,13 @@ open class SearchSessionHeaderView: SearchSessionBaseView { override open func setupUI() { super.setupUI() NSLayoutConstraint.activate([ - title.topAnchor.constraint(equalTo: contentView.topAnchor), - title.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), + titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), ]) NSLayoutConstraint.activate([ bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - bottomLine.leftAnchor.constraint(equalTo: title.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 1), ]) @@ -34,7 +34,7 @@ open class ConversationSearchController: NEBaseConversationSearchController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupSubviews() { diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift index 847b40ef..44fe8f72 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift @@ -4,55 +4,53 @@ // found in the LICENSE file. import Foundation +import NECommonKit import NIMSDK + open class NEMessageUtil { /// last message /// - Parameter message: message /// - Returns: result - open class func messageContent(message: NIMMessage) -> String { - var text = "" - switch message.messageType { - case .text: - if let messageText = message.text { - text = messageText - } - case .tip: + open class func messageContent(_ messageType: V2NIMMessageType, + _ text: String?, + _ attachment: V2NIMMessageAttachment?) -> String { + switch messageType { + case .MESSAGE_TYPE_TEXT: + return text ?? "" + case .MESSAGE_TYPE_TIP: return localizable("tip") - case .audio: - text = localizable("voice") - case .image: - text = localizable("picture") - case .video: - text = localizable("video") - case .location: - text = localizable("location") - case .notification: - text = localizable("notification") - case .file: - text = localizable("file") - case .custom: - text = contentOfCustomMessage(message: message) - case .rtcCallRecord: - let record = message.messageObject as? NIMRtcCallRecordObject - text = (record?.callType == .audio) ? localizable("internet_phone") : - localizable("video_chat") + case .MESSAGE_TYPE_AUDIO: + return localizable("voice") + case .MESSAGE_TYPE_IMAGE: + return localizable("picture") + case .MESSAGE_TYPE_VIDEO: + return localizable("video") + case .MESSAGE_TYPE_LOCATION: + return localizable("location") + case .MESSAGE_TYPE_NOTIFICATION: + return localizable("notification") + case .MESSAGE_TYPE_FILE: + return localizable("file") + case .MESSAGE_TYPE_CUSTOM: + return contentOfCustomMessage(attachment) + case .MESSAGE_TYPE_CALL: + if let attachment = attachment as? V2NIMMessageCallAttachment { + return attachment.type == 1 ? localizable("internet_phone") : localizable("video_chat") + } default: - text = localizable("unknown") + return localizable("unknown") } - - return text + return localizable("unknown") } /// 返回自定义消息的外显文案 - static func contentOfCustomMessage(message: NIMMessage?) -> String { - if message?.messageType == .custom, - let object = message?.messageObject as? NIMCustomObject, - let custom = object.attachment as? NECustomAttachment { - if custom.customType == customMultiForwardType { + static func contentOfCustomMessage(_ attachment: V2NIMMessageAttachment?) -> String { + if let customType = NECustomAttachment.typeOfCustomMessage(attachment) { + if customType == customMultiForwardType { return localizable("chat_history") } - if custom.customType == customRichTextType { - if let data = NECustomAttachment.dataOfCustomMessage(message: message), + if customType == customRichTextType { + if let data = NECustomAttachment.dataOfCustomMessage(attachment), let title = data["title"] as? String { return title } diff --git a/NEMapKit/NEMapKit.podspec b/NEMapKit/NEMapKit.podspec index 021417ed..e28a35fb 100644 --- a/NEMapKit/NEMapKit.podspec +++ b/NEMapKit/NEMapKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEMapKit' - s.version = '9.7.0' + s.version = '10.0.0-beta' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/Contents.json new file mode 100644 index 00000000..dc7b64a9 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "chat_loacaiton_img@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "chat_loacaiton_img@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/chat_loacaiton_img@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/chat_loacaiton_img@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5e1faa54caa8796c37f1732f3e8eb9283da3cdeb GIT binary patch literal 1069 zcmV+|1k(G7P)g5%xqL5LfbCn^^ zy0YSBrl()0Pft(!G$79}U%WbGpPS{!?DJ=@E`NMKIf>eDq{)_6*DhEr!IPp~uU7neFuuD}X6L6ZD}#NS=sf5I`6vtV3E40o zV)Kk43UE9)qYG#@CT(Q`^wwwvb7K;^6;1gYrhY|~l`ASMGcJ-*FFC3GW~Y53VI6jA z(uo6(nvZ;&XJ2V{9)K~^#1$t(#CbzjlOJ9y&uf4Q0E{dpks3mv1G#pvKQbh0n5c15 zi7HNdf8-Z9I)UC8j7p^f-=X+~L=(NcnZb9v+vO5p7MS(V(C{&mo~v#9r9pX)*rHiU z>3xX6=)_p~bOcCQ;xfPquK_U-Bu2^h9Enp=vZI|E$$k;qFN_03vf3fo7%2-5SYBPf zkg6R=OOovqp!Qs}BXA1)E@_*A6*^VyFm8OuK|5C70cWQtzg|qg;Z6}=yHxA|Qhd3y zd?%4r-PtU-%yCYPK`p|hD)w?pBt(XNM{Q31?=L|y<{t@lT n?i`#1o0s`A?_(8E}#G@Z@PN~Fv18UjF12bLqNVz$cP(} zcMzUYH>UtH^q~y@M(qZIs)NFMTgg)iP^(tTb+8PpiY=}ba)7vnh&hvSr0sj9VkWC>Thbcr>`X3P*)M!vUOCS9vPE|(kq;}(B@n}c>cFA!%~S^o#6>Jrq(pZn$aFaQQpYrG4N@iyOKY2Xn#h)}ppvpkuDJ~fkyx?C)y)FAS4n&H&(~ve&O{*{E9A~?6eXI` zocB!K%8jFgav2gJkz)C!!bCbPF8v9B+&+5M$S3o%U7M{iuTa z!G5U_CC^6y3Nf#g>Wo47B$cW~kwa@t71K?aA?XPkkWDoef1?NnFmgqv^|TQGd3{I9 zCzTLt?zk(S&?mBiFC+GADK#5L?TagKX+h*!x7%oq$tP8V{VTu#LO@RIR-=?krNvpO zQVg;LEz;!@$p-y=RQbI|+Mjm2nN$-ZJ>%Y7O@tl5b+4_2d>16+T9qty_gquEZGZ(S z@CR~9eOv}oc*6TO=v_)WJw{YG0|togiY{q1#bq#uV_PhBo$_6VHvgwEISQ-hy*3}#{Yo+T8Am4m z7!dPe?;{I)?g-9}lY^Lu?}{jUdqLn;grF~nx3U%{C8Vrs-7JK@4(nc1p~^r^7%0}; zGn8u7;|y^eNEC(W(7z?37N8GCj{vh zAjaco#IU`v384nz6U+w9;UUCU%mL}|D$hZl{}gsh@Fl z(1%9xsL{L-vJNp}6nVs@-u^%~3Q<(AM>P8AGctQa$)9_MwfW)&#f$6h;$wHS#akLH*x zdxXGN;}RTL!ke{1-aF%xT8oiLF}9l0=v)An!g4B8++o6m*6PeDArALwB^B1(j$4;7 z%W#9hbxIr7Vhj`nD~0}wQcktbkpd(HkJXE>LdjVe00Sx3txI@BlHvM2Bm|8uYTeL! zN?Z(_ws5HoH2QzD!kga=Oc-9~%`O-f6vu^e!L%P-$efybbwj@RJ@@I7A@}s#mw&`9 zaC$d2egT%^$Ae0#1_Q!MqItkEpGPF*lIa@}+a)_Lh*@qE)1D4O7&@$F(2i;^fR{bf z#lrIH#{5HLbQk2YDJchLR)0QV2^SaV4c#SyUGn+Mzpm(ckbJ)j(wX%ex!~Vm307+! z{Vo~Bl3k^;-EQ@v6vhS@?-p{@?%`g^8e!-#PBi0!5ALxhzIN}G zxmwl45|s&%+x$Db;8)}B;XV`$&0-AqURk(Ty8V%VgZ9;AWe9&<@nDVagK+N%=)yL& z2Vtm+F+N^=IG=j)iamqtB-gBzZE7E3gb_xthA%1^dP*!dDz*Rs002ovPDHLkV1f>e B>KXt5 literal 0 HcmV?d00001 diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/Contents.json new file mode 100644 index 00000000..aaaf8d3c --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "chat_map_back@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "chat_map_back@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/chat_map_back@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/chat_map_back@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..8d8cc916d2f7b7e2e4149b35cbfe3d426ed4272c GIT binary patch literal 668 zcmV;N0%QG&P)-R0U`YPf`=0F+52{fY$ItT!7~AL{xzG@I*|&F@z^#=K(y(D3{AS?R9P8iP#Eo z2N>u3_cogiw88Fh8v=x3_}y$acRJ3kR;yMl7MJ7kcn&3q31G-lskGECK}-NS1_`19 zh;|9$0*Gb_sR$riC8Q>RXq1qu0HRGo>H>%+30?#cEfTy5AQ~ii6+nDT@GgM(l%T3T z-tYGtola-x3_-FrPE>V=c~-zPgXBHQ);LkI1(L+upbINeafv z1;9AD02n71fW1LNW6UXF2D_LSFyXrZw*dQi7qA6KXs7Xy-~`b)sTJZuYQ&?nKdz>T z#!0ij68OzOVQ+Xk348i&MDv7MnJMHppKa?i`QR7mBU4oPaemhT0000cSYDJ;vjNMpIANU=y1 zq;qEqBxinh9a+F)OBkQaoKHH!o?V*%+smwXcBe$IvleP6R5es3FK0IQq3%Lm=QS)^ zu~g8GpubQSyz_Pjp<48Uh?fLli%%%q`klpry`mNBYPzxfWThljJ=7{Wiw2q2^3Um{ zaasVj{5m=BO_j2};TB(3@vuvuN)^H9$ z_%(7kc)%X8Ne&lP2v{eFiwXqz$(N7H3OTH}0A^eOGcJG`7r=}QV8#V7;{up*0n9`r z;Oy+|^Jp~sin_eKY>*J}YiL5o>2x}W`}_Mlt2wn=ZDVU|i_Xu_Z-^Q5djP`U+uM6~ zzvJNGV21=?T0l|wk}xY^R`~69dq4s(DPUIky)`{j0%8ash!hY@_&}V1n8F951jH6T5F;SQ@PP;c7$_BE_&|h!BuSzS zABevY8;{3i84&_5E-ro-EyJM+=La&12mwb&M=cy`ik4fJAm$K+<9v(~M4ba-lpyXT z9Ge7@y`Y#Ri0y{PB0=CjviU0rr literal 0 HcmV?d00001 diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/Contents.json new file mode 100644 index 00000000..f16da078 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "map_empty@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "map_empty@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/map_empty@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/map_empty@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4f2276acad5c74d726045074368aebf991be4df7 GIT binary patch literal 7683 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 c?EC|CS(`l<@9*wE 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 literal 0 HcmV?d00001 diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/Contents.json new file mode 100644 index 00000000..30781b52 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "chat_map_path@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "chat_map_path@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..6df8b22cb367c32f35b4d8d5b3271ed56189d5fc GIT binary patch literal 1701 zcmV;W23q-vP)%Ooh&V;4kb7qAbU=gCKxw(72=*CVP>_jTF{Fv9Qrezj20AMh zDeOcwO)gEKLlDi7v7=kQ?;eVDzLW0Gw|A?@Z$@*{>1F-=?e4d)6DW{cLsqbWx*<>% z20nm|s-*k;oPc}ukwBZyS#+lj1D2F{Zrf<0gH~%#v{4WOInomb)Jei>P*Pf?E4FD% z9Fx#yoWI>NA;VHh&PO^JhS9oiu0e=-P=35#ymL*Yn3kvyuKxK9nW**-3FGI+#BE5Q6f8` zf#OEU`8VX9F#Ga?!d}pn&{tIJ{8`vsTO{r7zgX&@v>J#&W6qiMsUioWhd|QB~o7Kr!?o^Eu5WR!R%5 zGe@B6;=8>TU!G5tMDrH3K%!3|wJaY)C4Jdu9CJiWl9RSA7zXgEbRTa zflny;y0J96Yj^io;NfE^V{8PnxX*6V?IZVoT=?}}Xd_Z*rLChvaJm8TONd$$8h^$2nh=sbN4sp4MA#X zB&%t3%VWbQi{3UL^wK0?w?m-h& zj^Sm3SA3bCz*AE+p+PYYE-um#>#Xl$76a=8C9scz(Oe$)K6d&a4+yl~VysPLq9N9N zX)t3M5a=Y;1xI1$KWGc3dp|!m2RGbYTBYwrp@kgb!2%u-<>JfTH%+llcZC@|8M@VL zwny$VRQKHuyIe}-Ub*FMc|xs99W$8Yvu{P{8p_DMa&rPStT}5?i1wTjePxnJfm~Sz zo9tdxhK>DEmx&s=W4&v@q=W9g)6Aj9p+!B8#;Dhg6v(+D z)d9S;+rso60{$XQP{YCvMjCnEd(*SLB{|L6z&Jyx%3>L2(@b~e-zy1ZynZuZoM+rty0!&% z6i$a+6DAH<7RY$@BL5+)7z1d??$Nw~^)y8`NG1Y(ylCWI?^WoD(>>MhBD6tnY)O;; z%e?=%3O(S<$nP)ESZCV0E(#?A87|i6UDOMXNM+)DlnLx-*_3P4+VOar(p~W$&Kx&S z(|R+FO36i_Pu-N2m%LHgmC1XQtH=Ovo%a!CwarDKsVdsS$W>qv7TN)7w(VZ8M!G_I v|1vK*9!QINE64;a;Wh>>QG%7Xups{f?xJji*U}Dr00000NkvXXu0mjfTR$+V literal 0 HcmV?d00001 diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8709b11195756ef9a06169e9090150b5fceb489f GIT binary patch literal 2558 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 LNkvXXu0mjfK50Cp literal 0 HcmV?d00001 diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/map_select_normal@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/map_select_normal@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8fd18fdfdce56931e15b902acebef6e27c576628 GIT binary patch literal 40017 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 literal 0 HcmV?d00001 diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/Contents.json new file mode 100644 index 00000000..d3b4787a --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "map_reset_select@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "map_reset_select@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..64e312625b80bb1dd53501c7018aca41e573f8e9 GIT binary patch literal 18912 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 literal 0 HcmV?d00001 diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..0862b4e51de7be72e00b4be7eebe851b59a639f0 GIT binary patch literal 39874 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Ν0qJI}A{F^8sx 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 currentIndex { @@ -446,18 +459,19 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } if let m = model { + // 地理位置回调 navigationController?.popViewController(animated: false) DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: DispatchWorkItem(block: { - if let block = self.completion { - block(m) + if let params = m.yx_modelToJSONObject() as? [String: Any] { + Router.shared.use(NERouterUrl.LocationSearchResult, parameters: params) } })) } else { - showToast(chatLocalizable("no_location")) + showToast(mapLocalizable("no_location")) } } - func searchTextFieldChange(textfield: SearchTextField) { + open func searchTextFieldChange(textfield: SearchTextField) { guard let searchText = textfield.text else { return } @@ -471,54 +485,56 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe if textRange == nil || ((textRange?.isEmpty) == nil) { weak var weakSelf = self if let text = textfield.text { - NEChatKitClient.instance.delegate?.searchPosition?(key: text, completion: { models, error in + NEMapClient.shared().searchPosition(withKey: text) { models, error in + weakSelf?.loadModels(models: models) - }) + } } } if searchText.count <= 0 { if let map = mapView { - NEChatKitClient.instance.delegate?.setMapCenter?(mapview: map) + NEMapClient.shared().setMapCenterWithMapview(map) } toSearchLocalWithMapView() } } - func toSearchLocalWithMapView() { + open func toSearchLocalWithMapView() { guard let map = mapView else { return } weak var weakSelf = self - NEChatKitClient.instance.delegate?.searchMapCenter?(mapview: map, completion: { models, error in + NEMapClient.shared().searchMapCenter(withMapview: map) { models, error in if let text = weakSelf?.searchTextField.text, text.count > 0 { return } + weakSelf?.resetButton.isSelected = false weakSelf?.loadModels(models: models) - }) + } } - func toSearchCurrentUserLocation() { + open func toSearchCurrentUserLocation() { weak var weakSelf = self - let className = className() - NEChatKitClient.instance.delegate?.searchRoundPosition?(completion: { models, error in - NELog.infoLog(className, desc: "toSearchCurrentUserLocation end : \(models) error: \(error?.localizedDescription ?? "") current text input : \(weakSelf?.searchTextField.text ?? "")") + NEMapClient.shared().searchRoundPosition { models, error in if let text = weakSelf?.searchTextField.text, text.count > 0 { return } + weakSelf?.resetButton.isSelected = false weakSelf?.loadModels(models: models) - }) + } } // MARK: NEMapGuideBottomViewDelegate + // 点击跳转三方应用 open func didClickGuide() { - showBottomSelectAlert(firstContent: chatLocalizable("gaode_map"), secondContent: chatLocalizable("tencent_map")) { value in + showBottomSelectAlert(firstContent: mapLocalizable("gaode_map"), secondContent: mapLocalizable("tencent_map")) { value in if value == 0 { if let gaodeApp = URL(string: "iosamap://") { + // 高德 if UIApplication.shared.canOpenURL(gaodeApp) == true { if let url = "iosamap://viewMap?sourceApplication=yunxin_im&backScheme=im_uikit&poiname=\(self.locationTitle ?? "")&lat=\(self.currentPoint.x)&lon=\(self.currentPoint.y)&dev=1".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - print("jump url : ", url) if let jumpUrl = URL(string: url) { if #available(iOS 10.0, *) { UIApplication.shared.open(jumpUrl) @@ -528,7 +544,7 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } } - } else if let url = URL(string: "https://itunes.apple.com/us/app/gao-tu-zhuan-ye-shou-ji-tu/id461703208?mt=8") { + } else if let url = URL(string: aMapDownloadUrl) { if #available(iOS 10.0, *) { UIApplication.shared.open(url) } else { @@ -537,10 +553,10 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } } } else if value == 1 { + // 腾讯 if let gaodeApp = URL(string: "qqmap://") { if UIApplication.shared.canOpenURL(gaodeApp) == true { if let url = "qqmap://map/marker?marker=coord:\(self.currentPoint.x),\(self.currentPoint.y);title:\(self.locationTitle ?? "")".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - print("jump url : ", url) if let jumpUrl = URL(string: url) { if #available(iOS 10.0, *) { UIApplication.shared.open(jumpUrl) @@ -550,7 +566,7 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } } - } else if let url = URL(string: "https://apps.apple.com/cn/app/%E8%85%BE%E8%AE%AF%E5%9C%B0%E5%9B%BE-%E8%B7%AF%E7%BA%BF%E8%A7%84%E5%88%92-%E5%AF%BC%E8%88%AA%E6%89%93%E8%BD%A6%E5%87%BA%E8%A1%8C/id481623196") { + } else if let url = URL(string: tencentMapDownloadUrl) { if #available(iOS 10.0, *) { UIApplication.shared.open(url) } else { @@ -562,18 +578,28 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } } - open func loadModels(models: [ChatLocaitonModel]) { + open func loadModels(models: [NELocaitonModel]) { currentIndex = 0 locations.removeAll() if let keyword = searchTextField.text, keyword.count > 0 { - models.forEach { model in + for model in models { model.attribute = model.title.highlight(keyWords: keyword, highlightColor: UIColor.ne_blueText) } } else { - models.forEach { model in + for model in models { model.attribute = NSMutableAttributedString(string: model.title) } } + if models.count > currentIndex { + let model = models[currentIndex] + if let map = mapView { + NEMapClient.shared().setMapviewLocationWithLat(model.lat, lng: model.lng, mapview: map) + } + if searchTextField.text?.count ?? 0 > 0 { + resetButton.isSelected = true + } + } + locations.append(contentsOf: models) tableView.reloadData() if models.count > 0 { @@ -594,7 +620,7 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } } -extension NEDetailMapController: UITableViewDelegate, UITableViewDataSource { +extension NELocationViewController: UITableViewDelegate, UITableViewDataSource { open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { locations.count } @@ -602,26 +628,29 @@ extension NEDetailMapController: UITableViewDelegate, UITableViewDataSource { open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( - withIdentifier: reuseId, + withIdentifier: NELocationAddressCell.className(), for: indexPath - ) as! NEMapAddressCell + ) as! NELocationAddressCell let model = locations[indexPath.row] cell.configure(model, currentIndex == indexPath.row) return cell } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let preiousCell = tableView.cellForRow(at: IndexPath(row: currentIndex, section: 0)) as? NEMapAddressCell { - preiousCell.selectImg.isHidden = true + if let preiousCell = tableView.cellForRow(at: IndexPath(row: currentIndex, section: 0)) as? NELocationAddressCell { + preiousCell.selectImgView.isHidden = true } - if let currentCell = tableView.cellForRow(at: indexPath) as? NEMapAddressCell { - currentCell.selectImg.isHidden = false + if let currentCell = tableView.cellForRow(at: indexPath) as? NELocationAddressCell { + currentCell.selectImgView.isHidden = false } let model = locations[indexPath.row] if let map = mapView { - NEChatKitClient.instance.delegate?.setMapviewLocation?(lat: model.lat, lng: model.lng, mapview: map) + NEMapClient.shared().setMapviewLocationWithLat(model.lat, lng: model.lng, mapview: map) } currentIndex = indexPath.row + if indexPath.row != 0 { + resetButton.isSelected = true + } refreshCurrentCache() } } diff --git a/NEMapKit/NEMapKit/Classes/NELocaitonModel.h b/NEMapKit/NEMapKit/Classes/NELocaitonModel.h new file mode 100644 index 00000000..ac24978e --- /dev/null +++ b/NEMapKit/NEMapKit/Classes/NELocaitonModel.h @@ -0,0 +1,21 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NELocaitonModel : NSObject + +@property(nonatomic, strong) NSString *title; +@property(nonatomic, strong) NSString *address; +@property(nonatomic, strong) NSString *city; +@property(nonatomic, assign) CGFloat lat; +@property(nonatomic, assign) CGFloat lng; +@property(nonatomic, assign) NSInteger distance; +@property(nonatomic, strong) NSMutableAttributedString *attribute; + +@end + +NS_ASSUME_NONNULL_END diff --git a/NEMapKit/NEMapKit/Classes/NELocaitonModel.m b/NEMapKit/NEMapKit/Classes/NELocaitonModel.m new file mode 100644 index 00000000..7c62edb2 --- /dev/null +++ b/NEMapKit/NEMapKit/Classes/NELocaitonModel.m @@ -0,0 +1,9 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +#import "NELocaitonModel.h" + +@implementation NELocaitonModel + +@end diff --git a/NEMapKit/NEMapKit/Classes/NEMapClient.h b/NEMapKit/NEMapKit/Classes/NEMapClient.h index b07b0040..9618534c 100644 --- a/NEMapKit/NEMapKit/Classes/NEMapClient.h +++ b/NEMapKit/NEMapKit/Classes/NEMapClient.h @@ -3,6 +3,7 @@ // found in the LICENSE file. #import +#import "NELocaitonModel.h" NS_ASSUME_NONNULL_BEGIN @@ -10,7 +11,62 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)shared; -- (void)setupMapClientWithAppkey:(NSString *)appkey; +/// 设置插件初始化 +/// @param appkey appkey +- (void)setupMapClientWithAppkey:(NSString *)appkey + __attribute__((deprecated("- (void)setupMapClientWithAppkey:(NSString *)appkey " + "withServerKey:(NSString *)serverKey instead"))); + +/// 设置插件初始化 +/// @param appkey appkey +/// @param serverKey serverKey +- (void)setupMapClientWithAppkey:(NSString *)appkey withServerKey:(NSString *)serverKey; + +/// 定位地理位置中心 +/// @param mapview 高德地图View +- (void)setMapCenterWithMapview:(id)mapview; + +/// 根据关键字搜索地理位置 +/// @param key 地理位置关键字 +/// @param completion 搜索结果回调 +- (void)searchPositionWithKey:(NSString *)key + completion:(void (^)(NSArray *_Nonnull, + NSError *_Nullable))completion; + +/// 搜索当前地图中心位置附近地理位置 +/// @param mapview 地图 view +- (void)searchMapCenterWithMapview:(id)mapview + completion:(void (^)(NSArray *_Nonnull, + NSError *_Nullable))completion; + +/// 设置搜索地图中心位置回调(当用户拖动地图时需要不断回调) +/// @param completion 回调 +- (void)searchRoundPositionWithCompletion:(void (^)(NSArray *_Nonnull, + NSError *_Nullable))completion; + +/// 设置地图定位到某个为止 +/// @param lat 维度 +/// @param lng 精度 +/// @param mapview 地图视图控件 +- (void)setMapviewLocationWithLat:(double)lat lng:(double)lng mapview:(id)mapview; + +/// 获取地图控件(缺省参数) +/// @return mapview 地图视图 +- (id)getMapView; + +/// 设置地图默认参数(地图缺省参数) +/// @param mapType 预留扩展,暂时无用 +- (void)setupMapControllerWithMapType:(NSInteger)mapType; + +/// 位置移动回调 +/// @param completion 回到block +- (void)didmoveMapWithCompletion:(void (^)(void))completion; + +/// 设置地图自定义定位图标 +/// @param image 自定义图片 +/// @param lat 纬度 +/// @param lng 精度 +- (void)setCustomAnnotationWithImage:(nullable UIImage *)image lat:(double)lat lng:(double)lng; @end diff --git a/NEMapKit/NEMapKit/Classes/NEMapClient.m b/NEMapKit/NEMapKit/Classes/NEMapClient.m index f4c06c41..17dab3c1 100644 --- a/NEMapKit/NEMapKit/Classes/NEMapClient.m +++ b/NEMapKit/NEMapKit/Classes/NEMapClient.m @@ -12,14 +12,14 @@ #import #import -typedef void (^SearchCompletion)(NSArray *, NSError *); +#import +#import + +typedef void (^SearchCompletion)(NSArray *, NSError *); typedef void (^MapMoveCompletion)(void); -@interface NEMapClient () +@interface NEMapClient () @property(nonatomic, strong) MAMapView *mapView; @@ -39,6 +39,8 @@ @interface NEMapClient () *_Nonnull param) { + NSObject *param1 = [param objectForKey:@"nav"]; + NSInteger type = NEMapTypeDetail; + NSNumber *typeValue = [param objectForKey:@"type"]; + if (typeValue != nil && ![typeValue isKindOfClass:[NSNull class]]) { + type = typeValue.integerValue; + } + if ([param1 isKindOfClass:[UINavigationController class]]) { + UINavigationController *nav = (UINavigationController *)param1; + NELocationViewController *controller = + [[NELocationViewController alloc] initWithType:type]; + if (type == NEMapTypeDetail) { + double lat = 0; + double lng = 0; + NSNumber *latValue = param[@"lat"]; + if (latValue != nil && ![latValue isKindOfClass:NSNull.class]) { + lat = latValue.doubleValue; + } + NSNumber *lngValue = param[@"lng"]; + if (lngValue != nil && ![lngValue isKindOfClass:NSNull.class]) { + lng = lngValue.doubleValue; + } + NSString *title = param[@"locationTitle"]; + NSString *subTitle = param[@"subTitle"]; + controller.currentPoint = CGPointMake(lat, lng); + controller.locationTitle = title; + controller.subTitle = subTitle; + } + + [nav pushViewController:controller animated:YES]; + } + }]; +} + - (void)setupMapClientWithAppkey:(NSString *)appkey { [[NEChatKitClient instance] addMapDelegate:self]; [self setupMapSdkConfigWithAppkey:appkey]; @@ -98,7 +138,7 @@ - (void)setCustomAnnotationWithImage:(UIImage *)image lat:(double)lat lng:(doubl } - (void)searchPositionWithKey:(NSString *)key - completion:(void (^)(NSArray *_Nonnull, + completion:(void (^)(NSArray *_Nonnull, NSError *_Nullable))completion { if (key.length <= 0) { return; @@ -154,8 +194,6 @@ - (id)getCellMapView { mapView.showsCompass = NO; // 隐藏比例尺 mapView.showsScale = NO; - // mapView.maxZoomLevel = 5; - // mapView.showsUserLocation = YES; mapView.userTrackingMode = MAUserTrackingModeNone; mapView.zoomEnabled = NO; @@ -177,7 +215,7 @@ - (void)setMapCenterWithMapview:(id)mapview { } } -- (void)searchRoundPositionWithCompletion:(void (^)(NSArray *_Nonnull, +- (void)searchRoundPositionWithCompletion:(void (^)(NSArray *_Nonnull, NSError *_Nullable))completion { self.searchRoundBlock = completion; self.needSearchRound = YES; @@ -193,7 +231,7 @@ - (void)searchRoundPositionWithLat:(double)lat lng:(double)lng { } - (void)searchMapCenterWithMapview:(id)mapview - completion:(void (^)(NSArray *_Nonnull, + completion:(void (^)(NSArray *_Nonnull, NSError *_Nullable))completion { if ([mapview isKindOfClass:[MAMapView class]]) { self.searchRoundBlock = completion; @@ -228,9 +266,9 @@ - (void)onGeocodeSearchDone:(AMapGeocodeSearchRequest *)request } - (void)parseAndPassWithData:(NSArray *)datas { - NSMutableArray *mutaData = [[NSMutableArray alloc] init]; + NSMutableArray *mutaData = [[NSMutableArray alloc] init]; for (AMapPOI *poi in datas) { - ChatLocaitonModel *model = [[ChatLocaitonModel alloc] init]; + NELocaitonModel *model = [[NELocaitonModel alloc] init]; [mutaData addObject:model]; model.title = poi.name; model.address = poi.address; @@ -252,7 +290,6 @@ - (void)parseAndPassWithData:(NSArray *)datas { - (void)mapView:(MAMapView *)mapView didUpdateUserLocation:(MAUserLocation *)userLocation updatingLocation:(BOOL)updatingLocation { - // NSLog(@"didUpdateUserLocation : %d", updatingLocation); if (updatingLocation && self.needSearchRound) { AMapReGeocodeSearchRequest *regeoRequest = [[AMapReGeocodeSearchRequest alloc] init]; regeoRequest.location = [AMapGeoPoint locationWithLatitude:userLocation.coordinate.latitude @@ -299,4 +336,12 @@ - (MAAnnotationView *)mapView:(MAMapView *)mapView viewForAnnotation:(id String { + mapCoreLoader.localizable(key) +} + +let tencentMapDownloadUrl = "https://apps.apple.com/cn/app/%E8%85%BE%E8%AE%AF%E5%9C%B0%E5%9B%BE-%E8%B7%AF%E7%BA%BF%E8%A7%84%E5%88%92-%E5%AF%BC%E8%88%AA%E6%89%93%E8%BD%A6%E5%87%BA%E8%A1%8C/id481623196" + +let aMapDownloadUrl = "https://itunes.apple.com/us/app/gao-tu-zhuan-ye-shou-ji-tu/id461703208?mt=8" + +public class MapCoreLoader: NSObject { + public var bundle: Bundle? + + override public init() { + super.init() + if let bundleURL = Bundle.main.url(forResource: "NEMapKit", withExtension: "bundle") { + bundle = Bundle(url: bundleURL) + } + } + + public func localizable(_ key: String) -> String { + let value = NEChatUIKitClient.instance.getLanguage(key: key) ?? "" + return value + } + + public func loadImage(_ name: String) -> UIImage? { + let image = NEChatUIKitClient.instance.getImageSource(imageName: name) + return image + } +} diff --git a/NEMapKit/NEMapKit/Classes/View/NELocationAddressCell.swift b/NEMapKit/NEMapKit/Classes/View/NELocationAddressCell.swift new file mode 100644 index 00000000..e8db469b --- /dev/null +++ b/NEMapKit/NEMapKit/Classes/View/NELocationAddressCell.swift @@ -0,0 +1,123 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import UIKit + +@objcMembers +open class NELocationAddressCell: UITableViewCell { + /// 位置指示图片 + public lazy var locationImgView: UIImageView = { + let locationImageView = UIImageView(image: mapCoreLoader.loadImage("chat_loacaiton_img")) + locationImageView.translatesAutoresizingMaskIntoConstraints = false + return locationImageView + }() + + /// 选中图片 + public lazy var selectImgView: UIImageView = { + let imgView = UIImageView(image: mapCoreLoader.loadImage("chat_map_select")) + imgView.translatesAutoresizingMaskIntoConstraints = false + return imgView + }() + + /// 位置主标题 + public lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor.ne_darkText + label.font = UIFont.systemFont(ofSize: 16) + label.text = "" + return label + }() + + /// 位子副标题 + public lazy var subTitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor.ne_emptyTitleColor + label.font = UIFont.systemFont(ofSize: 14) + label.text = "" + return label + }() + + /// 分割线视图 + public lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.ne_navLineColor + return view + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + setupSubviews() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func setupSubviews() { + selectionStyle = .none + contentView.addSubview(locationImgView) + contentView.addSubview(selectImgView) + contentView.addSubview(titleLabel) + contentView.addSubview(subTitleLabel) + contentView.addSubview(bottomLine) + + NSLayoutConstraint.activate([ + locationImgView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 15), + locationImgView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 17), + locationImgView.heightAnchor.constraint(equalToConstant: 18), + locationImgView.widthAnchor.constraint(equalToConstant: 18), + ]) + + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: locationImgView.rightAnchor, constant: 7), + titleLabel.centerYAnchor.constraint(equalTo: locationImgView.centerYAnchor), + titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -70), + ]) + + NSLayoutConstraint.activate([ + subTitleLabel.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), + subTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), + subTitleLabel.rightAnchor.constraint(equalTo: titleLabel.rightAnchor), + ]) + + NSLayoutConstraint.activate([ + selectImgView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -13), + selectImgView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + ]) + + NSLayoutConstraint.activate([ + bottomLine.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 12), + bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -12), + bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -1), + bottomLine.heightAnchor.constraint(equalToConstant: 1), + ]) + } + + func configure(_ model: NELocaitonModel, _ select: Bool) { + titleLabel.attributedText = model.attribute + var distanceStr = "" + if model.distance > 0 { + if model.distance <= 1000 { + distanceStr = "\(model.distance)m" + } else { + let kilometer = model.distance / 1000 + distanceStr = "\(kilometer)km" + } + subTitleLabel.text = "\(distanceStr)\(mapLocalizable("distance_inner"))|\(model.address)" + } else { + subTitleLabel.text = model.address + } + + if select == true { + selectImgView.isHidden = false + } else { + selectImgView.isHidden = true + } + } +} diff --git a/NEMapKit/NEMapKit/Classes/View/NELocationGuideBottomView.swift b/NEMapKit/NEMapKit/Classes/View/NELocationGuideBottomView.swift new file mode 100644 index 00000000..9d898612 --- /dev/null +++ b/NEMapKit/NEMapKit/Classes/View/NELocationGuideBottomView.swift @@ -0,0 +1,86 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objc +public protocol NELocationBottomViewDelegate: NSObjectProtocol { + func didClickGuide() +} + +@objcMembers +open class NELocationGuideBottomView: UIView { + public weak var delegate: NELocationBottomViewDelegate? + + /// 引导按钮 + lazy var guideButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(mapCoreLoader.loadImage("chat_map_path"), for: .normal) + button.setImage(mapCoreLoader.loadImage("chat_map_path"), for: .highlighted) + button.addTarget(self, action: #selector(guideBtnClick), for: .touchUpInside) + return button + }() + + /// 位置标题 + lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 16) + label.textColor = UIColor.ne_darkText + label.text = "" + return label + }() + + /// 位置副标题 + lazy var subtitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 14) + label.textColor = UIColor.ne_emptyTitleColor + label.text = "" + return label + }() + + override public init(frame: CGRect) { + super.init(frame: frame) + setupSubviews() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func setupSubviews() { + backgroundColor = .white + addSubview(guideButton) + addSubview(titleLabel) + addSubview(subtitleLabel) + + NSLayoutConstraint.activate([ + guideButton.topAnchor.constraint(equalTo: topAnchor, constant: 16), + guideButton.rightAnchor.constraint(equalTo: rightAnchor, constant: -12), + guideButton.widthAnchor.constraint(equalToConstant: 40), + guideButton.heightAnchor.constraint(equalToConstant: 40), + ]) + + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 12), + titleLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -52), + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16), + ]) + + NSLayoutConstraint.activate([ + subtitleLabel.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), + subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), + subtitleLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -52), + ]) + } + + func guideBtnClick() { + if let delegate = delegate { + delegate.didClickGuide() + } + } +} diff --git a/NERtcCallUIKit/NERtcCallUIKit.podspec b/NERtcCallUIKit/NERtcCallUIKit.podspec index 3bef760f..1f25fc58 100644 --- a/NERtcCallUIKit/NERtcCallUIKit.podspec +++ b/NERtcCallUIKit/NERtcCallUIKit.podspec @@ -8,16 +8,16 @@ Pod::Spec.new do |s| s.name = 'NERtcCallUIKit' - s.version = '2.2.0' + s.version = '2.4.0' s.summary = 'Netease XKit' s.homepage = 'http://netease.im' s.license = { :'type' => "Copyright", :'text' => " Copyright 2022 Netease " } s.author = 'yunxin engineering department' - s.ios.deployment_target = '10.0' + s.ios.deployment_target = '11.0' s.source = { :http => "" } s.source_files = 'NERtcCallUIKit/Classes/**/*' s.resource = 'NERtcCallUIKit/Assets/**/*' - s.dependency 'NERtcCallKit/NOS_Special','2.2.0' + s.dependency 'NERtcCallKit/NOS_Special','2.4.0' s.dependency 'SDWebImage' s.dependency 'NECoreKit' s.dependency 'NECommonKit' diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_connecting_en.mp3 b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_connecting_en.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..d78ec8e8c42e9687a5d614e2e11df1ace71caac0 GIT binary patch literal 13968 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 L;cxsO`^tX-Pb)uT literal 0 HcmV?d00001 diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_no_response_en.mp3 b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_no_response_en.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..4e64ff76778367d9243bf7bec6908b8ed425c292 GIT binary patch literal 33840 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 literal 0 HcmV?d00001 diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_busy_en.mp3 b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_busy_en.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..be2b8d29f27bd84c9b24c8aaf7a060be05f1ffda GIT binary patch literal 25920 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 a;Qv4W;(sZ3uTQ}LaX!fZy6OMnz<&Y98+(xe literal 0 HcmV?d00001 diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_reject_en.mp3 b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_reject_en.mp3 new file mode 100644 index 0000000000000000000000000000000000000000..eb8bfa49de8ea67916326772b19560eca62ca72e GIT binary patch literal 27216 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

~A-k_#nrGxD!tc5&suQk(fv-b+di=HRNFZ;hr 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 literal 0 HcmV?d00001 diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/en.lproj/Localizable.strings b/NERtcCallUIKit/NERtcCallUIKit/Assets/en.lproj/Localizable.strings index c684d74d..eb883c1f 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Assets/en.lproj/Localizable.strings +++ b/NERtcCallUIKit/NERtcCallUIKit/Assets/en.lproj/Localizable.strings @@ -4,40 +4,40 @@ // found in the LICENSE file. -"switch_to_audio"="Switch to audio"; -"switch_to_video"="Switch to video"; +"switch_to_audio"="Switch to Voice"; +"switch_to_video"="Switch to Video"; "accept_failed"="Answer Failed"; -"network_error"="Network error, try later"; -"switch_error"="Switch Error"; -"remote_cancel"="Cancelled by the opposite"; -"remote_busy"="Busy-line"; -"remote_timeout"="No Responding"; -"remote_reject"="Reject"; -"other_client_accept"="Accept by other client"; -"other_client_reject"="Reject by other client"; +"network_error"="Network exception, please try again later"; +"switch_error"="Failed to switch"; +"remote_cancel"="The call was canceled"; +"remote_busy"="The subscriber you dialed is busy"; +"remote_timeout"="Timeout and no response"; +"remote_reject"="The user has declined your call invitation"; +"other_client_accept"="The other client has answered the call"; +"other_client_reject"="The other client has rejected the call"; "permission"="Permission Require"; "reject"="Reject"; "agree"="Agree"; -"audio_to_video"="Opposite requests to switch to video, have to turn on your camera"; -"video_to_audio"="Opposite requests to switch to audio, will turn off your camera directly"; -"reject_tip"="Request Denied"; +"audio_to_video"="A request to convert video to audio will turn off your camera directly"; +"The other party requests to convert audio to video, which requires turning on your camera"; +"reject_tip"="The invitation was rejected."; -"invite_audio_call"="Invite for audio call"; -"invite_video_call"="Invite for video call"; +"invite_audio_call"="Invite you to voice call..."; +"invite_video_call"="Invite you to video call..."; -"waitting_remote_response"="Waiting for response"; +"waitting_remote_response"="Awaiting response..."; "call_cancel"="Cancel"; "call_reject"="Reject"; "call_accept"="Accept"; -"call_micro_phone"="Microphone"; +"call_micro_phone"="Mic"; "call_speaker"="Speaker"; -"waitting_remote_accept"="Waitting to connect"; +"waitting_remote_accept"="Waiting for the call..."; "calling"="Calling"; "cancel_failed"="The invitation has been accepted and cannot be canceled"; "operation_failed"="operation failed"; -"device_not_support"="The current device does not support virtualization"; +"device_not_support"="This device does not support the blurring feature"; -"connecting"="Connecting…"; +"connecting"="Connecting..."; diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NERtcCallUIKit/NERtcCallUIKit/Assets/zh-Hans.lproj/Localizable.strings index a34ae8e3..7487f0ff 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NERtcCallUIKit/NERtcCallUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -11,19 +11,19 @@ "remote_cancel"="对方取消"; "remote_busy"="对方占线"; "remote_timeout"="对方超时未响应"; -"remote_reject"="对方已拒绝"; -"other_client_accept"="已被其他端接受"; -"other_client_reject"="已被其他端拒绝"; +"remote_reject"="对方已经拒绝"; +"other_client_accept"="其他端已经接听"; +"other_client_reject"="其他端已经拒绝"; "permission"="权限请求"; "reject"="拒绝"; "agree"="同意"; "audio_to_video"="对方请求将音频转为视频,需要打开您的摄像头。"; "video_to_audio"="对方请求将视频转为音频,将直接关闭您的摄像头。"; -"reject_tip"="对方拒绝了您请求"; +"reject_tip"="对方拒绝了您的请求"; -"invite_audio_call"="邀请您音频通话"; -"invite_video_call"="邀请您视频通话"; +"invite_audio_call"="邀请您音频通话..."; +"invite_video_call"="邀请您视频通话..."; "waitting_remote_response"="正在等待对方响应..."; "call_cancel"="取消"; @@ -31,12 +31,12 @@ "call_accept"="接听"; "call_micro_phone"="麦克风"; "call_speaker"="扬声器"; -"waitting_remote_accept"="等待对方接听……"; +"waitting_remote_accept"="等待对方接听..."; "calling"="正在呼叫"; "cancel_failed"="邀请已接受无法取消"; "operation_failed"="操作失败"; -"device_not_support"="当前设备不支持虚化"; +"device_not_support"="该设备不支持虚化功能"; -"connecting"="正在接通中…"; +"connecting"="正在接通中..."; diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.h b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.h index 8245cf7d..4070de1d 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.h +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.h @@ -83,8 +83,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)refreshVideoView; -- (NSString *)localizableWithKey:(NSString *)key; - @end NS_ASSUME_NONNULL_END diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.m index 78aed788..f9849b0b 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.m @@ -6,6 +6,7 @@ #import #import #import +#import "NECallKitUtil.h" #import "NECallViewController.h" @interface NECallUIStateController () @@ -127,7 +128,7 @@ - (UILabel *)subTitleLabel { _subTitleLabel = [[UILabel alloc] init]; _subTitleLabel.font = [UIFont boldSystemFontOfSize:self.subTitleFontSize]; _subTitleLabel.textColor = [UIColor whiteColor]; - _subTitleLabel.text = [self localizableWithKey:@"waitting_remote_response"]; + _subTitleLabel.text = [NECallKitUtil localizableWithKey:@"waitting_remote_response"]; _subTitleLabel.textAlignment = NSTextAlignmentRight; _subTitleLabel.translatesAutoresizingMaskIntoConstraints = NO; } @@ -137,7 +138,7 @@ - (UILabel *)subTitleLabel { - (NECustomButton *)cancelBtn { if (!_cancelBtn) { _cancelBtn = [[NECustomButton alloc] init]; - _cancelBtn.titleLabel.text = [self localizableWithKey:@"call_cancel"]; + _cancelBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"call_cancel"]; _cancelBtn.imageView.image = [UIImage imageNamed:@"call_cancel" inBundle:self.bundle compatibleWithTraitCollection:nil]; @@ -152,7 +153,7 @@ - (NECustomButton *)cancelBtn { - (NECustomButton *)rejectBtn { if (!_rejectBtn) { _rejectBtn = [[NECustomButton alloc] init]; - _rejectBtn.titleLabel.text = [self localizableWithKey:@"call_reject"]; + _rejectBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"call_reject"]; _rejectBtn.imageView.image = [UIImage imageNamed:@"call_cancel" inBundle:self.bundle compatibleWithTraitCollection:nil]; @@ -167,7 +168,7 @@ - (NECustomButton *)rejectBtn { - (NECustomButton *)acceptBtn { if (!_acceptBtn) { _acceptBtn = [[NECustomButton alloc] init]; - _acceptBtn.titleLabel.text = [self localizableWithKey:@"call_accept"]; + _acceptBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"call_accept"]; _acceptBtn.imageView.image = [UIImage imageNamed:@"call_accept" inBundle:self.bundle compatibleWithTraitCollection:nil]; @@ -183,7 +184,7 @@ - (NECustomButton *)acceptBtn { - (NECustomButton *)microphoneBtn { if (nil == _microphoneBtn) { _microphoneBtn = [[NECustomButton alloc] init]; - _microphoneBtn.titleLabel.text = [self localizableWithKey:@"call_micro_phone"]; + _microphoneBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"call_micro_phone"]; _microphoneBtn.imageView.image = [UIImage imageNamed:@"micro_phone" inBundle:self.bundle compatibleWithTraitCollection:nil]; @@ -202,7 +203,7 @@ - (NECustomButton *)microphoneBtn { - (NECustomButton *)speakerBtn { if (nil == _speakerBtn) { _speakerBtn = [[NECustomButton alloc] init]; - _speakerBtn.titleLabel.text = [self localizableWithKey:@"call_speaker"]; + _speakerBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"call_speaker"]; _speakerBtn.imageView.image = [UIImage imageNamed:@"speaker_off" inBundle:self.bundle compatibleWithTraitCollection:nil]; @@ -223,7 +224,7 @@ - (UILabel *)centerSubtitleLabel { _centerSubtitleLabel = [[UILabel alloc] init]; _centerSubtitleLabel.textColor = [UIColor whiteColor]; _centerSubtitleLabel.font = [UIFont systemFontOfSize:self.subTitleFontSize]; - _centerSubtitleLabel.text = [self localizableWithKey:@"waitting_remote_accept"]; + _centerSubtitleLabel.text = [NECallKitUtil localizableWithKey:@"waitting_remote_accept"]; _centerSubtitleLabel.textAlignment = NSTextAlignmentCenter; _centerSubtitleLabel.translatesAutoresizingMaskIntoConstraints = NO; } @@ -307,9 +308,10 @@ - (void)refreshUI { } - (void)setupVideoCallingUI { - self.titleLabel.text = [NSString stringWithFormat:@"%@ %@", [self localizableWithKey:@"calling"], - self.callParam.remoteShowName]; - self.subTitleLabel.text = [self localizableWithKey:@"waitting_remote_accept"]; + self.titleLabel.text = + [NSString stringWithFormat:@"%@ %@", [NECallKitUtil localizableWithKey:@"calling"], + self.callParam.remoteShowName]; + self.subTitleLabel.text = [NECallKitUtil localizableWithKey:@"waitting_remote_accept"]; if (self.callParam.remoteAvatar.length <= 0) { UIView *cover = [self getDefaultHeaderView:self.callParam.remoteUserAccid @@ -332,9 +334,9 @@ - (void)setupVideoCallingUI { - (void)setupAudioCallingUI { self.centerTitleLabel.text = - [NSString stringWithFormat:@"%@ %@", [self localizableWithKey:@"calling"], + [NSString stringWithFormat:@"%@ %@", [NECallKitUtil localizableWithKey:@"calling"], self.callParam.remoteShowName]; - self.centerSubtitleLabel.text = [self localizableWithKey:@"waitting_remote_accept"]; + self.centerSubtitleLabel.text = [NECallKitUtil localizableWithKey:@"waitting_remote_accept"]; if (self.callParam.remoteAvatar.length <= 0) { UIView *cover = [self getDefaultHeaderView:self.callParam.remoteUserAccid font:[UIFont systemFontOfSize:self.titleFontSize] @@ -369,8 +371,8 @@ - (void)setupCalledUI { - (NSString *)getInviteText { return (self.callParam.callType == NERtcCallTypeAudio - ? [self localizableWithKey:@"invite_audio_call"] - : [self localizableWithKey:@"invite_video_call"]); + ? [NECallKitUtil localizableWithKey:@"invite_audio_call"] + : [NECallKitUtil localizableWithKey:@"invite_video_call"]); } - (void)refreshVideoView { @@ -383,7 +385,7 @@ - (void)refreshVideoView { NSLog(@"show my big view"); self.smallVideoView.maskView.hidden = !self.mainController.isRemoteMute; self.bigVideoView.maskView.hidden = !self.operationView.cameraBtn.selected; - self.bigVideoView.userID = self.callParam.currentUserAccid; + self.bigVideoView.userID = NIMSDK.sharedSDK.loginManager.currentAccount; self.smallVideoView.userID = self.callParam.remoteUserAccid; } else { [[NECallEngine sharedInstance] setupLocalView:self.smallVideoView.videoView]; @@ -392,7 +394,7 @@ - (void)refreshVideoView { self.bigVideoView.maskView.hidden = !self.mainController.isRemoteMute; self.smallVideoView.maskView.hidden = !self.operationView.cameraBtn.selected; self.bigVideoView.userID = self.callParam.remoteUserAccid; - self.smallVideoView.userID = self.callParam.currentUserAccid; + self.smallVideoView.userID = NIMSDK.sharedSDK.loginManager.currentAccount; } } @@ -418,9 +420,4 @@ - (UIView *)getDefaultHeaderView:(NSString *)accid return headerView; } -- (NSString *)localizableWithKey:(NSString *)key { - return [self.bundle localizedStringForKey:key value:nil table:@"Localizable"]; - ; -} - @end diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m index ac4761b6..4dfc8977 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m @@ -10,6 +10,7 @@ #import #import #include +#import "NECallKitUtil.h" #import "NECallUIStateController.h" #import "NECustomButton.h" #import "NEExpandButton.h" @@ -147,7 +148,7 @@ - (void)setupSDK { }); } weakSelf.videoCallingController.bigVideoView.userID = - weakSelf.callParam.currentUserAccid; + NIMSDK.sharedSDK.loginManager.currentAccount; } if (error) { @@ -307,7 +308,7 @@ - (void)setSwitchAudioStyle { self.mediaSwitchBtn.imageView.image = [UIImage imageNamed:@"switch_audio" inBundle:self.bundle compatibleWithTraitCollection:nil]; - self.mediaSwitchBtn.titleLabel.text = [self localizableWithKey:@"switch_to_audio"]; + self.mediaSwitchBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"switch_to_audio"]; self.mediaSwitchBtn.tag = NERtcCallTypeAudio; [self showVideoView]; [self setUrl:self.callParam.remoteAvatar withPlaceholder:@"avator"]; @@ -318,7 +319,7 @@ - (void)setSwitchVideoStyle { self.mediaSwitchBtn.imageView.image = [UIImage imageNamed:@"switch_video" inBundle:self.bundle compatibleWithTraitCollection:nil]; - self.mediaSwitchBtn.titleLabel.text = [self localizableWithKey:@"switch_to_video"]; + self.mediaSwitchBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"switch_to_video"]; self.mediaSwitchBtn.tag = NERtcCallTypeVideo; [self hideVideoView]; [self setUrl:self.callParam.remoteAvatar withPlaceholder:@"avator"]; @@ -354,13 +355,13 @@ - (void)updateUIonStatus:(NERtcCallStatus)status { self.mediaSwitchBtn.imageView.image = [UIImage imageNamed:@"switch_audio" inBundle:self.bundle compatibleWithTraitCollection:nil]; - self.mediaSwitchBtn.titleLabel.text = [self localizableWithKey:@"switch_to_audio"]; + self.mediaSwitchBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"switch_to_audio"]; [self setSwitchAudioStyle]; } else { self.mediaSwitchBtn.imageView.image = [UIImage imageNamed:@"switch_video" inBundle:self.bundle compatibleWithTraitCollection:nil]; - self.mediaSwitchBtn.titleLabel.text = [self localizableWithKey:@"switch_to_video"]; + self.mediaSwitchBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"switch_to_video"]; [self setSwitchVideoStyle]; } __weak typeof(self) weakSelf = self; @@ -483,7 +484,7 @@ - (void)rejectEvent:(UIButton *)button { - (void)acceptEvent:(UIButton *)button { if ([[NetManager shareInstance] isClose] == YES) { - [self.view ne_makeToast:[self localizableWithKey:@"network_error"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"network_error"]]; return; } @@ -491,26 +492,27 @@ - (void)acceptEvent:(UIButton *)button { self.calledController.acceptBtn.userInteractionEnabled = NO; __weak typeof(self) weakSelf = self; - [[NECallEngine sharedInstance] accept:^(NSError *_Nullable error, - NECallInfo *_Nullable callInfo) { - weakSelf.calledController.rejectBtn.userInteractionEnabled = YES; - weakSelf.calledController.acceptBtn.userInteractionEnabled = YES; - if (error) { - if (error.code != 10420) { - [weakSelf.preiousWindow - ne_makeToast:[NSString stringWithFormat:@"%@ %@", - [weakSelf localizableWithKey:@"accept_failed"], - error.localizedDescription]]; - } - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), - dispatch_get_main_queue(), ^{ - [weakSelf destroy]; - }); - } else { - self.calledController.connectingLabel.hidden = NO; - [self stopCurrentPlaying]; - } - }]; + [[NECallEngine sharedInstance] + accept:^(NSError *_Nullable error, NECallInfo *_Nullable callInfo) { + weakSelf.calledController.rejectBtn.userInteractionEnabled = YES; + weakSelf.calledController.acceptBtn.userInteractionEnabled = YES; + if (error) { + if (error.code != 10420) { + [weakSelf.preiousWindow + ne_makeToast:[NSString stringWithFormat:@"%@ %@", + [NECallKitUtil + localizableWithKey:@"accept_failed"], + error.localizedDescription]]; + } + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + [weakSelf destroy]; + }); + } else { + self.calledController.connectingLabel.hidden = NO; + [self stopCurrentPlaying]; + } + }]; } - (void)switchCameraBtn:(UIButton *)button { @@ -538,7 +540,7 @@ - (void)cameraBtnClick:(UIButton *)button { [[NECallEngine sharedInstance] muteLocalVideo:button.selected]; } [self changeDefaultImage:button.selected]; - [self cameraAvailble:!button.selected userId:self.callParam.currentUserAccid]; + [self cameraAvailble:!button.selected userId:NIMSDK.sharedSDK.loginManager.currentAccount]; } - (void)hangupBtnClick:(UIButton *)button { @@ -576,7 +578,7 @@ - (void)speakerBtnClick:(UIButton *)button { [self setAudioOutputToSpeaker]; } } else { - [self.view ne_makeToast:[self localizableWithKey:@"operation_failed"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"operation_failed"]]; } } @@ -597,7 +599,7 @@ - (void)virtualBackgroundBtnClick:(UIButton *)button { - (void)operationSwitchClick:(UIButton *)btn { if ([[NetManager shareInstance] isClose] == YES) { - [self.view ne_makeToast:[self localizableWithKey:@"network_error"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"network_error"]]; return; } __weak typeof(self) weakSelf = self; @@ -624,10 +626,10 @@ - (void)operationSwitchClick:(UIButton *)btn { } } else { [weakSelf.view - ne_makeToast:[NSString - stringWithFormat:@"%@: %@", - [weakSelf localizableWithKey:@"switch_error"], - error]]; + ne_makeToast:[NSString stringWithFormat:@"%@: %@", + [NECallKitUtil + localizableWithKey:@"switch_error"], + error]]; } }]; } @@ -637,13 +639,13 @@ - (void)operationSpeakerClick:(UIButton *)btn { if (ret == 0) { btn.selected = !btn.selected; } else { - [self.view ne_makeToast:[self localizableWithKey:@"operation_failed"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"operation_failed"]]; } } - (void)mediaClick:(UIButton *)btn { if ([[NetManager shareInstance] isClose] == YES) { - [self.view ne_makeToast:[self localizableWithKey:@"network_error"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"network_error"]]; return; } __weak typeof(self) weakSelf = self; @@ -670,10 +672,10 @@ - (void)mediaClick:(UIButton *)btn { } } else { [weakSelf.view - ne_makeToast:[NSString - stringWithFormat:@"%@ : %@", - [weakSelf localizableWithKey:@"switch_error"], - error]]; + ne_makeToast:[NSString stringWithFormat:@"%@ : %@", + [NECallKitUtil + localizableWithKey:@"switch_error"], + error]]; } }]; } @@ -745,35 +747,37 @@ - (void)onCallConnected:(NECallInfo *)info { - (void)onCallEnd:(NECallEndInfo *)info { switch (info.reasonCode) { case TerminalCodeTimeOut: - [self playRingWithType:CRTNoResponseRing]; + if (self.callParam.isCaller == YES) { + [self playRingWithType:CRTNoResponseRing]; + } if ([[NetManager shareInstance] isClose] == YES) { [self destroy]; return; } if (self.callParam.isCaller == YES) { - [self showToastWithContent:[self localizableWithKey:@"remote_timeout"]]; + [self showToastWithContent:[NECallKitUtil localizableWithKey:@"remote_timeout"]]; } break; case TerminalCodeBusy: - [self showToastWithContent:[self localizableWithKey:@"remote_busy"]]; + [self showToastWithContent:[NECallKitUtil localizableWithKey:@"remote_busy"]]; [self playRingWithType:CRTBusyRing]; break; case TerminalCalleeCancel: - [self showToastWithContent:[self localizableWithKey:@"remote_cancel"]]; + [self showToastWithContent:[NECallKitUtil localizableWithKey:@"remote_cancel"]]; break; case TerminalCallerRejcted: - [self showToastWithContent:[self localizableWithKey:@"remote_reject"]]; + [self showToastWithContent:[NECallKitUtil localizableWithKey:@"remote_reject"]]; [self playRingWithType:CRTRejectRing]; break; case TerminalOtherRejected: - [self.preiousWindow ne_makeToast:[self localizableWithKey:@"other_client_reject"]]; + [self.preiousWindow ne_makeToast:[NECallKitUtil localizableWithKey:@"other_client_reject"]]; break; case TerminalOtherAccepted: - [self.preiousWindow ne_makeToast:[self localizableWithKey:@"other_client_accept"]]; + [self.preiousWindow ne_makeToast:[NECallKitUtil localizableWithKey:@"other_client_accept"]]; break; case TerminalCallerCancel: @@ -800,16 +804,16 @@ - (void)onCallTypeChange:(NECallTypeChangeInfo *)info { return; } UIAlertController *alert = [UIAlertController - alertControllerWithTitle:[self localizableWithKey:@"permission"] + alertControllerWithTitle:[NECallKitUtil localizableWithKey:@"permission"] message:info.callType == NECallTypeVideo - ? [self localizableWithKey:@"audio_to_video"] - : [self localizableWithKey:@"video_to_audio"] + ? [NECallKitUtil localizableWithKey:@"audio_to_video"] + : [NECallKitUtil localizableWithKey:@"video_to_audio"] preferredStyle:UIAlertControllerStyleAlert]; self.alert = alert; __weak typeof(self) weakSelf = self; UIAlertAction *rejectAction = [UIAlertAction - actionWithTitle:[self localizableWithKey:@"reject"] + actionWithTitle:[NECallKitUtil localizableWithKey:@"reject"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnull action) { NESwitchParam *param = [[NESwitchParam alloc] init]; @@ -824,7 +828,7 @@ - (void)onCallTypeChange:(NECallTypeChangeInfo *)info { }]; }]; UIAlertAction *agreeAction = [UIAlertAction - actionWithTitle:[self localizableWithKey:@"agree"] + actionWithTitle:[NECallKitUtil localizableWithKey:@"agree"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnull action) { NESwitchParam *param = [[NESwitchParam alloc] init]; @@ -866,7 +870,7 @@ - (void)onCallTypeChange:(NECallTypeChangeInfo *)info { break; case NECallSwitchStateReject: [self hideBannerView]; - [self.view ne_makeToast:[self localizableWithKey:@"reject_tip"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"reject_tip"]]; break; default: break; @@ -952,8 +956,8 @@ - (NSString *)timeFormatted:(int)totalSeconds { - (NSString *)getInviteText { return (self.callParam.callType == NECallTypeAudio - ? [self localizableWithKey:@"invite_audio_call"] - : [self localizableWithKey:@"invite_video_call"]); + ? [NECallKitUtil localizableWithKey:@"invite_audio_call"] + : [NECallKitUtil localizableWithKey:@"invite_video_call"]); } - (void)hideBannerView { @@ -1043,7 +1047,7 @@ - (UIView *)bannerView { ]]; label.adjustsFontSizeToFitWidth = YES; - label.text = [self localizableWithKey:@"waitting_remote_response"]; + label.text = [NECallKitUtil localizableWithKey:@"waitting_remote_response"]; } return _bannerView; } @@ -1267,10 +1271,6 @@ - (UIView *)getDefaultHeaderView:(NSString *)accid return headerView; } -- (NSString *)localizableWithKey:(NSString *)key { - return [self.bundle localizedStringForKey:key value:nil table:@"Localizable"]; -} - #pragma mark CallEngine Key Value - (BOOL)isGlobalInit { @@ -1287,7 +1287,7 @@ - (void)onNERtcEngineVirtualBackgroundSourceEnabled:(BOOL)enabled reason: (NERtcVirtualBackgroundSourceStateReason)reason { if (reason == kNERtcVirtualBackgroundSourceStateReasonDeviceNotSupported) { - [self.view ne_makeToast:[self localizableWithKey:@"device_not_support"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"device_not_support"]]; self.operationView.virtualBtn.selected = NO; } } diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECalledViewController.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECalledViewController.m index c2e1d247..533aa452 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECalledViewController.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECalledViewController.m @@ -4,6 +4,7 @@ #import "NECalledViewController.h" #import +#import "NECallKitUtil.h" @interface NECalledViewController () @@ -54,7 +55,7 @@ - (void)setupUI { self.connectingLabel = [[UILabel alloc] init]; self.connectingLabel.translatesAutoresizingMaskIntoConstraints = NO; - self.connectingLabel.text = [self localizableWithKey:@"connecting"]; + self.connectingLabel.text = [NECallKitUtil localizableWithKey:@"connecting"]; self.connectingLabel.textColor = [UIColor whiteColor]; self.connectingLabel.font = [UIFont systemFontOfSize:14]; self.connectingLabel.hidden = YES; diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.h b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.h index b0879b0a..9d7a5d41 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.h +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.h @@ -1,8 +1,9 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. #import +#import "NECallUIKitConfig.h" NS_ASSUME_NONNULL_BEGIN @@ -24,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, strong, nullable) NSString *noResponseFilePath; /// 初始化 -- (instancetype)initWithBundle:(NSBundle *)bundle; +- (instancetype)initWithBundle:(NSBundle *)bundle language:(NECallUILanguage)language; @end diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.m index 314b8597..a995f213 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.m @@ -1,21 +1,53 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. #import "NERingFile.h" +#import "NERtcCallUIKit.h" @implementation NERingFile -- (instancetype)initWithBundle:(NSBundle *)bundle { +- (instancetype)initWithBundle:(NSBundle *)bundle language:(NECallUILanguage)language { self = [super init]; if (self) { - self.callerRingFilePath = [bundle pathForResource:@"avchat_connecting" ofType:@"mp3"]; - self.calleeRingFilePath = [bundle pathForResource:@"avchat_ring" ofType:@"mp3"]; - self.busyRingFilePath = [bundle pathForResource:@"avchat_peer_busy" ofType:@"mp3"]; - self.rejectRingFilePath = [bundle pathForResource:@"avchat_peer_reject" ofType:@"mp3"]; - self.noResponseFilePath = [bundle pathForResource:@"avchat_no_response" ofType:@"mp3"]; + switch (language) { + case NECallUILanguageEn: + [self setEnWithBundle:bundle]; + break; + case NECallUILanguageZhHans: + [self setZhWithBundle:bundle]; + break; + case NECallUILanguageAuto: { + NSString *language = [NSLocale preferredLanguages].firstObject; + if ([language hasPrefix:@"en"]) { + [self setEnWithBundle:bundle]; + } else { + // 非英文情况下全部使用默认中文 + [self setZhWithBundle:bundle]; + } + break; + } + default: + break; + } } return self; } +- (void)setZhWithBundle:(NSBundle *)bundle { + self.callerRingFilePath = [bundle pathForResource:@"avchat_connecting" ofType:@"mp3"]; + self.calleeRingFilePath = [bundle pathForResource:@"avchat_ring" ofType:@"mp3"]; + self.busyRingFilePath = [bundle pathForResource:@"avchat_peer_busy" ofType:@"mp3"]; + self.rejectRingFilePath = [bundle pathForResource:@"avchat_peer_reject" ofType:@"mp3"]; + self.noResponseFilePath = [bundle pathForResource:@"avchat_no_response" ofType:@"mp3"]; +} + +- (void)setEnWithBundle:(NSBundle *)bundle { + self.callerRingFilePath = [bundle pathForResource:@"avchat_connecting_en" ofType:@"mp3"]; + self.calleeRingFilePath = [bundle pathForResource:@"avchat_ring_en" ofType:@"mp3"]; + self.busyRingFilePath = [bundle pathForResource:@"avchat_peer_busy_en" ofType:@"mp3"]; + self.rejectRingFilePath = [bundle pathForResource:@"avchat_peer_reject_en" ofType:@"mp3"]; + self.noResponseFilePath = [bundle pathForResource:@"avchat_no_response_en" ofType:@"mp3"]; +} + @end diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NEUICallParam.h b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NEUICallParam.h index 14907900..4bdd6aef 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NEUICallParam.h +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NEUICallParam.h @@ -14,9 +14,6 @@ NS_ASSUME_NONNULL_BEGIN /// 被叫accid @property(nonatomic, strong) NSString *remoteUserAccid; -/// 主叫accid -@property(nonatomic, strong) NSString *currentUserAccid; - /// 通话页面被叫显示名称 @property(nonatomic, strong) NSString *remoteShowName; diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.h b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.h index c0e8c2fe..a1cc4ce7 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.h +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.h @@ -1,8 +1,9 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. #import +#import "NECallUIKitConfig.h" NS_ASSUME_NONNULL_BEGIN @@ -10,6 +11,10 @@ NS_ASSUME_NONNULL_BEGIN + (UIColor *)colorWithHexString:(NSString *)hexString; ++ (NSString *)localizableWithKey:(NSString *)key; + ++ (void)setLanguage:(NECallUILanguage)language; + @end NS_ASSUME_NONNULL_END diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.m index 7a733988..510057dc 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.m @@ -3,6 +3,9 @@ // found in the LICENSE file. #import "NECallKitUtil.h" +#import "NERtcCallUIKit.h" + +static NECallUILanguage _language = NECallUILanguageAuto; @implementation NECallKitUtil @@ -19,4 +22,31 @@ + (UIColor *)colorWithHexString:(NSString *)hexString { return color; } ++ (void)setLanguage:(NECallUILanguage)language { + _language = language; +} + ++ (NSString *)localizableWithKey:(NSString *)key { + switch (_language) { + case NECallUILanguageZhHans: { + NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle bundleForClass:[NERtcCallUIKit class]] + pathForResource:@"zh-Hans" + ofType:@"lproj"]]; + return [bundle localizedStringForKey:key value:nil table:nil]; + } + case NECallUILanguageEn: { + NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle bundleForClass:[NERtcCallUIKit class]] + pathForResource:@"en" + ofType:@"lproj"]]; + return [bundle localizedStringForKey:key value:nil table:nil]; + } + case NECallUILanguageAuto: + default: + break; + } + return [[NSBundle bundleForClass:NERtcCallUIKit.class] localizedStringForKey:key + value:nil + table:@"Localizable"]; +} + @end diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallUIKitConfig.h b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallUIKitConfig.h index 09c9a244..20e82325 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallUIKitConfig.h +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallUIKitConfig.h @@ -7,6 +7,16 @@ NS_ASSUME_NONNULL_BEGIN +/// 国际化类型 +typedef NS_ENUM(NSInteger, NECallUILanguage) { + /// 根据系统设置切换 + NECallUILanguageAuto = 0, + /// 简体中文 + NECallUILanguageZhHans, + /// 英文 + NECallUILanguageEn, +}; + @interface NECallUIConfig : NSObject /// 是否禁止音频通话转视频通话,默认YES,支持转换 @@ -42,6 +52,9 @@ NS_ASSUME_NONNULL_BEGIN /// 是否开启被叫预览,默认NO,不开启 @property(nonatomic, assign) BOOL enableCalleePreview; +/// 国际化配置 +@property(nonatomic, assign) NECallUILanguage language; + @end @interface NECallUIKitConfig : NSObject diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m index c4901e82..5ab92056 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m @@ -119,6 +119,10 @@ - (void)setupWithConfig:(NECallUIKitConfig *)config { self.transcodingDelegate = instance; } } + + self.bundle = [NSBundle bundleForClass:NERtcCallUIKit.class]; + self.ringFile = [[NERingFile alloc] initWithBundle:self.bundle language:config.uiConfig.language]; + [NECallKitUtil setLanguage:config.uiConfig.language]; } - (instancetype)init { @@ -152,29 +156,21 @@ - (instancetype)init { selector:@selector(appDidEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; - - self.bundle = [NSBundle bundleForClass:NERtcCallUIKit.class]; - self.ringFile = [[NERingFile alloc] initWithBundle:self.bundle]; self.smallVideoSize = CGSizeMake(90, 160); self.smallAudioSize = CGSizeMake(70, 70); } return self; } -- (NSString *)localizableWithKey:(NSString *)key { - return [self.bundle localizedStringForKey:key value:nil table:@"Localizable"]; -} - - (void)registerRouter { [[Router shared] register:@"imkit://callkit.page" closure:^(NSDictionary *_Nonnull param) { if ([[NetManager shareInstance] isClose] == YES) { [UIApplication.sharedApplication.keyWindow - ne_makeToast:[self localizableWithKey:@"network_error"]]; + ne_makeToast:[NECallKitUtil localizableWithKey:@"network_error"]]; return; } NEUICallParam *callParam = [[NEUICallParam alloc] init]; - callParam.currentUserAccid = [param objectForKey:@"currentUserAccid"]; callParam.remoteUserAccid = [param objectForKey:@"remoteUserAccid"]; callParam.remoteShowName = [param objectForKey:@"remoteShowName"]; callParam.remoteAvatar = [param objectForKey:@"remoteAvatar"]; @@ -323,7 +319,6 @@ - (void)onReceiveInvited:(NEInviteInfo *)info { ? imUser.userInfo.mobile : imUser.userInfo.nickName; callParam.remoteAvatar = imUser.userInfo.avatarUrl; - callParam.currentUserAccid = NIMSDK.sharedSDK.loginManager.currentAccount; callParam.enableAudioToVideo = self.config.uiConfig.enableAudioToVideo; callParam.enableVideoToAudio = self.config.uiConfig.enableVideoToAudio; callParam.enableVirtualBackground = self.config.uiConfig.enableVirtualBackground; @@ -380,7 +375,6 @@ - (void)showCalled:(NIMUser *)imUser callType:(NECallType)type attachment:(NSStr callParam.remoteUserAccid = imUser.userId; callParam.remoteShowName = imUser.userInfo.mobile; callParam.remoteAvatar = imUser.userInfo.avatarUrl; - callParam.currentUserAccid = NIMSDK.sharedSDK.loginManager.currentAccount; callParam.enableVideoToAudio = self.config.uiConfig.enableVideoToAudio; callParam.enableAudioToVideo = self.config.uiConfig.enableAudioToVideo; callParam.callType = type; @@ -432,6 +426,7 @@ - (UINavigationController *)getKeyWindowNav { } window.frame = [[UIScreen mainScreen] bounds]; window.windowLevel = UIWindowLevelStatusBar - 1; + window.backgroundColor = [UIColor clearColor]; self.keywindow = window; self.preiousKeywindow = UIApplication.sharedApplication.keyWindow; YXAlogInfo(@"create new window %@", self.keywindow); @@ -445,7 +440,7 @@ - (UINavigationController *)getKeyWindowNav { nav.view.backgroundColor = [UIColor clearColor]; [nav.navigationBar setHidden:YES]; self.keywindow.rootViewController = nav; - self.keywindow.window.backgroundColor = [UIColor clearColor]; + self.keywindow.backgroundColor = [UIColor clearColor]; [self.keywindow makeKeyAndVisible]; return nav; } @@ -776,7 +771,7 @@ - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPict #pragma mark - Version + (NSString *)version { - return @"2.2.0"; + return @"2.4.0"; } @end diff --git a/NETeamUIKit/NETeamUIKit.podspec b/NETeamUIKit/NETeamUIKit.podspec index c0f5c3d1..4a988889 100644 --- a/NETeamUIKit/NETeamUIKit.podspec +++ b/NETeamUIKit/NETeamUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NETeamUIKit' - s.version = '9.7.0' + s.version = '10.0.0-beta' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings b/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings index 62b596f1..b5f54ae9 100644 --- a/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings +++ b/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings @@ -36,7 +36,6 @@ "discuss_info"="Temp Group Info"; "group_info"="Group Info"; "discuss_introduce"="Temp Group Introduce"; -"search_friend"="Search Contact"; "no_result"="No Result"; "discuss_name"="Temp Group Name"; "dissolute_team_chat"="Wether to disband Group"; diff --git a/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings index 58405ed7..d98da3a7 100644 --- a/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -36,7 +36,6 @@ "discuss_info"="讨论组信息"; "group_info"="群信息"; "discuss_introduce"="讨论组介绍"; -"search_friend"="搜索"; "no_result"="暂无结果"; "discuss_name"="讨论组名称"; "dissolute_team_chat"="是否解散群聊?"; diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunHistoryMessageCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunHistoryMessageCell.swift index 254ed1b6..65ed6e49 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunHistoryMessageCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunHistoryMessageCell.swift @@ -1,5 +1,6 @@ import NIMSDK + // Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. @@ -11,40 +12,40 @@ open class FunHistoryMessageCell: NEBaseHistoryMessageCell { super.setupSubviews() rangeTextColor = .funTeamThemeColor - headImge.layer.cornerRadius = 4 + headView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ - headImge.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), - headImge.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 18), - headImge.widthAnchor.constraint(equalToConstant: 32), - headImge.heightAnchor.constraint(equalToConstant: 32), + headView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + headView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 18), + headView.widthAnchor.constraint(equalToConstant: 32), + headView.heightAnchor.constraint(equalToConstant: 32), ]) - title.font = .systemFont(ofSize: 12) - title.textColor = .funTeamHistoryCellTitleTextColor + titleLabel.font = .systemFont(ofSize: 12) + titleLabel.textColor = .funTeamHistoryCellTitleTextColor NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - title.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), - title.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + titleLabel.leftAnchor.constraint(equalTo: headView.rightAnchor, constant: 12), + titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), ]) - subTitle.font = .systemFont(ofSize: 15) - subTitle.textColor = .ne_darkText + subTitleLabel.font = .systemFont(ofSize: 15) + subTitleLabel.textColor = .ne_darkText NSLayoutConstraint.activate([ - subTitle.leftAnchor.constraint(equalTo: title.leftAnchor), - subTitle.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), - subTitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), + subTitleLabel.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), + subTitleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), + subTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), ]) NSLayoutConstraint.activate([ bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), - bottomLine.leftAnchor.constraint(equalTo: headImge.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: headView.leftAnchor), bottomLine.bottomAnchor.constraint(equalTo: bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 0.5), ]) NSLayoutConstraint.activate([ timeLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), - timeLabel.centerYAnchor.constraint(equalTo: title.centerYAnchor), + timeLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamArrowSettingCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamArrowSettingCell.swift index 32986031..84376b62 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamArrowSettingCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamArrowSettingCell.swift @@ -27,8 +27,8 @@ open class FunTeamArrowSettingCell: NEBaseTeamArrowSettingCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamDefaultIconCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamDefaultIconCell.swift index f87e0eb5..02defa0b 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamDefaultIconCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamDefaultIconCell.swift @@ -10,17 +10,17 @@ open class FunTeamDefaultIconCell: NEBaseTeamDefaultIconCell { override func setupUI() { super.setupUI() NSLayoutConstraint.activate([ - selectBack.rightAnchor.constraint(equalTo: contentView.rightAnchor), - selectBack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - selectBack.widthAnchor.constraint(equalToConstant: 56.0), - selectBack.heightAnchor.constraint(equalToConstant: 56.0), + selectBackView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + selectBackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + selectBackView.widthAnchor.constraint(equalToConstant: 56.0), + selectBackView.heightAnchor.constraint(equalToConstant: 56.0), ]) NSLayoutConstraint.activate([ - iconImage.centerXAnchor.constraint(equalTo: selectBack.centerXAnchor), - iconImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - iconImage.heightAnchor.constraint(equalToConstant: 40), - iconImage.widthAnchor.constraint(equalToConstant: 40), + iconImageView.centerXAnchor.constraint(equalTo: selectBackView.centerXAnchor), + iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconImageView.heightAnchor.constraint(equalToConstant: 40), + iconImageView.widthAnchor.constraint(equalToConstant: 40), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamManagerMemberCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamManagerMemberCell.swift index 9a0380d6..17b0a489 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamManagerMemberCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamManagerMemberCell.swift @@ -5,15 +5,6 @@ import UIKit class FunTeamManagerMemberCell: FunTeamMemberCell { -// lazy var removeLabel: UILabel = { -// let label = UILabel() -// label.translatesAutoresizingMaskIntoConstraints = false -// label.text = localizable("team_member_remove") -// label.textColor = .funTeamRemoveLabelColor -// label.font = UIFont.systemFont(ofSize: 14.0) -// return label -// }() - override func awakeFromNib() { super.awakeFromNib() // Initialization code diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingHeaderCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingHeaderCell.swift index feeee974..da5aa492 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingHeaderCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingHeaderCell.swift @@ -30,12 +30,12 @@ open class FunTeamSettingHeaderCell: NEBaseTeamSettingHeaderCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), ]) NSLayoutConstraint.activate([ - headerView.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), + headerView.centerYAnchor.constraint(equalTo: arrowView.centerYAnchor), headerView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -44.0), headerView.widthAnchor.constraint(equalToConstant: 42.0), headerView.heightAnchor.constraint(equalToConstant: 42.0), diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingLabelArrowCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingLabelArrowCell.swift index 35ca529b..f794fd7b 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingLabelArrowCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingLabelArrowCell.swift @@ -18,8 +18,8 @@ open class FunTeamSettingLabelArrowCell: FunTeamArrowSettingCell { super.setupUI() contentView.addSubview(arrowLabel) NSLayoutConstraint.activate([ - arrowLabel.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), - arrowLabel.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -4), + arrowLabel.centerYAnchor.constraint(equalTo: arrowView.centerYAnchor), + arrowLabel.rightAnchor.constraint(equalTo: arrowView.leftAnchor, constant: -4), ]) } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingSelectCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingSelectCell.swift index 77fc90ee..f73b7af6 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingSelectCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingSelectCell.swift @@ -15,7 +15,7 @@ open class FunTeamSettingSelectCell: NEBaseTeamSettingSelectCell { super.init(coder: coder) } - override func setupUI() { + override open func setupUI() { super.setupUI() contentView.updateLayoutConstraint(firstItem: dividerLine, seconedItem: contentView, attribute: .left, constant: 16) contentView.updateLayoutConstraint(firstItem: dividerLine, seconedItem: contentView, attribute: .right, constant: 0) @@ -33,8 +33,8 @@ open class FunTeamSettingSelectCell: NEBaseTeamSettingSelectCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamUserCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamUserCell.swift index d692aac4..641a628e 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamUserCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamUserCell.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -12,12 +12,12 @@ import UIKit open class FunTeamUserCell: NEBaseTeamUserCell { override func setupUI() { super.setupUI() - userHeader.layer.cornerRadius = 4 + userHeaderView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ - userHeader.rightAnchor.constraint(equalTo: contentView.rightAnchor), - userHeader.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - userHeader.widthAnchor.constraint(equalToConstant: 36.0), - userHeader.heightAnchor.constraint(equalToConstant: 36.0), + userHeaderView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + userHeaderView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + userHeaderView.widthAnchor.constraint(equalToConstant: 36.0), + userHeaderView.heightAnchor.constraint(equalToConstant: 36.0), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamAvatarViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamAvatarViewController.swift index 3a5c4631..01175c5c 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamAvatarViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamAvatarViewController.swift @@ -14,7 +14,7 @@ open class FunTeamAvatarViewController: NEBaseTeamAvatarViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { @@ -29,53 +29,53 @@ open class FunTeamAvatarViewController: NEBaseTeamAvatarViewController { view.backgroundColor = .funTeamBackgroundColor NSLayoutConstraint.activate([ - headerBack.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight), - headerBack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), - headerBack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), - headerBack.heightAnchor.constraint(equalToConstant: 128.0), + headerBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight), + headerBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), + headerBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), + headerBackView.heightAnchor.constraint(equalToConstant: 128.0), ]) NSLayoutConstraint.activate([ - photoImage.centerXAnchor.constraint(equalTo: headerView.rightAnchor), - photoImage.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), + photoImageView.rightAnchor.constraint(equalTo: headerView.rightAnchor), + photoImageView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), ]) NSLayoutConstraint.activate([ - defaultHeaderBack.leftAnchor.constraint(equalTo: headerBack.leftAnchor), - defaultHeaderBack.rightAnchor.constraint(equalTo: headerBack.rightAnchor), - defaultHeaderBack.topAnchor.constraint( - equalTo: headerBack.bottomAnchor, + defaultHeaderBackView.leftAnchor.constraint(equalTo: headerBackView.leftAnchor), + defaultHeaderBackView.rightAnchor.constraint(equalTo: headerBackView.rightAnchor), + defaultHeaderBackView.topAnchor.constraint( + equalTo: headerBackView.bottomAnchor, constant: 8.0 ), - defaultHeaderBack.heightAnchor.constraint(equalToConstant: 124.0), + defaultHeaderBackView.heightAnchor.constraint(equalToConstant: 124.0), ]) NSLayoutConstraint.activate([ - tag.leftAnchor.constraint(equalTo: defaultHeaderBack.leftAnchor, constant: 16.0), - tag.topAnchor.constraint(equalTo: defaultHeaderBack.topAnchor, constant: 16.0), - tag.heightAnchor.constraint(equalToConstant: 18), + tagLabel.leftAnchor.constraint(equalTo: defaultHeaderBackView.leftAnchor, constant: 16.0), + tagLabel.topAnchor.constraint(equalTo: defaultHeaderBackView.topAnchor, constant: 16.0), + tagLabel.heightAnchor.constraint(equalToConstant: 18), ]) - iconCollection.register( + iconsCollectionView.register( FunTeamDefaultIconCell.self, forCellWithReuseIdentifier: "\(FunTeamDefaultIconCell.self)" ) NSLayoutConstraint.activate([ - iconCollection.topAnchor.constraint(equalTo: tag.bottomAnchor, constant: 0), - iconCollection.leftAnchor.constraint( - equalTo: defaultHeaderBack.leftAnchor, + iconsCollectionView.topAnchor.constraint(equalTo: tagLabel.bottomAnchor, constant: 0), + iconsCollectionView.leftAnchor.constraint( + equalTo: defaultHeaderBackView.leftAnchor, constant: 16 ), - iconCollection.rightAnchor.constraint( - equalTo: defaultHeaderBack.rightAnchor, + iconsCollectionView.rightAnchor.constraint( + equalTo: defaultHeaderBackView.rightAnchor, constant: -16 ), - iconCollection.heightAnchor.constraint(equalToConstant: 90.0), + iconsCollectionView.heightAnchor.constraint(equalToConstant: 90.0), ]) } override open func uploadPhoto() { - if changePermission() { + if getChangePermission() { showCustomBottomAlert(self) } } @@ -86,7 +86,8 @@ open class FunTeamAvatarViewController: NEBaseTeamAvatarViewController { withReuseIdentifier: "\(FunTeamDefaultIconCell.self)", for: indexPath ) as? FunTeamDefaultIconCell { - cell.iconImage.image = coreLoader.loadImage("fun_icon_\(indexPath.row)") + cell.iconImageView.image = coreLoader.loadImage("fun_icon_\(indexPath.row)") + cell.iconImageView.accessibilityIdentifier = "id.default\(indexPath.row + 1)" return cell } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamHistoryMessageController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamHistoryMessageController.swift index 427153d4..5819bdcb 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamHistoryMessageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamHistoryMessageController.swift @@ -16,13 +16,13 @@ open class FunTeamHistoryMessageController: NEBaseTeamHistoryMessageController { return view }() - override public init(session: NIMSession?) { - super.init(session: session) + override public init(teamId: String?) { + super.init(teamId: teamId) tag = "FunTeamHistoryMessageController" } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -85,7 +85,7 @@ open class FunTeamHistoryMessageController: NEBaseTeamHistoryMessageController { withIdentifier: "\(NSStringFromClass(FunHistoryMessageCell.self))", for: indexPath ) as! NEBaseHistoryMessageCell - let cellModel = viewmodel.searchResultInfos?[indexPath.row] + let cellModel = viewModel.searchResultInfos?[indexPath.row] cell.searchText = searchStr cell.configData(message: cellModel) return cell diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamInfoViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamInfoViewController.swift index 3278d2ff..9e399574 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamInfoViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamInfoViewController.swift @@ -7,9 +7,9 @@ import UIKit @objcMembers open class FunTeamInfoViewController: NEBaseTeamInfoViewController { - override init(team: NIMTeam?) { + override init(team: V2NIMTeam?) { super.init(team: team) - cellClassDic = [ + registerCellDic = [ SettingCellType.SettingArrowCell.rawValue: FunTeamArrowSettingCell.self, SettingCellType.SettingHeaderCell.rawValue: FunTeamSettingHeaderCell.self, ] @@ -17,12 +17,12 @@ open class FunTeamInfoViewController: NEBaseTeamInfoViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() - viewmodel.cellDatas.forEach { cellModel in + for cellModel in viewModel.cellDatas { cellModel.cornerType = .none if cellModel.type == SettingCellType.SettingArrowCell.rawValue { cellModel.rowHeight = 56 @@ -42,7 +42,7 @@ open class FunTeamInfoViewController: NEBaseTeamInfoViewController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] if let cell = tableView.dequeueReusableCell( withIdentifier: "\(model.type)", for: indexPath @@ -54,15 +54,15 @@ open class FunTeamInfoViewController: NEBaseTeamInfoViewController { } override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] if indexPath.row == 0 { let avatar = FunTeamAvatarViewController() avatar.team = team weak var weakSelf = self avatar.block = { if let t = weakSelf?.team { - weakSelf?.viewmodel.getData(t) - weakSelf?.contentTable.reloadData() + weakSelf?.viewModel.getData(t) + weakSelf?.contentTableView.reloadData() } } navigationController?.pushViewController(avatar, animated: true) diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManageController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerController.swift similarity index 51% rename from NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManageController.swift rename to NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerController.swift index 0ad8eec9..4b4d73d8 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerController.swift @@ -5,7 +5,7 @@ import UIKit @objcMembers -open class FunTeamManageController: NEBaseTeamManageController { +open class FunTeamManagerController: NEBaseTeamManagerController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) cellClassDic = [ @@ -16,7 +16,7 @@ open class FunTeamManageController: NEBaseTeamManageController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -26,9 +26,10 @@ open class FunTeamManageController: NEBaseTeamManageController { navigationView.backgroundColor = .ne_lightBackgroundColor } + /// 加载数据,在子类中重新设置样式 override open func reloadSectionData() { - viewmodel.sectionData.forEach { setionModel in - setionModel.cellModels.forEach { cellModel in + for setionModel in viewModel.sectionData { + for cellModel in setionModel.cellModels { cellModel.cornerType = .none if cellModel.rowHeight > 70 { cellModel.rowHeight = 78 @@ -39,29 +40,9 @@ open class FunTeamManageController: NEBaseTeamManageController { } } - override open func getFooterView() -> UIView? { - let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - footer.addSubview(button) - button.backgroundColor = .white - button.clipsToBounds = true - button.setTitleColor(NEConstant.hexRGB(0xE6605C), for: .normal) - button.titleLabel?.font = NEConstant.defaultTextFont(16.0) - button.setTitle(localizable("transfer_owner"), for: .normal) - button.addTarget(self, action: #selector(transferOwner), for: .touchUpInside) - NSLayoutConstraint.activate([ - button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 0), - button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: 0), - button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), - button.heightAnchor.constraint(equalToConstant: 56), - ]) - return footer - } - override open func didManagerClick() { let controller = FunTeamManagerListController() - controller.teamId = viewmodel.teamInfoModel?.team?.teamId + controller.teamId = viewModel.teamInfoModel?.team?.teamId navigationController?.pushViewController(controller, animated: true) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift index 62c25c82..8c59e80f 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift @@ -13,7 +13,7 @@ open class FunTeamManagerListController: NEBaseTeamManagerListController { cellClassDic = [0: FunTeamArrowSettingCell.self, 1: FunTeamManagerMemberCell.self] } - lazy var emptyView: NEEmptyDataView = { + public lazy var emptyView: NEEmptyDataView = { let view = NEEmptyDataView(imageName: "fun_user_empty", content: localizable("no_manager_member"), frame: CGRect.zero) view.translatesAutoresizingMaskIntoConstraints = false view.isHidden = true @@ -58,8 +58,8 @@ open class FunTeamManagerListController: NEBaseTeamManagerListController { cell.delegate = self cell.index = indexPath.row cell.configure(viewmodel.managers[indexPath.row]) - if let type = viewmodel.currentMember?.type, type == .manager { - cell.removeBtn.isHidden = true + if let type = viewmodel.currentMember?.memberRole, type == .TEAM_MEMBER_ROLE_MANAGER { + cell.removeButton.isHidden = true cell.removeLabel.isHidden = true } return cell @@ -78,6 +78,7 @@ open class FunTeamManagerListController: NEBaseTeamManagerListController { if indexPath.section == 0 { let selectController = FunTeamMemberSelectController() selectController.teamId = teamId + selectController.selectMemberBlock = { [weak self] datas in self?.didAddManagers(datas) } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift index 02a31097..eabcbfac 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift @@ -24,7 +24,7 @@ open class FunTeamMemberSelectController: NEBaseTeamMemberSelectController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "\(indexPath.section)", for: indexPath) as! FunTeamMemberSelectCell - let member = viewmodel.showDatas[indexPath.row] + let member = viewModel.showDatas[indexPath.row] cell.configureMember(member) return cell } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift index 1976eeaa..b63064bd 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift @@ -17,15 +17,15 @@ open class FunTeamMembersController: NEBaseTeamMembersController { override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funTeamBackgroundColor - contentTable.register(FunTeamMemberCell.self, forCellReuseIdentifier: "\(FunTeamMemberCell.self)") - view.insertSubview(searchGrayBackView, belowSubview: back) + contentTableView.register(FunTeamMemberCell.self, forCellReuseIdentifier: "\(FunTeamMemberCell.self)") + view.insertSubview(searchGrayBackView, belowSubview: backView) NSLayoutConstraint.activate([ searchGrayBackView.leftAnchor.constraint(equalTo: view.leftAnchor), searchGrayBackView.rightAnchor.constraint(equalTo: view.rightAnchor), searchGrayBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - searchGrayBackView.bottomAnchor.constraint(equalTo: contentTable.topAnchor), + searchGrayBackView.bottomAnchor.constraint(equalTo: contentTableView.topAnchor), ]) - back.backgroundColor = UIColor.white + backView.backgroundColor = UIColor.white searchTextField.backgroundColor = UIColor.white emptyView.setEmptyImage(name: "fun_user_empty") @@ -39,19 +39,19 @@ open class FunTeamMembersController: NEBaseTeamMembersController { if let model = getRealModel(indexPath.row) { cell.configure(model) var isShowRemove = false - if isOwner(model.nimUser?.userId) { + if isOwner(model.nimUser?.user?.accountId) { cell.ownerLabel.isHidden = false cell.ownerLabel.text = localizable("team_owner") cell.setOwnerStyle() - } else if model.teamMember?.type == .manager { + } else if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { cell.ownerLabel.isHidden = false cell.ownerLabel.text = localizable("team_manager") cell.setManagerStyle() - if isOwner(IMKitClient.instance.imAccid()) { + if isOwner(IMKitClient.instance.account()) { isShowRemove = true } } else { - if isOwner(IMKitClient.instance.imAccid()) || viewmodel.currentMember?.type == .manager { + if isOwner(IMKitClient.instance.account()) || viewModel.currentMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { isShowRemove = true } cell.ownerLabel.isHidden = true @@ -59,7 +59,7 @@ open class FunTeamMembersController: NEBaseTeamMembersController { cell.index = indexPath.row cell.delegate = self cell.configure(model) - cell.removeBtn.isHidden = !isShowRemove + cell.removeButton.isHidden = !isShowRemove cell.removeLabel.isHidden = !isShowRemove } if isLastRow(indexPath.row) { @@ -78,11 +78,11 @@ open class FunTeamMembersController: NEBaseTeamMembersController { func isLastRow(_ index: Int) -> Bool { if let text = searchTextField.text, text.count > 0 { - if searchDatas.count - 1 == index { + if viewModel.searchDatas.count - 1 == index { return true } } - if viewmodel.datas.count - 1 == index { + if viewModel.datas.count - 1 == index { return true } return false diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamNameViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamNameViewController.swift index c4d43514..d49ffa52 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamNameViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamNameViewController.swift @@ -24,10 +24,10 @@ open class FunTeamNameViewController: NEBaseTeamNameViewController { ]) NSLayoutConstraint.activate([ - textView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), - textView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -32), - textView.centerYAnchor.constraint(equalTo: backView.centerYAnchor, constant: 0), - textView.heightAnchor.constraint(equalToConstant: 60), + textInputView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + textInputView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -32), + textInputView.centerYAnchor.constraint(equalTo: backView.centerYAnchor, constant: 0), + textInputView.heightAnchor.constraint(equalToConstant: 60), ]) NSLayoutConstraint.activate([ @@ -39,15 +39,15 @@ open class FunTeamNameViewController: NEBaseTeamNameViewController { } override open func disableSubmit() { - rightNavBtn.setTitleColor(.funTeamThemeDisableColor, for: .normal) - rightNavBtn.isEnabled = false + rightNavButton.setTitleColor(.funTeamThemeDisableColor, for: .normal) + rightNavButton.isEnabled = false navigationView.moreButton.setTitleColor(.funTeamThemeDisableColor, for: .normal) navigationView.moreButton.isEnabled = false } override open func enableSubmit() { - rightNavBtn.setTitleColor(.funTeamThemeColor, for: .normal) - rightNavBtn.isEnabled = true + rightNavButton.setTitleColor(.funTeamThemeColor, for: .normal) + rightNavButton.isEnabled = true navigationView.moreButton.setTitleColor(.funTeamThemeColor, for: .normal) navigationView.moreButton.isEnabled = true } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift index f9fe667e..c0522288 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift @@ -3,12 +3,63 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @objcMembers open class FunTeamSettingViewController: NEBaseTeamSettingViewController { + /// 顶部背景视图 + lazy var backView: UIView = { + let backView = UIView() + backView.frame = CGRect(x: 0, y: 0, width: NEConstant.screenWidth, height: 188) + return backView + }() + + /// 圆角视图 + lazy var cornerView: UIView = { + let cornerView = UIView() + return cornerView + }() + + /// 群信息跳转下一级页面指示箭头 + lazy var arrowImageView: UIImageView = { + let arrowImageView = UIImageView() + arrowImageView.translatesAutoresizingMaskIntoConstraints = false + arrowImageView.image = coreLoader.loadImage("arrowRight") + return arrowImageView + }() + + /// 分割线 + lazy var dividerLineView: UIView = { + let dividerLineView = UIView() + dividerLineView.translatesAutoresizingMaskIntoConstraints = false + dividerLineView.backgroundColor = NEConstant.hexRGB(0xF5F8FC) + return dividerLineView + }() + + /// 成员列表跳转下一级页面指示箭头 + public var memberArrowImageView: UIImageView = { + let memberArrowImageView = UIImageView() + memberArrowImageView.translatesAutoresizingMaskIntoConstraints = false + memberArrowImageView.image = coreLoader.loadImage("arrowRight") + return memberArrowImageView + }() + + /// 群成员列表按钮 + public var memberListButton: UIButton = { + let memberListButton = UIButton() + memberListButton.translatesAutoresizingMaskIntoConstraints = false + return memberListButton + }() + + /// 群信息页面跳转按钮 + public var infoButton: UIButton = { + let infoButton = UIButton() + infoButton.translatesAutoresizingMaskIntoConstraints = false + return infoButton + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) className = "FunTeamSettingViewController" @@ -20,12 +71,12 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func reloadSectionData() { - viewmodel.sectionData.forEach { setionModel in - setionModel.cellModels.forEach { cellModel in + for setionModel in viewModel.sectionData { + for cellModel in setionModel.cellModels { cellModel.cornerType = .none if cellModel.type == SettingCellType.SettingSelectCell.rawValue { cellModel.rowHeight = 78 @@ -39,85 +90,61 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { override open func setupUI() { super.setupUI() view.backgroundColor = .funTeamBackgroundColor - teamHeader.layer.cornerRadius = 4.0 - addBtn.setImage(coreLoader.loadImage("fun_add"), for: .normal) + teamHeaderView.layer.cornerRadius = 4.0 + addButton.setImage(coreLoader.loadImage("fun_add"), for: .normal) navigationController?.navigationBar.backgroundColor = .white navigationView.backgroundColor = .white navigationView.titleBarBottomLine.isHidden = false } + /// 获取顶部 override open func getHeaderView() -> UIView { - let back = UIView() - back.frame = CGRect(x: 0, y: 0, width: NEConstant.screenWidth, height: 188) - let cornerView = UIView() - back.addSubview(cornerView) + backView.addSubview(cornerView) cornerView.backgroundColor = .white cornerView.clipsToBounds = true cornerView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - cornerView.leftAnchor.constraint(equalTo: back.leftAnchor, constant: 0), - cornerView.rightAnchor.constraint(equalTo: back.rightAnchor, constant: 0), - cornerView.topAnchor.constraint(equalTo: back.topAnchor), - cornerView.bottomAnchor.constraint(equalTo: back.bottomAnchor), + cornerView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 0), + cornerView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: 0), + cornerView.topAnchor.constraint(equalTo: backView.topAnchor), + cornerView.bottomAnchor.constraint(equalTo: backView.bottomAnchor), ]) - cornerView.addSubview(teamHeader) + cornerView.addSubview(teamHeaderView) NSLayoutConstraint.activate([ - teamHeader.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16), - teamHeader.topAnchor.constraint(equalTo: cornerView.topAnchor, constant: 16), - teamHeader.widthAnchor.constraint(equalToConstant: 50), - teamHeader.heightAnchor.constraint(equalToConstant: 50), + teamHeaderView.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16), + teamHeaderView.topAnchor.constraint(equalTo: cornerView.topAnchor, constant: 16), + teamHeaderView.widthAnchor.constraint(equalToConstant: 50), + teamHeaderView.heightAnchor.constraint(equalToConstant: 50), ]) - if let url = viewmodel.teamInfoModel?.team?.avatarUrl, !url.isEmpty { - print("icon url : ", url) - teamHeader.sd_setImage(with: URL(string: url), completed: nil) - } else { - if let tid = teamId { - if let name = viewmodel.teamInfoModel?.team?.getShowName() { - teamHeader.setTitle(name) - } - teamHeader.backgroundColor = UIColor.colorWithString(string: "\(tid)") - } - } - teamNameLabel.text = viewmodel.teamInfoModel?.team?.getShowName() + setTeamHeaderInfo() cornerView.addSubview(teamNameLabel) NSLayoutConstraint.activate([ - teamNameLabel.leftAnchor.constraint(equalTo: teamHeader.rightAnchor, constant: 16), - teamNameLabel.centerYAnchor.constraint(equalTo: teamHeader.centerYAnchor), + teamNameLabel.leftAnchor.constraint(equalTo: teamHeaderView.rightAnchor, constant: 16), + teamNameLabel.centerYAnchor.constraint(equalTo: teamHeaderView.centerYAnchor), teamNameLabel.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -50), ]) - let arrow = UIImageView() - arrow.translatesAutoresizingMaskIntoConstraints = false - arrow.image = coreLoader.loadImage("arrowRight") - cornerView.addSubview(arrow) + cornerView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: teamHeader.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -16), + arrowImageView.centerYAnchor.constraint(equalTo: teamHeaderView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -16), ]) - let line = UIView() - line.translatesAutoresizingMaskIntoConstraints = false - line.backgroundColor = NEConstant.hexRGB(0xF5F8FC) - cornerView.addSubview(line) + cornerView.addSubview(dividerLineView) NSLayoutConstraint.activate([ - line.heightAnchor.constraint(equalToConstant: 1.0), - line.rightAnchor.constraint(equalTo: cornerView.rightAnchor), - line.leftAnchor.constraint(equalTo: teamHeader.leftAnchor, constant: 0), - line.topAnchor.constraint(equalTo: teamHeader.bottomAnchor, constant: 16.0), + dividerLineView.heightAnchor.constraint(equalToConstant: 1.0), + dividerLineView.rightAnchor.constraint(equalTo: cornerView.rightAnchor), + dividerLineView.leftAnchor.constraint(equalTo: teamHeaderView.leftAnchor, constant: 0), + dividerLineView.topAnchor.constraint(equalTo: teamHeaderView.bottomAnchor, constant: 16.0), ]) - let memberLabel = UILabel() - cornerView.addSubview(memberLabel) - memberLabel.translatesAutoresizingMaskIntoConstraints = false - memberLabel.textColor = NEConstant.hexRGB(0x333333) - memberLabel.font = NEConstant.defaultTextFont(16.0) cornerView.addSubview(memberLabel) NSLayoutConstraint.activate([ - memberLabel.leftAnchor.constraint(equalTo: line.leftAnchor), - memberLabel.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 16), + memberLabel.leftAnchor.constraint(equalTo: dividerLineView.leftAnchor), + memberLabel.topAnchor.constraint(equalTo: dividerLineView.bottomAnchor, constant: 16), ]) if teamSettingType == .Senior { @@ -126,73 +153,66 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { memberLabel.text = localizable("discuss_mebmer") } - let memberArrow = UIImageView() - cornerView.addSubview(memberArrow) - memberArrow.translatesAutoresizingMaskIntoConstraints = false - memberArrow.image = coreLoader.loadImage("arrowRight") + cornerView.addSubview(memberArrowImageView) NSLayoutConstraint.activate([ - memberArrow.rightAnchor.constraint(equalTo: arrow.rightAnchor, constant: 0), - memberArrow.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), + memberArrowImageView.rightAnchor.constraint(equalTo: arrowImageView.rightAnchor, constant: 0), + memberArrowImageView.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), ]) - let memberListBtn = UIButton() - cornerView.addSubview(memberListBtn) - memberListBtn.translatesAutoresizingMaskIntoConstraints = false + cornerView.addSubview(memberListButton) NSLayoutConstraint.activate([ - memberListBtn.leftAnchor.constraint(equalTo: memberLabel.leftAnchor), - memberListBtn.rightAnchor.constraint(equalTo: memberArrow.rightAnchor), - memberListBtn.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), - memberListBtn.heightAnchor.constraint(equalToConstant: 50), + memberListButton.leftAnchor.constraint(equalTo: memberLabel.leftAnchor), + memberListButton.rightAnchor.constraint(equalTo: memberArrowImageView.rightAnchor), + memberListButton.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), + memberListButton.heightAnchor.constraint(equalToConstant: 50), ]) - memberListBtn.addTarget(self, action: #selector(toMemberList), for: .touchUpInside) + memberListButton.addTarget(self, action: #selector(toMemberList), for: .touchUpInside) cornerView.addSubview(memberCountLabel) NSLayoutConstraint.activate([ - memberCountLabel.rightAnchor.constraint(equalTo: memberArrow.leftAnchor, constant: -8), - memberCountLabel.centerYAnchor.constraint(equalTo: memberArrow.centerYAnchor), + memberCountLabel.rightAnchor.constraint(equalTo: memberArrowImageView.leftAnchor, constant: -8), + memberCountLabel.centerYAnchor.constraint(equalTo: memberArrowImageView.centerYAnchor), ]) - memberCountLabel.text = "\(viewmodel.teamInfoModel?.team?.memberNumber ?? 0)" + memberCountLabel.text = "\(viewModel.teamInfoModel?.team?.memberCount ?? 0)" - cornerView.addSubview(addBtn) - addBtnWidth = addBtn.widthAnchor.constraint(equalToConstant: 36) - addBtnWidth?.isActive = true - addBtnLeftMargin = addBtn.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0) + cornerView.addSubview(addButton) + addButtonWidth = addButton.widthAnchor.constraint(equalToConstant: 36) + addButtonWidth?.isActive = true + addButtonLeftMargin = addButton.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0) NSLayoutConstraint.activate([ - addBtnLeftMargin!, - addBtn.topAnchor.constraint(equalTo: memberListBtn.bottomAnchor, constant: 0), + addButtonLeftMargin!, + addButton.topAnchor.constraint(equalTo: memberListButton.bottomAnchor, constant: 0), ]) - addBtn.addTarget(self, action: #selector(addUser), for: .touchUpInside) + addButton.addTarget(self, action: #selector(addUser), for: .touchUpInside) - if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, - let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, let member = viewmodel.memberInTeam, inviteMode == .manager, member.type != .manager { - addBtnWidth?.constant = 0 - addBtn.isHidden = true + if viewModel.isNormalTeam() == false, viewModel.isOwner() == false, + let inviteMode = viewModel.teamInfoModel?.team?.inviteMode, let member = viewModel.memberInTeam, inviteMode == .TEAM_INVITE_MODE_MANAGER, member.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + addButtonWidth?.constant = 0 + addButton.isHidden = true } setupUserInfoCollection(cornerView) - let infoBtn = UIButton() - infoBtn.translatesAutoresizingMaskIntoConstraints = false - cornerView.addSubview(infoBtn) + cornerView.addSubview(infoButton) NSLayoutConstraint.activate([ - infoBtn.leftAnchor.constraint(equalTo: teamHeader.leftAnchor), - infoBtn.topAnchor.constraint(equalTo: teamHeader.topAnchor), - infoBtn.bottomAnchor.constraint(equalTo: teamHeader.bottomAnchor), - infoBtn.rightAnchor.constraint(equalTo: arrow.rightAnchor), + infoButton.leftAnchor.constraint(equalTo: teamHeaderView.leftAnchor), + infoButton.topAnchor.constraint(equalTo: teamHeaderView.topAnchor), + infoButton.bottomAnchor.constraint(equalTo: teamHeaderView.bottomAnchor), + infoButton.rightAnchor.constraint(equalTo: arrowImageView.rightAnchor), ]) - infoBtn.addTarget(self, action: #selector(toInfoView), for: .touchUpInside) + infoButton.addTarget(self, action: #selector(toInfoView), for: .touchUpInside) - return back + return backView } override open func getFooterView() -> UIView? { guard let title = getBottomText() else { return nil } - let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) + let footerView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false - footer.addSubview(button) + footerView.addSubview(button) button.backgroundColor = .white button.clipsToBounds = true button.setTitleColor(NEConstant.hexRGB(0xE6605C), for: .normal) @@ -200,44 +220,44 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { button.setTitle(title, for: .normal) button.addTarget(self, action: #selector(removeTeamForMyself), for: .touchUpInside) NSLayoutConstraint.activate([ - button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 0), - button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: 0), - button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), + button.leftAnchor.constraint(equalTo: footerView.leftAnchor, constant: 0), + button.rightAnchor.constraint(equalTo: footerView.rightAnchor, constant: 0), + button.topAnchor.constraint(equalTo: footerView.topAnchor, constant: 12), button.heightAnchor.constraint(equalToConstant: 56), ]) - return footer + return footerView } override open func setupUserInfoCollection(_ cornerView: UIView) { - cornerView.addSubview(userinfoCollection) + cornerView.addSubview(userinfoCollectionView) NSLayoutConstraint.activate([ - userinfoCollection.leftAnchor.constraint(equalTo: addBtn.rightAnchor, constant: 16), - userinfoCollection.centerYAnchor.constraint(equalTo: addBtn.centerYAnchor), - userinfoCollection.rightAnchor.constraint( + userinfoCollectionView.leftAnchor.constraint(equalTo: addButton.rightAnchor, constant: 16), + userinfoCollectionView.centerYAnchor.constraint(equalTo: addButton.centerYAnchor), + userinfoCollectionView.rightAnchor.constraint( equalTo: cornerView.rightAnchor, constant: -16 ), - userinfoCollection.heightAnchor.constraint(equalToConstant: 36), + userinfoCollectionView.heightAnchor.constraint(equalToConstant: 36), ]) - userinfoCollection.register( + userinfoCollectionView.register( FunTeamUserCell.self, forCellWithReuseIdentifier: "\(FunTeamUserCell.self)" ) } override open func checkoutAddShowOrHide() { - if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, - let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, inviteMode == .manager { - if let member = viewmodel.memberInTeam, member.type == .manager { - addBtn.isHidden = false - addBtnWidth?.constant = 36.0 - addBtnLeftMargin?.constant = 16 + if viewModel.isNormalTeam() == false, viewModel.isOwner() == false, + let inviteMode = viewModel.teamInfoModel?.team?.inviteMode, inviteMode == .TEAM_INVITE_MODE_MANAGER { + if let member = viewModel.memberInTeam, member.memberRole == .TEAM_MEMBER_ROLE_MANAGER { + addButton.isHidden = false + addButtonWidth?.constant = 36.0 + addButtonLeftMargin?.constant = 16 checkMemberCountLimit() } else { - addBtn.isHidden = true - addBtnWidth?.constant = 0 - addBtnLeftMargin?.constant = 0 + addButton.isHidden = true + addButtonWidth?.constant = 0 + addButtonLeftMargin?.constant = 0 } } else { checkMemberCountLimit() @@ -245,29 +265,27 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { } func checkMemberCountLimit() { - if viewmodel.teamInfoModel?.team?.level == viewmodel.teamInfoModel?.team?.memberNumber { - addBtn.isHidden = true - addBtnWidth?.constant = 0 - addBtnLeftMargin?.constant = 0 + if viewModel.teamInfoModel?.team?.memberLimit == viewModel.teamInfoModel?.team?.memberCount { + addButton.isHidden = true + addButtonWidth?.constant = 0 + addButtonLeftMargin?.constant = 0 } else { - addBtn.isHidden = false - addBtnWidth?.constant = 36.0 - addBtnLeftMargin?.constant = 16 + addButton.isHidden = false + addButtonWidth?.constant = 36.0 + addButtonLeftMargin?.constant = 16 } } - // MARK: objc 方法 - override open func toInfoView() { - let info = FunTeamInfoViewController(team: viewmodel.teamInfoModel?.team) + let info = FunTeamInfoViewController(team: viewModel.teamInfoModel?.team) navigationController?.pushViewController(info, animated: true) } override open func didClickChangeNick() { let nick = FunTeamNameViewController() nick.type = .NickName - nick.team = viewmodel.teamInfoModel?.team - nick.teamMember = viewmodel.memberInTeam + nick.team = viewModel.teamInfoModel?.team + nick.teamMember = viewModel.memberInTeam navigationController?.pushViewController(nick, animated: true) } @@ -305,15 +323,14 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { } Router.shared.use( SearchMessageRouter, - parameters: ["nav": navigationController as Any, "teamId": tid], + parameters: ["nav": navigationController as Any, "teamId": tid, "teamInfo": viewModel.teamInfoModel as Any], closure: nil ) } override open func didClickTeamManage() { - let manageTeam = FunTeamManageController() - manageTeam.managerUsers = getManaterUsers() - manageTeam.viewmodel.teamInfoModel = viewmodel.teamInfoModel + let manageTeam = FunTeamManagerController() + manageTeam.viewModel.teamInfoModel = viewModel.teamInfoModel navigationController?.pushViewController(manageTeam, animated: true) } @@ -323,7 +340,7 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { withReuseIdentifier: "\(FunTeamUserCell.self)", for: indexPath ) as? FunTeamUserCell { - if let user = viewmodel.teamInfoModel?.users[indexPath.row] { + if let user = viewModel.teamInfoModel?.users[indexPath.row] { cell.user = user } return cell @@ -341,7 +358,7 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { } override open func toMemberList() { - let memberController = FunTeamMembersController(teamId: viewmodel.teamInfoModel?.team?.teamId) + let memberController = FunTeamMembersController(teamId: viewModel.teamInfoModel?.team?.teamId) navigationController?.pushViewController(memberController, animated: true) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamRouter.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamRouter.swift index f1e62df0..8cd03ce9 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamRouter.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamRouter.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK @@ -22,9 +22,11 @@ public extension TeamRouter { Router.shared.register(SearchMessageRouter) { param in let nav = param["nav"] as? UINavigationController - if let tid = param["teamId"] as? String { - let session = NIMSession(tid, type: .team) - let searchMsgCtrl = FunTeamHistoryMessageController(session: session) + if let teamId = param["teamId"] as? String { + let searchMsgCtrl = FunTeamHistoryMessageController(teamId: teamId) + if let info = param["teamInfo"] as? NETeamInfoModel { + searchMsgCtrl.viewModel.teamInfoModel = info + } nav?.pushViewController(searchMsgCtrl, animated: true) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NEBaseTeamRouter.swift b/NETeamUIKit/NETeamUIKit/Classes/NEBaseTeamRouter.swift index 60f68364..04af36c7 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NEBaseTeamRouter.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NEBaseTeamRouter.swift @@ -4,7 +4,7 @@ import Foundation import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK @@ -34,31 +34,31 @@ open class TeamRouter: NSObject { let iconUrl = (param["url"] as? String) ?? iconUrls[Int(arc4random()) % iconUrls.count] - let option = NIMCreateTeamOption() - option.type = .advanced - option.avatarUrl = iconUrl - option.name = name - option.joinMode = .noAuth - option.inviteMode = .all - option.beInviteMode = .noAuth - option.updateInfoMode = .all - option.updateClientCustomMode = .all + let param = V2NIMCreateTeamParams() + param.name = name + param.teamType = .TEAM_TYPE_NORMAL + param.avatar = iconUrl + param.joinMode = .TEAM_JOIN_MODE_FREE + param.inviteMode = .TEAM_INVITE_MODE_ALL + param.agreeMode = .TEAM_AGREE_MODE_NO_AUTH + param.updateInfoMode = .TEAM_UPDATE_INFO_MODE_ALL + param.updateExtensionMode = .TEAM_UPDATE_EXTENSION_MODE_ALL var disucssFlag = [String: Any]() disucssFlag[discussTeamKey] = true let jsonString = NECommonUtil.getJSONStringFromDictionary(disucssFlag) if jsonString.count > 0 { - option.clientCustomInfo = jsonString + param.serverExtension = jsonString } - repo.createAdvanceTeam(accids, option) { error, teamid, failedIds in + repo.createTeam(accids, param, nil, nil) { createResult, error in var result = [String: Any]() - if let err = error { + if let err = error as? NSError { result["code"] = err.code result["msg"] = err.localizedDescription } else { result["code"] = 0 result["msg"] = "ok" - result["teamId"] = teamid + result["teamId"] = createResult?.team?.teamId } Router.shared.use(TeamCreateDiscussResult, parameters: result, closure: nil) } @@ -76,26 +76,23 @@ open class TeamRouter: NSObject { let iconUrl = (param["url"] as? String) ?? iconUrls[Int(arc4random()) % iconUrls.count] - let option = NIMCreateTeamOption() - option.type = .advanced - option.avatarUrl = iconUrl - option.name = name - option.beInviteMode = .noAuth - - repo.createAdvanceTeam(accids, option) { error, teamid, failedIds in + let param = V2NIMCreateTeamParams() + param.name = name + param.avatar = iconUrl + param.teamType = .TEAM_TYPE_NORMAL + param.agreeMode = .TEAM_AGREE_MODE_NO_AUTH + // TODO: 拆分步骤 + repo.createTeam(accids, param, nil, nil) { creatResult, error in var result = [String: Any]() - if let err = error { + if let err = error as? NSError { result["code"] = err.code result["msg"] = err.localizedDescription } else { result["code"] = 0 result["msg"] = "ok" - result["teamId"] = teamid + result["teamId"] = creatResult?.team?.teamId - repo.sendCreateAdavanceNoti( - teamid ?? "", - localizable("create_senior_team_noti") - ) { error in + repo.sendCreateAdavanceNoti(creatResult?.team?.teamId ?? "", localizable("create_senior_team_noti")) { error in print("send noti message : ", error as Any) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/HistoryMessageCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/HistoryMessageCell.swift index 2c77cba5..0c57ab74 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/HistoryMessageCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/HistoryMessageCell.swift @@ -1,5 +1,6 @@ import NIMSDK + // Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. @@ -10,30 +11,30 @@ open class HistoryMessageCell: NEBaseHistoryMessageCell { override func setupSubviews() { super.setupSubviews() NSLayoutConstraint.activate([ - headImge.leftAnchor.constraint( + headView.leftAnchor.constraint( equalTo: contentView.leftAnchor, constant: NEConstant.screenInterval ), - headImge.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: -5), - headImge.widthAnchor.constraint(equalToConstant: 36), - headImge.heightAnchor.constraint(equalToConstant: 36), + headView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: -5), + headView.widthAnchor.constraint(equalToConstant: 36), + headView.heightAnchor.constraint(equalToConstant: 36), ]) NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - title.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - title.topAnchor.constraint(equalTo: headImge.topAnchor), + titleLabel.leftAnchor.constraint(equalTo: headView.rightAnchor, constant: 12), + titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + titleLabel.topAnchor.constraint(equalTo: headView.topAnchor), ]) NSLayoutConstraint.activate([ - subTitle.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - subTitle.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), - subTitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), + subTitleLabel.leftAnchor.constraint(equalTo: headView.rightAnchor, constant: 12), + subTitleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), + subTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), ]) NSLayoutConstraint.activate([ bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), - bottomLine.leftAnchor.constraint(equalTo: headImge.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: headView.leftAnchor), bottomLine.bottomAnchor.constraint(equalTo: bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 0.5), ]) diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamArrowSettingCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamArrowSettingCell.swift index e14d218a..d918cb72 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamArrowSettingCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamArrowSettingCell.swift @@ -16,8 +16,8 @@ open class TeamArrowSettingCell: NEBaseTeamArrowSettingCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamDefaultIconCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamDefaultIconCell.swift index 04e15474..e2377abf 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamDefaultIconCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamDefaultIconCell.swift @@ -11,17 +11,17 @@ open class TeamDefaultIconCell: NEBaseTeamDefaultIconCell { override func setupUI() { super.setupUI() NSLayoutConstraint.activate([ - selectBack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - selectBack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - selectBack.widthAnchor.constraint(equalToConstant: 48.0), - selectBack.heightAnchor.constraint(equalToConstant: 48.0), + selectBackView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + selectBackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + selectBackView.widthAnchor.constraint(equalToConstant: 48.0), + selectBackView.heightAnchor.constraint(equalToConstant: 48.0), ]) NSLayoutConstraint.activate([ - iconImage.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - iconImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - iconImage.heightAnchor.constraint(equalToConstant: 32), - iconImage.widthAnchor.constraint(equalToConstant: 32), + iconImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconImageView.heightAnchor.constraint(equalToConstant: 32), + iconImageView.widthAnchor.constraint(equalToConstant: 32), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingHeaderCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingHeaderCell.swift index 21dd5155..575545af 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingHeaderCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingHeaderCell.swift @@ -19,12 +19,12 @@ open class TeamSettingHeaderCell: NEBaseTeamSettingHeaderCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), ]) NSLayoutConstraint.activate([ - headerView.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), + headerView.centerYAnchor.constraint(equalTo: arrowView.centerYAnchor), headerView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -64.0), headerView.widthAnchor.constraint(equalToConstant: 42.0), headerView.heightAnchor.constraint(equalToConstant: 42.0), diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.swift index f4a52175..ff3b7752 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.swift @@ -19,8 +19,8 @@ open class TeamSettingLabelArrowCell: TeamArrowSettingCell { super.setupUI() contentView.addSubview(arrowLabel) NSLayoutConstraint.activate([ - arrowLabel.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), - arrowLabel.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -4), + arrowLabel.centerYAnchor.constraint(equalTo: arrowView.centerYAnchor), + arrowLabel.rightAnchor.constraint(equalTo: arrowView.leftAnchor, constant: -4), ]) } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingSelectCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingSelectCell.swift index e5273e3a..4812feb3 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingSelectCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingSelectCell.swift @@ -7,7 +7,7 @@ import UIKit @objcMembers open class TeamSettingSelectCell: NEBaseTeamSettingSelectCell { - override func setupUI() { + override open func setupUI() { super.setupUI() NSLayoutConstraint.activate([ titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 36), @@ -22,8 +22,8 @@ open class TeamSettingSelectCell: NEBaseTeamSettingSelectCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamUserCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamUserCell.swift index 28b23b25..cfaa8932 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamUserCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamUserCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -13,12 +13,12 @@ import UIKit open class TeamUserCell: NEBaseTeamUserCell { override func setupUI() { super.setupUI() - userHeader.layer.cornerRadius = 16.0 + userHeaderView.layer.cornerRadius = 16.0 NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: contentView.leftAnchor), - userHeader.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - userHeader.widthAnchor.constraint(equalToConstant: 32.0), - userHeader.heightAnchor.constraint(equalToConstant: 32.0), + userHeaderView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + userHeaderView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + userHeaderView.widthAnchor.constraint(equalToConstant: 32.0), + userHeaderView.heightAnchor.constraint(equalToConstant: 32.0), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamAvatarViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamAvatarViewController.swift index 09e24d8c..346faf7d 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamAvatarViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamAvatarViewController.swift @@ -21,55 +21,55 @@ open class TeamAvatarViewController: NEBaseTeamAvatarViewController { navigationView.backButton.setTitleColor(.ne_greyText, for: .normal) navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor - headerBack.layer.cornerRadius = 8.0 + headerBackView.layer.cornerRadius = 8.0 NSLayoutConstraint.activate([ - headerBack.topAnchor.constraint(equalTo: view.topAnchor, constant: 12.0 + NEConstant.navigationAndStatusHeight), - headerBack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - headerBack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - headerBack.heightAnchor.constraint(equalToConstant: 128.0), + headerBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 12.0 + NEConstant.navigationAndStatusHeight), + headerBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + headerBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + headerBackView.heightAnchor.constraint(equalToConstant: 128.0), ]) NSLayoutConstraint.activate([ - photoImage.rightAnchor.constraint(equalTo: headerView.rightAnchor), - photoImage.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), + photoImageView.rightAnchor.constraint(equalTo: headerView.rightAnchor), + photoImageView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), ]) let gesture = UITapGestureRecognizer() headerView.addGestureRecognizer(gesture) gesture.addTarget(self, action: #selector(uploadPhoto)) - defaultHeaderBack.layer.cornerRadius = 8.0 + defaultHeaderBackView.layer.cornerRadius = 8.0 NSLayoutConstraint.activate([ - defaultHeaderBack.leftAnchor.constraint(equalTo: headerBack.leftAnchor), - defaultHeaderBack.rightAnchor.constraint(equalTo: headerBack.rightAnchor), - defaultHeaderBack.topAnchor.constraint( - equalTo: headerBack.bottomAnchor, + defaultHeaderBackView.leftAnchor.constraint(equalTo: headerBackView.leftAnchor), + defaultHeaderBackView.rightAnchor.constraint(equalTo: headerBackView.rightAnchor), + defaultHeaderBackView.topAnchor.constraint( + equalTo: headerBackView.bottomAnchor, constant: 12.0 ), - defaultHeaderBack.heightAnchor.constraint(equalToConstant: 114.0), + defaultHeaderBackView.heightAnchor.constraint(equalToConstant: 114.0), ]) NSLayoutConstraint.activate([ - tag.leftAnchor.constraint(equalTo: defaultHeaderBack.leftAnchor, constant: 16.0), - tag.topAnchor.constraint(equalTo: defaultHeaderBack.topAnchor, constant: 15.0), + tagLabel.leftAnchor.constraint(equalTo: defaultHeaderBackView.leftAnchor, constant: 16.0), + tagLabel.topAnchor.constraint(equalTo: defaultHeaderBackView.topAnchor, constant: 15.0), ]) - iconCollection.register( + iconsCollectionView.register( TeamDefaultIconCell.self, forCellWithReuseIdentifier: "\(TeamDefaultIconCell.self)" ) NSLayoutConstraint.activate([ - iconCollection.topAnchor.constraint(equalTo: tag.bottomAnchor, constant: 16.0), - iconCollection.leftAnchor.constraint( - equalTo: defaultHeaderBack.leftAnchor, + iconsCollectionView.topAnchor.constraint(equalTo: tagLabel.bottomAnchor, constant: 16.0), + iconsCollectionView.leftAnchor.constraint( + equalTo: defaultHeaderBackView.leftAnchor, constant: 18 ), - iconCollection.rightAnchor.constraint( - equalTo: defaultHeaderBack.rightAnchor, + iconsCollectionView.rightAnchor.constraint( + equalTo: defaultHeaderBackView.rightAnchor, constant: -18.0 ), - iconCollection.heightAnchor.constraint(equalToConstant: 48.0), + iconsCollectionView.heightAnchor.constraint(equalToConstant: 48.0), ]) } @@ -79,8 +79,8 @@ open class TeamAvatarViewController: NEBaseTeamAvatarViewController { withReuseIdentifier: "\(TeamDefaultIconCell.self)", for: indexPath ) as? TeamDefaultIconCell { - cell.iconImage.image = coreLoader.loadImage("icon_\(indexPath.row)") - cell.iconImage.accessibilityIdentifier = "id.default\(indexPath.row + 1)" + cell.iconImageView.image = coreLoader.loadImage("icon_\(indexPath.row)") + cell.iconImageView.accessibilityIdentifier = "id.default\(indexPath.row + 1)" return cell } return UICollectionViewCell() diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift index 9ab49b1f..0ccf7ec0 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift @@ -8,15 +8,15 @@ import UIKit @objcMembers open class TeamHistoryMessageController: NEBaseTeamHistoryMessageController { - override public init(session: NIMSession?) { - super.init(session: session) + override public init(teamId: String?) { + super.init(teamId: teamId) tag = "TeamHistoryMessageController" navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupSubviews() { @@ -49,7 +49,7 @@ open class TeamHistoryMessageController: NEBaseTeamHistoryMessageController { withIdentifier: "\(NSStringFromClass(HistoryMessageCell.self))", for: indexPath ) as! NEBaseHistoryMessageCell - let cellModel = viewmodel.searchResultInfos?[indexPath.row] + let cellModel = viewModel.searchResultInfos?[indexPath.row] cell.searchText = searchStr cell.configData(message: cellModel) return cell diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift index 04528f6a..70a3fb2d 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift @@ -8,9 +8,9 @@ import UIKit @objcMembers open class TeamInfoViewController: NEBaseTeamInfoViewController { - override public init(team: NIMTeam?) { + override public init(team: V2NIMTeam?) { super.init(team: team) - cellClassDic = [ + registerCellDic = [ SettingCellType.SettingArrowCell.rawValue: TeamArrowSettingCell.self, SettingCellType.SettingHeaderCell.rawValue: TeamSettingHeaderCell.self, ] @@ -20,7 +20,7 @@ open class TeamInfoViewController: NEBaseTeamInfoViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { @@ -32,7 +32,7 @@ open class TeamInfoViewController: NEBaseTeamInfoViewController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] if let cell = tableView.dequeueReusableCell( withIdentifier: "\(model.type)", for: indexPath @@ -44,15 +44,15 @@ open class TeamInfoViewController: NEBaseTeamInfoViewController { } override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] if indexPath.row == 0 { let avatar = TeamAvatarViewController() avatar.team = team weak var weakSelf = self avatar.block = { if let t = weakSelf?.team { - weakSelf?.viewmodel.getData(t) - weakSelf?.contentTable.reloadData() + weakSelf?.viewModel.getData(t) + weakSelf?.contentTableView.reloadData() } } navigationController?.pushViewController(avatar, animated: true) @@ -72,7 +72,7 @@ open class TeamInfoViewController: NEBaseTeamInfoViewController { override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] return model.rowHeight } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManageController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManageController.swift deleted file mode 100644 index 9685464b..00000000 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManageController.swift +++ /dev/null @@ -1,55 +0,0 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NIMSDK -import UIKit - -@objcMembers -open class TeamManageController: NEBaseTeamManageController { - override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - navigationView.backgroundColor = .ne_lightBackgroundColor - navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor - cellClassDic = [ - SettingCellType.SettingArrowCell.rawValue: TeamSettingLabelArrowCell.self, - SettingCellType.SettingSwitchCell.rawValue: TeamSettingSwitchCell.self, - SettingCellType.SettingSelectCell.rawValue: TeamSettingSelectCell.self, - ] - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override open func viewDidLoad() { - super.viewDidLoad() - } - - override open func getFooterView() -> UIView? { - let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - footer.addSubview(button) - button.backgroundColor = .white - button.clipsToBounds = true - button.setTitleColor(NEConstant.hexRGB(0xE6605C), for: .normal) - button.titleLabel?.font = NEConstant.defaultTextFont(16.0) - button.setTitle(localizable("transfer_owner"), for: .normal) - button.addTarget(self, action: #selector(transferOwner), for: .touchUpInside) - button.layer.cornerRadius = 8.0 - NSLayoutConstraint.activate([ - button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 20), - button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: -20), - button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), - button.heightAnchor.constraint(equalToConstant: 40), - ]) - return footer - } - - override open func didManagerClick() { - let controller = TeamManagerListController() - controller.teamId = viewmodel.teamInfoModel?.team?.teamId - navigationController?.pushViewController(controller, animated: true) - } -} diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerController.swift new file mode 100644 index 00000000..82568a93 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerController.swift @@ -0,0 +1,34 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NIMSDK +import UIKit + +@objcMembers +open class TeamManagerController: NEBaseTeamManagerController { + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + navigationView.backgroundColor = .ne_lightBackgroundColor + navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor + cellClassDic = [ + SettingCellType.SettingArrowCell.rawValue: TeamSettingLabelArrowCell.self, + SettingCellType.SettingSwitchCell.rawValue: TeamSettingSwitchCell.self, + SettingCellType.SettingSelectCell.rawValue: TeamSettingSelectCell.self, + ] + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + } + + override open func didManagerClick() { + let controller = TeamManagerListController() + controller.teamId = viewModel.teamInfoModel?.team?.teamId + navigationController?.pushViewController(controller, animated: true) + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift index 7f367af2..c1feb609 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift @@ -59,8 +59,8 @@ open class TeamManagerListController: NEBaseTeamManagerListController { cell.delegate = self cell.index = indexPath.row cell.configure(viewmodel.managers[indexPath.row]) - if let type = viewmodel.currentMember?.type, type == .manager { - cell.removeBtn.isHidden = true + if let type = viewmodel.currentMember?.memberRole, type == .TEAM_MEMBER_ROLE_MANAGER { + cell.removeButton.isHidden = true cell.removeLabel.isHidden = true } return cell diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.swift index 0333e5a6..d8f6e790 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.swift @@ -23,7 +23,7 @@ open class TeamMemberSelectController: NEBaseTeamMemberSelectController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "\(indexPath.section)", for: indexPath) as! TeamMemberSelectCell - let member = viewmodel.showDatas[indexPath.row] + let member = viewModel.showDatas[indexPath.row] cell.configureMember(member) return cell } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.swift index 0e5c509e..fe7238b5 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.swift @@ -10,8 +10,8 @@ open class TeamMembersController: NEBaseTeamMembersController { super.viewDidLoad() navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white - back.backgroundColor = .ne_backcolor - contentTable.register(TeamMemberCell.self, forCellReuseIdentifier: "\(TeamMemberCell.self)") + backView.backgroundColor = .ne_backcolor + contentTableView.register(TeamMemberCell.self, forCellReuseIdentifier: "\(TeamMemberCell.self)") } override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -21,19 +21,19 @@ open class TeamMembersController: NEBaseTeamMembersController { ) as? TeamMemberCell { if let model = getRealModel(indexPath.row) { var isShowRemove = false - if isOwner(model.nimUser?.userId) { + if isOwner(model.nimUser?.user?.accountId) { cell.ownerLabel.isHidden = false cell.ownerLabel.text = localizable("team_owner") cell.ownerWidth?.constant = 40 - } else if model.teamMember?.type == .manager { + } else if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { cell.ownerLabel.isHidden = false cell.ownerLabel.text = localizable("team_manager") cell.ownerWidth?.constant = 52 - if isOwner(IMKitClient.instance.imAccid()) { + if isOwner(IMKitClient.instance.account()) { isShowRemove = true } } else { - if isOwner(IMKitClient.instance.imAccid()) || viewmodel.currentMember?.type == .manager { + if isOwner(IMKitClient.instance.account()) || viewModel.currentMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { isShowRemove = true } cell.ownerLabel.isHidden = true @@ -41,7 +41,7 @@ open class TeamMembersController: NEBaseTeamMembersController { cell.index = indexPath.row cell.delegate = self cell.configure(model) - cell.removeBtn.isHidden = !isShowRemove + cell.removeButton.isHidden = !isShowRemove cell.removeLabel.isHidden = !isShowRemove } return cell diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamNameViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamNameViewController.swift index 09b03938..ac12ed85 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamNameViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamNameViewController.swift @@ -27,10 +27,10 @@ open class TeamNameViewController: NEBaseTeamNameViewController { ]) NSLayoutConstraint.activate([ - textView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), - textView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -32), - textView.topAnchor.constraint(equalTo: backView.topAnchor, constant: 0), - textView.heightAnchor.constraint(equalToConstant: 44), + textInputView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + textInputView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -40), + textInputView.topAnchor.constraint(equalTo: backView.topAnchor, constant: 0), + textInputView.heightAnchor.constraint(equalToConstant: 44), ]) NSLayoutConstraint.activate([ diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift index e60ab70d..67fb2011 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -22,89 +22,116 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { ] } + /// 背景视图 + public lazy var backView: UIView = { + let backView = UIView() + backView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: 172) + return backView + }() + + /// 圆角视图 + public lazy var cornerView: UIView = { + let cornerView = UIView() + cornerView.backgroundColor = .white + cornerView.clipsToBounds = true + cornerView.translatesAutoresizingMaskIntoConstraints = false + cornerView.layer.cornerRadius = 8.0 + return cornerView + }() + + /// 群信息跳转下一级页面指示箭头 + lazy var arrowImageView: UIImageView = { + let arrowImageView = UIImageView() + arrowImageView.translatesAutoresizingMaskIntoConstraints = false + arrowImageView.image = coreLoader.loadImage("arrowRight") + return arrowImageView + }() + + /// 分割线 + lazy var dividerLineView: UIView = { + let dividerLineView = UIView() + dividerLineView.translatesAutoresizingMaskIntoConstraints = false + dividerLineView.backgroundColor = NEConstant.hexRGB(0xF5F8FC) + return dividerLineView + }() + + /// 成员列表跳转下一级页面指示箭头 + public var memberArrowImageView: UIImageView = { + let memberArrowImageView = UIImageView() + memberArrowImageView.translatesAutoresizingMaskIntoConstraints = false + memberArrowImageView.image = coreLoader.loadImage("arrowRight") + return memberArrowImageView + }() + + /// 群成员列表按钮 + public var memberListButton: UIButton = { + let memberListButton = UIButton() + memberListButton.translatesAutoresizingMaskIntoConstraints = false + return memberListButton + }() + + /// 群信息页面跳转按钮 + public var infoButton: UIButton = { + let infoButton = UIButton() + infoButton.translatesAutoresizingMaskIntoConstraints = false + return infoButton + }() + public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { super.setupUI() - teamHeader.layer.cornerRadius = 21.0 - addBtn.setImage(coreLoader.loadImage("add"), for: .normal) + teamHeaderView.layer.cornerRadius = 21.0 + addButton.setImage(coreLoader.loadImage("add"), for: .normal) } + /// 获取顶部视图 override open func getHeaderView() -> UIView { - let back = UIView() - back.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: 172) - let cornerView = UIView() - back.addSubview(cornerView) - cornerView.backgroundColor = .white - cornerView.clipsToBounds = true - cornerView.translatesAutoresizingMaskIntoConstraints = false - cornerView.layer.cornerRadius = 8.0 + backView.addSubview(cornerView) NSLayoutConstraint.activate([ - cornerView.leftAnchor.constraint(equalTo: back.leftAnchor, constant: 20), - cornerView.rightAnchor.constraint(equalTo: back.rightAnchor, constant: -20), - cornerView.bottomAnchor.constraint(equalTo: back.bottomAnchor), + cornerView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 20), + cornerView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -20), + cornerView.bottomAnchor.constraint(equalTo: backView.bottomAnchor), cornerView.heightAnchor.constraint(equalToConstant: 160), ]) - cornerView.addSubview(teamHeader) + cornerView.addSubview(teamHeaderView) NSLayoutConstraint.activate([ - teamHeader.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16), - teamHeader.topAnchor.constraint(equalTo: cornerView.topAnchor, constant: 16), - teamHeader.widthAnchor.constraint(equalToConstant: 42), - teamHeader.heightAnchor.constraint(equalToConstant: 42), + teamHeaderView.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16), + teamHeaderView.topAnchor.constraint(equalTo: cornerView.topAnchor, constant: 16), + teamHeaderView.widthAnchor.constraint(equalToConstant: 42), + teamHeaderView.heightAnchor.constraint(equalToConstant: 42), ]) - if let url = viewmodel.teamInfoModel?.team?.avatarUrl, !url.isEmpty { - print("icon url : ", url) - teamHeader.sd_setImage(with: URL(string: url), completed: nil) - } else { - if let tid = teamId { - if let name = viewmodel.teamInfoModel?.team?.getShowName() { - teamHeader.setTitle(name) - } - teamHeader.backgroundColor = UIColor.colorWithString(string: "\(tid)") - } - } - teamNameLabel.text = viewmodel.teamInfoModel?.team?.getShowName() + setTeamHeaderInfo() cornerView.addSubview(teamNameLabel) NSLayoutConstraint.activate([ - teamNameLabel.leftAnchor.constraint(equalTo: teamHeader.rightAnchor, constant: 11), - teamNameLabel.centerYAnchor.constraint(equalTo: teamHeader.centerYAnchor), + teamNameLabel.leftAnchor.constraint(equalTo: teamHeaderView.rightAnchor, constant: 11), + teamNameLabel.centerYAnchor.constraint(equalTo: teamHeaderView.centerYAnchor), teamNameLabel.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -34), ]) - let arrow = UIImageView() - arrow.translatesAutoresizingMaskIntoConstraints = false - arrow.image = coreLoader.loadImage("arrowRight") - cornerView.addSubview(arrow) + cornerView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: teamHeader.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -16), + arrowImageView.centerYAnchor.constraint(equalTo: teamHeaderView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -16), ]) - let line = UIView() - line.translatesAutoresizingMaskIntoConstraints = false - line.backgroundColor = NEConstant.hexRGB(0xF5F8FC) - cornerView.addSubview(line) + cornerView.addSubview(dividerLineView) NSLayoutConstraint.activate([ - line.heightAnchor.constraint(equalToConstant: 1.0), - line.rightAnchor.constraint(equalTo: cornerView.rightAnchor), - line.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0), - line.topAnchor.constraint(equalTo: teamHeader.bottomAnchor, constant: 12.0), + dividerLineView.heightAnchor.constraint(equalToConstant: 1.0), + dividerLineView.rightAnchor.constraint(equalTo: cornerView.rightAnchor), + dividerLineView.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0), + dividerLineView.topAnchor.constraint(equalTo: teamHeaderView.bottomAnchor, constant: 12.0), ]) - let memberLabel = UILabel() - cornerView.addSubview(memberLabel) - memberLabel.translatesAutoresizingMaskIntoConstraints = false - memberLabel.textColor = NEConstant.hexRGB(0x333333) - memberLabel.font = NEConstant.defaultTextFont(16.0) cornerView.addSubview(memberLabel) NSLayoutConstraint.activate([ - memberLabel.leftAnchor.constraint(equalTo: line.leftAnchor), - memberLabel.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), + memberLabel.leftAnchor.constraint(equalTo: dividerLineView.leftAnchor), + memberLabel.topAnchor.constraint(equalTo: dividerLineView.bottomAnchor, constant: 12), ]) if teamSettingType == .Senior { @@ -113,78 +140,71 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { memberLabel.text = localizable("discuss_mebmer") } - let memberArrow = UIImageView() - cornerView.addSubview(memberArrow) - memberArrow.translatesAutoresizingMaskIntoConstraints = false - memberArrow.image = coreLoader.loadImage("arrowRight") + cornerView.addSubview(memberArrowImageView) NSLayoutConstraint.activate([ - memberArrow.rightAnchor.constraint(equalTo: arrow.rightAnchor), - memberArrow.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), + memberArrowImageView.rightAnchor.constraint(equalTo: arrowImageView.rightAnchor), + memberArrowImageView.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), ]) - let memberListBtn = UIButton() - memberListBtn.accessibilityIdentifier = "id.member" - cornerView.addSubview(memberListBtn) - memberListBtn.translatesAutoresizingMaskIntoConstraints = false + cornerView.addSubview(memberListButton) + memberListButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - memberListBtn.leftAnchor.constraint(equalTo: memberLabel.leftAnchor), - memberListBtn.rightAnchor.constraint(equalTo: memberArrow.rightAnchor), - memberListBtn.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), - memberListBtn.heightAnchor.constraint(equalToConstant: 40), + memberListButton.leftAnchor.constraint(equalTo: memberLabel.leftAnchor), + memberListButton.rightAnchor.constraint(equalTo: memberArrowImageView.rightAnchor), + memberListButton.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), + memberListButton.heightAnchor.constraint(equalToConstant: 40), ]) - memberListBtn.addTarget(self, action: #selector(toMemberList), for: .touchUpInside) + memberListButton.addTarget(self, action: #selector(toMemberList), for: .touchUpInside) cornerView.addSubview(memberCountLabel) NSLayoutConstraint.activate([ - memberCountLabel.rightAnchor.constraint(equalTo: memberArrow.leftAnchor, constant: -2), - memberCountLabel.centerYAnchor.constraint(equalTo: memberArrow.centerYAnchor), + memberCountLabel.rightAnchor.constraint(equalTo: memberArrowImageView.leftAnchor, constant: -2), + memberCountLabel.centerYAnchor.constraint(equalTo: memberArrowImageView.centerYAnchor), ]) - memberCountLabel.text = "\(viewmodel.teamInfoModel?.team?.memberNumber ?? 0)" + memberCountLabel.text = "\(viewModel.teamInfoModel?.team?.memberCount ?? 0)" - cornerView.addSubview(addBtn) - addBtnWidth = addBtn.widthAnchor.constraint(equalToConstant: 32) - addBtnWidth?.isActive = true - addBtnLeftMargin = addBtn.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0) + cornerView.addSubview(addButton) + addButtonWidth = addButton.widthAnchor.constraint(equalToConstant: 32) + addButtonWidth?.isActive = true + addButtonLeftMargin = addButton.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0) NSLayoutConstraint.activate([ - addBtnLeftMargin!, - addBtn.topAnchor.constraint(equalTo: memberLabel.bottomAnchor, constant: 12), + addButtonLeftMargin!, + addButton.topAnchor.constraint(equalTo: memberLabel.bottomAnchor, constant: 12), ]) - addBtn.addTarget(self, action: #selector(addUser), for: .touchUpInside) + addButton.addTarget(self, action: #selector(addUser), for: .touchUpInside) - if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, - let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, let member = viewmodel.memberInTeam, inviteMode == .manager, member.type != .manager { - addBtnWidth?.constant = 0 - addBtn.isHidden = true + if viewModel.isNormalTeam() == false, viewModel.isOwner() == false, + let inviteMode = viewModel.teamInfoModel?.team?.inviteMode, let member = viewModel.memberInTeam, inviteMode == .TEAM_INVITE_MODE_MANAGER, member.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + addButtonWidth?.constant = 0 + addButton.isHidden = true } setupUserInfoCollection(cornerView) - let infoBtn = UIButton() - infoBtn.translatesAutoresizingMaskIntoConstraints = false - cornerView.addSubview(infoBtn) + cornerView.addSubview(infoButton) NSLayoutConstraint.activate([ - infoBtn.leftAnchor.constraint(equalTo: teamHeader.leftAnchor), - infoBtn.topAnchor.constraint(equalTo: teamHeader.topAnchor), - infoBtn.bottomAnchor.constraint(equalTo: teamHeader.bottomAnchor), - infoBtn.rightAnchor.constraint(equalTo: arrow.rightAnchor), + infoButton.leftAnchor.constraint(equalTo: teamHeaderView.leftAnchor), + infoButton.topAnchor.constraint(equalTo: teamHeaderView.topAnchor), + infoButton.bottomAnchor.constraint(equalTo: teamHeaderView.bottomAnchor), + infoButton.rightAnchor.constraint(equalTo: arrowImageView.rightAnchor), ]) - infoBtn.addTarget(self, action: #selector(toInfoView), for: .touchUpInside) + infoButton.addTarget(self, action: #selector(toInfoView), for: .touchUpInside) - return back + return backView } override open func checkoutAddShowOrHide() { - if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, - let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, inviteMode == .manager { - if let member = viewmodel.memberInTeam, member.type == .manager { - addBtn.isHidden = false - addBtnWidth?.constant = 36.0 - addBtnLeftMargin?.constant = 16 + if viewModel.isNormalTeam() == false, viewModel.isOwner() == false, + let inviteMode = viewModel.teamInfoModel?.team?.inviteMode, inviteMode == .TEAM_INVITE_MODE_MANAGER { + if let member = viewModel.memberInTeam, member.memberRole == .TEAM_MEMBER_ROLE_MANAGER { + addButton.isHidden = false + addButtonWidth?.constant = 36.0 + addButtonLeftMargin?.constant = 16 checkMemberCountLimit() } else { - addBtn.isHidden = true - addBtnWidth?.constant = 0 - addBtnLeftMargin?.constant = 0 + addButton.isHidden = true + addButtonWidth?.constant = 0 + addButtonLeftMargin?.constant = 0 } } else { checkMemberCountLimit() @@ -192,14 +212,14 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { } func checkMemberCountLimit() { - if viewmodel.teamInfoModel?.team?.level == viewmodel.teamInfoModel?.team?.memberNumber { - addBtn.isHidden = true - addBtnWidth?.constant = 0 - addBtnLeftMargin?.constant = 0 + if viewModel.teamInfoModel?.team?.memberLimit == viewModel.teamInfoModel?.team?.memberCount { + addButton.isHidden = true + addButtonWidth?.constant = 0 + addButtonLeftMargin?.constant = 0 } else { - addBtn.isHidden = false - addBtnWidth?.constant = 36.0 - addBtnLeftMargin?.constant = 16 + addButton.isHidden = false + addButtonWidth?.constant = 36.0 + addButtonLeftMargin?.constant = 16 } } @@ -207,10 +227,10 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { guard let title = getBottomText() else { return nil } - let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) + let footerView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false - footer.addSubview(button) + footerView.addSubview(button) button.backgroundColor = .white button.clipsToBounds = true button.setTitleColor(NEConstant.hexRGB(0xE6605C), for: .normal) @@ -219,27 +239,27 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { button.addTarget(self, action: #selector(removeTeamForMyself), for: .touchUpInside) button.layer.cornerRadius = 8.0 NSLayoutConstraint.activate([ - button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 20), - button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: -20), - button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), + button.leftAnchor.constraint(equalTo: footerView.leftAnchor, constant: 20), + button.rightAnchor.constraint(equalTo: footerView.rightAnchor, constant: -20), + button.topAnchor.constraint(equalTo: footerView.topAnchor, constant: 12), button.heightAnchor.constraint(equalToConstant: 40), ]) - return footer + return footerView } override open func setupUserInfoCollection(_ cornerView: UIView) { - cornerView.addSubview(userinfoCollection) + cornerView.addSubview(userinfoCollectionView) NSLayoutConstraint.activate([ - userinfoCollection.leftAnchor.constraint(equalTo: addBtn.rightAnchor, constant: 15), - userinfoCollection.centerYAnchor.constraint(equalTo: addBtn.centerYAnchor), - userinfoCollection.rightAnchor.constraint( + userinfoCollectionView.leftAnchor.constraint(equalTo: addButton.rightAnchor, constant: 15), + userinfoCollectionView.centerYAnchor.constraint(equalTo: addButton.centerYAnchor), + userinfoCollectionView.rightAnchor.constraint( equalTo: cornerView.rightAnchor, constant: -15 ), - userinfoCollection.heightAnchor.constraint(equalToConstant: 32), + userinfoCollectionView.heightAnchor.constraint(equalToConstant: 32), ]) - userinfoCollection.register( + userinfoCollectionView.register( TeamUserCell.self, forCellWithReuseIdentifier: "\(TeamUserCell.self)" ) @@ -248,15 +268,15 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { // MARK: objc 方法 override open func toInfoView() { - let info = TeamInfoViewController(team: viewmodel.teamInfoModel?.team) + let info = TeamInfoViewController(team: viewModel.teamInfoModel?.team) navigationController?.pushViewController(info, animated: true) } override open func didClickChangeNick() { let nick = TeamNameViewController() nick.type = .NickName - nick.team = viewmodel.teamInfoModel?.team - nick.teamMember = viewmodel.memberInTeam + nick.team = viewModel.teamInfoModel?.team + nick.teamMember = viewModel.memberInTeam navigationController?.pushViewController(nick, animated: true) } @@ -266,15 +286,14 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { } Router.shared.use( SearchMessageRouter, - parameters: ["nav": navigationController as Any, "teamId": tid], + parameters: ["nav": navigationController as Any, "teamId": tid, "teamInfo": viewModel.teamInfoModel as Any], closure: nil ) } override open func didClickTeamManage() { - let manageTeam = TeamManageController() - manageTeam.managerUsers = getManaterUsers() - manageTeam.viewmodel.teamInfoModel = viewmodel.teamInfoModel + let manageTeam = TeamManagerController() + manageTeam.viewModel.teamInfoModel = viewModel.teamInfoModel navigationController?.pushViewController(manageTeam, animated: true) } @@ -284,8 +303,9 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { withReuseIdentifier: "\(TeamUserCell.self)", for: indexPath ) as? TeamUserCell { - if let user = viewmodel.teamInfoModel?.users[indexPath.row] { - if let userId = user.nimUser?.userId, let nimUser = ChatUserCache.getUserInfo(userId) { + if let user = viewModel.teamInfoModel?.users[indexPath.row] { + // 从缓存中获取用户信息 + if let userId = user.nimUser?.user?.accountId, let nimUser = NEFriendUserCache.shared.getFriendInfo(userId) { user.nimUser = nimUser } cell.user = user @@ -302,7 +322,7 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { } override open func toMemberList() { - let memberController = TeamMembersController(teamId: viewmodel.teamInfoModel?.team?.teamId) + let memberController = TeamMembersController(teamId: viewModel.teamInfoModel?.team?.teamId) navigationController?.pushViewController(memberController, animated: true) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamRouter.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamRouter.swift index fb5d3f14..dd44b9b7 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamRouter.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamRouter.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK @@ -22,9 +22,11 @@ public extension TeamRouter { Router.shared.register(SearchMessageRouter) { param in let nav = param["nav"] as? UINavigationController - if let tid = param["teamId"] as? String { - let session = NIMSession(tid, type: .team) - let searchMsgCtrl = TeamHistoryMessageController(session: session) + if let teamId = param["teamId"] as? String { + let searchMsgCtrl = TeamHistoryMessageController(teamId: teamId) + if let info = param["teamInfo"] as? NETeamInfoModel { + searchMsgCtrl.viewModel.teamInfoModel = info + } nav?.pushViewController(searchMsgCtrl, animated: true) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/Common/NETeamMemberCache.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/Common/NETeamMemberCache.swift new file mode 100644 index 00000000..83dce434 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/Common/NETeamMemberCache.swift @@ -0,0 +1,221 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NIMSDK +import UIKit + +@objc +@objcMembers +open class NETeamMemberCache: NSObject, NETeamListener, NEIMKitClientListener, NEContactListener { + public static let shared = NETeamMemberCache() + /// 当前缓存的群id + var currentTeamId = "" + + /// 群模块API单例 + let teamRepo = TeamRepo.shared + + /// 通讯录API单例 + let contactRepo = ContactRepo.shared + + /// kit client 单例 + let client = IMKitClient.instance + + /// 缓存 + private var cacheDic = [String: NETeamMemberInfoModel]() + + /// 清理缓存定时器 + var timer: Timer? + + /// 初始化 + override private init() { + super.init() + teamRepo.addTeamListener(self) + client.addLoginListener(self) + contactRepo.addContactListener(self) + + NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil) + } + + deinit { + teamRepo.removeTeamListener(self) + client.removeLoginListener(self) + contactRepo.removeContactListener(self) + + NotificationCenter.default.removeObserver(self) + } + + /// 应用进入后台时执行的操作 + func appDidEnterBackground() { + clearCache() + } + + /// 应用即将进入后台(包括锁屏) + func appWillResignActive() { + clearCache() + } + + /// 设置缓存(对一个新群设置缓存会移除之前群的缓存,单例只保存一个群的成员缓存) + /// - Parameter teamId: 群id + /// - Parameter members: 群成员数据对象列表 + public func setCacheMembers(_ teamId: String, _ members: [NETeamMemberInfoModel]) { + cacheDic.removeAll() + currentTeamId = teamId + + // 启动新的定时器确保前面开启的停止 + endTimer() + + startTimer() + for model in members { + if let accountId = model.teamMember?.accountId { + cacheDic[accountId] = model + } + } + } + + /// 获取缓存 + /// - Parameter teamId: 群id + /// - returns 群成员缓存数据(可能为空) + public func getTeamMemberCache(_ teamId: String) -> [NETeamMemberInfoModel]? { + if currentTeamId == teamId { + var allCacheMembers = Array(cacheDic.values) + allCacheMembers.sort { model1, model2 in + if let time1 = model1.teamMember?.joinTime, let time2 = model2.teamMember?.joinTime { + return time2 > time1 + } + return false + } + if allCacheMembers.count > 0 { + return allCacheMembers + } + } + return nil + } + + /// 好友信息更新 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + if let account = friendInfo.accountId, let model = cacheDic[account] { + model.nimUser = NEUserWithFriend(friend: friendInfo) + } + } + + /// 好友删除 + /// - parameter accountId: 账号id + /// - parameter deletionType: 删除类型 + public func onFriendDeleted(_ accountId: String, deletionType: V2NIMFriendDeletionType) { + if let model = cacheDic[accountId] { + model.nimUser = nil + } + } + + /// 登录状态改变 + /// - Parameter status: 登录状态枚举 + public func onLoginStatus(_ status: V2NIMLoginStatus) { + if status == .LOGIN_STATUS_LOGOUT { + cacheDic.removeAll() + } + } + + /// 群成员离开回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + onMemberDidRemove(teamMembers) + } + + /// 群成员被踢回调 + /// - Parameter operatorAccountId: 操作者id + /// - Parameter teamMembers: 群成员 + public func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + onMemberDidRemove(teamMembers) + } + + /// 群成员加入回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { + onMemberDidAdd(teamMembers) + } + + /// 群成员更新回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + onMemberDidChanged(teamMembers) + } + + /// 群聊解散回调 + /// - Parameter team: 群对象 + public func onTeamDismissed(_ team: V2NIMTeam) { + if team.teamId == currentTeamId { + cacheDic.removeAll() + } + } + + /// 群成员变更统一处理 + /// - Parameter teamMembers: 群成员 + private func onMemberDidChanged(_ members: [V2NIMTeamMember]) { + for member in members { + if currentTeamId != member.teamId { + continue + } + if let model = cacheDic[member.accountId] { + model.teamMember = member + } + } + } + + /// 群成员减少统一处理处理 + /// - Parameter teamMembers: 群成员 + private func onMemberDidRemove(_ members: [V2NIMTeamMember]) { + for member in members { + if currentTeamId != member.teamId { + continue + } + cacheDic.removeValue(forKey: member.accountId) + } + } + + /// 群成员增加统一处理 + /// - Parameter teamMembers: 群成员 + private func onMemberDidAdd(_ members: [V2NIMTeamMember]) { + var allMembmers = [V2NIMTeamMember]() + for member in members { + if currentTeamId != member.teamId { + continue + } + NEALog.infoLog(className(), desc: "team cache did add member \(member.teamNick ?? ""))") + let model = NETeamMemberInfoModel() + model.teamMember = member + cacheDic[member.accountId] = model + allMembmers.append(member) + } + if allMembmers.count > 0 { + let accids = allMembmers.map(\.accountId) + contactRepo.getFriendInfoList(accountIds: accids) { [weak self] users, error in + users?.forEach { user in + if let accountId = user.friend?.accountId { + self?.cacheDic[accountId]?.nimUser = user + } + } + } + } + } + + /// 清除缓存 + public func clearCache() { + currentTeamId = "" + cacheDic.removeAll() + endTimer() + } + + /// 启动定时器 + func startTimer() { + timer = Timer.scheduledTimer(timeInterval: 300, target: self, selector: #selector(clearCache), userInfo: nil, repeats: true) + } + + /// 停止定时器 + func endTimer() { + timer?.invalidate() + timer = nil + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift index 8a25f8c0..38636cce 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift @@ -8,5 +8,5 @@ import UIKit @objcMembers open class NESelectTeamMember: NSObject { var isSelected: Bool = false - var member: TeamMemberInfoModel? + var member: NETeamMemberInfoModel? } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift index 608f1dff..92d1b777 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift @@ -12,7 +12,7 @@ open class SettingSectionModel: NSObject { // 设置圆角 open func setCornerType() { - cellModels.forEach { model in + for model in cellModels { if model == cellModels.first { model.cornerType = .topLeft.union(.topRight) if model == cellModels.last { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamAvatarViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamAvatarViewController.swift new file mode 100644 index 00000000..98b43867 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamAvatarViewController.swift @@ -0,0 +1,263 @@ + +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NECommonUIKit +import NIMSDK +import UIKit + +@objcMembers +open class NEBaseTeamAvatarViewController: NEBaseViewController, UICollectionViewDelegate, + UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate { + public typealias SaveCompletion = () -> Void + public var block: SaveCompletion? + public var team: V2NIMTeam? + public let repo = TeamRepo.shared + + /// 头像背景 + public let headerBackView = UIView() + /// 相机提示图片 + public let photoImageView = UIImageView() + /// 缺省背景 + public let defaultHeaderBackView = UIView() + + public let tagLabel = UILabel() + + public var iconUrls = TeamRouter.iconUrls + + public var viewModel = TeamAvatarViewModel() + + public lazy var headerView: NEUserHeaderView = { + let header = NEUserHeaderView(frame: .zero) + header.translatesAutoresizingMaskIntoConstraints = false + header.clipsToBounds = true + header.isUserInteractionEnabled = true + header.accessibilityIdentifier = "id.icon" + return header + }() + + public var headerUrl = "" + + /// 群头像点击按钮 + public lazy var clickButton: UIButton = { + let clickButton = UIButton(type: .custom) + clickButton.translatesAutoresizingMaskIntoConstraints = false + clickButton.backgroundColor = .clear + return clickButton + }() + + public lazy var iconsCollectionView: UICollectionView = { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + flowLayout.minimumLineSpacing = 0 + flowLayout.minimumInteritemSpacing = 0 + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.backgroundColor = .clear + collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false + collectionView.clipsToBounds = false + collectionView.isScrollEnabled = false + return collectionView + }() + + override open func viewDidLoad() { + super.viewDidLoad() + weak var weakSelf = self + viewModel.getCurrentUserTeamMember(team?.teamId) { error in + weakSelf?.setupUI() + if let err = error { + weakSelf?.view.makeToast(err.localizedDescription) + } + } + } + + /// UI 初始化 + open func setupUI() { + title = localizable("modify_headImage") + addRightAction(localizable("save"), #selector(savePhoto), self) + navigationView.setMoreButtonTitle(localizable("save")) + navigationView.addMoreButtonTarget(target: self, selector: #selector(savePhoto)) + + view.backgroundColor = .ne_lightBackgroundColor + + headerBackView.backgroundColor = .white + headerBackView.clipsToBounds = true + headerBackView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(headerBackView) + + /// 当前头像视图背景 + headerBackView.addSubview(headerView) + NSLayoutConstraint.activate([ + headerView.centerXAnchor.constraint(equalTo: headerBackView.centerXAnchor), + headerView.centerYAnchor.constraint(equalTo: headerBackView.centerYAnchor), + headerView.heightAnchor.constraint(equalToConstant: 80.0), + headerView.widthAnchor.constraint(equalToConstant: 80.0), + ]) + if let url = team?.avatar, !url.isEmpty { + headerView.sd_setImage(with: URL(string: url), completed: nil) + headerUrl = url + } + + photoImageView.translatesAutoresizingMaskIntoConstraints = false + photoImageView.image = coreLoader.loadImage("photo") + photoImageView.accessibilityIdentifier = "id.camera" + headerBackView.addSubview(photoImageView) + + headerBackView.addSubview(clickButton) + clickButton.addTarget(self, action: #selector(uploadPhoto), for: .touchUpInside) + NSLayoutConstraint.activate([ + clickButton.leftAnchor.constraint(equalTo: headerView.leftAnchor), + clickButton.topAnchor.constraint(equalTo: headerView.topAnchor), + clickButton.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), + clickButton.rightAnchor.constraint(equalTo: headerView.rightAnchor, constant: 10), + ]) + + defaultHeaderBackView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(defaultHeaderBackView) + defaultHeaderBackView.clipsToBounds = true + defaultHeaderBackView.backgroundColor = .white + + tagLabel.translatesAutoresizingMaskIntoConstraints = false + tagLabel.text = localizable("default_icon") + tagLabel.font = NEConstant.defaultTextFont(16.0) + tagLabel.textColor = NEConstant.hexRGB(0x333333) + defaultHeaderBackView.addSubview(tagLabel) + + defaultHeaderBackView.addSubview(iconsCollectionView) + + for index in 0 ..< iconUrls.count { + let url = iconUrls[index] + if url == headerUrl { + let indexPath = IndexPath(row: index, section: 0) + iconsCollectionView.selectItem(at: indexPath, animated: false, scrollPosition: .right) + } + } + + /// 判断权限决定是否展示保存按钮 + if getChangePermission() == false { + rightNavButton.isHidden = true + navigationView.moreButton.isHidden = true + photoImageView.isHidden = true + defaultHeaderBackView.isHidden = true + } + } + + /// 获取当前是否有修改权限 + func getChangePermission() -> Bool { + if let ownerId = team?.ownerAccountId, IMKitClient.instance.isMe(ownerId) { + return true + } + if let mode = team?.updateInfoMode, mode == .TEAM_UPDATE_INFO_MODE_ALL { + return true + } + if let member = viewModel.currentTeamMember, member.memberRole == .TEAM_MEMBER_ROLE_MANAGER { + return true + } + return false + } + + open func uploadPhoto() { + if getChangePermission() { + showBottomAlert(self) + } + } + + /// 保存相册 + open func savePhoto() { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + if let tid = team?.teamId { + view.makeToastActivity(.center) + weak var weakSelf = self + weakSelf?.viewModel.updateTeamAvatar(headerUrl, tid, nil) { error in + NEALog.infoLog(ModuleName + " " + self.className(), desc: #function + "CALLBACK " + (error?.localizedDescription ?? "no error")) + weakSelf?.view.hideToastActivity() + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if error?.code == noPermissionOperationCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + if let completion = weakSelf?.block { + completion() + } + weakSelf?.navigationController?.popViewController(animated: true) + } + } + } + } + + open func collectionView(_ collectionView: UICollectionView, + numberOfItemsInSection section: Int) -> Int { + 5 + } + + open func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + UICollectionViewCell() + } + + open func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + .zero + } + + open func collectionView(_ collectionView: UICollectionView, + didSelectItemAt indexPath: IndexPath) { + if iconUrls.count > indexPath.row { + headerUrl = iconUrls[indexPath.row] + headerView.sd_setImage(with: URL(string: headerUrl), completed: nil) + } + } + + open func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController + .InfoKey: Any]) { + let image: UIImage = info[UIImagePickerController.InfoKey.editedImage] as! UIImage + uploadHeadImage(image: image) + picker.dismiss(animated: true, completion: nil) + } + + /// 上传头像 + /// - Parameter image: 头像 + open func uploadHeadImage(image: UIImage) { + weak var weakSelf = self + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + view.makeToastActivity(.center) + if let imageData = image.jpegData(compressionQuality: 0.6) as NSData?, + var filePath = NEPathUtils.getDirectoryForDocuments(dir: "NEIMUIKit/image/") { + filePath += "\(team?.teamId ?? "team")_avatar.jpg" + let succcess = imageData.write(toFile: filePath, atomically: true) + if succcess { + let fileTask = viewModel.createTask(filePath) + viewModel.uploadImageFile(fileTask, nil) { urlString, error in + if error == nil { + // 显示设置的照片 + weakSelf?.headerView.image = image + if let url = urlString { + weakSelf?.headerUrl = url + } + print("upload image success") + } else { + print("upload image failed,error = \(error!)") + } + weakSelf?.view.hideToastActivity() + } + } + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift index 22af08ea..29f086ec 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift @@ -3,51 +3,21 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NECommonKit import NIMSDK import UIKit @objcMembers open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource { - public let viewmodel = TeamSettingViewModel() - public var teamSession: NIMSession? + public let viewModel = TeamHistoryMessageViewModel() + + /// 群id + public var teamId: String? public var searchStr = "" var tag = "TeamHistoryMessageController" - public init(session: NIMSession?) { - super.init(nibName: nil, bundle: nil) - teamSession = session - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override open func viewDidLoad() { - super.viewDidLoad() - setupSubviews() - initialConfig() - } - - open func setupSubviews() { - view.addSubview(tableView) - view.addSubview(searchTextField) - view.addSubview(emptyView) - - NSLayoutConstraint.activate([ - emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), - emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), - emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), - emptyView.topAnchor.constraint(equalTo: tableView.topAnchor), - ]) - } - - open func initialConfig() { - title = localizable("historical_record") - } - - // MARK: lazy method - + /// 历史消息列表 public lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) tableView.translatesAutoresizingMaskIntoConstraints = false @@ -66,6 +36,7 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField return tableView }() + /// 搜索文本框 public lazy var searchTextField: SearchTextField = { let textField = SearchTextField() let leftImageView = UIImageView(image: coreLoader.loadImage("search_icon")) @@ -80,7 +51,6 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField textField.backgroundColor = UIColor(hexString: "0xF2F4F5") textField.clearButtonMode = .always textField.returnKeyType = .search - textField.addTarget(self, action: #selector(searchTextFieldChange), for: .editingChanged) textField.delegate = self if let clearButton = textField.value(forKey: "_clearButton") as? UIButton { clearButton.accessibilityIdentifier = "id.clear" @@ -90,6 +60,7 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField }() + /// 空占位图 public lazy var emptyView: NEEmptyDataView = { let view = NEEmptyDataView( imageName: "emptyView", @@ -102,27 +73,71 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField }() - // MARK: private method + /// 正在搜索标志,防止多次点击多次搜索 + public var isSearching = false - func searchTextFieldChange(textfield: SearchTextField) { - guard let searchText = textfield.text else { + public init(teamId: String?) { + super.init(nibName: nil, bundle: nil) + self.teamId = teamId + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + weak var weakSelf = self + + viewModel.getTeamInfo(teamId) { team, error in + if team?.isValidTeam == false || team == nil { + weakSelf?.view.makeToast(localizable("team_not_exist")) + } + } + setupSubviews() + initialConfig() + } + + open func setupSubviews() { + view.addSubview(tableView) + view.addSubview(searchTextField) + view.addSubview(emptyView) + + NSLayoutConstraint.activate([ + emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), + emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), + emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), + emptyView.topAnchor.constraint(equalTo: tableView.topAnchor), + ]) + } + + open func initialConfig() { + title = localizable("historical_record") + } + + /// 搜索历史消息 + func toSearchHistory() { + guard let searchText = searchTextField.text else { return } if searchText.count <= 0 { - viewmodel.searchResultInfos?.removeAll() + viewModel.searchResultInfos?.removeAll() emptyView.isHidden = true tableView.reloadData() return } - guard let session = teamSession else { + guard let teamId = teamId else { + return + } + if isSearching == true { return } weak var weakSelf = self searchStr = searchText - let option = NIMMessageSearchOption() - option.searchContent = searchText - weakSelf?.viewmodel.searchMessages(session, option: option) { error, messages in - NELog.infoLog( + isSearching = true + weakSelf?.viewModel.searchHistoryMessages(teamId, searchText) { error, messages in + weakSelf?.isSearching = false + NEALog.infoLog( ModuleName + " " + self.tag, desc: "CALLBACK searchMessages " + (error?.localizedDescription ?? "no error") ) @@ -134,7 +149,7 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField } weakSelf?.tableView.reloadData() } else { - NELog.errorLog( + NEALog.errorLog( ModuleName + " " + (weakSelf?.tag ?? "TeamHistoryMessageController"), desc: "❌searchMessages failed, error = \(error!)" ) @@ -142,10 +157,20 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField } } + /// 监听键盘搜索按钮点击 + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return false + } + toSearchHistory() + return true + } + // MARK: UITableViewDelegate, UITableViewDataSource open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewmodel.searchResultInfos?.count ?? 0 + viewModel.searchResultInfos?.count ?? 0 } open func tableView(_ tableView: UITableView, @@ -154,14 +179,13 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let cellModel = viewmodel.searchResultInfos?[indexPath.row] - if cellModel?.imMessage?.session?.sessionType == .team { - if let sid = cellModel?.imMessage?.session?.sessionId, - let message = cellModel?.imMessage { - let session = NIMSession(sid, type: .team) + let cellModel = viewModel.searchResultInfos?[indexPath.row] + if cellModel?.imMessage?.conversationType == .CONVERSATION_TYPE_TEAM { + if let message = cellModel?.imMessage, let conversationId = message.conversationId { Router.shared.use( PushTeamChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any, + parameters: ["nav": navigationController as Any, + "conversationId": conversationId as Any, "anchor": message], closure: nil ) diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamInfoViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamInfoViewController.swift new file mode 100644 index 00000000..3dfb1ee2 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamInfoViewController.swift @@ -0,0 +1,96 @@ + +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NECoreIM2Kit +import NIMSDK +import UIKit + +@objcMembers +open class NEBaseTeamInfoViewController: NEBaseViewController, UITableViewDelegate, + UITableViewDataSource, NETeamInfoDelegate { + public let viewModel = TeamInfoViewModel() + + public var team: V2NIMTeam? + + public var registerCellDic = [Int: NEBaseTeamSettingCell.Type]() + + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 0 + return tableView + }() + + init(team: V2NIMTeam?) { + super.init(nibName: nil, bundle: nil) + self.team = team + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if team?.serverExtension?.contains(discussTeamKey) == true { + title = localizable("discuss_info") + } else { + title = localizable("group_info") + } + } + + override open func viewDidLoad() { + super.viewDidLoad() + viewModel.delegate = self + viewModel.getData(team) + setupUI() + } + + /// UI 初始化 + open func setupUI() { + view.addSubview(contentTableView) + /// 列表视图布局 + NSLayoutConstraint.activate([ + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant + 12), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + /// 列表视图内容注册 + for (key, value) in registerCellDic { + contentTableView.register(value, forCellReuseIdentifier: "\(key)") + } + } + + // MARK: UITableViewDelegate, UITableViewDataSource + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + viewModel.cellDatas.count + } + + /// 数据绑定,在子类中绑定 + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + UITableViewCell() + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {} + + /// 列表高度回调 + open func tableView(_ tableView: UITableView, + heightForRowAt indexPath: IndexPath) -> CGFloat { + let model = viewModel.cellDatas[indexPath.row] + return model.rowHeight + } + + public func teamInfoDidUpdate(_ t: V2NIMTeam) { + team = t + contentTableView.reloadData() + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamIntroduceViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamIntroduceViewController.swift similarity index 58% rename from NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamIntroduceViewController.swift rename to NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamIntroduceViewController.swift index eafa4c6e..3ca27885 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamIntroduceViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamIntroduceViewController.swift @@ -9,25 +9,26 @@ import UIKit @objcMembers open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDelegate { - public var team: NIMTeam? + public var team: V2NIMTeam? public let textLimit = 100 - public let repo = TeamRepo.shared public let backView = UIView() - public let viewmodel = TeamIntroduceViewModel() + public let viewModel = TeamIntroduceViewModel() + /// 介绍输入框 public lazy var textView: UITextView = { - let text = UITextView() - text.translatesAutoresizingMaskIntoConstraints = false - text.textColor = NEConstant.hexRGB(0x333333) - text.font = NEConstant.defaultTextFont(14.0) - text.delegate = self - text.textContainerInset = UIEdgeInsets.zero - text.layoutManager.allowsNonContiguousLayout = false - text.accessibilityIdentifier = "id.introduce" - return text + let textView = UITextView() + textView.translatesAutoresizingMaskIntoConstraints = false + textView.textColor = NEConstant.hexRGB(0x333333) + textView.font = NEConstant.defaultTextFont(14.0) + textView.delegate = self + textView.textContainerInset = UIEdgeInsets.zero + textView.layoutManager.allowsNonContiguousLayout = false + textView.accessibilityIdentifier = "id.introduce" + return textView }() + /// 清除按钮 public lazy var clearButton: UIButton = { let text = UIButton() text.translatesAutoresizingMaskIntoConstraints = false @@ -37,6 +38,7 @@ open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDe return text }() + /// 字数计数显示 public lazy var countLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -48,18 +50,23 @@ open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDe override open func viewDidLoad() { super.viewDidLoad() - viewmodel.getCurrentUserTeamMember(team?.teamId) - setupUI() + weak var weakSelf = self + viewModel.getCurrentUserTeamMember(team?.teamId) { error in + if let err = error { + weakSelf?.view.makeToast(err.localizedDescription) + } + weakSelf?.setupUI() + } } + /// 布局初始化 open func setupUI() { navigationView.setMoreButtonTitle(localizable("save")) navigationView.addMoreButtonTarget(target: self, selector: #selector(saveIntr)) - - if let type = team?.type, type == .advanced { - title = localizable("team_intr") - } else { + if let serverExtension = team?.serverExtension, serverExtension.contains(discussTeamKey) { title = localizable("discuss_introduce") + } else { + title = localizable("team_intr") } backView.backgroundColor = .white @@ -78,26 +85,28 @@ open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDe figureTextCount(team?.intro ?? "") - if changePermission() == false { + if getPermission() == false { textView.isEditable = false - rightNavBtn.isHidden = true + rightNavButton.isHidden = true navigationView.moreButton.isHidden = true } } - func changePermission() -> Bool { - if let ownerId = team?.owner, IMKitClient.instance.isMySelf(ownerId) { + /// 权限改变 + func getPermission() -> Bool { + if let ownerId = team?.ownerAccountId, IMKitClient.instance.isMe(ownerId) { return true } - if let mode = team?.updateInfoMode, mode == .all { + if let mode = team?.updateInfoMode, mode == .TEAM_UPDATE_INFO_MODE_ALL { return true } - if let member = viewmodel.currentTeamMember, member.type == .manager { + if let member = viewModel.currentTeamMember, member.memberRole == .TEAM_MEMBER_ROLE_MANAGER { return true } return false } + /// 保存简介 func saveIntr() { textView.resignFirstResponder() weak var weakSelf = self @@ -105,51 +114,64 @@ open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDe weakSelf?.showToast(commonLocalizable("network_error")) return } - + // 上传请求 if let teamid = team?.teamId { let text = textView.text ?? "" view.makeToastActivity(.center) - repo.updateTeamIntroduce(text, teamid) { error in - NELog.infoLog( + viewModel.updateTeamIntroduce(teamid, text) { error in + NEALog.infoLog( ModuleName + " " + self.className(), desc: "CALLBACK updateTeamIntroduce " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { + if let err = error { + if err.code == protocolSendFailed { weakSelf?.showToast(commonLocalizable("network_error")) + } else if error?.code == noPermissionOperationCode { + weakSelf?.showToast(localizable("no_permission_tip")) } else { weakSelf?.showToast(localizable("failed_operation")) } } else { - weakSelf?.team?.intro = text weakSelf?.navigationController?.popViewController(animated: true) } } } } + /// 计算当前输入字数 func figureTextCount(_ text: String) { textView.text = text - countLabel.text = "\(text.count)/\(textLimit)" - clearButton.isHidden = !changePermission() || text.count <= 0 + countLabel.text = "\(text.utf16.count)/\(textLimit)" + clearButton.isHidden = !getPermission() || text.utf16.count <= 0 } + /// 清空输入 func clearText() { figureTextCount("") } - // MARK: UITextViewDelegate - + /// 输入文本变更回调 open func textViewDidChange(_ textView: UITextView) { if let _ = textView.markedTextRange { return } - if var text = textView.text { - if text.count > textLimit { - text = String(text.prefix(textLimit)) - } + if let text = textView.text { figureTextCount(text) } } + + /// 文本变更回调 + /// - Parameter textView: 文本控件对象 + /// - Parameter range: 变更范围 + /// - Parameter text: 变更内容 + public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if !text.isEmpty { + let finalStr = (textView.text as NSString).replacingCharacters(in: range, with: text) + if finalStr.utf16.count > textLimit { + return false + } + } + return true + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManageController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManageController.swift deleted file mode 100644 index be2a7f90..00000000 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManageController.swift +++ /dev/null @@ -1,386 +0,0 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NEChatKit -import NECoreIMKit -import NIMSDK -import UIKit - -@objcMembers -open class NEBaseTeamManageController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource, TeamManageViewModelDelegate { - public let viewmodel = TeamManageViewModel() - - public var managerUsers = [TeamMemberInfoModel]() - - public var cellClassDic = [Int: NEBaseTeamSettingCell.Type]() - - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - table - .tableFooterView = - UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) - if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 - } - return table - }() - - override open func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - title = localizable("manage_team") - viewmodel.managerUsers = managerUsers - viewmodel.delegate = self - view.backgroundColor = .ne_lightBackgroundColor - view.addSubview(contentTable) - - NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), - contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in - contentTable.register(value, forCellReuseIdentifier: "\(key)") - } - if let tid = viewmodel.teamInfoModel?.team?.teamId { - viewmodel.getTeamInfo(tid) { [weak self] error in - self?.reloadSectionData() - self?.contentTable.reloadData() - } - } - } - - open func reloadSectionData() {} - - // MARK: UITableViewDataSource, UITableViewDelegate - - open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if viewmodel.sectionData.count > section { - let model = viewmodel.sectionData[section] - return model.cellModels.count - } - return 0 - } - - open func numberOfSections(in tableView: UITableView) -> Int { - viewmodel.sectionData.count - } - - open func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] - if let cell = tableView.dequeueReusableCell( - withIdentifier: "\(model.type)", - for: indexPath - ) as? NEBaseTeamSettingCell { - cell.configure(model) - return cell - } - return UITableViewCell() - } - - open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] - if let block = model.cellClick { - block() - } - } - - open func tableView(_ tableView: UITableView, - heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] - return model.rowHeight - } - - open func tableView(_ tableView: UITableView, - heightForHeaderInSection section: Int) -> CGFloat { - if viewmodel.sectionData.count > section { - let model = viewmodel.sectionData[section] - if model.cellModels.count > 0 { - return 12.0 - } - } - return 0 - } - - open func tableView(_ tableView: UITableView, - viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header - } - - open func tableView(_ tableView: UITableView, - heightForFooterInSection section: Int) -> CGFloat { - if section == viewmodel.sectionData.count - 1 { - return 12.0 - } - return 0 - } - - open func getFooterView() -> UIView? { - nil - } - - open func transferOwner() {} - - func updateTeamInfoAllAction(_ model: SettingCellModel) { - weak var weakSelf = self - view.makeToastActivity(.center) - viewmodel.repo - .updateTeamInfoPrivilege(.all, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in - NELog.infoLog( - ModuleName + " " + self.className(), - desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") - ) - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .all - model.subTitle = localizable("team_all") - weakSelf?.contentTable.reloadData() - } - } - } - - open func didUpdateTeamInfoClick(_ model: SettingCellModel) { - weak var weakSelf = self - - let actionSheetController = UIAlertController( - title: nil, - message: nil, - preferredStyle: .actionSheet - ) - - let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in - print("Cancel") - } - cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - actionSheetController.addAction(cancelActionButton) - - let all = UIAlertAction(title: localizable("team_all"), style: .default) { _ in - weakSelf?.updateTeamInfoAllAction(model) - } - all.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - all.accessibilityIdentifier = "id.teamAllMember" - actionSheetController.addAction(all) - - let manager = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { _ in - weakSelf?.updateTeamInfoOwnerAction(model) - } - manager.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - manager.accessibilityIdentifier = "id.teamOwner" - actionSheetController.addAction(manager) - - actionSheetController.fixIpadAction() - - navigationController?.present(actionSheetController, animated: true, completion: nil) - } - - func updateTeamInfoOwnerAction(_ model: SettingCellModel) { - weak var weakSelf = self - view.makeToastActivity(.center) - viewmodel.repo - .updateTeamInfoPrivilege(.manager, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in - NELog.infoLog( - ModuleName + " " + self.className(), - desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") - ) - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .manager - model.subTitle = localizable("team_owner_and_manager") - weakSelf?.contentTable.reloadData() - } - } - } - - func updateInviteModeOwnerAction(_ model: SettingCellModel) { - weak var weakSelf = self - view.makeToastActivity(.center) - viewmodel.repo.updateInviteMode(.manager, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in - NELog.infoLog( - ModuleName + " " + self.className(), - desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") - ) - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .manager - model.subTitle = localizable("team_owner_and_manager") - weakSelf?.contentTable.reloadData() - } - } - } - - func updateInviteModeAllAction(_ model: SettingCellModel) { - weak var weakSelf = self - view.makeToastActivity(.center) - viewmodel.repo.updateInviteMode(.all, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in - NELog.infoLog( - ModuleName + " " + self.className(), - desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") - ) - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .all - model.subTitle = localizable("team_all") - weakSelf?.contentTable.reloadData() - } - } - } - - open func didChangeInviteModeClick(_ model: SettingCellModel) { - weak var weakSelf = self - - let actionSheetController = UIAlertController( - title: nil, - message: nil, - preferredStyle: .actionSheet - ) - - let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in - print("Cancel") - } - cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - actionSheetController.addAction(cancelActionButton) - - let allActionButton = UIAlertAction(title: localizable("team_all"), style: .default) { _ in - weakSelf?.updateInviteModeAllAction(model) - } - - allActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - allActionButton.accessibilityIdentifier = "id.teamAllMember" - actionSheetController.addAction(allActionButton) - - let ownerActionButton = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { _ in - weakSelf?.updateInviteModeOwnerAction(model) - } - ownerActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - ownerActionButton.accessibilityIdentifier = "id.teamOwner" - actionSheetController.addAction(ownerActionButton) - - actionSheetController.fixIpadAction() - navigationController?.present(actionSheetController, animated: true, completion: nil) - } - - open func didAtPermissionClick(_ model: SettingCellModel) { - weak var weakSelf = self - - let actionSheetController = UIAlertController( - title: nil, - message: nil, - preferredStyle: .actionSheet - ) - - let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in - print("Cancel") - } - cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - actionSheetController.addAction(cancelActionButton) - - let all = UIAlertAction(title: localizable("team_all"), style: .default) { _ in - weakSelf?.viewmodel.updateTeamAtPermission(false) { error in - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.sendTipNoti(false) { error in - } - model.subTitle = localizable("team_all") - weakSelf?.contentTable.reloadData() - } - } - } - all.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - all.accessibilityIdentifier = "id.teamAllMember" - actionSheetController.addAction(all) - actionSheetController.fixIpadAction() - - let manager = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { _ in - weakSelf?.viewmodel.updateTeamAtPermission(true) { error in - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.sendTipNoti(true) { error in - } - model.subTitle = localizable("team_owner_and_manager") - weakSelf?.contentTable.reloadData() - } - } - } - manager.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - manager.accessibilityIdentifier = "id.teamOwner" - actionSheetController.addAction(manager) - - navigationController?.present(actionSheetController, animated: true, completion: nil) - } - - open func didManagerClick() {} - - open func didRefreshData() { - reloadSectionData() - contentTable.reloadData() - } - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ -} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerController.swift new file mode 100644 index 00000000..8931152d --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerController.swift @@ -0,0 +1,411 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIM2Kit +import NIMSDK +import UIKit + +@objcMembers +open class NEBaseTeamManagerController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource, TeamManagerViewModelDelegate { + public let viewModel = TeamManagerViewModel() + + /// UI样式注册(用户可以自定义) + public var cellClassDic = [Int: NEBaseTeamSettingCell.Type]() + + /// 内容视图 + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + tableView + .tableFooterView = + UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + title = localizable("manage_team") + viewModel.delegate = self + view.backgroundColor = .ne_lightBackgroundColor + view.addSubview(contentTableView) + + if let teamId = viewModel.teamInfoModel?.team?.teamId { + viewModel.getCurrentUserTeamMember(IMKitClient.instance.account(), teamId) { member, error in + } + } + + NSLayoutConstraint.activate([ + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + for (key, value) in cellClassDic { + contentTableView.register(value, forCellReuseIdentifier: "\(key)") + } + } + + /// 页面出现回到(系统类生命周期函数) + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let tid = viewModel.teamInfoModel?.team?.teamId { + viewModel.getTeamWithMembers(tid) { [weak self] error in + self?.reloadSectionData() + self?.contentTableView.reloadData() + } + } + } + + /// 从新加载数据回调,在子类中实现 + open func reloadSectionData() {} + + // MARK: UITableViewDataSource, UITableViewDelegate + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if viewModel.sectionData.count > section { + let model = viewModel.sectionData[section] + return model.cellModels.count + } + return 0 + } + + open func numberOfSections(in tableView: UITableView) -> Int { + viewModel.sectionData.count + } + + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] + if let cell = tableView.dequeueReusableCell( + withIdentifier: "\(model.type)", + for: indexPath + ) as? NEBaseTeamSettingCell { + cell.configure(model) + return cell + } + return UITableViewCell() + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] + if let block = model.cellClick { + block() + } + } + + open func tableView(_ tableView: UITableView, + heightForRowAt indexPath: IndexPath) -> CGFloat { + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] + return model.rowHeight + } + + open func tableView(_ tableView: UITableView, + heightForHeaderInSection section: Int) -> CGFloat { + if viewModel.sectionData.count > section { + let model = viewModel.sectionData[section] + if model.cellModels.count > 0 { + return 12.0 + } + } + return 0 + } + + open func tableView(_ tableView: UITableView, + viewForHeaderInSection section: Int) -> UIView? { + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView + } + + open func tableView(_ tableView: UITableView, + heightForFooterInSection section: Int) -> CGFloat { + if section == viewModel.sectionData.count - 1 { + return 12.0 + } + return 0 + } + + /// 更新编辑群信息权限为任意群成员可以发at消息 + /// - Parameter model: 数据模型 + func updateEditTeamInfoPermissionToEveryone(_ model: SettingCellModel) { + weak var weakSelf = self + view.makeToastActivity(.center) + viewModel.updateTeamInfoPrivilege(weakSelf?.viewModel.teamInfoModel?.team?.teamId ?? "", .TEAM_UPDATE_INFO_MODE_ALL) { error, team in + NEALog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewModel.teamInfoModel?.team = team + model.subTitle = localizable("team_all") + weakSelf?.contentTableView.reloadData() + } + } + } + + /// 更新编辑群信权限为管理员可发送权限 + /// - Parameter model: 数据模型 + func updateEditTeamInfoPermissionToManager(_ model: SettingCellModel) { + weak var weakSelf = self + view.makeToastActivity(.center) + viewModel.updateTeamInfoPrivilege(viewModel.teamInfoModel?.team?.teamId ?? "", .TEAM_UPDATE_INFO_MODE_MANAGER) { error, team in + NEALog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewModel.teamInfoModel?.team = team + model.subTitle = localizable("team_owner_and_manager") + weakSelf?.contentTableView.reloadData() + } + } + } + + /// 更新邀请模式为管理员可邀请 + /// - Parameter model: 数据模型 + func updateInvitePermissionToManager(_ model: SettingCellModel) { + weak var weakSelf = self + view.makeToastActivity(.center) + viewModel.updateInviteMode(viewModel.teamInfoModel?.team?.teamId ?? "", .TEAM_INVITE_MODE_MANAGER) { error, team in + NEALog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewModel.teamInfoModel?.team = team + model.subTitle = localizable("team_owner_and_manager") + weakSelf?.contentTableView.reloadData() + } + } + } + + /// 更新邀请人权限为所有人 + /// - Parameter model: 数据模型 + func updateInvitePermissionToEveryone(_ model: SettingCellModel) { + weak var weakSelf = self + view.makeToastActivity(.center) + viewModel.updateInviteMode(viewModel.teamInfoModel?.team?.teamId ?? "", .TEAM_INVITE_MODE_ALL) { error, team in + NEALog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewModel.teamInfoModel?.team = team + model.subTitle = localizable("team_all") + weakSelf?.contentTableView.reloadData() + } + } + } + + /// 点击修改群信息权限回调 + open func didUpdateTeamInfoClick(_ model: SettingCellModel) { + weak var weakSelf = self + + let actionSheetController = UIAlertController( + title: nil, + message: nil, + preferredStyle: .actionSheet + ) + + let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in + print("Cancel") + } + cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + actionSheetController.addAction(cancelActionButton) + + let allAction = UIAlertAction(title: localizable("team_all"), style: .default) { [weak self] _ in + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.updateEditTeamInfoPermissionToEveryone(model) + } + allAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + allAction.accessibilityIdentifier = "id.teamAllMember" + actionSheetController.addAction(allAction) + + let managerAction = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { [weak self] _ in + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.updateEditTeamInfoPermissionToManager(model) + } + managerAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + managerAction.accessibilityIdentifier = "id.teamOwner" + actionSheetController.addAction(managerAction) + + actionSheetController.fixIpadAction() + + navigationController?.present(actionSheetController, animated: true, completion: nil) + } + + /// 点击修改邀请权限回调 + open func didChangeInviteModeClick(_ model: SettingCellModel) { + weak var weakSelf = self + + let actionSheetController = UIAlertController( + title: nil, + message: nil, + preferredStyle: .actionSheet + ) + + let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in + print("Cancel") + } + cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + actionSheetController.addAction(cancelActionButton) + + let allAction = UIAlertAction(title: localizable("team_all"), style: .default) { [weak self] _ in + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.updateInvitePermissionToEveryone(model) + } + + allAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + allAction.accessibilityIdentifier = "id.teamAllMember" + actionSheetController.addAction(allAction) + + let managerAction = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { [weak self] _ in + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.updateInvitePermissionToManager(model) + } + managerAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + managerAction.accessibilityIdentifier = "id.teamOwner" + actionSheetController.addAction(managerAction) + + actionSheetController.fixIpadAction() + navigationController?.present(actionSheetController, animated: true, completion: nil) + } + + /// 点击修改at权限回调 + open func didAtPermissionClick(_ model: SettingCellModel) { + weak var weakSelf = self + + let actionSheetController = UIAlertController( + title: nil, + message: nil, + preferredStyle: .actionSheet + ) + + let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in + print("Cancel") + } + cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + actionSheetController.addAction(cancelActionButton) + + let allAction = UIAlertAction(title: localizable("team_all"), style: .default) { [weak self] _ in + + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.viewModel.updateTeamAtAllPermission(false) { error in + if let err = error as? NSError { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + model.subTitle = localizable("team_all") + weakSelf?.contentTableView.reloadData() + } + } + } + allAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + allAction.accessibilityIdentifier = "id.teamAllMember" + actionSheetController.addAction(allAction) + actionSheetController.fixIpadAction() + + let managerAction = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { [weak self] _ in + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.viewModel.updateTeamAtAllPermission(true) { error in + if let err = error as? NSError { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + model.subTitle = localizable("team_owner_and_manager") + weakSelf?.contentTableView.reloadData() + } + } + } + managerAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + managerAction.accessibilityIdentifier = "id.teamOwner" + actionSheetController.addAction(managerAction) + + navigationController?.present(actionSheetController, animated: true, completion: nil) + } + + open func didManagerClick() {} + + /// 刷新数据 + open func didRefreshData() { + reloadSectionData() + contentTableView.reloadData() + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift index b804e0d6..bea3795a 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift @@ -3,6 +3,7 @@ // found in the LICENSE file. import NEChatKit +import NECommonKit import NECommonUIKit import UIKit @@ -12,23 +13,24 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel let viewmodel = TeamManagerListViewModel() - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.keyboardDismissMode = .onDrag - table.sectionHeaderHeight = 12.0 - table + /// 内容视图 + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.keyboardDismissMode = .onDrag + tableView.sectionHeaderHeight = 12.0 + tableView .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - return table + return tableView }() public var cellClassDic = [Int: UITableViewCell.Type]() // key 值为 table section 值 @@ -52,16 +54,16 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel } } } - view.addSubview(contentTable) + view.addSubview(contentTableView) NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), - contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0), + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), + contentTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0), ]) - cellClassDic.forEach { (key: Int, value: UITableViewCell.Type) in - contentTable.register(value, forCellReuseIdentifier: "\(key)") + for (key, value) in cellClassDic { + contentTableView.register(value, forCellReuseIdentifier: "\(key)") } } @@ -88,14 +90,14 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel if indexPath.section == 1 { let model = viewmodel.managers[indexPath.row] if let user = model.nimUser { - if IMKitClient.instance.isMySelf(user.userId) { + if IMKitClient.instance.isMe(user.user?.accountId) { Router.shared.use( MeSettingRouter, parameters: ["nav": navigationController as Any], closure: nil ) } else { - if let uid = user.userId { + if let uid = user.user?.accountId { Router.shared.use( ContactUserInfoPageRouter, parameters: ["nav": navigationController as Any, "uid": uid], @@ -107,17 +109,17 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel } } - open func didAddManagers(_ managers: [TeamMemberInfoModel]) { + open func didAddManagers(_ managers: [NETeamMemberInfoModel]) { if let tid = teamId { var uids = [String]() - managers.forEach { member in - if let uid = member.nimUser?.userId { + for member in managers { + if let uid = member.nimUser?.user?.accountId { uids.append(uid) } } viewmodel.addTeamManager(tid, uids) { [weak self] error in - if let err = error { - self?.view.makeToast(err.localizedDescription) + if error != nil { + self?.view.makeToast(localizable("failed_operation")) } else { self?.viewmodel.managers.insert(contentsOf: managers, at: 0) self?.sortAndReloadData() @@ -126,12 +128,20 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel } } - func didClickRemoveButton(_ model: TeamMemberInfoModel?, _ index: Int) { + func didClickRemoveButton(_ model: NETeamMemberInfoModel?, _ index: Int) { print("did click remove button") weak var weakSelf = self // let content = String(format: localizable("confirm_delete_text"), model?.atNameInTeam() ?? "") + localizable("question_mark") + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } showAlert(title: localizable("remove_manager_title"), message: localizable("remove_manager_tip")) { - if let tid = weakSelf?.teamId, let uid = model?.nimUser?.userId { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + if let tid = weakSelf?.teamId, let uid = model?.nimUser?.user?.accountId { weakSelf?.viewmodel.removeTeamManager(tid, [uid]) { error in if let err = error { weakSelf?.view.makeToast(err.localizedDescription) @@ -148,8 +158,8 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel open func getFilters() -> Set { var filters = Set() - viewmodel.managers.forEach { model in - if let uid = model.nimUser?.userId { + for model in viewmodel.managers { + if let uid = model.nimUser?.user?.accountId { filters.insert(uid) } } @@ -159,12 +169,12 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel open func sortAndReloadData() { // 数据源根据时间排序 viewmodel.managers.sort { model1, model2 -> Bool in - if let time1 = model1.teamMember?.createTime, let time2 = model2.teamMember?.createTime { + if let time1 = model1.teamMember?.joinTime, let time2 = model2.teamMember?.joinTime { return time2 > time1 } return false } - contentTable.reloadData() + contentTableView.reloadData() } open func didNeedReloadData() { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift index 86d629ae..e00ab240 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift @@ -5,41 +5,58 @@ import NECommonKit import UIKit -public typealias NESelectTeamMemberBlock = ([TeamMemberInfoModel]) -> Void +public typealias NESelectTeamMemberBlock = ([NETeamMemberInfoModel]) -> Void @objcMembers open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate, TeamMemberSelectViewModelDelegate { public var selectMemberBlock: NESelectTeamMemberBlock? - let viewmodel = TeamMemberSelectViewModel() + let viewModel = TeamMemberSelectViewModel() + /// 群id var teamId: String? public var cellClassDic = [Int: UITableViewCell.Type]() // key 值为 table section 值 - public let searchInput = UITextField() + /// 搜索输入框 + public lazy var searchInput: UITextField = { + let searchInput = UITextField() + searchInput.textColor = UIColor(hexString: "333333") + searchInput.placeholder = localizable("search_member") + searchInput.font = UIFont.systemFont(ofSize: 14.0) + searchInput.returnKeyType = .search + searchInput.delegate = self + searchInput.clearButtonMode = .always + return searchInput + }() + /// 选择数量限制 public var selectCountLimit = 10 - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - table.keyboardDismissMode = .onDrag - table + /// 内容列表 + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + tableView.keyboardDismissMode = .onDrag + tableView .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - return table + return tableView }() + /// 搜索框背景视图 + let searchBackView = UIView() + + /// 数据为空占位图 public lazy var emptyView: NEEmptyDataView = { let view = NEEmptyDataView( imageName: "user_empty", @@ -56,82 +73,73 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe super.viewDidLoad() // Do any additional setup after loading the view. - viewmodel.delegate = self + viewModel.delegate = self setupUI() if let tid = teamId { - viewmodel.getTeamInfo(tid) { [weak self] error in + viewModel.getTeamInfo(tid) { [weak self] error in if let err = error { self?.view.makeToast(err.localizedDescription) } else { - self?.contentTable.reloadData() + self?.contentTableView.reloadData() } } } } + let searchImageView: UIImageView = { + let searchImageView = UIImageView() + searchImageView.image = coreLoader.loadImage("search") + searchImageView.translatesAutoresizingMaskIntoConstraints = false + return searchImageView + }() + open func setupUI() { title = localizable("team_member_select") view.backgroundColor = .white - view.addSubview(contentTable) - - let searchBack = UIView() - view.addSubview(searchBack) - searchBack.backgroundColor = UIColor(hexString: "F2F4F5") - searchBack.translatesAutoresizingMaskIntoConstraints = false - searchBack.clipsToBounds = true - searchBack.layer.cornerRadius = 4.0 + view.addSubview(contentTableView) + + view.addSubview(searchBackView) + searchBackView.backgroundColor = UIColor(hexString: "F2F4F5") + searchBackView.translatesAutoresizingMaskIntoConstraints = false + searchBackView.clipsToBounds = true + searchBackView.layer.cornerRadius = 4.0 NSLayoutConstraint.activate([ - searchBack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - searchBack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - searchBack.topAnchor.constraint(equalTo: view.topAnchor, constant: 13 + topConstant), - searchBack.heightAnchor.constraint(equalToConstant: 32), + searchBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + searchBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + searchBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 13 + topConstant), + searchBackView.heightAnchor.constraint(equalToConstant: 32), ]) - let searchImage = UIImageView() - searchBack.addSubview(searchImage) - searchImage.image = coreLoader.loadImage("search") - searchImage.translatesAutoresizingMaskIntoConstraints = false + searchBackView.addSubview(searchImageView) NSLayoutConstraint.activate([ - searchImage.centerYAnchor.constraint(equalTo: searchBack.centerYAnchor), - searchImage.leftAnchor.constraint(equalTo: searchBack.leftAnchor, constant: 18), - searchImage.widthAnchor.constraint(equalToConstant: 13), - searchImage.heightAnchor.constraint(equalToConstant: 13), + searchImageView.centerYAnchor.constraint(equalTo: searchBackView.centerYAnchor), + searchImageView.leftAnchor.constraint(equalTo: searchBackView.leftAnchor, constant: 18), + searchImageView.widthAnchor.constraint(equalToConstant: 13), + searchImageView.heightAnchor.constraint(equalToConstant: 13), ]) - searchBack.addSubview(searchInput) + searchBackView.addSubview(searchInput) searchInput.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - searchInput.leftAnchor.constraint(equalTo: searchImage.rightAnchor, constant: 5), - searchInput.rightAnchor.constraint(equalTo: searchBack.rightAnchor, constant: -18), - searchInput.topAnchor.constraint(equalTo: searchBack.topAnchor), - searchInput.bottomAnchor.constraint(equalTo: searchBack.bottomAnchor), + searchInput.leftAnchor.constraint(equalTo: searchImageView.rightAnchor, constant: 5), + searchInput.rightAnchor.constraint(equalTo: searchBackView.rightAnchor, constant: -18), + searchInput.topAnchor.constraint(equalTo: searchBackView.topAnchor), + searchInput.bottomAnchor.constraint(equalTo: searchBackView.bottomAnchor), ]) - searchInput.textColor = UIColor(hexString: "333333") - searchInput.placeholder = localizable("search_member") - searchInput.font = UIFont.systemFont(ofSize: 14.0) - searchInput.returnKeyType = .search - searchInput.delegate = self - searchInput.clearButtonMode = .always + if let clearButton = searchInput.value(forKey: "_clearButton") as? UIButton { clearButton.accessibilityIdentifier = "id.clear" } searchInput.accessibilityIdentifier = "id.addFriendAccount" -// NotificationCenter.default.addObserver( -// self, -// selector: #selector(textFieldChange), -// name: UITextField.textDidChangeNotification, -// object: nil -// ) - NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), - contentTable.topAnchor.constraint(equalTo: searchBack.bottomAnchor), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.topAnchor.constraint(equalTo: searchBackView.bottomAnchor), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: UITableViewCell.Type) in - contentTable.register(value, forCellReuseIdentifier: "\(key)") + for (key, value) in cellClassDic { + contentTableView.register(value, forCellReuseIdentifier: "\(key)") } view.addSubview(emptyView) @@ -149,12 +157,12 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if viewmodel.showDatas.count <= 0 { + if viewModel.showDatas.count <= 0 { emptyView.isHidden = false } else { emptyView.isHidden = true } - return viewmodel.showDatas.count + return viewModel.showDatas.count } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -162,22 +170,17 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let model = viewmodel.showDatas[indexPath.row] + let model = viewModel.showDatas[indexPath.row] guard let cell = tableView.cellForRow(at: indexPath) as? NEBaseTeamMemberSelectCell else { return } - if let member = model.member, let accid = member.teamMember?.userId { - if viewmodel.selectDic[accid] != nil { - viewmodel.selectDic[accid] = nil + if let member = model.member, let accid = member.teamMember?.accountId { + if viewModel.selectDic[accid] != nil { + viewModel.selectDic[accid] = nil model.isSelected = false cell.checkImageView.isHighlighted = false } else { - if viewmodel.selectDic.count >= selectCountLimit { - let toastString = String(format: localizable("select_limit_tip"), selectCountLimit) - view.makeToast(toastString) - return - } - viewmodel.selectDic[accid] = member + viewModel.selectDic[accid] = member model.isSelected = true cell.checkImageView.isHighlighted = true } @@ -200,42 +203,46 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe } open func textFieldShouldClear(_ textField: UITextField) -> Bool { - viewmodel.showDatas = viewmodel.datas - contentTable.reloadData() + viewModel.showDatas = viewModel.datas + contentTableView.reloadData() return true } + /// 文本输入变更 open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { let finalString = (textField.text! as NSString).replacingCharacters(in: range, with: string) if string.count <= 0 { if finalString.count <= 0 { - viewmodel.showDatas = viewmodel.datas - contentTable.reloadData() + viewModel.showDatas = viewModel.datas + contentTableView.reloadData() } else { - viewmodel.showDatas = viewmodel.searchAllData(finalString) - contentTable.reloadData() + viewModel.showDatas = viewModel.searchAllData(finalString) + contentTableView.reloadData() } } else { - viewmodel.showDatas = viewmodel.searchAllData(finalString) - contentTable.reloadData() + viewModel.showDatas = viewModel.searchAllData(finalString) + contentTableView.reloadData() } return true } + /// 选择成员变更回调,内部根据选择数量来做右上角状态变更 func didChangeSelectMember() { - if viewmodel.selectDic.count > 0 { - let title = localizable("member_select_sure") + "(\(viewmodel.selectDic.count))" + if viewModel.selectDic.count > 0 { + let title = localizable("member_select_sure") + "(\(viewModel.selectDic.count))" navigationView.moreButton.setTitle(title, for: .normal) } else { navigationView.moreButton.setTitle(localizable("member_select_sure"), for: .normal) } } + /// 刷新回调 open func didNeedRefresh() { - contentTable.reloadData() + contentTableView.reloadData() didChangeSelectMember() } + /// 点击确定添加回调 open func didClickSure() { print("sure click") @@ -244,18 +251,18 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe return } - if viewmodel.selectDic.count + viewmodel.managerSet.count > selectCountLimit { + if viewModel.selectDic.count + viewModel.managerSet.count > selectCountLimit { view.makeToast(localizable("max_managers_tip")) return } - if viewmodel.selectDic.count <= 0 { + if viewModel.selectDic.count <= 0 { view.makeToast(localizable("member_empty_tip")) return } - var retArray = [TeamMemberInfoModel]() - viewmodel.selectDic.forEach { (key: String, value: TeamMemberInfoModel) in + var retArray = [NETeamMemberInfoModel]() + for (_, value) in viewModel.selectDic { retArray.append(value) } if let block = selectMemberBlock { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift index 48862110..7a402e08 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift @@ -5,35 +5,36 @@ import NEChatKit import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @objcMembers open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource, TeamMemberCellDelegate, TeamMembersViewModelDelegate { + /// 群id public var teamId: String? - - public var memberDatas: [TeamMemberInfoModel]? { + /// 群成员数据 + public var memberDatas: [NETeamMemberInfoModel]? { didSet { - viewmodel.setShowDatas(memberDatas) + viewModel.setShowDatas(memberDatas) } } + /// 创建者account id public var ownerId: String? public var isSenior = false - public var searchDatas = [TeamMemberInfoModel]() - - public let back = UIView() + public let backView = UIView() - let viewmodel = TeamMembersViewModel() + let viewModel = TeamMembersViewModel() + /// 搜索输入控件 public lazy var searchTextField: UITextField = { let field = UITextField() field.translatesAutoresizingMaskIntoConstraints = false - field.placeholder = localizable("search_friend") + field.placeholder = commonLocalizable("search") field.clearButtonMode = .always field.textColor = .ne_greyText field.font = UIFont.systemFont(ofSize: 14.0) @@ -45,120 +46,126 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat return field }() - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - table + /// 群成员列表视图 + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + tableView .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - table.keyboardDismissMode = .onDrag - return table + tableView.keyboardDismissMode = .onDrag + return tableView }() - lazy var emptyView: NEEmptyDataView = { + /// 空占位图 + public lazy var emptyView: NEEmptyDataView = { + // member_select_no_member let view = NEEmptyDataView(imageName: "user_empty", content: localizable("no_result"), frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.isHidden = true return view }() + /// 搜索背景图 + public lazy var searchIconImageView: UIImageView = { + let searchIconImageView = UIImageView() + searchIconImageView.image = coreLoader.loadImage("search_icon") + searchIconImageView.translatesAutoresizingMaskIntoConstraints = false + return searchIconImageView + }() + public init(teamId: String?) { super.init(nibName: nil, bundle: nil) self.teamId = teamId } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() addObserver() + viewModel.delegate = self + viewModel.teamId = teamId - viewmodel.delegate = self - let team = TeamProvider.shared.getTeam(teamId: teamId ?? "") - ownerId = team?.owner - if team?.isDisscuss() == false { - isSenior = true - } - if let tid = team?.teamId { - viewmodel.getMemberInfo(tid) - } - - setupUI() - - TeamRepo.shared.fetchTeamInfo(teamId ?? "") { [weak self] error, teamModel in - if error != nil { - self?.emptyView.isHidden = false - } else { - self?.viewmodel.setShowDatas(teamModel?.users) - self?.didNeedRefreshUI() + weak var weakSelf = self + if let tid = teamId { + weakSelf?.viewModel.getTeamInfo(tid) { teamInfo, error in + weakSelf?.ownerId = teamInfo?.team?.ownerAccountId + if error != nil { + weakSelf?.emptyView.isHidden = false + if let err = error { + weakSelf?.showToast(err.localizedDescription) + } + } else { + if teamInfo?.team?.isDisscuss() == false { + weakSelf?.isSenior = true + weakSelf?.title = localizable("group_memmber") + } else { + weakSelf?.title = localizable("discuss_mebmer") + } + weakSelf?.didNeedRefreshUI() + } } } + setupUI() } + /// UI 初始化 open func setupUI() { - if isSenior { - title = localizable("group_memmber") - } else { - title = localizable("discuss_mebmer") - } - - back.backgroundColor = .clear - back.translatesAutoresizingMaskIntoConstraints = false - back.clipsToBounds = true - back.layer.cornerRadius = 4.0 + backView.backgroundColor = .clear + backView.translatesAutoresizingMaskIntoConstraints = false + backView.clipsToBounds = true + backView.layer.cornerRadius = 4.0 - view.addSubview(back) + view.addSubview(backView) NSLayoutConstraint.activate([ - back.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0 + topConstant), - back.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - back.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - back.heightAnchor.constraint(equalToConstant: 32), + backView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0 + topConstant), + backView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + backView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + backView.heightAnchor.constraint(equalToConstant: 32), ]) - let searchIcon = UIImageView() - searchIcon.image = coreLoader.loadImage("search_icon") - searchIcon.translatesAutoresizingMaskIntoConstraints = false - back.addSubview(searchIcon) + backView.addSubview(searchIconImageView) NSLayoutConstraint.activate([ - searchIcon.centerYAnchor.constraint(equalTo: back.centerYAnchor), - searchIcon.leftAnchor.constraint(equalTo: back.leftAnchor, constant: 16.0), + searchIconImageView.centerYAnchor.constraint(equalTo: backView.centerYAnchor), + searchIconImageView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16.0), ]) - back.addSubview(searchTextField) + backView.addSubview(searchTextField) NSLayoutConstraint.activate([ - searchTextField.leftAnchor.constraint(equalTo: back.leftAnchor, constant: 36.0), - searchTextField.rightAnchor.constraint(equalTo: back.rightAnchor, constant: -16.0), - searchTextField.topAnchor.constraint(equalTo: back.topAnchor), - searchTextField.bottomAnchor.constraint(equalTo: back.bottomAnchor), + searchTextField.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 36.0), + searchTextField.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -16.0), + searchTextField.topAnchor.constraint(equalTo: backView.topAnchor), + searchTextField.bottomAnchor.constraint(equalTo: backView.bottomAnchor), ]) - view.addSubview(contentTable) + view.addSubview(contentTableView) NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), - contentTable.topAnchor.constraint(equalTo: back.bottomAnchor, constant: 10), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.topAnchor.constraint(equalTo: backView.bottomAnchor, constant: 10), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - contentTable.register(NEBaseTeamMemberCell.self, forCellReuseIdentifier: "\(NEBaseTeamMemberCell.self)") + contentTableView.register(NEBaseTeamMemberCell.self, forCellReuseIdentifier: "\(NEBaseTeamMemberCell.self)") view.addSubview(emptyView) NSLayoutConstraint.activate([ - emptyView.leftAnchor.constraint(equalTo: contentTable.leftAnchor), - emptyView.rightAnchor.constraint(equalTo: contentTable.rightAnchor), - emptyView.topAnchor.constraint(equalTo: contentTable.topAnchor, constant: 50), - emptyView.bottomAnchor.constraint(equalTo: contentTable.bottomAnchor), + emptyView.leftAnchor.constraint(equalTo: contentTableView.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: contentTableView.rightAnchor), + emptyView.topAnchor.constraint(equalTo: contentTableView.topAnchor, constant: 50), + emptyView.bottomAnchor.constraint(equalTo: contentTableView.bottomAnchor), ]) } @@ -169,7 +176,6 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat name: UITextField.textDidChangeNotification, object: nil ) - NotificationCenter.default.addObserver(self, selector: #selector(didNeedRefreshUI), name: NENotificationName.updateFriendInfo, object: nil) } func isOwner(_ userId: String?) -> Bool { @@ -183,31 +189,31 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat } func textChange() { - searchDatas.removeAll() + viewModel.searchDatas.removeAll() if let text = searchTextField.text, text.count > 0 { - viewmodel.datas.forEach { model in - if let uid = model.nimUser?.userId, uid.contains(text) { - searchDatas.append(model) - } else if let nick = model.nimUser?.userInfo?.nickName, nick.contains(text) { - searchDatas.append(model) - } else if let alias = model.nimUser?.alias, alias.contains(text) { - searchDatas.append(model) - } else if let tNick = model.teamMember?.nickname, tNick.contains(text) { - searchDatas.append(model) + for model in viewModel.datas { + if let uid = model.nimUser?.user?.accountId, uid.contains(text) { + viewModel.searchDatas.append(model) + } else if let nick = model.nimUser?.user?.name, nick.contains(text) { + viewModel.searchDatas.append(model) + } else if let alias = model.nimUser?.friend?.alias, alias.contains(text) { + viewModel.searchDatas.append(model) + } else if let tNick = model.teamMember?.teamNick, tNick.contains(text) { + viewModel.searchDatas.append(model) } } - emptyView.isHidden = searchDatas.count > 0 + } else { emptyView.isHidden = true } didNeedRefreshUI() } - func getRealModel(_ index: Int) -> TeamMemberInfoModel? { + func getRealModel(_ index: Int) -> NETeamMemberInfoModel? { if let text = searchTextField.text, text.count > 0 { - return searchDatas[index] + return viewModel.searchDatas[index] } - return viewmodel.datas[index] + return viewModel.datas[index] } deinit { @@ -218,9 +224,9 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if let text = searchTextField.text, text.count > 0 { - return searchDatas.count + return viewModel.searchDatas.count } - return viewmodel.datas.count + return viewModel.datas.count } open func tableView(_ tableView: UITableView, @@ -231,7 +237,7 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat ) as? NEBaseTeamMemberCell { if let model = getRealModel(indexPath.row) { cell.configure(model) - cell.ownerLabel.isHidden = !isOwner(model.nimUser?.userId) + cell.ownerLabel.isHidden = !isOwner(model.nimUser?.user?.accountId) } return cell } @@ -245,14 +251,14 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if let model = getRealModel(indexPath.row), let user = model.nimUser { - if IMKitClient.instance.isMySelf(user.userId) { + if IMKitClient.instance.isMe(user.user?.accountId) { Router.shared.use( MeSettingRouter, parameters: ["nav": navigationController as Any], closure: nil ) } else { - if let uid = user.userId { + if let uid = user.user?.accountId { Router.shared.use( ContactUserInfoPageRouter, parameters: ["nav": navigationController as Any, "uid": uid], @@ -263,20 +269,20 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat } } - func didClickRemoveButton(_ model: TeamMemberInfoModel?, _ index: Int) { + /// 移除群成员 + /// - Parameter model: 成员信息 + /// - Parameter index: 成员索引 + func didClickRemoveButton(_ model: NETeamMemberInfoModel?, _ index: Int) { print("did click remove button") weak var weakSelf = self - - // 注释掉的暂时留存,后续可能更改提示语 let content = String(format: localizable("confirm_delete_text"), model?.atNameInTeam() ?? "") + localizable("question_mark") - showAlert(title: localizable("remove_manager_title"), message: localizable("remove_member_tip")) { if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { weakSelf?.view.makeToast(commonLocalizable("network_error")) return } - if let tid = weakSelf?.teamId, let uid = model?.nimUser?.userId { - weakSelf?.viewmodel.removeTeamMember(tid, [uid]) { error in + if let tid = weakSelf?.teamId, let uid = model?.nimUser?.user?.accountId { + weakSelf?.viewModel.removeTeamMember(tid, [uid]) { error in if let err = error { if err.code == noPermissionCode { weakSelf?.view.makeToast(localizable("no_permission_tip")) @@ -285,11 +291,17 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat } } else { if let text = weakSelf?.searchTextField.text, text.count > 0 { - weakSelf?.searchDatas.remove(at: index) - weakSelf?.viewmodel.removeModel(model) + weakSelf?.viewModel.searchDatas.remove(at: index) + weakSelf?.viewModel.searchDatas.removeAll(where: { model in + if model.teamMember?.accountId == uid { + return true + } + return false + }) + weakSelf?.viewModel.removeModel(model) weakSelf?.didNeedRefreshUI() } else { - weakSelf?.viewmodel.removeModel(model) + weakSelf?.viewModel.removeModel(model) weakSelf?.didNeedRefreshUI() } } @@ -298,9 +310,10 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat } } - // 查找移除数据在 data 中的位置 - func didNeedRefreshUI() { - contentTable.reloadData() + if let text = searchTextField.text, text.count > 0 { + emptyView.isHidden = viewModel.searchDatas.count > 0 + } + contentTableView.reloadData() } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamNameViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamNameViewController.swift similarity index 58% rename from NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamNameViewController.swift rename to NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamNameViewController.swift index 237e5d71..8b6727ee 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamNameViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamNameViewController.swift @@ -9,16 +9,22 @@ import UIKit @objcMembers open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegate { - public var team: NIMTeam? + /// 群对象 + public var team: V2NIMTeam? + /// 修改类型 public var type = ChangeType.TeamName - public var teamMember: NIMTeamMember? + /// 群成员 + public var teamMember: V2NIMTeamMember? + /// 数据单例 public var repo = TeamRepo.shared - public let textLimit = 30 - + /// 输入长度限制 + public var textLimit = 30 + /// 背景视图 public let backView = UIView() let viewModel = TeamNameViewModel() + /// 计数文本显示标签 public lazy var countLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -29,16 +35,18 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat return label }() - public lazy var textView: UITextView = { - let text = UITextView() - text.translatesAutoresizingMaskIntoConstraints = false - text.textColor = NEConstant.hexRGB(0x333333) - text.font = NEConstant.defaultTextFont(14.0) - text.delegate = self - text.accessibilityIdentifier = "id.nickname" - return text + /// 名称输入框 + public lazy var textInputView: UITextView = { + let textView = UITextView() + textView.translatesAutoresizingMaskIntoConstraints = false + textView.textColor = NEConstant.hexRGB(0x333333) + textView.font = NEConstant.defaultTextFont(14.0) + textView.delegate = self + textView.accessibilityIdentifier = "id.nickname" + return textView }() + /// 清除按钮 public lazy var clearButton: UIButton = { let text = UIButton() text.translatesAutoresizingMaskIntoConstraints = false @@ -50,16 +58,22 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat override open func viewDidLoad() { super.viewDidLoad() - viewModel.getCurrentUserTeamMember(team?.teamId) - setupUI() + weak var weakSelf = self + viewModel.getCurrentUserTeamMember(team?.teamId) { error in + if let err = error { + weakSelf?.view.makeToast(err.localizedDescription) + } + weakSelf?.setupUI() + } } + /// UI 控件出事还 open func setupUI() { navigationView.setMoreButtonTitle(localizable("save")) navigationView.addMoreButtonTarget(target: self, selector: #selector(saveName)) view.addSubview(backView) - backView.addSubview(textView) + backView.addSubview(textInputView) backView.addSubview(clearButton) backView.addSubview(countLabel) @@ -73,16 +87,16 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat ]) var name = "" - if type == .TeamName, let n = team?.teamName { + if type == .TeamName, let n = team?.name { name = n - if changePermission() == false { - rightNavBtn.isHidden = true + if getEditablePermission() == false { + rightNavButton.isHidden = true navigationView.moreButton.isHidden = true - textView.isEditable = false + textInputView.isEditable = false } } else if type == .NickName { title = localizable("team_nick") - if let n = teamMember?.nickname { + if let n = teamMember?.teamNick { name = n } } @@ -94,58 +108,55 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat } } - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ - - func changePermission() -> Bool { + /// 查看是否有编辑权限 + func getEditablePermission() -> Bool { if type == .NickName { return true } - - if let type = team?.type, type == .normal { - return true - } - - if let ownerId = team?.owner, IMKitClient.instance.isMySelf(ownerId) { + if let mode = team?.updateInfoMode, mode == .TEAM_UPDATE_INFO_MODE_ALL { return true } - if let mode = team?.updateInfoMode, mode == .all { + if let ownerId = team?.ownerAccountId, IMKitClient.instance.isMe(ownerId) { return true } - if let member = viewModel.currentTeamMember, member.type == .manager { + + if let member = viewModel.currentTeamMember, member.memberRole == .TEAM_MEMBER_ROLE_MANAGER { return true } return false } + /// 提交按钮显示不可提交状态 open func disableSubmit() { - rightNavBtn.setTitleColor(NEConstant.hexRGBAlpha(0x337EFF, 0.5), for: .normal) - rightNavBtn.isEnabled = false + rightNavButton.setTitleColor(NEConstant.hexRGBAlpha(0x337EFF, 0.5), for: .normal) + rightNavButton.isEnabled = false navigationView.moreButton.setTitleColor(NEConstant.hexRGBAlpha(0x337EFF, 0.5), for: .normal) navigationView.moreButton.isEnabled = false } + /// 提交按钮显示可提交状态 open func enableSubmit() { - rightNavBtn.setTitleColor(NEConstant.hexRGB(0x337EFF), for: .normal) - rightNavBtn.isEnabled = true + rightNavButton.setTitleColor(NEConstant.hexRGB(0x337EFF), for: .normal) + rightNavButton.isEnabled = true navigationView.moreButton.setTitleColor(NEConstant.hexRGB(0x337EFF), for: .normal) navigationView.moreButton.isEnabled = true } + /// 保存群名称 open func saveName() { guard let tid = team?.teamId else { showToast(localizable("failed_operation")) return } - if let text = textView.text, + weak var weakSelf = self + + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + + if let text = textInputView.text, !text.isEmpty { let trimText = text.trimmingCharacters(in: .whitespaces) if trimText.isEmpty { @@ -155,32 +166,30 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat } } - weak var weakSelf = self - if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { - weakSelf?.showToast(commonLocalizable("network_error")) - return - } + textInputView.resignFirstResponder() - textView.resignFirstResponder() if type == .TeamName { - let n = textView.text ?? "" + let n = textInputView.text ?? "" view.makeToastActivity(.center) repo.updateTeamName(tid, n) { error in weakSelf?.view.hideToastActivity() - if let err = error { + if error != nil { + if error?.code == noPermissionOperationCode { + weakSelf?.showToast(localizable("no_permission_tip")) + return + } weakSelf?.showToast(localizable("failed_operation")) } else { - weakSelf?.team?.teamName = n weakSelf?.navigationController?.popViewController(animated: true) } } - } else if type == .NickName, let uid = teamMember?.userId { - let n = textView.text ?? "" + } else if type == .NickName, let uid = teamMember?.accountId { + let n = textInputView.text ?? "" view.makeToastActivity(.center) repo.updateMemberNick(tid, uid, n) { error in weakSelf?.view.hideToastActivity() - if let err = error { + if error != nil { weakSelf?.showToast(localizable("failed_operation")) } else { weakSelf?.navigationController?.popViewController(animated: true) @@ -189,14 +198,17 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat } } + /// 清除文本 func clearText() { figureTextCount("") } + /// 计算显示数量 + /// - Parameter text: 文本内容 func figureTextCount(_ text: String) { - textView.text = text - countLabel.text = "\(text.count)/\(textLimit)" - clearButton.isHidden = !changePermission() || text.count <= 0 + textInputView.text = text + countLabel.text = "\(text.utf16.count)/\(textLimit)" + clearButton.isHidden = !getEditablePermission() || text.utf16.count <= 0 if type == .NickName { return } @@ -207,17 +219,28 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat } } - // MARK: UITextViewDelegate - + /// 文本变更回调 + /// - Parameter textView: 文本控件对象 open func textViewDidChange(_ textView: UITextView) { if let _ = textView.markedTextRange { return } - if var text = textView.text { - if text.count > textLimit { - text = String(text.prefix(textLimit)) - } + if let text = textView.text { figureTextCount(text) } } + + /// 文本变更回调 + /// - Parameter textView: 文本控件对象 + /// - Parameter range: 变更范围 + /// - Parameter text: 变更内容 + public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if !text.isEmpty { + let finalStr = (textView.text as NSString).replacingCharacters(in: range, with: text) + if finalStr.utf16.count > textLimit { + return false + } + } + return true + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamSettingViewController.swift similarity index 58% rename from NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingViewController.swift rename to NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamSettingViewController.swift index 75b580fc..e0631472 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamSettingViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -12,41 +12,44 @@ import UIKit open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UITableViewDataSource, UITableViewDelegate, TeamSettingViewModelDelegate { - public let viewmodel = TeamSettingViewModel() - + /// 数据管理类 + public let viewModel = TeamSettingViewModel() + /// 群id public var teamId: String? - public var addBtnWidth: NSLayoutConstraint? + public var addButtonWidth: NSLayoutConstraint? - public var addBtnLeftMargin: NSLayoutConstraint? + public var addButtonLeftMargin: NSLayoutConstraint? + /// 群类型 public var teamSettingType: TeamSettingType = .Discuss - public var isSeniorDiscuss = false // 是否是高级群扩展的讨论组 + /// 是否是高级群扩展的讨论组 + public var isSeniorDiscuss = false var className = "TeamSettingViewController" public var cellClassDic = [Int: NEBaseTeamSettingCell.Type]() - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - table + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + tableView .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - return table + return tableView }() - public lazy var teamHeader: NEUserHeaderView = { + public lazy var teamHeaderView: NEUserHeaderView = { let imageView = NEUserHeaderView(frame: .zero) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.clipsToBounds = true @@ -63,6 +66,15 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi return label }() + public lazy var memberLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = NEConstant.hexRGB(0x333333) + label.font = NEConstant.defaultTextFont(16.0) + label.accessibilityIdentifier = "id.member" + return label + }() + public lazy var memberCountLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -72,21 +84,22 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi return label }() - public lazy var userinfoCollection: UICollectionView = { - let flow = UICollectionViewFlowLayout() - flow.scrollDirection = .horizontal - flow.minimumLineSpacing = 0 - flow.minimumInteritemSpacing = 0 - let collection = UICollectionView(frame: .zero, collectionViewLayout: flow) - collection.translatesAutoresizingMaskIntoConstraints = false - collection.delegate = self - collection.dataSource = self - collection.backgroundColor = .clear - collection.showsHorizontalScrollIndicator = false - return collection + public lazy var userinfoCollectionView: UICollectionView = { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + flowLayout.minimumLineSpacing = 0 + flowLayout.minimumInteritemSpacing = 0 + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.backgroundColor = .clear + collectionView.showsHorizontalScrollIndicator = false + collectionView.isScrollEnabled = false + return collectionView }() - public lazy var addBtn: ExpandButton = { + public lazy var addButton: ExpandButton = { let button = ExpandButton() button.translatesAutoresizingMaskIntoConstraints = false button.accessibilityIdentifier = "id.add" @@ -95,11 +108,11 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if let model = viewmodel.teamInfoModel { - if let url = model.team?.avatarUrl, !url.isEmpty { - teamHeader.sd_setImage(with: URL(string: url)) + if let model = viewModel.teamInfoModel { + if let url = model.team?.avatar, !url.isEmpty { + teamHeaderView.sd_setImage(with: URL(string: url)) } - if let name = model.team?.teamName { + if let name = model.team?.name { teamNameLabel.text = name } } @@ -110,73 +123,108 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi title = localizable("setting") weak var weakSelf = self - viewmodel.delegate = self + viewModel.delegate = self if let tid = teamId { - viewmodel.getTeamInfo(tid) { error in - NELog.infoLog( - ModuleName + " " + self.className, - desc: "CALLBACK getTeamInfo " + (error?.localizedDescription ?? "no error") - ) - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else { - weakSelf?.showToast(localizable("team_not_exist")) - } + viewModel.getCurrentMember(IMKitClient.instance.account(), tid) { member, error in + if let currentMember = member { + weakSelf?.requestSettingData(tid, currentMember) } else { - if let type = weakSelf?.viewmodel.teamInfoModel?.team?.type { - if type == .normal { - weakSelf?.teamSettingType = .Discuss - } else if type == .advanced { - if let custom = weakSelf?.viewmodel.teamInfoModel?.team?.clientCustomInfo, custom.contains(discussTeamKey) { - weakSelf?.teamSettingType = .Discuss - weakSelf?.isSeniorDiscuss = true - } else { - weakSelf?.teamSettingType = .Senior - } - } - } - if let type = weakSelf?.teamSettingType { - weakSelf?.viewmodel.teamSettingType = type + if let err = error { + weakSelf?.showToast(err.localizedDescription) } - weakSelf?.reloadSectionData() - weakSelf?.contentTable.tableHeaderView = weakSelf?.getHeaderView() - weakSelf?.contentTable.tableFooterView = weakSelf?.getFooterView() - weakSelf?.contentTable.reloadData() - weakSelf?.didRefreshUserinfoCollection() - weakSelf?.checkoutAddShowOrHide() } } + } else { + showToast("team id is nil") } - // Do any additional setup after loading the view. setupUI() - - NotificationCenter.default.addObserver(self, selector: #selector(didRefreshUserinfoCollection), name: NENotificationName.updateFriendInfo, object: nil) } open func reloadSectionData() {} open func didRefreshUserinfoCollection() { - userinfoCollection.reloadData() + userinfoCollectionView.reloadData() } + /// 初始化 open func setupUI() { view.backgroundColor = .ne_lightBackgroundColor - view.addSubview(contentTable) + view.addSubview(contentTableView) NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), - contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in - contentTable.register(value, forCellReuseIdentifier: "\(key)") + for (key, value) in cellClassDic { + contentTableView.register(value, forCellReuseIdentifier: "\(key)") } if let pan = navigationController?.interactivePopGestureRecognizer { - contentTable.panGestureRecognizer.require(toFail: pan) + contentTableView.panGestureRecognizer.require(toFail: pan) + } + } + + /// 设置群设置顶部视图展示内容 + open func setTeamHeaderInfo() { + if let url = viewModel.teamInfoModel?.team?.avatar, !url.isEmpty { + print("icon url : ", url) + teamHeaderView.sd_setImage(with: URL(string: url), completed: nil) + } else { + if let tid = teamId { + if let name = viewModel.teamInfoModel?.team?.getShowName() { + teamHeaderView.setTitle(name) + } + teamHeaderView.backgroundColor = UIColor.colorWithString(string: "\(tid)") + } + } + teamNameLabel.text = viewModel.teamInfoModel?.team?.getShowName() + } + + /// 获取群设置数据 + public func requestSettingData(_ tid: String, _ member: V2NIMTeamMember) { + weak var weakSelf = self + viewModel.getTeamWithMembers(tid) { error in + NEALog.infoLog( + ModuleName + " " + self.className, + desc: "CALLBACK getTeamInfo " + (error?.localizedDescription ?? "no error") + ) + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == teamNotExistCode { + weakSelf?.showToast(localizable("team_not_exist")) + } else { + weakSelf?.showToast(err.localizedDescription) + } + } else { + if let type = weakSelf?.viewModel.teamInfoModel?.team?.teamType { + if type == .TEAM_TYPE_NORMAL { + if let custom = weakSelf?.viewModel.teamInfoModel?.team?.serverExtension, custom.contains(discussTeamKey) { + weakSelf?.teamSettingType = .Discuss + weakSelf?.isSeniorDiscuss = true + } else { + weakSelf?.teamSettingType = .Senior + } + } + } + if let type = weakSelf?.teamSettingType { + weakSelf?.viewModel.teamSettingType = type + } + weakSelf?.resetupUI() + } } } + /// 有数据返回之后重新刷新UI + public func resetupUI() { + reloadSectionData() + contentTableView.tableHeaderView = getHeaderView() + contentTableView.tableFooterView = getFooterView() + contentTableView.reloadData() + didRefreshUserinfoCollection() + checkoutAddShowOrHide() + } + open func getHeaderView() -> UIView { UIView() } @@ -189,38 +237,44 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi if teamSettingType == .Discuss { return localizable("leave_discuss") } else if teamSettingType == .Senior { - return viewmodel.isOwner() ? localizable("dismiss_team") : localizable("leave_team") + return viewModel.isOwner() ? localizable("dismiss_team") : localizable("leave_team") } return nil } open func setupUserInfoCollection(_ cornerView: UIView) {} - // MARK: objc 方法 - open func addUser() { weak var weakSelf = self Router.shared.register(ContactSelectedUsersRouter) { param in print("addUser weak self ", weakSelf as Any) if let accids = param["accids"] as? [String], - let tid = self.viewmodel.teamInfoModel?.team?.teamId, - let beInviteMode = self.viewmodel.teamInfoModel?.team?.beInviteMode, - let type = self.viewmodel.teamInfoModel?.team?.type { - if beInviteMode == .noAuth || type == .normal { - self.didAddUserAndRefreshUI(accids, tid) + let tid = weakSelf?.viewModel.teamInfoModel?.team?.teamId, + let beInviteMode = weakSelf?.viewModel.teamInfoModel?.team?.agreeMode, + let type = weakSelf?.viewModel.teamInfoModel?.team?.teamType { + if beInviteMode == .TEAM_AGREE_MODE_NO_AUTH || type == .TEAM_TYPE_NORMAL { + weakSelf?.didAddUserAndRefreshUI(accids, tid) } else { - self.didAddUser(accids, tid) + weakSelf?.didAddUser(accids, tid) } } } + var param = [String: Any]() param["nav"] = navigationController as Any var filters = Set() - viewmodel.teamInfoModel?.users.forEach { model in - if let uid = model.nimUser?.userId { - filters.insert(uid) + if viewModel.allMembersDic.count > 0 { + for (key, value) in viewModel.allMembersDic { + filters.insert(value.accountId) + } + } else { + viewModel.teamInfoModel?.users.forEach { model in + if let uid = model.nimUser?.user?.accountId { + filters.insert(uid) + } } } + if filters.count > 0 { param["filters"] = filters } @@ -229,11 +283,21 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi Router.shared.use(ContactUserSelectRouter, parameters: param, closure: nil) } + /// 退出/解散群聊 open func removeTeamForMyself() { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + weak var weakSelf = self if teamSettingType == .Senior { - showAlert(message: viewmodel.isOwner() ? localizable("dissolute_team_chat") : localizable("quit_team_chat")) { - if weakSelf?.viewmodel.isOwner() == true { + showAlert(message: viewModel.isOwner() ? localizable("dissolute_team_chat") : localizable("quit_team_chat")) { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + if weakSelf?.viewModel.isOwner() == true { weakSelf?.dismissTeam() } else { weakSelf?.leaveTeam() @@ -241,16 +305,21 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } } else if teamSettingType == .Discuss { showAlert(message: localizable("quit_discuss_chat")) { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } weakSelf?.leaveDiscuss() } } } + /// 离开讨论组 open func leaveDiscuss() { weak var weakSelf = self - if isSeniorDiscuss == true, viewmodel.isOwner() { + if isSeniorDiscuss == true, viewModel.isOwner() { view.makeToastActivity(.center) - viewmodel.transferTeamOwner { error in + viewModel.transferTeamOwner { error in weakSelf?.view.hideToastActivity() if let err = error as? NSError { weakSelf?.didError(err) @@ -265,8 +334,9 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func toInfoView() {} + /// 跳转成员列表 open func toMemberList() { - let memberController = NEBaseTeamMembersController(teamId: viewmodel.teamInfoModel?.team?.teamId) + let memberController = NEBaseTeamMembersController(teamId: viewModel.teamInfoModel?.team?.teamId) navigationController?.pushViewController(memberController, animated: true) } @@ -274,8 +344,7 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - print("numberOfItemsInSection ", viewmodel.teamInfoModel?.users.count as Any) - return viewmodel.teamInfoModel?.users.count ?? 0 + viewModel.teamInfoModel?.users.count ?? 0 } open func collectionView(_ collectionView: UICollectionView, @@ -291,16 +360,16 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - if let member = viewmodel.teamInfoModel?.users[indexPath.row], + if let member = viewModel.teamInfoModel?.users[indexPath.row], let nimUser = member.nimUser { - if IMKitClient.instance.isMySelf(nimUser.userId) { + if IMKitClient.instance.isMe(nimUser.user?.accountId) { Router.shared.use( MeSettingRouter, parameters: ["nav": navigationController as Any], closure: nil ) } else { - if let uid = nimUser.userId { + if let uid = nimUser.user?.accountId { Router.shared.use( ContactUserInfoPageRouter, parameters: ["nav": navigationController as Any, "uid": uid], @@ -314,20 +383,20 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi // MARK: UITableViewDataSource, UITableViewDelegate open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if viewmodel.sectionData.count > section { - let model = viewmodel.sectionData[section] + if viewModel.sectionData.count > section { + let model = viewModel.sectionData[section] return model.cellModels.count } return 0 } open func numberOfSections(in tableView: UITableView) -> Int { - viewmodel.sectionData.count + viewModel.sectionData.count } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] if let cell = tableView.dequeueReusableCell( withIdentifier: "\(model.type)", for: indexPath @@ -339,7 +408,7 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] if let block = model.cellClick { block() } @@ -347,14 +416,14 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] return model.rowHeight } open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - if viewmodel.sectionData.count > section { - let model = viewmodel.sectionData[section] + if viewModel.sectionData.count > section { + let model = viewModel.sectionData[section] if model.cellModels.count > 0 { return 12.0 } @@ -364,48 +433,37 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView } open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - if section == viewmodel.sectionData.count - 1 { + if section == viewModel.sectionData.count - 1 { return 12.0 } return 0 } + /// 添加好友并刷新UI func didAddUserAndRefreshUI(_ accids: [String], _ tid: String) { weak var weakSelf = self view.makeToastActivity(.center) - viewmodel.inviterUsers(accids, tid) { error, members in + + viewModel.inviteUsers(accids, tid) { error, members in if let err = error { weakSelf?.view.hideToastActivity() - if err.code == noNetworkCode { + if err.code == protocolSendFailed { weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { + } else if err.code == noPermissionInviteCode { weakSelf?.showToast(localizable("no_permission_tip")) } else { weakSelf?.showToast(localizable("failed_operation")) } } else { print("add users success : ", members as Any) - if let ms = members, let model = weakSelf?.viewmodel.teamInfoModel { - weakSelf?.viewmodel.repo.splitGroupMember(ms, model) { error, team in - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - weakSelf?.didError(err) - } else { - weakSelf?.refreshMemberCount() - weakSelf?.didRefreshUserinfoCollection() - weakSelf?.checkoutAddShowOrHide() - } - } - } else { - weakSelf?.view.hideToastActivity() - } + weakSelf?.view.hideToastActivity() } } } @@ -413,13 +471,13 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi func didAddUser(_ accids: [String], _ tid: String) { weak var weakSelf = self view.makeToastActivity(.center) - viewmodel.repo.inviteUser(accids, tid, nil, nil) { error, members in - NELog.infoLog( + viewModel.teamRepo.inviteUsers(tid, accids) { error, members in + NEALog.infoLog( ModuleName + " " + self.className(), desc: "CALLBACK inviteUser " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { weakSelf?.didError(err) } else { weakSelf?.showToast(localizable("invite_has_send")) @@ -427,17 +485,18 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } } + /// 结算群聊 func dismissTeam() { if let tid = teamId { weak var weakSelf = self view.makeToastActivity(.center) - viewmodel.dismissTeam(tid) { error in - NELog.infoLog( + viewModel.dismissTeam(tid) { error in + NEALog.infoLog( ModuleName + " " + self.className, desc: "CALLBACK dismissTeam " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { weakSelf?.didError(err) } else { NotificationCenter.default.post(name: NotificationName.popGroupChatVC, object: nil) @@ -446,28 +505,30 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } } + /// 刷新群成员个数 func refreshMemberCount() { - if let count = viewmodel.teamInfoModel?.team?.memberNumber { + if let count = viewModel.teamInfoModel?.team?.memberCount { memberCountLabel.text = "\(count)" } } + /// 离开群聊 func leaveTeam() { if let tid = teamId { view.makeToastActivity(.center) - viewmodel.quitTeam(tid) { [weak self] error in - NELog.infoLog( + viewModel.leaveTeam(tid) { [weak self] error in + NEALog.infoLog( ModuleName + " " + (self?.className ?? "TeamSettingViewController"), desc: "CALLBACK quitTeam " + (error?.localizedDescription ?? "no error") ) self?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { self?.didError(err) } else { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - self?.viewmodel.getTeamInfo(tid) { _ in - if self?.viewmodel.teamInfoModel?.team?.memberNumber == 1, - self?.viewmodel.isOwner() == true { + self?.viewModel.getTeamWithMembers(tid) { _ in + if self?.viewModel.teamInfoModel?.team?.memberCount == 1, + self?.viewModel.isOwner() == true { self?.dismissTeam() } else { NotificationCenter.default.post(name: NotificationName.popGroupChatVC, object: nil) @@ -479,26 +540,33 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } } - func didClickMark() { + public func didClickMark() { if let tid = teamId { - let session = NIMSession(tid, type: .team) - Router.shared.use(PushPinMessageVCRouter, parameters: ["nav": navigationController as Any, "session": session as Any], closure: nil) + let conversationId = V2NIMConversationIdUtil.teamConversationId(tid) + Router.shared.use(PushPinMessageVCRouter, parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any], closure: nil) } } - func didError(_ error: NSError) { - if error.code == noNetworkCode { + public func didError(_ error: NSError) { + if error.code == protocolSendFailed { showToast(commonLocalizable("network_error")) } else { showToast(localizable("failed_operation")) } } - func didNeedRefreshUI() { - contentTable.reloadData() + public func didShowNoNetworkToast() { + showToast(commonLocalizable("network_error")) + } + + /// 通知页面刷新回调 + public func didNeedRefreshUI() { + reloadSectionData() + contentTableView.reloadData() refreshMemberCount() didRefreshUserinfoCollection() checkoutAddShowOrHide() + setTeamHeaderInfo() } open func didClickTeamManage() {} @@ -508,18 +576,18 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi func updateInviteModeOwnerAction(_ model: SettingCellModel) { weak var weakSelf = self weakSelf?.view.makeToastActivity(.center) - weakSelf?.viewmodel.repo.updateInviteMode(.manager, weakSelf?.teamId ?? "") { error in - NELog.infoLog( + weakSelf?.viewModel.teamRepo.updateInviteMode(weakSelf?.teamId ?? "", .TEAM_INVITE_MODE_MANAGER) { error, team in + NEALog.infoLog( ModuleName + " " + self.className(), desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { weakSelf?.didError(err) } else { - weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .manager + weakSelf?.viewModel.teamInfoModel?.team = team model.subTitle = localizable("team_owner") - weakSelf?.contentTable.reloadData() + weakSelf?.contentTableView.reloadData() } } } @@ -527,18 +595,18 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi func updateInviteModeAllAction(_ model: SettingCellModel) { weak var weakSelf = self weakSelf?.view.makeToastActivity(.center) - weakSelf?.viewmodel.repo.updateInviteMode(.all, weakSelf?.teamId ?? "") { error in - NELog.infoLog( + weakSelf?.viewModel.teamRepo.updateInviteMode(weakSelf?.teamId ?? "", .TEAM_INVITE_MODE_ALL) { error, team in + NEALog.infoLog( ModuleName + " " + self.className(), desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { weakSelf?.didError(err) } else { - weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .all + weakSelf?.viewModel.teamInfoModel?.team = team model.subTitle = localizable("team_all") - weakSelf?.contentTable.reloadData() + weakSelf?.contentTableView.reloadData() } } } @@ -579,19 +647,19 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi func updateTeamInfoOwnerAction(_ model: SettingCellModel) { weak var weakSelf = self weakSelf?.view.makeToastActivity(.center) - weakSelf?.viewmodel.repo - .updateTeamInfoPrivilege(.manager, weakSelf?.teamId ?? "") { error in - NELog.infoLog( + weakSelf?.viewModel.teamRepo + .updateTeamInfoPrivilege(weakSelf?.teamId ?? "", .TEAM_UPDATE_INFO_MODE_MANAGER) { error, team in + NEALog.infoLog( ModuleName + " " + self.className(), desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { weakSelf?.didError(err) } else { - weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .manager + weakSelf?.viewModel.teamInfoModel?.team = team model.subTitle = localizable("team_owner") - weakSelf?.contentTable.reloadData() + weakSelf?.contentTableView.reloadData() } } } @@ -599,19 +667,19 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi func updateTeamInfoAllAction(_ model: SettingCellModel) { weak var weakSelf = self weakSelf?.view.makeToastActivity(.center) - weakSelf?.viewmodel.repo - .updateTeamInfoPrivilege(.all, weakSelf?.teamId ?? "") { error in - NELog.infoLog( + weakSelf?.viewModel.teamRepo + .updateTeamInfoPrivilege(weakSelf?.teamId ?? "", .TEAM_UPDATE_INFO_MODE_ALL) { error, team in + NEALog.infoLog( ModuleName + " " + self.className(), desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { weakSelf?.didError(err) } else { - weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .all + weakSelf?.viewModel.teamInfoModel?.team = team model.subTitle = localizable("team_all") - weakSelf?.contentTable.reloadData() + weakSelf?.contentTableView.reloadData() } } } @@ -652,10 +720,10 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func didClickHistoryMessage() {} - open func getManaterUsers() -> [TeamMemberInfoModel] { - var members = [TeamMemberInfoModel]() - viewmodel.teamInfoModel?.users.forEach { model in - if model.teamMember?.type == .manager { + open func getManagerUsers() -> [NETeamMemberInfoModel] { + var members = [NETeamMemberInfoModel]() + viewModel.teamInfoModel?.users.forEach { model in + if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { members.append(model) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift index d6a5097c..05d178ec 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift @@ -1,5 +1,6 @@ import NIMSDK + // Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. @@ -7,58 +8,13 @@ import UIKit @objcMembers open class NEBaseHistoryMessageCell: UITableViewCell { + /// 搜索文案(用户匹配高亮) public var searchText: String? + /// 高亮颜色 public var rangeTextColor = UIColor.ne_blueText - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setupSubviews() - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupSubviews() { - selectionStyle = .none - contentView.addSubview(headImge) - contentView.addSubview(title) - contentView.addSubview(subTitle) - contentView.addSubview(bottomLine) - contentView.addSubview(timeLabel) - } - - func configData(message: HistoryMessageModel?) { - guard let resultText = message?.content else { return } - - guard let searchStr = searchText else { return } - - let attributedStr = NSMutableAttributedString(string: resultText) - // range 表示从索引几开始取几个字符 - let range = attributedStr.mutableString.range(of: searchStr) - attributedStr.addAttribute( - .foregroundColor, - value: rangeTextColor, - range: range - ) - subTitle.attributedText = attributedStr - - title.text = message?.name - timeLabel.text = message?.time - - if let imageName = message?.avatar, !imageName.isEmpty { - headImge.setTitle("") - headImge.sd_setImage(with: URL(string: imageName), completed: nil) - } else { - headImge.setTitle(message?.name ?? "") - headImge.sd_setImage(with: nil, completed: nil) - headImge.backgroundColor = UIColor.colorWithString(string: message?.imMessage?.from) - } - } - - // MARK: lazy Method - - public lazy var headImge: NEUserHeaderView = { + /// 用户头像视图 + public lazy var headView: NEUserHeaderView = { let headView = NEUserHeaderView(frame: .zero) headView.titleLabel.textColor = .white headView.titleLabel.font = NEConstant.defaultTextFont(14) @@ -68,24 +24,29 @@ open class NEBaseHistoryMessageCell: UITableViewCell { return headView }() - public lazy var title: UILabel = { + /// 用户昵称 + public lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_darkText label.font = NEConstant.defaultTextFont(14) label.textAlignment = .left + label.accessibilityIdentifier = "id.name" return label }() - public lazy var subTitle: UILabel = { + /// 消息内容 + public lazy var subTitleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_lightText label.font = NEConstant.defaultTextFont(12) label.textAlignment = .left + label.accessibilityIdentifier = "id.message" return label }() + /// 分割线 public lazy var bottomLine: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -93,12 +54,63 @@ open class NEBaseHistoryMessageCell: UITableViewCell { return view }() + /// 消息时间 public lazy var timeLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = NEConstant.hexRGB(0xCCCCCC) label.font = NEConstant.defaultTextFont(12) label.textAlignment = .right + label.accessibilityIdentifier = "id.time" return label }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupSubviews() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func setupSubviews() { + selectionStyle = .none + contentView.addSubview(headView) + contentView.addSubview(titleLabel) + contentView.addSubview(subTitleLabel) + contentView.addSubview(bottomLine) + contentView.addSubview(timeLabel) + } + + func configData(message: HistoryMessageModel?) { + guard let resultText = message?.content else { return } + + guard let searchStr = searchText else { return } + + let attributedStr = NSMutableAttributedString(string: resultText) + // range 表示从索引几开始取几个字符 + let range = attributedStr.mutableString.range(of: searchStr) + attributedStr.addAttribute( + .foregroundColor, + value: rangeTextColor, + range: range + ) + subTitleLabel.attributedText = attributedStr + + if message?.fullName?.count ?? 0 <= 0 { + message?.fullName = message?.imMessage?.senderId + } + titleLabel.text = message?.fullName + timeLabel.text = message?.time + + if let imageName = message?.avatar, !imageName.isEmpty { + headView.setTitle("") + headView.sd_setImage(with: URL(string: imageName), completed: nil) + } else { + headView.setTitle(message?.shortName ?? "") + headView.sd_setImage(with: nil, completed: nil) + headView.backgroundColor = UIColor.colorWithString(string: message?.imMessage?.senderId) + } + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift index 1a036a8f..e5553638 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift @@ -23,6 +23,6 @@ open class NEBaseTeamArrowSettingCell: NEBaseTeamSettingCell { open func setupUI() { contentView.addSubview(titleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamAvatarViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamAvatarViewController.swift deleted file mode 100644 index 2a93848c..00000000 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamAvatarViewController.swift +++ /dev/null @@ -1,237 +0,0 @@ - -// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NECommonUIKit -import NIMSDK -import UIKit - -@objcMembers -open class NEBaseTeamAvatarViewController: NEBaseViewController, UICollectionViewDelegate, - UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate { - public typealias SaveCompletion = () -> Void - public var block: SaveCompletion? - public var team: NIMTeam? - public let repo = TeamRepo.shared - - public let headerBack = UIView() - public let photoImage = UIImageView() - public let defaultHeaderBack = UIView() - public let tag = UILabel() - public var iconUrls = TeamRouter.iconUrls - - public var viewmodel = TeamAvatarViewModel() - - public lazy var headerView: NEUserHeaderView = { - let header = NEUserHeaderView(frame: .zero) - header.translatesAutoresizingMaskIntoConstraints = false - header.clipsToBounds = true - header.isUserInteractionEnabled = true - header.accessibilityIdentifier = "id.icon" - return header - }() - - public var headerUrl = "" - - public lazy var iconCollection: UICollectionView = { - let flow = UICollectionViewFlowLayout() - flow.scrollDirection = .horizontal - flow.minimumLineSpacing = 0 - flow.minimumInteritemSpacing = 0 - let collection = UICollectionView(frame: .zero, collectionViewLayout: flow) - collection.translatesAutoresizingMaskIntoConstraints = false - collection.delegate = self - collection.dataSource = self - collection.backgroundColor = .clear - collection.showsHorizontalScrollIndicator = false - collection.showsVerticalScrollIndicator = false - collection.clipsToBounds = false - collection.isScrollEnabled = false - return collection - }() - - override open func viewDidLoad() { - super.viewDidLoad() - viewmodel.getCurrentUserTeamMember(team?.teamId) - setupUI() - } - - open func setupUI() { - title = localizable("modify_headImage") - addRightAction(localizable("save"), #selector(savePhoto), self) - navigationView.setMoreButtonTitle(localizable("save")) - navigationView.addMoreButtonTarget(target: self, selector: #selector(savePhoto)) - - view.backgroundColor = .ne_lightBackgroundColor - - headerBack.backgroundColor = .white - headerBack.clipsToBounds = true - headerBack.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(headerBack) - - headerBack.addSubview(headerView) - NSLayoutConstraint.activate([ - headerView.centerXAnchor.constraint(equalTo: headerBack.centerXAnchor), - headerView.centerYAnchor.constraint(equalTo: headerBack.centerYAnchor), - headerView.heightAnchor.constraint(equalToConstant: 80.0), - headerView.widthAnchor.constraint(equalToConstant: 80.0), - ]) - if let url = team?.avatarUrl, !url.isEmpty { - headerView.sd_setImage(with: URL(string: url), completed: nil) - headerUrl = url - } - - photoImage.translatesAutoresizingMaskIntoConstraints = false - photoImage.image = coreLoader.loadImage("photo") - photoImage.accessibilityIdentifier = "id.camera" - headerBack.addSubview(photoImage) - - let gesture = UITapGestureRecognizer() - headerView.addGestureRecognizer(gesture) - gesture.addTarget(self, action: #selector(uploadPhoto)) - - defaultHeaderBack.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(defaultHeaderBack) - defaultHeaderBack.clipsToBounds = true - defaultHeaderBack.backgroundColor = .white - - tag.translatesAutoresizingMaskIntoConstraints = false - tag.text = localizable("default_icon") - tag.font = NEConstant.defaultTextFont(16.0) - tag.textColor = NEConstant.hexRGB(0x333333) - defaultHeaderBack.addSubview(tag) - - defaultHeaderBack.addSubview(iconCollection) - - for index in 0 ..< iconUrls.count { - let url = iconUrls[index] - if url == headerUrl { - let indexPath = IndexPath(row: index, section: 0) - iconCollection.selectItem(at: indexPath, animated: false, scrollPosition: .right) - } - } - - if changePermission() == false { - rightNavBtn.isHidden = true - navigationView.moreButton.isHidden = true - photoImage.isHidden = true - defaultHeaderBack.isHidden = true - } - } - - func changePermission() -> Bool { - if let type = team?.type, type == .normal { - return true - } - if let ownerId = team?.owner, IMKitClient.instance.isMySelf(ownerId) { - return true - } - if let mode = team?.updateInfoMode, mode == .all { - return true - } - if let member = viewmodel.currentTeamMember, member.type == .manager { - return true - } - return false - } - - // MARK: objc 方法 - - open func uploadPhoto() { - if changePermission() { - showBottomAlert(self) - } - } - - open func savePhoto() { - print("save photo") - if let tid = team?.teamId { - view.makeToastActivity(.center) - weak var weakSelf = self - weakSelf?.repo.updateTeamIcon(headerUrl, tid) { error in - NELog.infoLog(ModuleName + " " + self.className(), desc: #function + "CALLBACK " + (error?.localizedDescription ?? "no error")) - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.team?.avatarUrl = weakSelf?.headerUrl - if let completion = weakSelf?.block { - completion() - } - weakSelf?.navigationController?.popViewController(animated: true) - } - } - } - } - - // MAKR: UICollectionViewDelegate, UICollectionViewDataSource,UICollectionViewDelegateFlowLayout - open func collectionView(_ collectionView: UICollectionView, - numberOfItemsInSection section: Int) -> Int { - 5 - } - - open func collectionView(_ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - UICollectionViewCell() - } - - open func collectionView(_ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeForItemAt indexPath: IndexPath) -> CGSize { - .zero - } - - open func collectionView(_ collectionView: UICollectionView, - didSelectItemAt indexPath: IndexPath) { - if iconUrls.count > indexPath.row { - headerUrl = iconUrls[indexPath.row] - // headerView.image = coreLoader.loadImage("icon_\(indexPath.row)") - headerView.sd_setImage(with: URL(string: headerUrl), completed: nil) - } - } - - // MARK: UINavigationControllerDelegate - - open func imagePickerController(_ picker: UIImagePickerController, - didFinishPickingMediaWithInfo info: [UIImagePickerController - .InfoKey: Any]) { - let image: UIImage = info[UIImagePickerController.InfoKey.editedImage] as! UIImage - uploadHeadImage(image: image) - picker.dismiss(animated: true, completion: nil) - } - - open func uploadHeadImage(image: UIImage) { - weak var weakSelf = self - if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { - weakSelf?.showToast(commonLocalizable("network_error")) - return - } - view.makeToastActivity(.center) - if let imageData = image.jpegData(compressionQuality: 0.6) as NSData? { - let filePath = NSHomeDirectory().appending("/Documents/") - .appending(IMKitClient.instance.imAccid()) - let succcess = imageData.write(toFile: filePath, atomically: true) - if succcess { - NIMSDK.shared().resourceManager - .upload(filePath, progress: nil) { urlString, error in - if error == nil { - // 显示设置的照片 - weakSelf?.headerView.image = image - if let url = urlString { - weakSelf?.headerUrl = url - } - print("upload image success") - } else { - print("upload image failed,error = \(error!)") - } - weakSelf?.view.hideToastActivity() - } - } - } - } -} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamDefaultIconCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamDefaultIconCell.swift index 7f597c84..c38a95e4 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamDefaultIconCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamDefaultIconCell.swift @@ -8,26 +8,16 @@ import UIKit @objcMembers open class NEBaseTeamDefaultIconCell: UICollectionViewCell { - override public init(frame: CGRect) { - super.init(frame: frame) - setupUI() - } - - override public var isSelected: Bool { - didSet { - print("default icon select ", isSelected) - selectBack.isHidden = !isSelected - } - } - - lazy var iconImage: UIImageView = { - let image = UIImageView() - image.translatesAutoresizingMaskIntoConstraints = false - image.contentMode = .scaleAspectFill - return image + /// icon 图片 + public lazy var iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + return imageView }() - lazy var selectBack: UIView = { + /// 选中背景 + public lazy var selectBackView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = NEConstant.hexRGB(0xF4F4F4) @@ -37,12 +27,24 @@ open class NEBaseTeamDefaultIconCell: UICollectionViewCell { return view }() + override public init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + override public var isSelected: Bool { + didSet { + print("default icon select ", isSelected) + selectBackView.isHidden = !isSelected + } + } + public required init?(coder: NSCoder) { super.init(coder: coder) } func setupUI() { - contentView.addSubview(selectBack) - contentView.addSubview(iconImage) + contentView.addSubview(selectBackView) + contentView.addSubview(iconImageView) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamInfoViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamInfoViewController.swift deleted file mode 100644 index f7f8ee64..00000000 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamInfoViewController.swift +++ /dev/null @@ -1,84 +0,0 @@ - -// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NIMSDK -import UIKit - -@objcMembers -open class NEBaseTeamInfoViewController: NEBaseViewController, UITableViewDelegate, - UITableViewDataSource { - public let viewmodel = TeamInfoViewModel() - - public var team: NIMTeam? - - public var cellClassDic = [Int: NEBaseTeamSettingCell.Type]() - - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorStyle = .none - table.sectionHeaderHeight = 0 - return table - }() - - init(team: NIMTeam?) { - super.init(nibName: nil, bundle: nil) - self.team = team - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if let type = team?.type, type == .normal { - title = localizable("discuss_info") - } else { - title = localizable("group_info") - } - } - - override open func viewDidLoad() { - super.viewDidLoad() - viewmodel.getData(team) - setupUI() - } - - open func setupUI() { - view.addSubview(contentTable) - NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), - contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant + 12), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in - contentTable.register(value, forCellReuseIdentifier: "\(key)") - } - } - - // MARK: UITableViewDelegate, UITableViewDataSource - - open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewmodel.cellDatas.count - } - - open func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { - UITableViewCell() - } - - open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {} - - open func tableView(_ tableView: UITableView, - heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.cellDatas[indexPath.row] - return model.rowHeight - } -} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift index 59cbf140..83ca66bb 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift @@ -7,12 +7,12 @@ import NECommonUIKit import UIKit protocol TeamMemberCellDelegate: NSObject { - func didClickRemoveButton(_ model: TeamMemberInfoModel?, _ index: Int) + func didClickRemoveButton(_ model: NETeamMemberInfoModel?, _ index: Int) } @objcMembers open class NEBaseTeamMemberCell: UITableViewCell { - var currentModel: TeamMemberInfoModel? + var currentModel: NETeamMemberInfoModel? weak var delegate: TeamMemberCellDelegate? @@ -22,7 +22,7 @@ open class NEBaseTeamMemberCell: UITableViewCell { var index = 0 - lazy var headerView: NEUserHeaderView = { + public lazy var headerView: NEUserHeaderView = { let header = NEUserHeaderView(frame: .zero) header.titleLabel.font = NEConstant.defaultTextFont(14) header.titleLabel.textColor = UIColor.white @@ -32,7 +32,7 @@ open class NEBaseTeamMemberCell: UITableViewCell { return header }() - lazy var ownerLabel: UILabel = { + public lazy var ownerLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(12.0) @@ -48,7 +48,7 @@ open class NEBaseTeamMemberCell: UITableViewCell { return label }() - lazy var nameLabel: UILabel = { + public lazy var nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(16.0) @@ -57,7 +57,7 @@ open class NEBaseTeamMemberCell: UITableViewCell { return label }() - lazy var removeLabel: UILabel = { + public lazy var removeLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = localizable("team_member_remove") @@ -66,7 +66,7 @@ open class NEBaseTeamMemberCell: UITableViewCell { return label }() - lazy var removeBtn: UIButton = { + public lazy var removeButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false return button @@ -109,31 +109,32 @@ open class NEBaseTeamMemberCell: UITableViewCell { ]) } - func configure(_ model: TeamMemberInfoModel) { - if let userId = model.nimUser?.userId, let user = ChatUserCache.getUserInfo(userId) { - model.nimUser = user + func configure(_ model: NETeamMemberInfoModel) { + // 更新用户信息 + if let userId = model.nimUser?.user?.accountId, let user = NEFriendUserCache.shared.getFriendInfo(userId) { +// model.nimUser = user } currentModel = model - if let url = model.nimUser?.userInfo?.avatarUrl, !url.isEmpty { + if let url = model.nimUser?.user?.avatar, !url.isEmpty { headerView.sd_setImage(with: URL(string: url), completed: nil) headerView.setTitle("") } else { headerView.image = nil - headerView.setTitle(model.showNickInTeam()) - headerView.backgroundColor = UIColor.colorWithString(string: model.nimUser?.userId) + headerView.setTitle(model.showNickInTeam() ?? "") + headerView.backgroundColor = UIColor.colorWithString(string: model.nimUser?.user?.accountId) } nameLabel.text = model.atNameInTeam() } func setupRemoveButton() { - contentView.addSubview(removeBtn) + contentView.addSubview(removeButton) NSLayoutConstraint.activate([ - removeBtn.topAnchor.constraint(equalTo: contentView.topAnchor), - removeBtn.rightAnchor.constraint(equalTo: contentView.rightAnchor), - removeBtn.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - removeBtn.widthAnchor.constraint(equalToConstant: 100), + removeButton.topAnchor.constraint(equalTo: contentView.topAnchor), + removeButton.rightAnchor.constraint(equalTo: contentView.rightAnchor), + removeButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + removeButton.widthAnchor.constraint(equalToConstant: 100), ]) - removeBtn.addTarget(self, action: #selector(didClickRemove), for: .touchUpInside) + removeButton.addTarget(self, action: #selector(didClickRemove), for: .touchUpInside) } func didClickRemove() { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift index b473143d..c3ddbd12 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift @@ -16,17 +16,17 @@ open class NEBaseTeamMemberSelectCell: UITableViewCell { return imageView }() - lazy var headerView: NEUserHeaderView = { - let header = NEUserHeaderView(frame: .zero) - header.titleLabel.font = NEConstant.defaultTextFont(14) - header.titleLabel.textColor = UIColor.white - header.clipsToBounds = true - header.translatesAutoresizingMaskIntoConstraints = false - header.accessibilityIdentifier = "id.avatar" - return header + public lazy var headerView: NEUserHeaderView = { + let headerView = NEUserHeaderView(frame: .zero) + headerView.titleLabel.font = NEConstant.defaultTextFont(14) + headerView.titleLabel.textColor = UIColor.white + headerView.clipsToBounds = true + headerView.translatesAutoresizingMaskIntoConstraints = false + headerView.accessibilityIdentifier = "id.avatar" + return headerView }() - lazy var nameLabel: UILabel = { + public lazy var nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(16.0) @@ -53,13 +53,13 @@ open class NEBaseTeamMemberSelectCell: UITableViewCell { open func configureMember(_ model: NESelectTeamMember?) { checkImageView.isHighlighted = model?.isSelected ?? false - if let url = model?.member?.nimUser?.userInfo?.avatarUrl, !url.isEmpty { + if let url = model?.member?.nimUser?.user?.avatar, !url.isEmpty { headerView.sd_setImage(with: URL(string: url), completed: nil) headerView.setTitle("") } else { headerView.image = nil headerView.setTitle(model?.member?.showNickInTeam() ?? "") - headerView.backgroundColor = UIColor.colorWithString(string: model?.member?.nimUser?.userId) + headerView.backgroundColor = UIColor.colorWithString(string: model?.member?.nimUser?.user?.accountId) } nameLabel.text = model?.member?.atNameInTeam() } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift index ec8fcff6..14ee89c1 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift @@ -18,6 +18,7 @@ public enum TeamSettingType: Int { @objcMembers open class NEBaseTeamSettingCell: CornerCell { + /// 群设置数据模型 var model: SettingCellModel? public lazy var titleLabel: UILabel = { @@ -29,7 +30,7 @@ open class NEBaseTeamSettingCell: CornerCell { return label }() - public lazy var arrow: UIImageView = { + public lazy var arrowView: UIImageView = { let imageView = UIImageView(image: coreLoader.loadImage("arrowRight")) imageView.translatesAutoresizingMaskIntoConstraints = false return imageView diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift index 4668b0fd..b86c4f95 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift @@ -33,13 +33,13 @@ open class NEBaseTeamSettingHeaderCell: NEBaseTeamSettingCell { headerView.setTitle("") } else { headerView.setTitle(model?.defaultHeadData ?? "") - headerView.backgroundColor = UIColor.colorWithString(string: model?.defaultHeadData) + headerView.backgroundColor = UIColor.colorWithString(string: model?.subTitle) } } open func setupUI() { contentView.addSubview(titleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) contentView.addSubview(headerView) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift index a35e0c9d..20945da9 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift @@ -7,7 +7,7 @@ import UIKit @objcMembers open class NEBaseTeamSettingSelectCell: NEBaseTeamSettingCell { - lazy var subTitleLabel: UILabel = { + public lazy var subTitleLabel: UILabel = { let label = UILabel() label.textColor = NEConstant.hexRGB(0x999999) label.font = NEConstant.defaultTextFont(14.0) @@ -31,9 +31,9 @@ open class NEBaseTeamSettingSelectCell: NEBaseTeamSettingCell { subTitleLabel.text = model?.subTitle } - func setupUI() { + open func setupUI() { contentView.addSubview(titleLabel) contentView.addSubview(subTitleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift index 15bbe475..63ea2b9a 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift @@ -4,37 +4,47 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @objcMembers open class NEBaseTeamUserCell: UICollectionViewCell { - var user: TeamMemberInfoModel? { + public var user: NETeamMemberInfoModel? { didSet { if let name = user?.showNickInTeam() { - userHeader.setTitle(name) + userHeaderView.setTitle(name) } - if let userId = user?.nimUser?.userId, let nimUser = ChatUserCache.getUserInfo(userId) { - user?.nimUser = nimUser - } - if let url = user?.nimUser?.userInfo?.avatarUrl, !url.isEmpty { - userHeader.sd_setImage(with: URL(string: url), completed: nil) - userHeader.setTitle("") - } else if let id = user?.nimUser?.userId { - userHeader.image = nil - userHeader.backgroundColor = UIColor.colorWithString(string: "\(id)") + + if let userId = user?.nimUser?.user?.accountId { + if let u = NEFriendUserCache.shared.getFriendInfo(userId) { + if let url = u.user?.avatar, !url.isEmpty { + userHeaderView.sd_setImage(with: URL(string: url), completed: nil) + userHeaderView.setTitle("") + } else if let id = u.user?.accountId { + userHeaderView.image = nil + userHeaderView.backgroundColor = UIColor.colorWithString(string: "\(id)") + } + } else { + if let url = user?.nimUser?.user?.avatar, !url.isEmpty { + userHeaderView.sd_setImage(with: URL(string: url), completed: nil) + userHeaderView.setTitle("") + } else if let id = user?.nimUser?.user?.accountId { + userHeaderView.image = nil + userHeaderView.backgroundColor = UIColor.colorWithString(string: "\(id)") + } + } } } } - lazy var userHeader: NEUserHeaderView = { - let header = NEUserHeaderView(frame: .zero) - header.translatesAutoresizingMaskIntoConstraints = false - header.titleLabel.font = NEConstant.defaultTextFont(11.0) - header.clipsToBounds = true - return header + public lazy var userHeaderView: NEUserHeaderView = { + let headerView = NEUserHeaderView(frame: .zero) + headerView.translatesAutoresizingMaskIntoConstraints = false + headerView.titleLabel.font = NEConstant.defaultTextFont(11.0) + headerView.clipsToBounds = true + return headerView }() override public init(frame: CGRect) { @@ -47,6 +57,6 @@ open class NEBaseTeamUserCell: UICollectionViewCell { } func setupUI() { - contentView.addSubview(userHeader) + contentView.addSubview(userHeaderView) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift index 22af44a5..60aed92c 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift @@ -12,10 +12,10 @@ open class TeamSettingRightCustomCell: TeamSettingSubtitleCell { if let icon = model?.rightCustomViewIcon, icon.count > 0 { customRightView.isHidden = false customRightView.setImage(coreLoader.loadImage(icon), for: .normal) - arrow.isHidden = true + arrowView.isHidden = true } else { customRightView.isHidden = true - arrow.isHidden = false + arrowView.isHidden = false } } @@ -37,10 +37,10 @@ open class TeamSettingRightCustomCell: TeamSettingSubtitleCell { } public lazy var customRightView: UIButton = { - let btn = UIButton() - btn.translatesAutoresizingMaskIntoConstraints = false - btn.accessibilityIdentifier = "id.accountCopy" - return btn + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityIdentifier = "id.accountCopy" + return button }() open func customRightViewClick() { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift index 115bd1b5..31cfb7fe 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift @@ -9,6 +9,17 @@ import UIKit open class TeamSettingSubtitleCell: NEBaseTeamSettingCell { public var titleWidthAnchor: NSLayoutConstraint? + /// 标题 + public lazy var subTitleLabel: UILabel = { + let label = UILabel() + label.textColor = UIColor(hexString: "0xA6ADB6") + label.font = NEConstant.defaultTextFont(12.0) + label.translatesAutoresizingMaskIntoConstraints = false + label.textAlignment = .right + label.accessibilityIdentifier = "id.subTitleLabel" + return label + }() + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none @@ -16,13 +27,13 @@ open class TeamSettingSubtitleCell: NEBaseTeamSettingCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { contentView.addSubview(titleLabel) contentView.addSubview(subTitleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) NSLayoutConstraint.activate([ titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 36), @@ -33,14 +44,14 @@ open class TeamSettingSubtitleCell: NEBaseTeamSettingCell { titleWidthAnchor?.isActive = true NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), - arrow.widthAnchor.constraint(equalToConstant: 7), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + arrowView.widthAnchor.constraint(equalToConstant: 7), ]) NSLayoutConstraint.activate([ subTitleLabel.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: 10), - subTitleLabel.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -10), + subTitleLabel.rightAnchor.constraint(equalTo: arrowView.leftAnchor, constant: -10), subTitleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } @@ -52,14 +63,4 @@ open class TeamSettingSubtitleCell: NEBaseTeamSettingCell { subTitleLabel.text = m.subTitle } } - - public lazy var subTitleLabel: UILabel = { - let label = UILabel() - label.textColor = UIColor(hexString: "0xA6ADB6") - label.font = NEConstant.defaultTextFont(12.0) - label.translatesAutoresizingMaskIntoConstraints = false - label.textAlignment = .right - label.accessibilityIdentifier = "id.subTitleLabel" - return label - }() } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.swift index 2073ab57..19b987cc 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.swift @@ -8,13 +8,52 @@ import UIKit @objcMembers open class TeamAvatarViewModel: NSObject { - let repo = ChatRepo.shared - var currentTeamMember: NIMTeamMember? + /// 群API单例 + let teamRepo = TeamRepo.shared + /// 当前用户群成员对象 + var currentTeamMember: V2NIMTeamMember? - func getCurrentUserTeamMember(_ teamId: String?) { + /// 获取当前用户群信息 + /// - Parameter teamId 群id + func getCurrentUserTeamMember(_ teamId: String?, _ completion: @escaping (NSError?) -> Void) { if let tid = teamId { - let currentUserAccid = IMKitClient.instance.imAccid() - currentTeamMember = repo.getTeamMemberList(userId: currentUserAccid, teamId: tid) + let currentUserAccid = IMKitClient.instance.account() + teamRepo.getTeamMember(tid, currentUserAccid) { member, error in + self.currentTeamMember = member + completion(error) + } } } + + /// 更新群组头像 + /// - Parameter url: 群组头像Url + /// - Parameter teamId : 群组ID + /// - Parameter antispamConfig: 反垃圾配置 + /// - Parameter completion: 完成后的回调 + public func updateTeamAvatar(_ url: String, _ teamId: String, _ antispamConfig: V2NIMAntispamConfig?, + _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", url:\(url)") + teamRepo.updateTeamIcon(teamId, url, antispamConfig) { error in + completion(error) + } + } + + /// 创建文件上传任务 + /// - Parameter filePath 文件路径 + /// - Parameter sceneName 场景名 + public func createTask(_ filePath: String, + _ sceneName: String? = nil) -> V2NIMUploadFileTask { + ResourceRepo.shared.createUploadFileTask(filePath, sceneName) + } + + /// 上传文件 + /// - Parameter filepath: 上传文件路径 + /// - Parameter progress: 进度回调 + /// - Parameter completion: 完成回调 + public func uploadImageFile(_ fileTask: V2NIMUploadFileTask, + _ progress: ((Float) -> Void)?, + _ completion: ((String?, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", taskId:\(fileTask.taskId)") + ResourceRepo.shared.upload(fileTask, progress, completion) + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamHistoryMessageViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamHistoryMessageViewModel.swift new file mode 100644 index 00000000..9da22315 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamHistoryMessageViewModel.swift @@ -0,0 +1,224 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NECoreIM2Kit +import NIMSDK +import UIKit + +@objcMembers +open class TeamHistoryMessageViewModel: NSObject, NETeamListener, NEContactListener { + /// 群信息 + public var teamInfoModel: NETeamInfoModel? + /// 搜索结果 + public var searchResultInfos: [HistoryMessageModel]? + + /// 群模块API单例 + public let teamRepo = TeamRepo.shared + + /// 通讯录模块API单例 + public let contactRepo = ContactRepo.shared + + /// 消息模块API单例 + public let chatRepo = ChatRepo.shared + + /// 群成员缓存 + public var memberModelCacheDic = [String: NETeamMemberInfoModel]() + + override public init() { + super.init() + teamRepo.addTeamListener(self) + contactRepo.addContactListener(self) + } + + deinit {} + + /// 设置从上一个页面传入的成员 + public func setupCache() { + teamInfoModel?.users.forEach { member in + if let accountId = member.teamMember?.accountId { + memberModelCacheDic[accountId] = member + } + } + } + + /// 消息搜索 + /// - Parameter teamId: 群id + /// - Parameter searchContent: 搜索内容 + open func searchHistoryMessages(_ teamId: String?, _ searchContent: String, _ completion: @escaping (Error?, [HistoryMessageModel]?) -> Void) { + var infoDic = [String: NETeamMemberInfoModel]() + for (key, value) in memberModelCacheDic { + if let accountId = value.teamMember?.accountId { + infoDic[accountId] = value + } + } + + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", searchContent:\(searchContent)") + guard let teamId = teamId else { + completion(NSError(domain: "teamId is nil", code: -1), nil) + return + } + + let param = V2NIMMessageSearchParams() + param.keyword = searchContent + param.teamIds = [teamId] + + weak var weakSelf = self + chatRepo.searchMessages(param: param) { error, messages in + + if error == nil { + // 未找到用户信息信息记录 + var noFindUserSet = Set() + for message in messages ?? [] { + if let uid = message.imMessage?.senderId { + if let member = infoDic[uid] { + message.avatar = member.nimUser?.user?.avatar + message.fullName = member.atNameInTeam() + message.shortName = member.getShortName(member.showNickInTeam() ?? "") + } else { + noFindUserSet.insert(uid) + } + } + } + if noFindUserSet.count > 0 { + let accids = Array(noFindUserSet) + weakSelf?.getSearchMessageMembers(teamId, accids) { error, members in + if let err = error { + completion(err, nil) + } else { + members?.forEach { member in + if let accountId = member.teamMember?.accountId { + infoDic[accountId] = member + weakSelf?.memberModelCacheDic[accountId] = member + } + } + if let historyMessages = messages { + weakSelf?.bindMessageUserInfo(historyMessages, infoDic) + } + weakSelf?.searchResultInfos = messages + completion(nil, messages) + } + } + } else { + weakSelf?.searchResultInfos = messages + completion(nil, messages) + } + } else { + completion(error, nil) + } + } + } + + /// 获取消息对应的用户信息 + public func bindMessageUserInfo(_ messages: [HistoryMessageModel], _ infoDic: [String: NETeamMemberInfoModel]) { + for message in messages { + if let uid = message.imMessage?.senderId { + if let member = infoDic[uid] { + message.avatar = member.nimUser?.user?.avatar + message.fullName = member.atNameInTeam() + message.shortName = member.getShortName(member.showNickInTeam() ?? "") + } + } + } + } + + /// 获取群信息 + public func getTeamInfo(_ teamId: String?, _ completion: @escaping (V2NIMTeam?, NSError?) -> Void) { + guard let tid = teamId else { + return + } + teamRepo.getTeamInfo(tid) { team, error in + completion(team, error) + } + } + + /// 获取搜索消息中关联的群成员信息 + /// - Parameter teamId: 群id + /// - Parameter accounts: 群成员id列表 + /// - Parameter completion: 完成回调 + public func getSearchMessageMembers(_ teamId: String, _ accounts: [String], _ completion: @escaping (NSError?, [NETeamMemberInfoModel]?) -> Void) { + weak var weakSelf = self + teamRepo.getTeamMemberList(teamId, accounts) { members, error in + if let err = error { + completion(err, nil) + } else { + if let ms = members { + weakSelf?.getUsersInfo(ms) { error, memberInfos in + var retMembers = [NETeamMemberInfoModel]() + memberInfos?.forEach { member in + retMembers.append(member) + } + completion(nil, retMembers) + } + } else { + completion(nil, nil) + } + } + } + } + + /// 根据成员信息获取用户信息 + /// - Parameter members: 群成员列表 + /// - Parameter completion: 完成回调 + public func getUsersInfo(_ members: [V2NIMTeamMember], _ completion: @escaping (NSError?, [NETeamMemberInfoModel]?) -> Void) { + var memberModels = [NETeamMemberInfoModel]() + var accids = [String]() + + for member in members { + accids.append(member.accountId) + let model = NETeamMemberInfoModel() + model.teamMember = member + memberModels.append(model) + } + + contactRepo.getFriendInfoList(accountIds: accids) { users, v2Error in + + if v2Error != nil { + completion(nil, memberModels) + } else { + var dic = [String: NEUserWithFriend]() + if let us = users { + for user in us { + if let accid = user.user?.accountId { + dic[accid] = user + } + } + for model in memberModels { + if let accid = model.teamMember?.accountId { + if let user = dic[accid] { + model.nimUser = user + } + } + } + completion(nil, memberModels) + } + } + } + } + + /// 好友变更回调 + /// - parameter friendInfo: 好友信息对象 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + if let accountId = friendInfo.accountId { + memberModelCacheDic[accountId]?.nimUser?.friend = friendInfo + } + } + + /// 群成员变更回调 + /// - parameter teamMembers: 群成员信息对象列表 + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + guard let currentTeamId = teamInfoModel?.team?.teamId else { + return + } + // 判断是否有属于当前群 + var currentMembers = [V2NIMTeamMember]() + for member in teamMembers { + if member.teamId == currentTeamId { + currentMembers.append(member) + } + } + for member in currentMembers { + memberModelCacheDic[member.accountId]?.teamMember = member + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift index d3c5a532..ea415ad3 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift @@ -3,21 +3,45 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NIMSDK +/// 群信息变更回调协议 +@objc public protocol NETeamInfoDelegate: NSObjectProtocol { + /// 群信息变更 + func teamInfoDidUpdate(_ team: V2NIMTeam) +} + @objcMembers -open class TeamInfoViewModel: NSObject { +open class TeamInfoViewModel: NSObject, NETeamListener { + /// UI 列表数据源 var cellDatas = [SettingCellModel]() - func getData(_ team: NIMTeam?) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId:\(team?.teamId ?? "nil")") + /// chat kit 群 api 单例 + public let teamRepo = TeamRepo.shared + + /// 群 + public var v2Team: V2NIMTeam? + + /// 代理 + public weak var delegate: NETeamInfoDelegate? + + override public init() { + super.init() + teamRepo.addTeamListener(self) + } + + /// 获取群信息 + /// - Parameter team: 群 + func getData(_ team: V2NIMTeam?) { + v2Team = team + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId:\(team?.teamId ?? "nil")") cellDatas.removeAll() let headerCell = SettingCellModel() headerCell.cornerType = .topLeft.union(.topRight) headerCell.type = SettingCellType.SettingHeaderCell.rawValue - headerCell.headerUrl = team?.avatarUrl + headerCell.headerUrl = team?.avatar headerCell.rowHeight = 74.0 let nameCell = SettingCellModel() @@ -40,4 +64,13 @@ open class TeamInfoViewModel: NSObject { intrCell.cellName = localizable("team_intr") } } + + /// 群信息更新 + /// - Parameter team: 群 + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + if let teamId = v2Team?.teamId, teamId == team.teamId { + getData(team) + delegate?.teamInfoDidUpdate(team) + } + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.swift index 381db61d..0bb81874 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.swift @@ -9,13 +9,31 @@ import UIKit @objc @objcMembers open class TeamIntroduceViewModel: NSObject { - let repo = ChatRepo.shared - var currentTeamMember: NIMTeamMember? + /// 群API单例 + public let teamRepo = TeamRepo.shared + /// 群信息 + public var currentTeamMember: V2NIMTeamMember? - func getCurrentUserTeamMember(_ teamId: String?) { + /// 获取当前用户群信息 + /// - Parameter teamId 群id + func getCurrentUserTeamMember(_ teamId: String?, _ completion: @escaping (NSError?) -> Void) { if let tid = teamId { - let currentUserAccid = IMKitClient.instance.imAccid() - currentTeamMember = repo.getTeamMemberList(userId: currentUserAccid, teamId: tid) + let currentUserAccid = IMKitClient.instance.account() + teamRepo.getTeamMember(tid, currentUserAccid) { member, error in + self.currentTeamMember = member + completion(error) + } + } + } + + /// 更新群介绍 + /// - Parameter teamId: 群组ID + /// - Parameter introduce: 群介绍 + /// - Parameter completion: 完成后的回调 + public func updateTeamIntroduce(_ teamId: String, _ introduce: String, + _ completion: @escaping (NSError?) -> Void) { + teamRepo.updateTeamIntroduce(teamId, introduce) { error in + completion(error) } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManageViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManageViewModel.swift deleted file mode 100644 index ba330f3b..00000000 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManageViewModel.swift +++ /dev/null @@ -1,230 +0,0 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NEChatKit -import NECommonKit -import NIMSDK -import UIKit - -public protocol TeamManageViewModelDelegate: NSObjectProtocol { - func didChangeInviteModeClick(_ model: SettingCellModel) - func didUpdateTeamInfoClick(_ model: SettingCellModel) - func didAtPermissionClick(_ model: SettingCellModel) - func didManagerClick() - func didRefreshData() -} - -@objc -@objcMembers -open class TeamManageViewModel: NSObject, NIMTeamManagerDelegate { - let repo = TeamRepo.shared - - let chatRepo = ChatRepo.shared - - public var teamInfoModel: TeamInfoModel? - - var sectionData = [SettingSectionModel]() - - public var managerUsers = [TeamMemberInfoModel]() - - var isRequestData = false - - weak var delegate: TeamManageViewModelDelegate? - - override public init() { - super.init() - NIMSDK.shared().teamManager.add(self) - } - - open func getSectionData() { - sectionData.removeAll() - if teamInfoModel?.team?.owner == IMKitClient.instance.imAccid() { - sectionData.append(getTopSection()) - } - sectionData.append(getMidSection()) - } - - open func getTeamInfo(_ tid: String, _ completion: @escaping (Error?) -> Void) { - if isRequestData == true { - return - } - weak var weakSelf = self - isRequestData = true - repo.fetchTeamInfo(tid) { error, teamInfo in - weakSelf?.isRequestData = false - if error == nil { - weakSelf?.managerUsers.removeAll() - } - teamInfo?.users.forEach { model in - if model.teamMember?.type == .manager { - weakSelf?.managerUsers.append(model) - } - } - weakSelf?.teamInfoModel = teamInfo - weakSelf?.getSectionData() - completion(error) - } - } - - open func getTopSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - weak var weakSelf = self - let model = SettingSectionModel() - let manager = SettingCellLabelArrowModel() - manager.cellName = localizable("manage_manger") - manager.type = SettingCellType.SettingArrowCell.rawValue - manager.rowHeight = 56 - manager.arrowLabelText = "\(managerUsers.count)" - model.cellModels.append(contentsOf: [manager]) - model.setCornerType() - manager.cellClick = { - weakSelf?.delegate?.didManagerClick() - } - return model - } - - open func getMidSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - weak var weakSelf = self - let model = SettingSectionModel() - - let editTeam = SettingCellModel() - editTeam.cellName = localizable("who_edit_team_info") - editTeam.type = SettingCellType.SettingSelectCell.rawValue - editTeam.rowHeight = 73 - - if let updateMode = teamInfoModel?.team?.updateInfoMode, updateMode == .all { - editTeam.subTitle = localizable("team_all") - } else { - editTeam.subTitle = localizable("team_owner_and_manager") - } - - editTeam.cellClick = { - weakSelf?.delegate?.didUpdateTeamInfoClick(editTeam) - } - - let invitePermission = SettingCellModel() - invitePermission.cellName = localizable("who_edit_user_info") - invitePermission.type = SettingCellType.SettingSelectCell.rawValue - invitePermission.rowHeight = 73 - if let inviteMode = teamInfoModel?.team?.inviteMode, inviteMode == .all { - invitePermission.subTitle = localizable("team_all") - } else { - invitePermission.subTitle = localizable("team_owner_and_manager") - } - - invitePermission.cellClick = { - weakSelf?.delegate?.didChangeInviteModeClick(invitePermission) - } - - let atAll = SettingCellModel() - atAll.cellName = localizable("who_at_all") - atAll.type = SettingCellType.SettingSelectCell.rawValue - atAll.rowHeight = 73 - atAll.subTitle = localizable("team_owner_and_manager") - atAll.subTitle = getTeamAtPermissionValue() - - atAll.cellClick = { - weakSelf?.delegate?.didAtPermissionClick(atAll) - } - - model.cellModels.append(contentsOf: [editTeam, invitePermission, atAll]) - model.setCornerType() - - return model - } - - open func updateTeamAtPermission(_ isManager: Bool, _ completion: @escaping (Error?) -> Void) { - let value = isManager == true ? allowAtManagerValue : allowAtAllValue - guard let tid = teamInfoModel?.team?.teamId else { - return - } - let latestTeam = repo.getTeam(tid) - if let custom = latestTeam?.clientCustomInfo { - if var dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { - dic[keyAllowAtAll] = value - let info = NECommonUtil.getJSONStringFromDictionary(dic) - repo.updateTeamCustomInfo(info, tid) { error in - completion(error) - } - } - } else { - var dic = [String: Any]() - dic[keyAllowAtAll] = value - let info = NECommonUtil.getJSONStringFromDictionary(dic) - repo.updateTeamCustomInfo(info, tid) { error in - completion(error) - } - } - } - - func getTeamAtPermissionValue() -> String { - if let custom = teamInfoModel?.team?.clientCustomInfo { - if let dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { - if let value = dic[keyAllowAtAll] as? String { - if value == allowAtManagerValue { - return localizable("team_owner_and_manager") - } - } - } - } - return localizable("team_all") - } - - open func onTeamMemberChanged(_ team: NIMTeam) { - updateTeamInfo(team) - } - - open func onTeamUpdated(_ team: NIMTeam) { - updateTeamInfo(team) - } - - func sendTipNoti(_ isManagerPermission: Bool, _ completion: @escaping (Error?) -> Void) { - guard let teamId = teamInfoModel?.team?.teamId else { - return - } - let session = NIMSession(teamId, type: .team) - let tipContent = isManagerPermission ? localizable("team_tip_noti_at_manager") : localizable("team_tip_noti_at_all") - let tipMessage = NIMMessage() - let object = NIMTipObject(attach: nil, callbackExt: nil) - tipMessage.messageObject = object - tipMessage.text = tipContent - let setting = NIMMessageSetting() - setting.shouldBeCounted = false - setting.apnsEnabled = false - tipMessage.setting = setting - chatRepo.sendMessage(message: tipMessage, session: session) { error in - completion(error) - } - } - - private func updateTeamInfo(_ team: NIMTeam) { - guard let tid = teamInfoModel?.team?.teamId else { - return - } - - if isRequestData == true { - return - } - isRequestData = true - repo.fetchTeamInfo(tid) { [weak self] error, info in - if error == nil, info != nil { - self?.teamInfoModel = info - self?.managerUsers.removeAll() - info?.users.forEach { userInfo in - if userInfo.teamMember?.type == .manager { - self?.managerUsers.append(userInfo) - } - } - - self?.sectionData.removeAll() - self?.getSectionData() - self?.delegate?.didRefreshData() - - print("onTeamMemberChanged managers count : ", self?.managerUsers.count as Any) - } - self?.isRequestData = false - } - } -} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift index d768b721..12aacd95 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift @@ -10,33 +10,42 @@ public protocol TeamManagerListViewModelDelegate: NSObject { func didNeedReloadData() } -open class TeamManagerListViewModel: NSObject, NIMTeamManagerDelegate { - let repo = TeamRepo.shared +@objcMembers +open class TeamManagerListViewModel: NSObject, NETeamListener, NEContactListener { + /// 群API 单例 + public let teamRepo = TeamRepo.shared + /// 当前用户的群成员对象 + public var currentMember: V2NIMTeamMember? - public var currentMember: NIMTeamMember? - - public var managers = [TeamMemberInfoModel]() + public var managers = [NETeamMemberInfoModel]() weak var delegate: TeamManagerListViewModelDelegate? - + /// 群 id public var teamId: String? + /// 是否正在请求数据 + public var isRequest = false + override public init() { super.init() - NIMSDK.shared().teamManager.add(self) - NotificationCenter.default.addObserver(self, selector: #selector(refreshData), name: NENotificationName.updateFriendInfo, object: nil) + teamRepo.addTeamListener(self) + ContactRepo.shared.addContactListener(self) } deinit { - NIMSDK.shared().teamManager.remove(self) + teamRepo.removeTeamListener(self) + ContactRepo.shared.removeContactListener(self) } - open func getManagerDatas(_ tid: String, _ completion: @escaping (Error?) -> Void) { - repo.fetchTeamInfo(tid) { [weak self] error, teamInfo in + /// 获取群成员信息 + /// - Parameter teamId: 群id + /// - Parameter completion: 结果回调 + open func getManagerDatas(_ teamId: String, _ completion: @escaping (Error?) -> Void) { + getTeamInfo(teamId) { [weak self] model, error in if error == nil { self?.managers.removeAll() - teamInfo?.users.forEach { model in - if model.teamMember?.type == .manager { + model?.users.forEach { model in + if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { self?.managers.append(model) } } @@ -47,23 +56,48 @@ open class TeamManagerListViewModel: NSObject, NIMTeamManagerDelegate { } } + /// 获当前登录用户的群成员信息 + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getCurrentUserTeamMember(_ teamId: String, _ completion: @escaping (NSError?) -> Void) { + weak var weakSelf = self + teamRepo.getTeamMember(teamId, IMKitClient.instance.account()) { member, error in + weakSelf?.currentMember = member + completion(error) + } + } + + /// 添加管理员 + /// - Parameter teamId: 群id + /// - Parameter uids: 用户id + /// - Parameter completion: 结果回调 open func addTeamManager(_ teamId: String, _ uids: [String], _ completion: @escaping (Error?) -> Void) { - repo.addTeamManagers(teamId, uids) { error in + teamRepo.addManagers(teamId, uids) { error in completion(error) } } + /// 移除管理员 + /// - Parameter teamId: 群id + /// - Parameter uids: 用户id + /// - Parameter completion: 结果回调 open func removeTeamManager(_ teamId: String, _ uids: [String], _ completion: @escaping (Error?) -> Void) { - repo.removeTeamManagers(teamId, uids) { error in + teamRepo.removeManagers(teamId, uids) { error in completion(error) } } + /// 获取当前群成员信息 + /// - Parameter teamId: 群id open func getCurrentMember(_ teamId: String) { - currentMember = repo.getMemberInfo(IMKitClient.instance.imAccid(), teamId) + weak var weakSelf = self + teamRepo.getTeamMember(teamId, IMKitClient.instance.account()) { member, error in + weakSelf?.currentMember = member + } } - @objc func refreshData() { + /// 刷新数据 + func refreshData() { guard let tid = teamId else { return } @@ -74,16 +108,220 @@ open class TeamManagerListViewModel: NSObject, NIMTeamManagerDelegate { } } - public func onTeamMemberChanged(_ team: NIMTeam) { - guard let tid = teamId else { + /// 群成员离开 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员被踢 + /// - Parameter operatorAccountId: 操作者id + /// - Parameter teamMembers: 群成员 + public func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员加入 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + public func onTeamLeft(_ team: V2NIMTeam, isKicked: Bool) {} + + /// 群信息更新 + /// - Parameter team: 群对象 + private func onTeamMemberChanged(_ members: [V2NIMTeamMember]) { + var isCurrentTeam = false + for member in members { + if let currentTid = teamId, currentTid == member.teamId { + isCurrentTeam = true + break + } + } + + if isCurrentTeam == true { + guard let tid = teamId else { + return + } + + getManagerDatas(tid) { [weak self] error in + if error == nil { + self?.delegate?.didNeedReloadData() + } + } + } + } + + /// 好友信息变更 + /// - parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + for memberInfo in managers { + if memberInfo.teamMember?.accountId == friendInfo.accountId { + let user = NEUserWithFriend(friend: friendInfo) + memberInfo.nimUser = user + delegate?.didNeedReloadData() + } + } + } + + /// 获取群信息(包含管理员) + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getTeamInfo(_ teamId: String, _ completion: @escaping (NETeamInfoModel?, NSError?) -> Void) { + weak var weakSelf = self + if isRequest == true { + return + } + isRequest = true + getCurrentUserTeamMember(teamId) { error in + if let err = error { + weakSelf?.isRequest = false + completion(nil, err) + } else { + weakSelf?.teamRepo.getTeamInfo(teamId) { team, error in + if let err = error { + weakSelf?.isRequest = false + completion(nil, err) + } else { + let model = NETeamInfoModel() + model.team = team + weakSelf?.getTeamManagers(model, .TEAM_MEMBER_ROLE_QUERY_TYPE_MANAGER) { error, teamInfo in + weakSelf?.isRequest = false + if let err = error { + completion(nil, err) + } else { + if let datas = teamInfo?.users { + weakSelf?.managers.removeAll() + weakSelf?.managers.append(contentsOf: datas) + } + completion(teamInfo, error) + } + } + } + } + } + } + } + + /// 获取群管理员 + /// - Parameter teamModel:群信息对象 + /// - Parameter queryType: 查询类型 + /// - Parameter completion: 完成后的回调 + private func getTeamManagers(_ teamInfo: NETeamInfoModel, + _ queryType: V2NIMTeamMemberRoleQueryType, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamid:\(teamInfo.team?.teamId ?? "")") + guard let teamId = teamInfo.team?.teamId else { return } - if tid != team.teamId { + + weak var weakSelf = self + var memberLists = [V2NIMTeamMember]() + + weakSelf?.getAllTeamManagerInfos(teamId, nil, &memberLists, queryType) { ms, error in + if let e = error { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "CALLBACK fetchTeamMember \(String(describing: error))") + completion(e, nil) + } else { + if let members = ms { + weakSelf?.splitTeamManagers(members, teamInfo, 150) { error, model in + completion(error, model) + } + } else { + completion(error, teamInfo) + } + } + } + } + + /// 分页查询群成员信息 + /// - Parameter members: 要查询的群成员列表 + /// - Parameter model : 群信息 + /// - Parameter maxSizeByPage: 单页最大查询数量 + /// - Parameter completion: 完成后的回调 + private func splitTeamManagers(_ members: [V2NIMTeamMember], + _ model: NETeamInfoModel, + _ maxSizeByPage: Int = 150, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", members.count:\(members.count)") + var remaind = [[V2NIMTeamMember]]() + remaind.append(contentsOf: members.chunk(maxSizeByPage)) + fetchManagersInfo(&remaind, model, completion) + } + + /// 从云信服务器批量获取用户资料 + /// - Parameter remainUserIds: 用户集合 + /// - Parameter model: 群信息 + /// - Parameter completion: 成功回调 + private func fetchManagersInfo(_ remainUserIds: inout [[V2NIMTeamMember]], + _ model: NETeamInfoModel, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", remainUserIds.count:\(remainUserIds.count)") + guard let members = remainUserIds.first else { + completion(nil, model) return } - getManagerDatas(tid) { [weak self] error in - if error != nil { - self?.delegate?.didNeedReloadData() + + let accids = members.map(\.accountId) + var temArray = remainUserIds + weak var weakSelf = self + + ContactRepo.shared.getFriendInfoList(accountIds: accids) { infos, v2Error in + if let err = v2Error { + completion(err as NSError, model) + } else { + if let users = infos { + for index in 0 ..< members.count { + let memberInfoModel = NETeamMemberInfoModel() + memberInfoModel.teamMember = members[index] + if users.count > index { + let user = users[index] + memberInfoModel.nimUser = user + } + model.users.append(memberInfoModel) + } + } + temArray.removeFirst() + weakSelf?.fetchManagersInfo(&temArray, model, completion) + } + } + } + + /// 获取群管理员 + /// - Parameter teamId: 群ID + /// - Parameter nextToken: 下一页标识 + /// - Parameter completion: 完成回调 + private func getAllTeamManagerInfos(_ teamId: String, _ nextToken: String? = nil, _ memberList: inout [V2NIMTeamMember], _ queryType: V2NIMTeamMemberRoleQueryType, _ completion: @escaping ([V2NIMTeamMember]?, NSError?) -> Void) { + let option = V2NIMTeamMemberQueryOption() + option.limit = 100 + option.direction = .QUERY_DIRECTION_ASC + option.onlyChatBanned = false + option.roleQueryType = queryType + if let token = nextToken { + option.nextToken = token + } else { + option.nextToken = "" + } + var temMemberLists = memberList + teamRepo.getTeamMemberList(teamId, .TEAM_TYPE_NORMAL, option) { [weak self] result, error in + if let err = error { + completion(nil, err) + } else { + if let members = result?.memberList { + temMemberLists.append(contentsOf: members) + } + if let finished = result?.finished { + if finished == true { + completion(temMemberLists, nil) + } else { + self?.getAllTeamManagerInfos(teamId, result?.nextToken, &temMemberLists, queryType, completion) + } + } } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerViewModel.swift new file mode 100644 index 00000000..c875460b --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerViewModel.swift @@ -0,0 +1,358 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECommonKit +import NIMSDK +import UIKit + +public protocol TeamManagerViewModelDelegate: NSObjectProtocol { + /// 邀请权限变更回调 + /// - Parameter model: 设置数据模型 + func didChangeInviteModeClick(_ model: SettingCellModel) + /// 群信息修改权限变更回调 + /// - Parameter model: 设置数据模型 + func didUpdateTeamInfoClick(_ model: SettingCellModel) + /// at权限变更回调 + /// - Parameter model: 设置数据模型 + func didAtPermissionClick(_ model: SettingCellModel) + /// 管理员点击回调 + func didManagerClick() + /// 通知页面刷新回调 + func didRefreshData() +} + +@objc +@objcMembers +open class TeamManagerViewModel: NSObject, NETeamListener { + /// 群API单例 + public let teamRepo = TeamRepo.shared + /// 聊天API单例 + public let chatRepo = ChatRepo.shared + /// 群信息 + public var teamInfoModel: NETeamInfoModel? + /// UI 分区数据 + public var sectionData = [SettingSectionModel]() + /// 管理员数据 + public var managerUsers = [V2NIMTeamMember]() + + var isRequestData = false + + public weak var delegate: TeamManagerViewModelDelegate? + + /// 当前用户的群成员对象 + public var teamMember: V2NIMTeamMember? + + override public init() { + super.init() + teamRepo.addTeamListener(self) + } + + deinit { + teamRepo.removeTeamListener(self) + } + + /// 获取UI分组数据 + open func getSectionData() { + sectionData.removeAll() + if teamInfoModel?.team?.ownerAccountId == IMKitClient.instance.account() { + sectionData.append(getFirstSection()) + } + sectionData.append(getSecondSection()) + } + + /// 获取当前用户在群中的信息 + /// - Parameter userId: 用户id + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getCurrentUserTeamMember(_ userId: String, _ teamId: String?, completion: @escaping (V2NIMTeamMember?, NSError?) -> Void) { + if let tid = teamId { + teamRepo.getTeamMember(tid, userId) { [weak self] member, error in + if let currentMember = member { + self?.teamMember = currentMember + completion(currentMember, nil) + } else { + completion(member, error) + } + } + } + } + + /// 获取管理员信息 + + /// 群信息(包含群成员) + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + open func getTeamWithMembers(_ teamId: String, _ completion: @escaping (Error?) -> Void) { + if isRequestData == true { + return + } + weak var weakSelf = self + isRequestData = true + + teamRepo.getTeamInfo(teamId) { team, error in + if let err = error { + weakSelf?.isRequestData = false + completion(err) + } else { + var memberList = [V2NIMTeamMember]() + weakSelf?.getAllTeamManagers(teamId, nil, &memberList, .TEAM_MEMBER_ROLE_QUERY_TYPE_MANAGER) { members, error in + if let err = error { + weakSelf?.isRequestData = false + completion(err) + } else { + weakSelf?.isRequestData = false + let model = NETeamInfoModel() + model.team = team + weakSelf?.teamInfoModel = model + weakSelf?.managerUsers.removeAll() + if let managers = members { + for member in managers { + if member.memberRole != .TEAM_MEMBER_ROLE_OWNER { + weakSelf?.managerUsers.append(member) + } + } + } + weakSelf?.sectionData.removeAll() + weakSelf?.getSectionData() + completion(nil) + } + } + } + } + } + + /// 获取顶部section(管理员数量) + open func getFirstSection() -> SettingSectionModel { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + weak var weakSelf = self + let model = SettingSectionModel() + let manager = SettingCellLabelArrowModel() + manager.cellName = localizable("manage_manger") + manager.type = SettingCellType.SettingArrowCell.rawValue + manager.rowHeight = 56 + manager.arrowLabelText = "\(managerUsers.count)" + model.cellModels.append(contentsOf: [manager]) + model.setCornerType() + manager.cellClick = { + weakSelf?.delegate?.didManagerClick() + } + return model + } + + /// 获取中间section数据(权限) + open func getSecondSection() -> SettingSectionModel { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + weak var weakSelf = self + let model = SettingSectionModel() + + let editTeamPermission = SettingCellModel() + editTeamPermission.cellName = localizable("who_edit_team_info") + editTeamPermission.type = SettingCellType.SettingSelectCell.rawValue + editTeamPermission.rowHeight = 73 + + if let updateMode = teamInfoModel?.team?.updateInfoMode, updateMode == .TEAM_UPDATE_INFO_MODE_ALL { + editTeamPermission.subTitle = localizable("team_all") + } else { + editTeamPermission.subTitle = localizable("team_owner_and_manager") + } + + editTeamPermission.cellClick = { + weakSelf?.delegate?.didUpdateTeamInfoClick(editTeamPermission) + } + + let invitePermission = SettingCellModel() + invitePermission.cellName = localizable("who_edit_user_info") + invitePermission.type = SettingCellType.SettingSelectCell.rawValue + invitePermission.rowHeight = 73 + if let inviteMode = teamInfoModel?.team?.inviteMode, inviteMode == .TEAM_INVITE_MODE_ALL { + invitePermission.subTitle = localizable("team_all") + } else { + invitePermission.subTitle = localizable("team_owner_and_manager") + } + + invitePermission.cellClick = { + weakSelf?.delegate?.didChangeInviteModeClick(invitePermission) + } + + let atAllPermission = SettingCellModel() + atAllPermission.cellName = localizable("who_at_all") + atAllPermission.type = SettingCellType.SettingSelectCell.rawValue + atAllPermission.rowHeight = 73 + atAllPermission.subTitle = localizable("team_owner_and_manager") + atAllPermission.subTitle = getTeamAtAllPermissionValue() + + atAllPermission.cellClick = { + weakSelf?.delegate?.didAtPermissionClick(atAllPermission) + } + + model.cellModels.append(contentsOf: [editTeamPermission, invitePermission, atAllPermission]) + model.setCornerType() + + return model + } + + /// 更新at权限 + /// - Parameter isManager: 是否只有管理员能at,false 允许所有人发送at消息 + /// - Parameter completion: 完成回调 + open func updateTeamAtAllPermission(_ isManager: Bool, _ completion: @escaping (Error?) -> Void) { + let value = isManager == true ? allowAtManagerValue : allowAtAllValue + guard let tid = teamInfoModel?.team?.teamId else { + return + } + weak var weakSelf = self + teamRepo.getTeamInfo(tid) { team, error in + if let custom = team?.serverExtension { + if var dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { + dic[keyAllowAtAll] = value + let info = NECommonUtil.getJSONStringFromDictionary(dic) + weakSelf?.teamRepo.updateTeamExtension(tid, info) { error in + completion(error) + } + } + } else { + var dic = [String: Any]() + dic[keyAllowAtAll] = value + let info = NECommonUtil.getJSONStringFromDictionary(dic) + weakSelf?.teamRepo.updateTeamExtension(tid, info) { error in + completion(error) + } + } + } + } + + /// 获取at权限值 + func getTeamAtAllPermissionValue() -> String { + if let custom = teamInfoModel?.team?.serverExtension { + if let dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { + if let value = dic[keyAllowAtAll] as? String { + if value == allowAtManagerValue { + return localizable("team_owner_and_manager") + } + } + } + } + return localizable("team_all") + } + + /// 群成员离开 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员加入 + /// - Parameter operatorAccountId: 操作者 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群信息更新 + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + updateTeamInfo(team.teamId) + } + + /// 更改群组更新信息的权限 + /// - Parameter teamId : 群组ID + /// - Parameter mode : 群信息修改权限 + /// - Parameter completion: 完成后的回调 + public func updateTeamInfoPrivilege(_ teamId: String, _ mode: V2NIMTeamUpdateInfoMode, + _ completion: @escaping (NSError?, V2NIMTeam?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", mode:\(mode.rawValue)") + teamRepo.updateTeamInfoPrivilege(teamId, mode) { error, team in + completion(error, team) + } + } + + /// 更新群组邀请他人方式 + /// - Parameter teamId: 群组ID + /// - Parameter mode: 邀请模式 + /// - Parameter completion: 完成后的回调 + public func updateInviteMode(_ teamId: String, _ mode: V2NIMTeamInviteMode, + _ completion: @escaping (NSError?, V2NIMTeam?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", mode:\(mode.rawValue)") + teamRepo.updateInviteMode(teamId, mode) { error, team in + completion(error, team) + } + } + + /// 群信息更新 + /// - Parameter teamId: 群id + private func updateTeamInfo(_ teamId: String) { + guard let tid = teamInfoModel?.team?.teamId else { + return + } + + if tid != teamId { + return + } + + getTeamWithMembers(teamId) { [weak self] error in + if error == nil { + self?.delegate?.didRefreshData() + } + } + } + + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + print("team manage onTeamMemberInfoUpdated") + onTeamMemberChanged(teamMembers) + } + + /// 处理群成员变更 + /// - Parameter members: 群成员 + private func onTeamMemberChanged(_ members: [V2NIMTeamMember]) { + var isCurrentTeam = false + for member in members { + if let currentTid = teamInfoModel?.team?.teamId, currentTid == member.teamId { + isCurrentTeam = true + } + if member.accountId == IMKitClient.instance.account() { + teamMember = member + } + } + + if isCurrentTeam == true { + if let currentTid = teamInfoModel?.team?.teamId { + print("team manage updateTeamInfo") + updateTeamInfo(currentTid) + } + } + } + + /// 获取群管理员 + /// - Parameter teamId: 群ID + /// - Parameter nextToken: 下一页标识 + /// - Parameter completion: 完成回调 + private func getAllTeamManagers(_ teamId: String, _ nextToken: String? = nil, _ memberList: inout [V2NIMTeamMember], _ queryType: V2NIMTeamMemberRoleQueryType, _ completion: @escaping ([V2NIMTeamMember]?, NSError?) -> Void) { + let option = V2NIMTeamMemberQueryOption() + option.limit = 100 + option.direction = .QUERY_DIRECTION_ASC + option.onlyChatBanned = false + option.roleQueryType = queryType + if let token = nextToken { + option.nextToken = token + } else { + option.nextToken = "" + } + var temMemberLists = memberList + teamRepo.getTeamMemberList(teamId, .TEAM_TYPE_NORMAL, option) { [weak self] result, error in + if let err = error { + completion(nil, err) + } else { + if let members = result?.memberList { + temMemberLists.append(contentsOf: members) + } + if let finished = result?.finished { + if finished == true { + completion(temMemberLists, nil) + } else { + self?.getAllTeamManagers(teamId, result?.nextToken, &temMemberLists, queryType, completion) + } + } + } + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift index 7b863baa..d181812a 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -12,20 +12,21 @@ public protocol TeamMemberSelectViewModelDelegate: NSObject { } class TeamMemberSelectViewModel: NSObject, NIMTeamManagerDelegate { - let repo = TeamRepo.shared - + /// 群API单例 + let teamRepo = TeamRepo.shared + /// 选中成员数据 var datas = [NESelectTeamMember]() var showDatas = [NESelectTeamMember]() - - var teamInfoModel: TeamInfoModel? - + /// 群信息 + var teamInfoModel: NETeamInfoModel? + /// 代理 weak var delegate: TeamMemberSelectViewModelDelegate? - - var selectDic = [String: TeamMemberInfoModel]() // key 值为用户 id - + /// 当前选中的数据 + var selectDic = [String: NETeamMemberInfoModel]() // key 值为用户 id + /// 是否正在发送请求 var isRequest = false - + /// 管理员account id 存放 var managerSet = Set() override init() { @@ -37,13 +38,16 @@ class TeamMemberSelectViewModel: NSObject, NIMTeamManagerDelegate { NIMSDK.shared().teamManager.remove(self) } + /// 群信息(包含群成员) + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 func getTeamInfo(_ teamId: String, _ completion: @escaping (Error?) -> Void) { if isRequest == true { return } weak var weakSelf = self isRequest = true - repo.fetchTeamInfo(teamId) { error, teamInfo in + teamRepo.getTeamWithMembers(teamId, .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL) { error, teamInfo in weakSelf?.isRequest = false if error == nil { weakSelf?.datas.removeAll() @@ -55,23 +59,24 @@ class TeamMemberSelectViewModel: NSObject, NIMTeamManagerDelegate { } } + /// 获取选择器数据 func getData() { var temFilters = Set() - selectDic.forEach { (key: String, value: TeamMemberInfoModel) in + for (key, _) in selectDic { temFilters.insert(key) } managerSet.removeAll() teamInfoModel?.users.forEach { [weak self] userModel in - if let uid = userModel.nimUser?.userId { + if let uid = userModel.nimUser?.user?.accountId { temFilters.remove(uid) - if uid == IMKitClient.instance.imAccid() { + if uid == IMKitClient.instance.account() { return } - if uid == self?.teamInfoModel?.team?.owner { + if uid == self?.teamInfoModel?.team?.ownerAccountId { return } - if userModel.teamMember?.type == .manager { + if userModel.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { self?.managerSet.insert(uid) self?.selectDic.removeValue(forKey: uid) return @@ -82,12 +87,12 @@ class TeamMemberSelectViewModel: NSObject, NIMTeamManagerDelegate { self?.datas.append(selectMember) self?.showDatas.append(selectMember) } - temFilters.forEach { uid in + for uid in temFilters { selectDic.removeValue(forKey: uid) } - datas.forEach { member in - if let accid = member.member?.nimUser?.userId { - if selectDic.contains(where: { (key: String, value: TeamMemberInfoModel) in + for member in datas { + if let accid = member.member?.nimUser?.user?.accountId { + if selectDic.contains(where: { (key: String, value: NETeamMemberInfoModel) in key == accid }) { member.isSelected = true @@ -96,24 +101,29 @@ class TeamMemberSelectViewModel: NSObject, NIMTeamManagerDelegate { } } + /// 搜索所有数据 + /// - Parameter searchText: 搜索关键字 func searchAllData(_ searchText: String) -> [NESelectTeamMember] { let result = datas.filter { findContainStr(searchText, $0) } return result } + /// 所有展示数据 + /// - Parameter searchText: 搜索关键字 func searchShowData(_ searchText: String) -> [NESelectTeamMember] { let result = showDatas.filter { findContainStr(searchText, $0) } return result } + /// 判断选择器对象是否包含搜索字段 func findContainStr(_ text: String, _ selectModel: NESelectTeamMember) -> Bool { - if let uid = selectModel.member?.nimUser?.userId, uid.contains(text) { + if let uid = selectModel.member?.nimUser?.user?.accountId, uid.contains(text) { return true - } else if let nick = selectModel.member?.nimUser?.userInfo?.nickName, nick.contains(text) { + } else if let nick = selectModel.member?.nimUser?.user?.name, nick.contains(text) { return true - } else if let alias = selectModel.member?.nimUser?.alias, alias.contains(text) { + } else if let alias = selectModel.member?.nimUser?.friend?.alias, alias.contains(text) { return true - } else if let tNick = selectModel.member?.teamMember?.nickname, tNick.contains(text) { + } else if let tNick = selectModel.member?.teamMember?.teamNick, tNick.contains(text) { return true } return false diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift index 055f439f..eca10d45 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift @@ -3,6 +3,7 @@ // found in the LICENSE file. import NEChatKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -10,33 +11,65 @@ protocol TeamMembersViewModelDelegate: NSObject { func didNeedRefreshUI() } -class TeamMembersViewModel: NSObject { +class TeamMembersViewModel: NSObject, NETeamListener, NEContactListener { + /// 是否正在请求数据 + public var isRequest = false + /// 群id + var teamId: String? + weak var delegate: TeamMembersViewModelDelegate? - var datas = [TeamMemberInfoModel]() + var datas = [NETeamMemberInfoModel]() + + /// 搜索结果数据 + public var searchDatas = [NETeamMemberInfoModel]() + + let teamRepo = TeamRepo.shared - let repo = TeamRepo.shared - public var currentMember: NIMTeamMember? + public var currentMember: V2NIMTeamMember? - func getMemberInfo(_ teamId: String) { - currentMember = repo.getMemberInfo(IMKitClient.instance.imAccid(), teamId) + override init() { + super.init() + teamRepo.addTeamListener(self) + ContactRepo.shared.addContactListener(self) } + deinit { + teamRepo.removeTeamListener(self) + ContactRepo.shared.removeContactListener(self) + } + + /// 获取群成员信息 + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getMemberInfo(_ teamId: String, _ completion: @escaping (NSError?) -> Void) { + weak var weakSelf = self + teamRepo.getTeamMember(teamId, IMKitClient.instance.account()) { member, error in + weakSelf?.currentMember = member + completion(error) + } + } + + /// 移除群成员 + /// - Parameter teamdId: 群id + /// - Parameter uids: 用户id func removeTeamMember(_ teamdId: String, _ uids: [String], _ completion: @escaping (NSError?) -> Void) { - repo.removeMembers(teamdId, uids) { error in + teamRepo.removeMembers(teamdId, uids) { error in completion(error as NSError?) } } - func setShowDatas(_ memberDatas: [TeamMemberInfoModel]?) { - var owner: TeamMemberInfoModel? - var managers = [TeamMemberInfoModel]() - var normalMembers = [TeamMemberInfoModel]() + /// 设置成员数据 + /// - Parameter memberDatas: 成员数据 + func setShowDatas(_ memberDatas: [NETeamMemberInfoModel]?) { + var owner: NETeamMemberInfoModel? + var managers = [NETeamMemberInfoModel]() + var normalMembers = [NETeamMemberInfoModel]() memberDatas?.forEach { model in - if model.teamMember?.type == .owner { + if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_OWNER { owner = model - } else if model.teamMember?.type == .manager { + } else if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { managers.append(model) } else { normalMembers.append(model) @@ -49,14 +82,14 @@ class TeamMembersViewModel: NSObject { } // managers 根据 时间排序 排序 managers.sort { model1, model2 in - if let time1 = model1.teamMember?.createTime, let time2 = model2.teamMember?.createTime { + if let time1 = model1.teamMember?.joinTime, let time2 = model2.teamMember?.joinTime { return time2 > time1 } return false } // normalMembers 根据 时间排序 排序 normalMembers.sort { model1, model2 in - if let time1 = model1.teamMember?.createTime, let time2 = model2.teamMember?.createTime { + if let time1 = model1.teamMember?.joinTime, let time2 = model2.teamMember?.joinTime { return time2 > time1 } return false @@ -66,12 +99,14 @@ class TeamMembersViewModel: NSObject { delegate?.didNeedRefreshUI() } - func removeModel(_ model: TeamMemberInfoModel?) { + /// 移除成员数据(UI数据源) + /// - Parameter model: 成员数据 + func removeModel(_ model: NETeamMemberInfoModel?) { guard let rmModel = model else { return } datas.removeAll(where: { model in - if let rmUid = rmModel.nimUser?.userId, let uid = model.nimUser?.userId { + if let rmUid = rmModel.nimUser?.user?.accountId, let uid = model.nimUser?.user?.accountId { if rmUid == uid { return true } @@ -79,4 +114,266 @@ class TeamMembersViewModel: NSObject { return false }) } + + // MARK: - NEContactListener + + /// 好友信息变更回调 + /// - Parameter friendInfo: 好友信息 + func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + datas.forEach { [weak self] model in + if let accountId = model.nimUser?.user?.accountId, accountId == friendInfo.accountId { + if let tid = self?.teamId { + self?.getTeamInfo(tid) { model, error in + if error == nil { + self?.delegate?.didNeedRefreshUI() + } + } + } + return + } + } + } + + /// 群成员信息更新 + /// - Parameter teamMembers: 群成员信息 + func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + changeMembers(teamMembers) + } + + /// 群成员离开 + /// - Parameter teamMembers: 群成员信息 + func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + removeSearchData(teamMembers) + changeMembers(teamMembers) + } + + /// 判断离开用户是不是当前搜索展示用户 + /// - Parameter teamMembers: 群成员信息 + public func removeSearchData(_ teamMembers: [V2NIMTeamMember]) { + if searchDatas.count <= 0 { + return + } + var memberSet = Set() + for member in teamMembers { + if let tid = teamId, tid == member.teamId { + memberSet.insert(member.accountId) + } + } + + if memberSet.count <= 0 { + return + } + searchDatas.removeAll { model in + if let accid = model.teamMember?.accountId, memberSet.contains(accid) { + return true + } + return false + } + } + + /// 群成员加入 + /// - Parameter teamMembers: 群成员信息 + func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { + changeMembers(teamMembers) + } + + /// 群成员被踢 + /// - Parameter operatorAccountId: 操作者id + /// - Parameter teamMembers: 群成员信息 + func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + removeSearchData(teamMembers) + changeMembers(teamMembers) + } + + /// 群成员信息更新统一处理方法 + /// - Parameter teamMembers: 群成员信息 + func changeMembers(_ teamMembers: [V2NIMTeamMember]) { + guard let tid = teamId else { + return + } + var isNeedRefresh = false + + for member in teamMembers { + if member.teamId == tid { + isNeedRefresh = true + break + } + } + + if isNeedRefresh == true { + getTeamInfo(tid) { model, error in + if error == nil { + self.delegate?.didNeedRefreshUI() + } + } + } + } + + /// 获取群信息(包含群成员) + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getTeamInfo(_ teamId: String, _ completion: @escaping (NETeamInfoModel?, NSError?) -> Void) { + weak var weakSelf = self + if isRequest == true { + return + } + isRequest = true + + getMemberInfo(teamId) { error in + if let err = error { + weakSelf?.isRequest = false + completion(nil, err) + } else { + weakSelf?.teamRepo.getTeamInfo(teamId) { team, error in + if let err = error { + weakSelf?.isRequest = false + completion(nil, err) + } else { + let model = NETeamInfoModel() + model.team = team + weakSelf?.getTeamMembers(model, .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL) { error, teamInfo in + weakSelf?.isRequest = false + if let err = error { + completion(nil, err) + } else { + if let datas = teamInfo?.users { + weakSelf?.setShowDatas(datas) + } + completion(teamInfo, error) + } + } + } + } + } + } + } + + /// 获取群成员 + /// - Parameter queryType: 查询类型 + /// - Parameter teamModel:群信息对象 + /// - Parameter completion: 完成后的回调 + private func getTeamMembers(_ teamInfo: NETeamInfoModel, + _ queryType: V2NIMTeamMemberRoleQueryType, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamid:\(teamInfo.team?.teamId ?? "")") + guard let teamId = teamInfo.team?.teamId else { + return + } + + weak var weakSelf = self + + if let members = NETeamMemberCache.shared.getTeamMemberCache(teamId) { + teamInfo.users = members + completion(nil, teamInfo) + NEALog.infoLog(weakSelf?.className() ?? "", desc: "load team member from cache success.") + return + } + + var memberLists = [V2NIMTeamMember]() + + weakSelf?.getAllTeamMemberInfos(teamId, nil, &memberLists, queryType) { ms, error in + if let e = error { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "CALLBACK fetchTeamMember \(String(describing: error))") + completion(e, nil) + } else { + if let members = ms { + weakSelf?.splitTeamMembers(members, teamInfo, 150) { error, model in + if let users = model?.users, users.count > 0 { + NEALog.infoLog(weakSelf?.className() ?? "", desc: "set team member cache success.") + NETeamMemberCache.shared.setCacheMembers(teamId, users) + } + completion(error, model) + } + } else { + completion(error, teamInfo) + } + } + } + } + + /// 分页查询群成员信息 + /// - Parameter members: 要查询的群成员列表 + /// - Parameter model : 群信息 + /// - Parameter maxSizeByPage: 单页最大查询数量 + /// - Parameter completion: 完成后的回调 + private func splitTeamMembers(_ members: [V2NIMTeamMember], + _ model: NETeamInfoModel, + _ maxSizeByPage: Int = 150, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", members.count:\(members.count)") + var remaind = [[V2NIMTeamMember]]() + remaind.append(contentsOf: members.chunk(maxSizeByPage)) + fetchUsersInfo(&remaind, model, completion) + } + + /// 从云信服务器批量获取用户资料 + /// - Parameter remainUserIds: 用户集合 + /// - Parameter model: 群信息 + /// - Parameter completion: 成功回调 + private func fetchUsersInfo(_ remainUserIds: inout [[V2NIMTeamMember]], + _ model: NETeamInfoModel, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", remainUserIds.count:\(remainUserIds.count)") + guard let members = remainUserIds.first else { + completion(nil, model) + return + } + + let accids = members.map(\.accountId) + var temArray = remainUserIds + weak var weakSelf = self + + ContactRepo.shared.getFriendInfoList(accountIds: accids) { infos, v2Error in + if let err = v2Error { + completion(err as NSError, model) + } else { + if let users = infos { + for index in 0 ..< members.count { + let memberInfoModel = NETeamMemberInfoModel() + memberInfoModel.teamMember = members[index] + if users.count > index { + let user = users[index] + memberInfoModel.nimUser = user + } + model.users.append(memberInfoModel) + } + } + temArray.removeFirst() + weakSelf?.fetchUsersInfo(&temArray, model, completion) + } + } + } + + /// 获取群成员 + /// - Parameter teamId: 群ID + /// - Parameter completion: 完成回调 + private func getAllTeamMemberInfos(_ teamId: String, _ nextToken: String? = nil, _ memberList: inout [V2NIMTeamMember], _ queryType: V2NIMTeamMemberRoleQueryType, _ completion: @escaping ([V2NIMTeamMember]?, NSError?) -> Void) { + let option = V2NIMTeamMemberQueryOption() + option.limit = 1000 + option.direction = .QUERY_DIRECTION_ASC + option.onlyChatBanned = false + option.roleQueryType = queryType + if let token = nextToken { + option.nextToken = token + } else { + option.nextToken = "" + } + var temMemberLists = memberList + teamRepo.getTeamMemberList(teamId, .TEAM_TYPE_NORMAL, option) { [weak self] result, error in + if let err = error { + completion(nil, err) + } else { + if let members = result?.memberList { + temMemberLists.append(contentsOf: members) + } + if let finished = result?.finished { + if finished == true { + completion(temMemberLists, nil) + } else { + self?.getAllTeamMemberInfos(teamId, result?.nextToken, &temMemberLists, queryType, completion) + } + } + } + } + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.swift index bf92a566..1d349147 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.swift @@ -9,13 +9,18 @@ import UIKit @objc @objcMembers open class TeamNameViewModel: NSObject { - let repo = ChatRepo.shared - var currentTeamMember: NIMTeamMember? + let teamRepo = TeamRepo.shared + var currentTeamMember: V2NIMTeamMember? - func getCurrentUserTeamMember(_ teamId: String?) { + /// 获取当前用户群信息 + /// - Parameter teamId 群id + func getCurrentUserTeamMember(_ teamId: String?, _ completion: @escaping (NSError?) -> Void) { if let tid = teamId { - let currentUserAccid = IMKitClient.instance.imAccid() - currentTeamMember = repo.getTeamMemberList(userId: currentUserAccid, teamId: tid) + let currentUserAccid = IMKitClient.instance.account() + teamRepo.getTeamMember(tid, currentUserAccid) { member, error in + self.currentTeamMember = member + completion(error) + } } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift index 7ecec906..2ed52712 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift @@ -3,11 +3,11 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit -protocol TeamSettingViewModelDelegate: NSObjectProtocol { +public protocol TeamSettingViewModelDelegate: NSObjectProtocol { func didClickChangeNick() func didChangeInviteModeClick(_ model: SettingCellModel) func didUpdateTeamInfoClick(_ model: SettingCellModel) @@ -16,50 +16,73 @@ protocol TeamSettingViewModelDelegate: NSObjectProtocol { func didError(_ error: NSError) func didClickMark() func didClickTeamManage() + func didShowNoNetworkToast() } @objcMembers -open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { +open class TeamSettingViewModel: NSObject, NETeamListener, NEConversationListener { + /// 分区区域数据 public var sectionData = [SettingSectionModel]() public var searchResultInfos: [HistoryMessageModel]? + /// 群信息(包含群成员列表) + public var teamInfoModel: NETeamInfoModel? + /// 群模块接口单例 + public let teamRepo = TeamRepo.shared + /// 通讯录接口单例 + public let contactRepo = ContactRepo.shared + /// 当前用户的群成员信息 + public var memberInTeam: V2NIMTeamMember? + /// 群设置代理 + public weak var delegate: TeamSettingViewModelDelegate? - public var teamInfoModel: TeamInfoModel? + private let className = "TeamSettingViewModel" + /// 群类型 + public var teamSettingType: TeamSettingType = .Discuss + /// 群对应的会话信息 + public var conversation: V2NIMConversation? + /// 会话API单例 + public var conversationRepo = ConversationRepo.shared + /// 是否获取过群设置数据 + public var isRequestSettingData = false - public let repo = TeamRepo.shared + /// 结束标志(如果页面销毁的情况下,群成员还未取完,结束后续获取动作) + public var isBreakFalg = false - public var memberInTeam: NIMTeamMember? + /// 是否取完所有成员数据,如果取完所有数据跳转到成员列表页面不用重新获取直接复用设置页的数据,如果没有取完所有数据,跳转到成员列表页重新获取所有成员 + public var isGetAllMemberDatas = false - weak var delegate: TeamSettingViewModelDelegate? - - private let className = "TeamSettingViewModel" - - public var teamSettingType: TeamSettingType = .Discuss + /// 群成员 + public var allMembersDic = [String: V2NIMTeamMember]() override public init() { super.init() - NIMSDK.shared().teamManager.add(self) + teamRepo.addTeamListener(self) + conversationRepo.addListener(self) } deinit { - NIMSDK.shared().teamManager.remove(self) + teamRepo.removeTeamListener(self) + conversationRepo.removeListener(self) + NETeamMemberCache.shared.clearCache() } func getData() { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) sectionData.removeAll() sectionData.append(getTwoSection()) - print("current team type : ", teamInfoModel?.team?.type.rawValue as Any) - if let type = teamInfoModel?.team?.type, type == .advanced { - if teamInfoModel?.team?.clientCustomInfo?.contains(discussTeamKey) == true { + print("current team type : ", teamInfoModel?.team?.teamType.rawValue as Any) + if let type = teamInfoModel?.team?.teamType, type == .TEAM_TYPE_NORMAL { + if teamInfoModel?.team?.serverExtension?.contains(discussTeamKey) == true { return } sectionData.append(getThreeSection()) } } + // 头像 成员列表 private func getOneSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) let model = SettingSectionModel() let cellModel = SettingCellModel() cellModel.type = SettingCellType.SettingHeaderCell.rawValue @@ -69,13 +92,20 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return model } + // 标记 历史记录 消息提醒 private func getTwoSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) + let model = SettingSectionModel() + guard let tid = teamInfoModel?.team?.teamId else { + NEALog.infoLog(ModuleName + " " + className, desc: #function + " teamId is nil") + return model + } + weak var weakSelf = self - // 标记 + // 标记 置顶 昵称 let mark = SettingCellModel() mark.cellName = localizable("mark") mark.type = SettingCellType.SettingArrowCell.rawValue @@ -96,15 +126,22 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { remind.cellName = localizable("message_remind") remind.type = SettingCellType.SettingSwitchCell.rawValue - if let noti = teamInfoModel?.team?.notifyStateForNewMsg, noti == .all { + let mode = teamRepo.getTeamMuteStatus(tid) + if mode == .TEAM_MESSAGE_MUTE_MODE_OFF { remind.switchOpen = true } + remind.swichChange = { isOpen in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + remind.switchOpen = !isOpen + weakSelf?.delegate?.didShowNoNetworkToast() + weakSelf?.delegate?.didNeedRefreshUI() + return + } if let tid = weakSelf?.teamInfoModel?.team?.teamId { if isOpen == true { - // weakSelf?.repo.updateNoti(.all, tid) - weakSelf?.repo.setTeamNotify(.all, tid) { error in - if let err = error as? NSError { + weakSelf?.teamRepo.setTeamMuteStatus(tid, .TEAM_MESSAGE_MUTE_MODE_OFF) { error in + if let err = error { weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) } else { @@ -112,9 +149,8 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { } } } else { - // weakSelf?.repo.updateNoti(.none, tid) - weakSelf?.repo.setTeamNotify(.none, tid) { error in - if let err = error as? NSError { + weakSelf?.teamRepo.setTeamMuteStatus(tid, .TEAM_MESSAGE_MUTE_MODE_ON) { error in + if let err = error { weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) } else { @@ -130,23 +166,22 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { setTop.cellName = localizable("session_set_top") setTop.type = SettingCellType.SettingSwitchCell.rawValue - if let tid = teamInfoModel?.team?.teamId { - let session = NIMSession(tid, type: .team) - setTop.switchOpen = repo.isStickTop(session) + if let currentConversation = conversation { + setTop.switchOpen = currentConversation.stickTop } + // 置顶 setTop.swichChange = { isOpen in - if let tid = weakSelf?.teamInfoModel?.team?.teamId { - let session = NIMSession(tid, type: .team) - if isOpen { - // 不存在最近会话的置顶,先创建最近会话 - if weakSelf?.getRecenterSession() == nil { - NELog.infoLog(weakSelf?.className() ?? "", desc: #function + "addRecentetSession") - weakSelf?.addRecentetSession() - } - let params = NIMAddStickTopSessionParams(session: session) - weakSelf?.repo.addStickTop(params: params) { error, info in - NELog.infoLog(weakSelf?.className() ?? "", desc: #function + "addStickTop error : \(error?.localizedDescription ?? "") ") + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + setTop.switchOpen = !isOpen + weakSelf?.delegate?.didShowNoNetworkToast() + weakSelf?.delegate?.didNeedRefreshUI() + return + } + if isOpen { + if let teamId = weakSelf?.teamInfoModel?.team?.teamId { + weakSelf?.teamRepo.addStickTop(teamId) { error in + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + "addStickTop error : \(error?.localizedDescription ?? "") ") if let err = error { weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) @@ -154,22 +189,23 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { setTop.switchOpen = true } } - } else { - if let info = weakSelf?.repo.getTopSessionInfo(session) { - weakSelf?.repo.removeStickTop(params: info) { error, info in - NELog.infoLog(weakSelf?.className() ?? "", desc: #function + "removeStickTop error : \(error?.localizedDescription ?? "") ") - if let err = error { - weakSelf?.delegate?.didNeedRefreshUI() - weakSelf?.delegate?.didError(err) - } else { - setTop.switchOpen = false - } + } + } else { + if let teamId = weakSelf?.teamInfoModel?.team?.teamId { + weakSelf?.teamRepo.removeStickTop(teamId) { error in + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + "removeStickTop error : \(error?.localizedDescription ?? "") ") + if let err = error { + weakSelf?.delegate?.didNeedRefreshUI() + weakSelf?.delegate?.didError(err) + } else { + setTop.switchOpen = false } } } } } + // 群昵称 let nick = SettingCellModel() nick.cellName = localizable("team_nick") nick.type = SettingCellType.SettingArrowCell.rawValue @@ -189,9 +225,9 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return model } - // 群昵称/群禁言 + // 群昵称 群禁言 private func getThreeSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) let model = SettingSectionModel() weak var weakSelf = self @@ -199,14 +235,25 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { forbiddenWords.cellName = localizable("team_no_speak") forbiddenWords.type = SettingCellType.SettingSwitchCell.rawValue - if let mute = teamInfoModel?.team?.inAllMuteMode() { - forbiddenWords.switchOpen = mute + if let chatBanndedMode = teamInfoModel?.team?.chatBannedMode { + if chatBanndedMode == .TEAM_CHAT_BANNED_MODE_BANNED_ALL || chatBanndedMode == .TEAM_CHAT_BANNED_MODE_BANNED_NORMAL { + forbiddenWords.switchOpen = true + } else { + forbiddenWords.switchOpen = false + } } + forbiddenWords.swichChange = { isOpen in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + forbiddenWords.switchOpen = !isOpen + weakSelf?.delegate?.didShowNoNetworkToast() + weakSelf?.delegate?.didNeedRefreshUI() + return + } if let tid = weakSelf?.teamInfoModel?.team?.teamId { - weakSelf?.repo.muteAllMembers(isOpen, tid) { error in + weakSelf?.teamRepo.setTeamChatBannedMode(tid, isOpen ? .TEAM_CHAT_BANNED_MODE_BANNED_NORMAL : .TEAM_CHAT_BANNED_MODE_NONE) { error in print("update mute error : ", error as Any) - if let err = error as? NSError { + if let err = error { forbiddenWords.switchOpen = !isOpen weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) @@ -236,8 +283,9 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return model } + // 邀请 修改群信息 private func getFourSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) weak var weakSelf = self let model = SettingSectionModel() @@ -246,7 +294,7 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { invitePermission.type = SettingCellType.SettingSelectCell.rawValue invitePermission.rowHeight = 73 - if let inviteMode = teamInfoModel?.team?.inviteMode, inviteMode == .all { + if let inviteMode = teamInfoModel?.team?.inviteMode, inviteMode == .TEAM_INVITE_MODE_ALL { invitePermission.subTitle = localizable("team_all") } else { invitePermission.subTitle = localizable("team_owner") @@ -260,7 +308,7 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { modifyPermission.cellName = localizable("modify_team_info_permission") modifyPermission.type = SettingCellType.SettingSelectCell.rawValue modifyPermission.rowHeight = 73 - if let updateMode = teamInfoModel?.team?.updateInfoMode, updateMode == .all { + if let updateMode = teamInfoModel?.team?.updateInfoMode, updateMode == .TEAM_UPDATE_INFO_MODE_ALL { modifyPermission.subTitle = localizable("team_all") } else { modifyPermission.subTitle = localizable("team_owner") @@ -278,81 +326,323 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return model } - func getTeamInfo(_ teamId: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") + /// 获取群(内部获取群成员) + /// - Parameter teamId: 群id + /// - Parameter completion: 回调 + func getTeamWithMembers(_ teamId: String, _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") weak var weakSelf = self - repo.fetchTeamInfo(teamId) { error, teamInfo in - weakSelf?.teamInfoModel = teamInfo + if isRequestSettingData == true { + return + } + getTeamInfoWithSomeMembers(teamId) { error, finished in + weakSelf?.isRequestSettingData = false if error == nil { weakSelf?.getData() - weakSelf?.getCurrentMember(IMKitClient.instance.imAccid(), teamId) + weakSelf?.isGetAllMemberDatas = true } completion(error) } } - public func dismissTeam(_ teamId: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") - repo.dismissTeam(teamId, completion) + /// 获取群信息 + /// - Parameter teamId: 群id + /// - Parameter queryType: 查询类型 + /// - Parameter completion: 完成后的回调 + private func getTeamInfoWithMembers(_ teamId: String, + _ queryType: V2NIMTeamMemberRoleQueryType, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamid:\(teamId)") + weak var weakSelf = self + + teamRepo.getTeamInfo(teamId) { team, error in + if let err = error { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "CALLBACK fetchTeamInfo \(String(describing: error))") + completion(err, nil) + } else { + let model = NETeamInfoModel() + model.team = team + if let tid = team?.teamId, let users = NETeamMemberCache.shared.getTeamMemberCache(tid) { + model.users = users + completion(nil, model) + NEALog.infoLog(weakSelf?.className() ?? "", desc: "load team member from cache success.") + return + } + var memberLists = [V2NIMTeamMember]() + + weakSelf?.getAllTeamMembers(teamId, nil, &memberLists, queryType) { ms, error in + if let e = error { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "CALLBACK fetchTeamMember \(String(describing: error))") + completion(e, nil) + } else { + if let members = ms { + weakSelf?.splitGroupMembers(members, model, 150) { error, model in + if let tid = team?.teamId, let users = model?.users, users.count > 0 { + NEALog.infoLog(weakSelf?.className() ?? "", desc: "set team member cache success.") + NETeamMemberCache.shared.setCacheMembers(tid, users) + } + completion(error, model) + } + } else { + completion(error, model) + } + } + } + } + } } - open func quitTeam(_ teamId: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") - repo.quitTeam(teamId, completion) + /// 分页查询群成员信息 + /// - Parameter members: 要查询的群成员列表 + /// - Parameter model : 群信息 + /// - Parameter maxSizeByPage: 单页最大查询数量 + /// - Parameter completion: 完成后的回调 + private func splitGroupMembers(_ members: [V2NIMTeamMember], + _ model: NETeamInfoModel, + _ maxSizeByPage: Int = 150, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", members.count:\(members.count)") + var remaind = [[V2NIMTeamMember]]() + remaind.append(contentsOf: members.chunk(maxSizeByPage)) + fetchUserInfos(&remaind, model, completion) } - open func getTopSessionInfo(_ session: NIMSession) -> NIMStickTopSessionInfo { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(session.sessionId)") - return repo.getTopSessionInfo(session) + /// 从云信服务器批量获取用户资料 + /// - Parameter remainUserIds: 用户集合 + /// - Parameter completion: 成功回调 + private func fetchUserInfos(_ remainUserIds: inout [[V2NIMTeamMember]], + _ model: NETeamInfoModel, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", remainUserIds.count:\(remainUserIds.count)") + guard let members = remainUserIds.first else { + completion(nil, model) + return + } + + let accids = members.map(\.accountId) + var temArray = remainUserIds + weak var weakSelf = self + + contactRepo.getFriendInfoList(accountIds: accids) { infos, v2Error in + if let err = v2Error { + completion(err as NSError, model) + } else { + if let users = infos { + for index in 0 ..< members.count { + let memberInfoModel = NETeamMemberInfoModel() + memberInfoModel.teamMember = members[index] + if users.count > index { + let user = users[index] + memberInfoModel.nimUser = user + } + model.users.append(memberInfoModel) + } + } + temArray.removeFirst() + weakSelf?.fetchUserInfos(&temArray, model, completion) + } + } } - open func removeStickTop(params: NIMStickTopSessionInfo, - _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(params.session.sessionId)") - repo.removeStickTop(params: params, completion) + /// 获取群成员 + /// - Parameter teamId: 群ID + /// - Parameter completion: 完成回调 + public func getAllTeamMembers(_ teamId: String, _ nextToken: String? = nil, _ memberList: inout [V2NIMTeamMember], _ queryType: V2NIMTeamMemberRoleQueryType, _ completion: @escaping ([V2NIMTeamMember]?, NSError?) -> Void) { + let option = V2NIMTeamMemberQueryOption() + option.limit = 100 + option.direction = .QUERY_DIRECTION_ASC + option.onlyChatBanned = false + option.roleQueryType = queryType + if let token = nextToken { + option.nextToken = token + } else { + option.nextToken = "" + } + var temMemberLists = memberList + teamRepo.getTeamMemberList(teamId, .TEAM_TYPE_NORMAL, option) { [weak self] result, error in + if let err = error { + completion(nil, err) + } else { + if let members = result?.memberList { + temMemberLists.append(contentsOf: members) + } + if let finished = result?.finished { + if finished == true { + completion(temMemberLists, nil) + } else { + self?.getAllTeamMembers(teamId, result?.nextToken, &temMemberLists, queryType, completion) + } + } + } + } } - @discardableResult - func getCurrentMember(_ userId: String, _ teamId: String?) -> NIMTeamMember? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:\(userId)") - if memberInTeam == nil, let tid = teamId { - memberInTeam = repo.getMemberInfo(userId, tid) + /// 获取群信息(只获取第一页群成员) + func getTeamInfoWithSomeMembers(_ teamId: String, _ completion: @escaping (NSError?, Bool?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") + weak var weakSelf = self + if let cid = V2NIMConversationIdUtil.teamConversationId(teamId) { + conversationRepo.getConversation(cid) { conversation, error in + if let err = error { + weakSelf?.isRequestSettingData = false + completion(err, false) + } else { + weakSelf?.conversation = conversation + weakSelf?.teamRepo.getTeamInfo(teamId) { team, error in + if let err = error { + completion(err, false) + } else { + let teamInfo = NETeamInfoModel() + teamInfo.team = team + weakSelf?.teamInfoModel = teamInfo + let option = V2NIMTeamMemberQueryOption() + option.nextToken = "" + option.limit = 20 + option.direction = .QUERY_DIRECTION_ASC + option.onlyChatBanned = false + option.roleQueryType = .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL + + weakSelf?.teamRepo.getTeamMemberList(teamId, .TEAM_TYPE_NORMAL, option) { result, error in + if let members = result?.memberList { + weakSelf?.getUserInfo(members) { error, models in + + if let err = error { + completion(err, result?.finished) + } else { + if let users = models { + weakSelf?.teamInfoModel?.users = users + } + completion(nil, result?.finished) + } + } + } + } + } + } + } + } + } + } + + /// 根据成员信息获取用户信息 + public func getUserInfo(_ members: [V2NIMTeamMember], _ completion: @escaping (NSError?, [NETeamMemberInfoModel]?) -> Void) { + var accids = [String]() + var memberModels = [NETeamMemberInfoModel]() + for member in members { + accids.append(member.accountId) + let model = NETeamMemberInfoModel() + model.teamMember = member + memberModels.append(model) + } + + ContactRepo.shared.getFriendInfoList(accountIds: accids) { users, v2Error in + + if v2Error != nil { + completion(nil, memberModels) + } else { + var dic = [String: NEUserWithFriend]() + if let us = users { + for user in us { + if let accid = user.user?.accountId { + dic[accid] = user + } + } + for model in memberModels { + if let accid = model.teamMember?.accountId { + if let user = dic[accid] { + model.nimUser = user + } + } + } + completion(nil, memberModels) + } + } + } + } + + /// 解散群聊 + /// - Parameter teamId : 群id + /// - Parameter completion: 完成回调 + public func dismissTeam(_ teamId: String, _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") + teamRepo.dismissTeam(teamId, completion) + } + + /// 退出群 + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + open func leaveTeam(_ teamId: String, _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") + teamRepo.leaveTeam(teamId, completion) + } + + /// 取消置顶 + /// - Parameter completion: 完成回调 + open func removeStickTop(_ completion: @escaping (NSError?) + -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function) + if let teamId = teamInfoModel?.team?.teamId { + teamRepo.removeStickTop(teamId) { error in + completion(error) + } + } + } + + /// 获取当前用户在群中的信息 + /// - Parameter userId: 用户id + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getCurrentMember(_ userId: String, _ teamId: String?, completion: @escaping (V2NIMTeamMember?, NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", userId:\(userId)") + if let tid = teamId { + teamRepo.getTeamMember(tid, userId) { [weak self] member, error in + if let currentMember = member { + self?.memberInTeam = currentMember + completion(currentMember, nil) + } else { + completion(member, error) + } + } } - return memberInTeam } + /// 判断是不是创建者 func isOwner() -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) - if let accid = teamInfoModel?.team?.owner { - if IMKitClient.instance.isMySelf(accid) { + if let accid = teamInfoModel?.team?.ownerAccountId { + if IMKitClient.instance.isMe(accid) { return true } } return false } + /// 是不是管理员 func isManager() -> Bool { - if let tid = teamInfoModel?.team?.teamId, let currentTeamMebmer = repo.getMemberInfo(IMKitClient.instance.imAccid(), tid) { - if currentTeamMebmer.type == .manager { + if let currentTeamMebmer = memberInTeam { + if currentTeamMebmer.memberRole == .TEAM_MEMBER_ROLE_MANAGER { return true } } return false } - private func sampleMemberId(arr: [TeamMemberInfoModel], owner: String) -> String? { - var index = arc4random_uniform(UInt32(arr.count)) - while arr[Int(index)].teamMember?.userId == owner { - if arr.count == 1 { - return owner + private func sampleMemberId(arr: [NETeamMemberInfoModel], owner: String) -> String? { + let sortArr = arr.sorted { model1, model2 in + (model1.teamMember?.joinTime ?? 0) < (model2.teamMember?.joinTime ?? 0) + } + + for model in sortArr { + if model.teamMember?.accountId != owner { + return model.teamMember?.accountId } - index = arc4random_uniform(UInt32(arr.count)) } - return arr[Int(index)].teamMember?.userId + return owner } + /// 移交群主 + /// - Parameter completion: 完成回调 func transferTeamOwner(_ completion: @escaping (Error?) -> Void) { if isOwner() == false { completion(NSError(domain: "imuikit", code: -1, userInfo: [NSLocalizedDescriptionKey: "not team manager"])) @@ -364,7 +654,7 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return } - var userId = NIMSDK.shared().loginManager.currentAccount() + var userId = IMKitClient.instance.account() if members.count == 1 { dismissTeam(teamId, completion) return @@ -372,103 +662,124 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { userId = sampleOwnerId } - if userId == NIMSDK.shared().loginManager.currentAccount() { + if userId == IMKitClient.instance.account() { dismissTeam(teamId, completion) return } - - NIMSDK.shared().teamManager.transferManager(withTeam: teamId, newOwnerId: userId, isLeave: true) { error in + teamRepo.transferTeam(teamId, userId, true) { error in completion(error) } } - open func updateInfoMode(_ mode: NIMTeamUpdateInfoMode, _ teamId: String, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", mode:\(mode.rawValue)") - repo.updateTeamInfoPrivilege(mode, teamId, completion) - } - - open func updateInviteMode(_ mode: NIMTeamInviteMode, _ teamId: String, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", mode:\(mode.rawValue)") - repo.updateInviteMode(mode, teamId, completion) - } - + /// 是不是普通群 open func isNormalTeam() -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function) - if let type = teamInfoModel?.team?.type, type == .normal { - return true - } - if teamInfoModel?.team?.clientCustomInfo?.contains(discussTeamKey) == true { + NEALog.infoLog(ModuleName + " " + className, desc: #function) + if teamInfoModel?.team?.serverExtension?.contains(discussTeamKey) == true { return true } return false } - open func searchMessages(_ session: NIMSession, option: NIMMessageSearchOption, - _ completion: @escaping (NSError?, [HistoryMessageModel]?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", session:\(session.sessionId)") - weak var weakSelf = self - repo.searchMessages(session, option: option) { error, messages in - if error == nil { - weakSelf?.searchResultInfos = messages - completion(nil, weakSelf?.searchResultInfos) - } else { - completion(error, nil) - } + /// 群信息更改回调 + /// - Parameter team: 群信息类 + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + if let tid = teamInfoModel?.team?.teamId, tid == team.teamId { + teamInfoModel?.team = team + getData() + delegate?.didNeedRefreshUI() } } - open func onTeamMemberRemoved(_ team: NIMTeam, withMembers memberIDs: [String]?) { - if let accids = memberIDs { - accids.forEach { accid in - if let users = teamInfoModel?.users { - for (i, m) in users.enumerated() { - if m.nimUser?.userId == accid { - teamInfoModel?.users.remove(at: i) - } - } - } - } - delegate?.didNeedRefreshUI() - } + /// 群成员离开回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) } - public func onTeamMemberChanged(_ team: NIMTeam) { - if let tid = teamInfoModel?.team?.teamId, tid != team.teamId { - return - } - teamInfoModel?.team = team - getCurrentMember(IMKitClient.instance.imAccid(), teamInfoModel?.team?.teamId) - getData() - delegate?.didNeedRefreshUI() + /// 群成员被踢回调 + /// - Parameter operatorAccountId: 操作者id + /// - Parameter teamMembers: 群成员 + public func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) } - open func onTeamUpdated(_ team: NIMTeam) { - if let teamId = teamInfoModel?.team?.teamId, teamId != team.teamId { - return + /// 群成员加入回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员更新回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + weak var weakSelf = self + for member in teamMembers { + if let currentTid = teamInfoModel?.team?.teamId, currentTid == member.teamId, member.accountId == IMKitClient.instance.account() { + weakSelf?.memberInTeam = member + break + } } - teamInfoModel?.team = team + + onTeamMemberChanged(teamMembers) } - open func inviterUsers(_ accids: [String], _ tid: String, _ completion: @escaping (NSError?, [NIMTeamMember]?) -> Void) { - repo.inviteUser(accids, tid, nil, nil) { error, members in - completion(error as NSError?, members) + /// 离开群回调 + /// - Parameter teamMembers: 群成员 + /// - Parameter team: 群信息 + public func onTeamLeft(_ team: V2NIMTeam, isKicked: Bool) {} + + /// 群成员变更统一处理 + /// - Parameter teamMembers: 群成员 + private func onTeamMemberChanged(_ members: [V2NIMTeamMember]) { + var isCurrentTeam = false + for member in members { + if let currentTid = teamInfoModel?.team?.teamId, currentTid == member.teamId { + isCurrentTeam = true + } + + if member.accountId == IMKitClient.instance.account(), let teamId = teamInfoModel?.team?.teamId { + getCurrentMember(IMKitClient.instance.account(), teamId) { [weak self] member, error in + NEALog.infoLog(self?.className() ?? "", desc: "current member : \(self?.memberInTeam?.yx_modelToJSONString() ?? "")") + } + } + } + + if isCurrentTeam == true { + guard let tid = teamInfoModel?.team?.teamId else { + return + } + weak var weakSelf = self + // NETeamMemberCache.shared.clearCache() + getTeamWithMembers(tid) { error in + if error == nil { + weakSelf?.delegate?.didNeedRefreshUI() + } + } } } - public func addRecentetSession() { - if let tid = teamInfoModel?.team?.teamId { - let currentSession = NIMSession(tid, type: .team) - repo.addRecentSession(currentSession) + /// 邀请用户 + /// - Parameter members: 用户id数组 + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + open func inviteUsers(_ members: [String], _ teamId: String, _ completion: @escaping (NSError?, [V2NIMTeamMember]?) -> Void) { + teamRepo.inviteUsers(teamId, members) { error, members in + completion(error, members) } } - public func getRecenterSession() -> NIMRecentSession? { - if let tid = teamInfoModel?.team?.teamId { - let currentSession = NIMSession(tid, type: .team) - return repo.getRecentSession(currentSession) + /// 会话变更 + /// - Parameter conversations: 会话 + public func onConversationChanged(_ conversations: [V2NIMConversation]) { + if let currentConversation = conversation { + for changeConversation in conversations { + if currentConversation.conversationId == changeConversation.conversationId { + conversation = changeConversation + getData() + delegate?.didNeedRefreshUI() + break + } + } } - return nil } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift b/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift index 04f5cdb1..1ad2d229 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift @@ -7,8 +7,9 @@ import Foundation @_exported import NEChatKit @_exported import NECommonKit @_exported import NECommonUIKit -@_exported import NECoreIMKit +@_exported import NECoreIM2Kit @_exported import NECoreKit + let coreLoader = CoreLoader() func localizable(_ key: String) -> String { coreLoader.localizable(key)