From 8e4184e7316efaf85d8ba729d50a0d480f590423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=AF=97=E6=96=87?= Date: Mon, 24 Jun 2024 16:23:16 +0800 Subject: [PATCH 1/5] v10.2.1 --- NEChatUIKit/NEChatUIKit.podspec | 2 +- .../Classes/Chat/Emoji/NEEmotionTool.swift | 20 ++++++++++++++++--- .../View/ChatView/NEBaseChatInputView.swift | 3 ++- NEContactUIKit/NEContactUIKit.podspec | 2 +- .../NEConversationUIKit.podspec | 2 +- NEMapKit/NEMapKit.podspec | 4 ++-- NETeamUIKit/NETeamUIKit.podspec | 2 +- Podfile | 18 ++++++++--------- app.xcodeproj/project.pbxproj | 8 ++++---- 9 files changed, 38 insertions(+), 23 deletions(-) diff --git a/NEChatUIKit/NEChatUIKit.podspec b/NEChatUIKit/NEChatUIKit.podspec index 17383d57..6fcdcabd 100644 --- a/NEChatUIKit/NEChatUIKit.podspec +++ b/NEChatUIKit/NEChatUIKit.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = 'NEChatUIKit' - s.version = '10.2.0' + s.version = '10.2.1' s.summary = 'Chat Module of IM.' # This description is used to generate tags and improve search results. diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift index f5879a7e..da31610a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift @@ -6,8 +6,10 @@ import UIKit open class NEEmotionTool: NSObject { - class func getAttWithStr(str: String, font: UIFont, - _ offset: CGPoint = CGPoint(x: 0, y: -3)) -> NSMutableAttributedString { + /// 找出所有表情的位置集合 + /// - Parameter str: 字符串 + /// - Returns: 表情位置 + class func getRegularArray(str: String) -> [NSTextCheckingResult]? { let regular = "\\[[^\\[|^\\]]+\\]" var reExpression: NSRegularExpression? @@ -21,6 +23,18 @@ open class NEEmotionTool: NSObject { range: NSRange(location: 0, length: str.utf16.count) ) + return regularArr + } + + /// 表情替换 + /// - Parameters: + /// - str: 原始文本 + /// - font: 字体 + /// - offset: 偏移量 + /// - Returns: 替换表情后的富文本 + class func getAttWithStr(str: String, font: UIFont, + _ offset: CGPoint = CGPoint(x: 0, y: -3)) -> NSMutableAttributedString { + let regularArr = getRegularArray(str: str) let emoticons = NIMInputEmoticonManager.shared .emoticonCatalog(catalogID: NIMKit_EmojiCatalog)?.emoticons let attStr = NSMutableAttributedString(string: str, attributes: [ @@ -32,7 +46,7 @@ open class NEEmotionTool: NSObject { for i in (0 ... regArr.count - 1).reversed() { let result = regArr[i] - for (idx, obj) in targetEmotions.enumerated() { + for obj in targetEmotions { let ocStr = str as NSString if ocStr.substring(with: result.range) == obj.tag { attStr.replaceCharacters( diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift index 9ba6cabd..53d2b15e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift @@ -305,7 +305,8 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, } // 处理粘贴,表情解析(存在表情则字符数量>=3) - if text.count >= 3 { + if text.count >= 3, + (NEEmotionTool.getRegularArray(str: text)?.count ?? 0) > 0 { let mutaString = NSMutableAttributedString(attributedString: textView.attributedText) let addString = NEEmotionTool.getAttWithStr(str: text, font: .systemFont(ofSize: 16)) mutaString.replaceCharacters(in: range, with: addString) diff --git a/NEContactUIKit/NEContactUIKit.podspec b/NEContactUIKit/NEContactUIKit.podspec index 8b13378e..b6450f25 100644 --- a/NEContactUIKit/NEContactUIKit.podspec +++ b/NEContactUIKit/NEContactUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEContactUIKit' - s.version = '10.2.0' + s.version = '10.2.1' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NEConversationUIKit/NEConversationUIKit.podspec b/NEConversationUIKit/NEConversationUIKit.podspec index 7273c925..c28da51c 100644 --- a/NEConversationUIKit/NEConversationUIKit.podspec +++ b/NEConversationUIKit/NEConversationUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEConversationUIKit' - s.version = '10.2.0' + s.version = '10.2.1' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NEMapKit/NEMapKit.podspec b/NEMapKit/NEMapKit.podspec index 8e60f63f..e3e9e35f 100644 --- a/NEMapKit/NEMapKit.podspec +++ b/NEMapKit/NEMapKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEMapKit' - s.version = '10.2.0' + s.version = '10.2.1' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. @@ -33,7 +33,7 @@ TODO: Add long description of the pod here. s.source_files = 'NEMapKit/Classes/**/*' # s.resource = 'NEMapKit/Assets/**/*' - s.dependency 'AMap2DMap' + s.dependency 'AMap3DMap' s.dependency 'AMapSearch' s.dependency 'AMapLocation' s.static_framework = true diff --git a/NETeamUIKit/NETeamUIKit.podspec b/NETeamUIKit/NETeamUIKit.podspec index 6eebc308..fd795100 100644 --- a/NETeamUIKit/NETeamUIKit.podspec +++ b/NETeamUIKit/NETeamUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NETeamUIKit' - s.version = '10.2.0' + s.version = '10.2.1' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/Podfile b/Podfile index 089ce32f..8b1f5a94 100644 --- a/Podfile +++ b/Podfile @@ -1,5 +1,5 @@ # Uncomment the next line to define a global platform for your project - platform :ios, '11.0' + platform :ios, '12.0' source 'https://github.com/CocoaPods/Specs.git' target 'app' do @@ -8,16 +8,16 @@ target 'app' do # 基础库 pod 'NIMSDK_LITE','10.2.6-beta' - pod 'NEChatKit', '10.2.0' + pod 'NEChatKit', '10.2.1' # UI 组件,依次为通讯录组件、会话列表组件、会话(聊天)组件、群相关设置组件 - pod 'NEChatUIKit', '10.2.0' - pod 'NEContactUIKit', '10.2.0' - pod 'NEConversationUIKit', '10.2.0' - pod 'NETeamUIKit', '10.2.0' + pod 'NEChatUIKit', '10.2.1' + pod 'NEContactUIKit', '10.2.1' + pod 'NEConversationUIKit', '10.2.1' + pod 'NETeamUIKit', '10.2.1' - # 扩展库-地理位置组件 - pod 'NEMapKit', '10.2.0' +# 扩展库-地理位置组件 + pod 'NEMapKit', '10.2.1' # 扩展库-呼叫组件 pod 'NERtcCallKit/NOS_Special', '2.4.0' @@ -45,7 +45,7 @@ post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' - config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' end end end diff --git a/app.xcodeproj/project.pbxproj b/app.xcodeproj/project.pbxproj index d2a283c7..ef1dd334 100644 --- a/app.xcodeproj/project.pbxproj +++ b/app.xcodeproj/project.pbxproj @@ -646,12 +646,12 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UIUserInterfaceStyle = Light; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 10.2.0; + MARKETING_VERSION = 10.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.netease.yunxin.app.im; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -695,12 +695,12 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UIUserInterfaceStyle = Light; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 10.2.0; + MARKETING_VERSION = 10.2.1; PRODUCT_BUNDLE_IDENTIFIER = com.netease.yunxin.app.im; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = inHouseYunxin; From 4a0898ee4afeaf8fa96a45bd9c815293ce3ae9dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=AF=97=E6=96=87?= Date: Tue, 25 Jun 2024 11:32:26 +0800 Subject: [PATCH 2/5] OC project with V10 --- .../project.pbxproj | 4 +- .../IMUIKitOCExample/Main/AppDelegate.m | 9 ++-- .../Main/InhertViewController.swift | 2 +- .../Main/NETabbarController.m | 4 +- .../Main/TestViewController.swift | 1 + IMUIKitOC/IMUIKitOCExample/Podfile | 53 +++++++++++-------- 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample.xcodeproj/project.pbxproj b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample.xcodeproj/project.pbxproj index 0a9d44c6..b4a7d1e3 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample.xcodeproj/project.pbxproj +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample.xcodeproj/project.pbxproj @@ -601,7 +601,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -637,7 +637,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 11.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m index 83c37813..d7ab8619 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m @@ -11,7 +11,8 @@ #import #import #import -#import +#import +#import #import // #import #import "CustomRouterViewController.h" @@ -42,7 +43,8 @@ - (BOOL)application:(UIApplication *)application - (void)setupInit { // 初始化NIMSDK NIMSDKOption *option = [NIMSDKOption optionWithAppKey:AppKey]; - [[IMKitClient instance] setupCoreKitIM:option]; + option.v2 = YES; + [[IMKitClient instance] setupIM:option]; // 统一登录组件 YXConfig *config = [[YXConfig alloc] init]; @@ -86,7 +88,7 @@ - (void)loginWithUI { - (void)setupXKit:(YXUserInfo *)user { // 登录云信IM if (user.imToken && user.imAccid) { - [[IMKitClient instance] loginIM:user.imAccid :user.imToken :^(NSError * _Nullable error) { + [[IMKitClient instance] login:user.imAccid :user.imToken : nil :^(NSError * _Nullable error) { if (!error) { [ChatRouter setupInit]; [self setupTabbar]; @@ -124,6 +126,7 @@ - (void)registerRouter { [ChatRouter register]; [ConversationRouter register]; [ContactRouter register]; + [TeamRouter register]; // 聊天页面自定义面板示例 /* diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/InhertViewController.swift b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/InhertViewController.swift index d28217c4..09821cf0 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/InhertViewController.swift +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/InhertViewController.swift @@ -8,7 +8,7 @@ import UIKit public class InhertViewController: ConversationController { override public func viewDidLoad() { super.viewDidLoad() - let test = TestViewController(sessionId: "") + let test = TestViewController(conversationId: "") // Do any additional setup after loading the view. } diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/NETabbarController.m b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/NETabbarController.m index b87216eb..3bbca1d3 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/NETabbarController.m +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/NETabbarController.m @@ -11,7 +11,7 @@ #import #import #import -#import +#import #import // #import @@ -44,7 +44,7 @@ - (void)setUpControllers { [[NENavigationController alloc] initWithRootViewController:sessionCtrl]; // 通讯录 - ContactsViewController *contactCtrl = [[ContactsViewController alloc] init]; + ContactViewController *contactCtrl = [[ContactViewController alloc] init]; contactCtrl.tabBarItem = [[UITabBarItem alloc] initWithTitle:NSLocalizedString(@"contact", @"") image:[UIImage imageNamed:@"contact"] diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/TestViewController.swift b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/TestViewController.swift index 9d0b33f3..f00d5aa1 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/TestViewController.swift +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/TestViewController.swift @@ -4,6 +4,7 @@ import NEChatUIKit import UIKit + public class TestViewController: P2PChatViewController { override public func viewDidLoad() { super.viewDidLoad() diff --git a/IMUIKitOC/IMUIKitOCExample/Podfile b/IMUIKitOC/IMUIKitOCExample/Podfile index ae9e436f..18a20bbd 100644 --- a/IMUIKitOC/IMUIKitOCExample/Podfile +++ b/IMUIKitOC/IMUIKitOCExample/Podfile @@ -1,6 +1,6 @@ # Uncomment the next line to define a global platform for your project -platform :ios, '11.0' +platform :ios, '12.0' source 'https://github.com/CocoaPods/Specs.git' target 'IMUIKitOCExample' do @@ -8,28 +8,35 @@ target 'IMUIKitOCExample' do use_frameworks! #登录组件 pod 'YXLogin', '1.1.0' - - #可选UI库 - pod 'NEContactUIKit', '9.7.0' - pod 'NEConversationUIKit', '9.7.0' - pod 'NEChatUIKit', '9.7.0' - pod 'NETeamUIKit', '9.7.0' - pod 'NEMapKit', '9.7.0' - - #基础kit库 - pod 'NECoreKit', '9.6.6' - pod 'NECoreIMKit', '9.6.7' - pod 'NECommonKit', '9.6.6' - pod 'NECommonUIKit', '9.6.6' - pod 'NEChatKit', '9.7.0' - + + # 基础库 + pod 'NIMSDK_LITE','10.2.6-beta' + pod 'NEChatKit', '10.2.1' + + # UI 组件,依次为通讯录组件、会话列表组件、会话(聊天)组件、群相关设置组件 + pod 'NEChatUIKit', '10.2.1' + pod 'NEContactUIKit', '10.2.1' + pod 'NEConversationUIKit', '10.2.1' + pod 'NETeamUIKit', '10.2.1' + + # 扩展库-地理位置组件 + pod 'NEMapKit', '10.2.1' + # 如果需要查看UI部分源码请注释掉以上在线依赖,打开下面的本地依赖 -# pod 'NEQChatUIKit', :path => '../NEQChatUIKit/NEQChatUIKit.podspec' -# pod 'NEContactUIKit', :path => '../NEContactUIKit/NEContactUIKit.podspec' -# pod 'NEConversationUIKit', :path => '../NEConversationUIKit/NEConversationUIKit.podspec' -# pod 'NETeamUIKit', :path => '../NETeamUIKit/NETeamUIKit.podspec' -# pod 'NEChatUIKit', :path => '../NEChatUIKit/NEChatUIKit.podspec' - + # pod 'NEQChatUIKit', :path => '../NEQChatUIKit/NEQChatUIKit.podspec' + # pod 'NEContactUIKit', :path => '../NEContactUIKit/NEContactUIKit.podspec' + # pod 'NEConversationUIKit', :path => '../NEConversationUIKit/NEConversationUIKit.podspec' + # pod 'NETeamUIKit', :path => '../NETeamUIKit/NETeamUIKit.podspec' + # pod 'NEChatUIKit', :path => '../NEChatUIKit/NEChatUIKit.podspec' + end - +# ⚠️如果pod依赖报错,可打开以下注释 +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |config| + config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' + end + end +end From ca3427c036ae7c43f90cc75dc912df93e0b1f799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=AF=97=E6=96=87?= Date: Tue, 16 Jul 2024 17:33:22 +0800 Subject: [PATCH 3/5] v10.3.0 --- IMUIKitOC/IMUIKitOCExample/Podfile | 8 +- NEAISearchKit/NEAISearchKit.podspec | 44 + .../NEAISearchKit.xcassets/Contents.json | 6 + .../ai_expand.imageset/Contents.json | 22 + .../ai_expand.imageset/ai_expand@2x.png | Bin 0 -> 474 bytes .../ai_expand.imageset/ai_expand@3x.png | Bin 0 -> 585 bytes .../op_ai_word.imageset/Contents.json | 0 .../op_ai_word.imageset/op_ai_word@2x.png | Bin .../op_ai_word.imageset/op_ai_word@3x.png | Bin .../Assets/en.lproj/Localizable.strings | 18 + .../NEAISearchKit/Assets/ne_loading_data.json | 1 + .../Assets/zh-Hans.lproj/Localizable.strings | 18 + .../AIWordSearch/NEAIWordSearchConstant.swift | 26 + .../AIWordSearch/NEAIWordSearchModel.swift | 16 + .../AIWordSearch/NEAIWordSearchTextCell.swift | 70 ++ .../NEAIWordSearchViewController.swift | 535 +++++++++++++ .../NEAIWordSearchViewModel.swift | 181 +++++ .../NEAISearchKit/Classes/NEAISearchLoader.h | 12 + .../NEAISearchKit/Classes/NEAISearchLoader.m | 20 + .../Classes/NEAISearchManager.swift | 58 ++ NEChatUIKit/NEChatUIKit.podspec | 4 +- .../chat_translation.imageset/Contents.json | 22 + .../Group 1817-2.png | Bin 0 -> 3995 bytes .../chat_translation.imageset/Group 1817.png | Bin 0 -> 2696 bytes .../language_selected.imageset/Contents.json | 22 + .../language_selected@2x.png | Bin 0 -> 483 bytes .../language_selected@3x.png | Bin 0 -> 638 bytes .../Chat/use_arrow.imageset/Contents.json | 22 + .../Chat/use_arrow.imageset/use_arrow@2x.png | Bin 0 -> 343 bytes .../Chat/use_arrow.imageset/use_arrow@3x.png | Bin 0 -> 447 bytes .../op_delete.imageset/Contents.json | 4 +- .../op_delete.imageset/Frame 45@2x.png | Bin 647 -> 0 bytes .../op_delete.imageset/Frame 45@3x.png | Bin 886 -> 0 bytes .../op_delete.imageset/op_delete@2x.png | Bin 0 -> 779 bytes .../op_delete.imageset/op_delete@3x.png | Bin 0 -> 1268 bytes .../Emoji/emoji_ios_en.plist | 4 +- .../Assets/en.lproj/Localizable.strings | 15 +- .../NEChatUIKit/Assets/ne_loading_data.json | 1 + .../Assets/zh-Hans.lproj/Localizable.strings | 17 +- .../Base/BaseView/NEChatBaseCell.swift | 4 +- .../NEChatBaseViewController.swift | 43 +- .../NEChatUIKit/Classes/Base/NEChatLoader.h | 12 + .../NEChatUIKit/Classes/Base/NEChatLoader.m | 35 + .../Classes/Base/NEChatLoaderService.swift | 21 + .../Classes/Base/NEChatService.swift | 52 ++ .../Chat/Controller/ChatViewController.swift | 757 +++++++++++++----- .../CustomPresentationController.swift | 78 ++ .../MultiForwardViewController.swift | 11 +- .../NEBaseCollectionMessageController.swift | 32 +- .../NEBaseForwardAlertViewController.swift | 4 +- .../NEBasePinMessageViewController.swift | 29 +- .../Controller/NEBaseReadViewController.swift | 202 ++--- .../NEBaseSelectLanguageViewController.swift | 86 ++ .../NEBaseSelectUserViewController.swift | 75 +- .../NEBaseUserSettingViewController.swift | 53 +- .../Classes/Chat/Emoji/EmojiPageView.swift | 2 +- .../Emoji/InputEmoticonContainerView.swift | 8 +- .../Chat/Emoji/InputEmoticonTabView.swift | 2 +- .../Chat/Emoji/NEEmotionAttachment.swift | 6 +- .../Classes/Chat/Emoji/NEEmotionTool.swift | 26 +- .../Chat/Emoji/NIMInputEmoticonManager.swift | 27 +- .../Chat/Helper/ChatMessageHelper.swift | 191 ++++- .../Classes/Chat/Helper/ChatTeamCache.swift | 129 --- .../Classes/Chat/Helper/ChatUserCache.swift | 56 -- .../Chat/Helper/NEP2PChatUserCache.swift | 104 +++ .../Chat/Helper/NETeamUserManager.swift | 519 ++++++++++++ .../Helper/NotificationMessageUtils.swift | 8 +- .../Chat/Model/CollectionMessageModel.swift | 1 + .../Chat/Model/MessageCallRecordModel.swift | 6 +- .../Chat/Model/MessageContentModel.swift | 16 +- .../Chat/Model/MessageCustomModel.swift | 2 +- .../Chat/Model/MessageImageModel.swift | 2 +- .../Classes/Chat/Model/MessageModel.swift | 80 -- .../Chat/Model/MessageRichTextModel.swift | 28 +- .../Classes/Chat/Model/MessageTextModel.swift | 114 +-- .../Classes/Chat/Model/MessageTipsModel.swift | 9 +- .../Chat/Model/MessageVideoModel.swift | 14 +- .../Classes/Chat/Model/NEMoreItemModel.swift | 1 + .../Chat/Model/NEPinMessageModel.swift | 6 +- ...tem.swift => OperationItemExtension.swift} | 46 +- .../NEBaseCollectionMessageCell.swift | 2 +- .../NEBaseCollectionMessageVideoCell.swift | 2 +- .../View/Cell/NEBaseChatMessageCell.swift | 52 +- .../View/Cell/NEBaseChatTeamMemberCell.swift | 4 +- .../Chat/View/Cell/NEBaseLanguageCell.swift | 53 ++ .../View/Cell/NEBaseUserSettingCell.swift | 12 +- .../Chat/View/Cell/OperationCell.swift | 34 +- .../Cell/PinCell/NEBasePinMessageCell.swift | 2 +- .../PinCell/NEBasePinMessageVideoCell.swift | 2 +- .../ChatView/ChatActivityIndicatorView.swift | 1 - .../View/ChatView/MessageOperationView.swift | 2 +- .../View/ChatView/NEAITranslateView.swift | 390 +++++++++ .../View/ChatView/NEBaseChatInputView.swift | 15 +- .../Chat/View/ChatView/NEChatTextView.swift | 19 + .../Chat/ViewModel/ChatViewModel.swift | 734 +++++++++++++---- .../CollectionMessageViewModel.swift | 65 +- .../ViewModel/MultiForwardViewModel.swift | 2 +- .../Chat/ViewModel/P2PChatViewModel.swift | 14 +- .../Chat/ViewModel/PinMessageViewModel.swift | 69 +- .../Chat/ViewModel/ReadViewModel.swift | 18 +- .../ViewModel/SelectLanguageViewModel.swift | 40 + .../Chat/ViewModel/TeamChatViewModel.swift | 111 +-- .../Chat/ViewModel/TeamMemberSelectVM.swift | 134 +--- .../Chat/ViewModel/UserSettingViewModel.swift | 64 +- .../Classes/ChatConfig/ChatUIConfig.swift | 3 +- .../Classes/ChatRouter/NEBaseChatRouter.swift | 13 - .../Common/ChatCellConstantValue.swift | 3 + .../Classes/Common/NEChatUIKitClient.swift | 15 + .../Common/NETranslateLanguageManager.swift | 40 + .../Classes/Common/NSBundleExtension.swift | 2 +- .../Classes/Extension/AlertVCExtention.swift | 5 +- .../FunUI/Cell/FunChatMessageBaseCell.swift | 11 +- .../FunUI/Cell/FunChatMessageFileCell.swift | 4 +- .../Cell/FunChatMessageMultiForwardCell.swift | 2 +- .../FunUI/Cell/FunChatMessageReplyCell.swift | 8 +- .../Cell/FunChatMessageRichTextCell.swift | 135 +++- .../FunUI/Cell/FunChatMessageTextCell.swift | 154 +++- .../FunUI/Cell/FunChatMessageVideoCell.swift | 18 +- .../Classes/FunUI/Cell/FunLanguageCell.swift | 49 ++ .../Controller/FunChatViewController.swift | 65 +- .../FunCollectionMessageController.swift | 2 +- .../FunMultiForwardViewController.swift | 7 +- .../Controller/FunP2PChatViewController.swift | 48 ++ .../FunSelectLanguageViewController.swift | 79 ++ .../FunSelectUserViewController.swift | 4 +- .../FunTeamChatViewController.swift | 9 +- .../FunUserSettingViewController.swift | 2 +- .../Classes/FunUI/View/FunChatInputView.swift | 2 +- .../NormalUI/Cell/ChatMessageFileCell.swift | 4 +- .../Cell/ChatMessageMultiForwardCell.swift | 2 +- .../NormalUI/Cell/ChatMessageReplyCell.swift | 4 +- .../Cell/ChatMessageRichTextCell.swift | 120 ++- .../NormalUI/Cell/ChatMessageTextCell.swift | 153 +++- .../NormalUI/Cell/ChatMessageVideoCell.swift | 18 +- .../Classes/NormalUI/Cell/LanguageCell.swift | 38 + .../Controller/NormalChatViewController.swift | 63 +- .../NormalMultiForwardViewController.swift | 8 +- .../Controller/P2PChatViewController.swift | 48 ++ .../Controller/ReadViewController.swift | 8 +- .../SelectLanguageViewController.swift | 80 ++ .../Controller/SelectUserViewController.swift | 4 +- .../Controller/TeamChatViewController.swift | 9 +- .../UserSettingViewController.swift | 7 +- .../Protocol/ChatInputViewDelegate.swift | 1 + NEContactUIKit/NEContactUIKit.podspec | 4 +- .../funAIUser.imageset/Contents.json | 22 + .../funAIUser.imageset/ai_robot@2x.png | Bin 0 -> 1071 bytes .../funAIUser.imageset/ai_robot@3x.png | Bin 0 -> 1549 bytes .../search_icon.imageset/Contents.json | 22 + .../search_icon.imageset/search_icon@2x.png | Bin 0 -> 791 bytes .../search_icon.imageset/search_icon@3x.png | Bin 0 -> 1131 bytes .../aiUser.imageset/Contents.json | 22 + .../aiUser.imageset/ai_robot@2x.png | Bin 0 -> 1859 bytes .../aiUser.imageset/ai_robot@3x 1.png | Bin 0 -> 2786 bytes .../Assets/en.lproj/Localizable.strings | 6 +- .../Assets/zh-Hans.lproj/Localizable.strings | 6 +- .../Classes/AIRobot/AIUserViewModel.swift | 27 + .../AIRobot/NEBaseAIUserController.swift | 226 ++++++ .../AIRobot/NEBaseAIUserListCell.swift | 79 ++ .../{NEKitContactUI.h => NEContactLoader.h} | 9 + .../Classes/Base/NEContactLoader.m | 31 + .../Classes/Base/NEContactLoaderService.swift | 21 + .../Classes/Base/NEContactService.swift | 31 + .../NEBaseBlackListViewController.swift | 73 +- .../ViewModel/BlackListViewModel.swift | 111 ++- .../ContactConfig/ContactUIConfig.swift | 4 +- .../FunUI/Cell/FunAIUserListCell.swift | 43 + .../FunUI/Cell/FunContactSelectedCell.swift | 6 +- .../FunUI/Cell/FunContactTableViewCell.swift | 6 +- .../Cell/FunFusionContactSelectedCell.swift | 53 ++ .../Cell/FunFusionContactUnCheckCell.swift | 26 + .../Classes/FunUI/FunContactRouter.swift | 20 + .../Classes/FunUI/FunContactUIColor.swift | 5 + .../ViewController/FunAIUserController.swift | 75 ++ ...ft => FunContactAliasViewController.swift} | 5 +- .../FunContactSelectedPageController.swift | 78 ++ .../FunContactUserViewController.swift | 4 +- .../FunContactViewController.swift | 11 +- .../FunFindFriendViewController.swift | 9 +- .../FunFusionContactSelectedController.swift | 28 + .../FunMultiSelectViewController.swift | 3 +- .../FunValidationMessageViewController.swift | 10 +- .../Model/NEFusionContactCellModel.swift | 44 + .../Cell/NEBaseSelectedListCell.swift | 4 +- .../NEBaseMultiSelectViewController.swift | 39 +- .../NEBaseMultiSelectedViewController.swift | 10 + .../NormalUI/Cell/AIUserListCell.swift | 8 + .../Cell/FusionContactSelectedCell.swift | 53 ++ .../Cell/FusionContactUnCheckCell.swift | 26 + .../NormalUI/NromalContactRouter.swift | 21 + .../NormalUI/NromalContactUIColor.swift | 6 + .../ViewController/AIUserController.swift | 34 + .../BlackListViewController.swift | 5 +- ...swift => ContactAliasViewController.swift} | 13 +- .../ContactSelectedPageController.swift | 76 ++ .../ContactSelectedViewController.swift | 6 +- .../ContactUserViewController.swift | 4 +- .../ContactViewController.swift | 11 +- .../FindFriendViewController.swift | 8 +- .../FusionContactSelectedController.swift | 23 + .../ValidationMessageViewController.swift | 14 +- .../NEBaseTeamListViewController.swift | 67 +- ...EBaseValidationMessageViewController.swift | 39 +- .../Classes/ViewModel/ContactViewModel.swift | 44 +- .../FusionContactSelectedViewModel.swift | 77 ++ .../Cell/NEBaseContactTableViewCell.swift | 6 +- .../NEBaseFusionContactSelectedCell.swift | 102 +++ ...=> NEBaseContactAliasViewController.swift} | 27 +- .../NEBaseContactSelectedPageController.swift | 288 +++++++ .../NEBaseContactSelectedViewController.swift | 23 +- .../NEBaseContactUserViewController.swift | 148 ++-- .../Views/NEBaseContactViewController.swift | 35 +- .../NEBaseFindFriendViewController.swift | 12 +- ...EBaseFusionContactSelectedController.swift | 212 +++++ .../Views/NEContactBaseViewController.swift | 78 +- .../NEConversationUIKit.podspec | 4 +- .../Assets/en.lproj/Localizable.strings | 3 + .../Assets/zh-Hans.lproj/Localizable.strings | 3 + .../Classes/Common/NEConversationLoader.h | 12 + .../Classes/Common/NEConversationLoader.m | 31 + .../Common/NEConversationLoaderService.swift | 21 + .../Common/NEConversationService.swift | 36 + .../Cell/NEBaseConversationListCell.swift | 9 +- .../Cell/NEBaseStickTopCell.swift | 75 ++ .../NEBaseConversationController.swift | 196 ++++- .../NEBaseConversationSearchController.swift | 12 +- .../NEConversationBaseViewController.swift | 50 +- .../Model/NEConversationListModel.swift | 33 + .../ConversationSearchViewModel.swift | 34 +- .../ViewModel/ConversationViewModel.swift | 56 +- .../ConversationUIConfig.swift | 4 +- .../FunUI/Cell/FunConversationListCell.swift | 9 +- .../Cell/FunConversationSearchCell.swift | 2 +- .../Classes/FunUI/Cell/FunStickTopCell.swift | 26 + .../FunConversationController.swift | 34 + .../FunConversationSearchController.swift | 9 +- .../Classes/Manager/NEAtMessageManager.swift | 70 +- .../NormalUI/Cell/ConversationListCell.swift | 6 +- .../Classes/NormalUI/Cell/StickTopCell.swift | 24 + .../Controller/ConversationController.swift | 47 ++ .../ConversationSearchController.swift | 17 +- NEMapKit/NEMapKit.podspec | 4 +- .../Controller/NELocationViewController.swift | 9 + NETeamUIKit/NETeamUIKit.podspec | 7 +- .../Classes/Common/NEBaseTeamRouter.swift | 26 +- .../NETeamUIKit/Classes/Common/NETeamLoader.h | 12 + .../NETeamUIKit/Classes/Common/NETeamLoader.m | 31 + .../Classes/Common/NETeamLoaderService.swift | 21 + .../Classes/Common/NETeamService.swift | 30 + .../FunTeamSettingViewController.swift | 2 +- .../TeamHistoryMessageController.swift | 5 +- .../Controller/TeamInfoViewController.swift | 5 +- .../Controller/TeamManagerController.swift | 4 +- .../TeamSettingViewController.swift | 7 +- .../Setting/Common/NETeamMemberCache.swift | 21 + .../NEBaseTeamAvatarViewController.swift | 2 +- .../NEBaseTeamHistoryMessageController.swift | 15 +- .../NEBaseTeamInfoViewController.swift | 11 + .../Setting/NEBaseTeamManagerController.swift | 8 + .../NEBaseTeamManagerListController.swift | 9 +- .../NEBaseTeamMemberSelectController.swift | 8 +- .../Setting/NEBaseTeamMembersController.swift | 23 +- .../NEBaseTeamSettingViewController.swift | 15 +- .../View/NEBaseHistoryMessageCell.swift | 7 +- .../TeamHistoryMessageViewModel.swift | 41 +- .../ViewModel/TeamManagerListViewModel.swift | 53 +- .../ViewModel/TeamManagerViewModel.swift | 2 +- .../ViewModel/TeamMemberSelectViewModel.swift | 44 +- .../ViewModel/TeamMembersViewModel.swift | 23 +- .../ViewModel/TeamSettingViewModel.swift | 46 +- Podfile | 26 +- app.xcodeproj/project.pbxproj | 4 +- app/Main/AppDelegate.swift | 148 ++-- app/Main/NETabBarController.swift | 4 +- .../Controller/ConfigTestViewController.swift | 39 +- .../IMSDKConfigViewController.swift | 331 ++++++++ .../InputPersonInfoController.swift | 4 +- .../IntroduceBrandViewController.swift | 9 +- app/Mine/Controller/MeViewController.swift | 16 +- .../MessageRemindViewController.swift | 8 + .../MineSettingViewController.swift | 385 ++++----- .../Controller/NEAboutWebViewController.swift | 1 + .../Controller/NELoginViewController.swift | 226 ++++++ .../Controller/NENodeViewController.swift | 7 + .../Controller/PersonInfoViewController.swift | 97 ++- .../StyleSelectionViewController.swift | 1 + app/Mine/Model/CustomSettingCellModel.swift | 20 + app/Mine/View/BirthdayDatePickerView.swift | 2 +- app/Mine/View/MineTableViewCell.swift | 2 +- .../View/Theme/CustomSettingInputCell.swift | 103 +++ .../Theme/CustomSettingTextviewCell.swift | 104 +++ app/Mine/ViewModel/MineSettingViewModel.swift | 11 + app/Mine/ViewModel/PersonInfoViewModel.swift | 2 +- 293 files changed, 10442 insertions(+), 2192 deletions(-) create mode 100644 NEAISearchKit/NEAISearchKit.podspec create mode 100644 NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/Contents.json create mode 100644 NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/ai_expand.imageset/Contents.json create mode 100644 NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/ai_expand.imageset/ai_expand@2x.png create mode 100644 NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/ai_expand.imageset/ai_expand@3x.png rename {NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation => NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets}/op_ai_word.imageset/Contents.json (100%) rename {NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation => NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets}/op_ai_word.imageset/op_ai_word@2x.png (100%) rename {NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation => NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets}/op_ai_word.imageset/op_ai_word@3x.png (100%) create mode 100644 NEAISearchKit/NEAISearchKit/Assets/en.lproj/Localizable.strings create mode 100644 NEAISearchKit/NEAISearchKit/Assets/ne_loading_data.json create mode 100644 NEAISearchKit/NEAISearchKit/Assets/zh-Hans.lproj/Localizable.strings create mode 100644 NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchConstant.swift create mode 100644 NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchModel.swift create mode 100644 NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchTextCell.swift create mode 100644 NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchViewController.swift create mode 100644 NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchViewModel.swift create mode 100644 NEAISearchKit/NEAISearchKit/Classes/NEAISearchLoader.h create mode 100644 NEAISearchKit/NEAISearchKit/Classes/NEAISearchLoader.m create mode 100644 NEAISearchKit/NEAISearchKit/Classes/NEAISearchManager.swift create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/chat_translation.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/chat_translation.imageset/Group 1817-2.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/chat_translation.imageset/Group 1817.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/language_selected.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/language_selected.imageset/language_selected@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/language_selected.imageset/language_selected@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/use_arrow.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/use_arrow.imageset/use_arrow@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/use_arrow.imageset/use_arrow@3x.png delete mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/Frame 45@2x.png delete mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/Frame 45@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/op_delete@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/op_delete@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/ne_loading_data.json create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoader.h create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoader.m create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoaderService.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Base/NEChatService.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/CustomPresentationController.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectLanguageViewController.swift delete mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatTeamCache.swift delete mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatUserCache.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NEP2PChatUserCache.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NETeamUserManager.swift delete mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift rename NEChatUIKit/NEChatUIKit/Classes/Chat/Model/{OperationItem.swift => OperationItemExtension.swift} (68%) create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseLanguageCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEAITranslateView.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatTextView.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/SelectLanguageViewModel.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Common/NETranslateLanguageManager.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunLanguageCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectLanguageViewController.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/LanguageCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectLanguageViewController.swift create mode 100644 NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funAIUser.imageset/Contents.json create mode 100644 NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funAIUser.imageset/ai_robot@2x.png create mode 100644 NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funAIUser.imageset/ai_robot@3x.png create mode 100644 NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/search_icon.imageset/Contents.json create mode 100644 NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/search_icon.imageset/search_icon@2x.png create mode 100644 NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/search_icon.imageset/search_icon@3x.png create mode 100644 NEContactUIKit/NEContactUIKit/Assets/NormalContactUIKit.xcassets/aiUser.imageset/Contents.json create mode 100644 NEContactUIKit/NEContactUIKit/Assets/NormalContactUIKit.xcassets/aiUser.imageset/ai_robot@2x.png create mode 100644 NEContactUIKit/NEContactUIKit/Assets/NormalContactUIKit.xcassets/aiUser.imageset/ai_robot@3x 1.png create mode 100644 NEContactUIKit/NEContactUIKit/Classes/AIRobot/AIUserViewModel.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/AIRobot/NEBaseAIUserController.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/AIRobot/NEBaseAIUserListCell.swift rename NEContactUIKit/NEContactUIKit/Classes/Base/{NEKitContactUI.h => NEContactLoader.h} (81%) create mode 100644 NEContactUIKit/NEContactUIKit/Classes/Base/NEContactLoader.m create mode 100644 NEContactUIKit/NEContactUIKit/Classes/Base/NEContactLoaderService.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/Base/NEContactService.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunAIUserListCell.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunFusionContactSelectedCell.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunFusionContactUnCheckCell.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunAIUserController.swift rename NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/{FunContactRemakNameViewController.swift => FunContactAliasViewController.swift} (79%) create mode 100644 NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactSelectedPageController.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFusionContactSelectedController.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/Model/NEFusionContactCellModel.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/AIUserListCell.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/FusionContactSelectedCell.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/FusionContactUnCheckCell.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/AIUserController.swift rename NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/{ContactRemakNameViewController.swift => ContactAliasViewController.swift} (62%) create mode 100644 NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactSelectedPageController.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FusionContactSelectedController.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/ViewModel/FusionContactSelectedViewModel.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseFusionContactSelectedCell.swift rename NEContactUIKit/NEContactUIKit/Classes/Views/{NEBaseContactRemakNameViewController.swift => NEBaseContactAliasViewController.swift} (82%) create mode 100644 NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactSelectedPageController.swift create mode 100644 NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFusionContactSelectedController.swift create mode 100644 NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoader.h create mode 100644 NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoader.m create mode 100644 NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoaderService.swift create mode 100644 NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationService.swift create mode 100644 NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseStickTopCell.swift create mode 100644 NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Model/NEConversationListModel.swift create mode 100644 NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunStickTopCell.swift create mode 100644 NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/StickTopCell.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoader.h create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoader.m create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoaderService.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Common/NETeamService.swift create mode 100644 app/Mine/Controller/IMSDKConfigViewController.swift create mode 100644 app/Mine/Controller/NELoginViewController.swift create mode 100644 app/Mine/Model/CustomSettingCellModel.swift create mode 100644 app/Mine/View/Theme/CustomSettingInputCell.swift create mode 100644 app/Mine/View/Theme/CustomSettingTextviewCell.swift diff --git a/IMUIKitOC/IMUIKitOCExample/Podfile b/IMUIKitOC/IMUIKitOCExample/Podfile index 18a20bbd..bcfea52c 100644 --- a/IMUIKitOC/IMUIKitOCExample/Podfile +++ b/IMUIKitOC/IMUIKitOCExample/Podfile @@ -14,20 +14,20 @@ target 'IMUIKitOCExample' do pod 'NEChatKit', '10.2.1' # UI 组件,依次为通讯录组件、会话列表组件、会话(聊天)组件、群相关设置组件 - pod 'NEChatUIKit', '10.2.1' +# pod 'NEChatUIKit', '10.2.1' pod 'NEContactUIKit', '10.2.1' pod 'NEConversationUIKit', '10.2.1' pod 'NETeamUIKit', '10.2.1' # 扩展库-地理位置组件 - pod 'NEMapKit', '10.2.1' +# pod 'NEMapKit', '10.2.1' # 如果需要查看UI部分源码请注释掉以上在线依赖,打开下面的本地依赖 - # pod 'NEQChatUIKit', :path => '../NEQChatUIKit/NEQChatUIKit.podspec' +# pod 'NEQChatUIKit', :path => '../NEQChatUIKit/NEQChatUIKit.podspec' # pod 'NEContactUIKit', :path => '../NEContactUIKit/NEContactUIKit.podspec' # pod 'NEConversationUIKit', :path => '../NEConversationUIKit/NEConversationUIKit.podspec' # pod 'NETeamUIKit', :path => '../NETeamUIKit/NETeamUIKit.podspec' - # pod 'NEChatUIKit', :path => '../NEChatUIKit/NEChatUIKit.podspec' + pod 'NEChatUIKit', :path => '../../NEChatUIKit/NEChatUIKit.podspec' end diff --git a/NEAISearchKit/NEAISearchKit.podspec b/NEAISearchKit/NEAISearchKit.podspec new file mode 100644 index 00000000..3b1c3878 --- /dev/null +++ b/NEAISearchKit/NEAISearchKit.podspec @@ -0,0 +1,44 @@ +# +# Be sure to run `pod lib lint NEAISearchKit.podspec' to ensure this is a +# valid spec before submitting. +# +# Any lines starting with a # are optional, but their use is encouraged +# To learn more about a Podspec see https://guides.cocoapods.org/syntax/podspec.html +# + +Pod::Spec.new do |s| + s.name = 'NEAISearchKit' + s.version = '1.0.0' + s.summary = 'Netease XKit' + +# This description is used to generate tags and improve search results. +# * Think: What does it do? Why did you write it? What is the focus? +# * Try to keep it short, snappy and to the point. +# * Write the description between the DESC delimiters below. +# * Finally, don't worry about the indent, CocoaPods strips it! + + s.description = <<-DESC +TODO: Add long description of the pod here. + DESC + + s.homepage = 'http://netease.im' + s.license = { :'type' => 'Copyright', :'text' => ' Copyright 2022 Netease '} + s.author = 'yunxin engineering department' + s.source = { :git => 'https://github.com/netease/NEKitGroupUI.git', :tag => s.version.to_s } + + s.pod_target_xcconfig = { + 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64', + 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' + } + + s.ios.deployment_target = '12.0' + s.swift_version = '5.0' + + s.source_files = 'NEAISearchKit/Classes/**/*' + s.resource = 'NEAISearchKit/Assets/**/*' + + s.dependency 'NEChatKit' + s.dependency 'NECommonUIKit' + s.dependency 'lottie-ios','4.4.0' + +end diff --git a/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/Contents.json b/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/ai_expand.imageset/Contents.json b/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/ai_expand.imageset/Contents.json new file mode 100644 index 00000000..0580c78b --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/ai_expand.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ai_expand@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ai_expand@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/ai_expand.imageset/ai_expand@2x.png b/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/ai_expand.imageset/ai_expand@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2f5e7a6234b1b42d418b252df713a325579a2770 GIT binary patch literal 474 zcmV<00VV#4P)Px#1am@3R0s$N2z&@+hyVZqgh@m}R9J=WmO)a&FcgOW7plXO$^i_X0B;~SaA9Xy z!Vx?HcmnVSE*aS13GfE$UNByOuyh>S=Ypxzl#5J-O8s|H^g-V0 z8lCei7}7TAb8CKIepCY@2s&BeKx`MRzwzc&sTIaq3@pa={*|XMLBUrX zen{R@r3}fomeSLmW9BrkV^22;JOBU&-K&ZF_s2%}IfJ??a_*KI*!K&E1D1j$fEa)`>s1sSurER3aKJJHp_r-H@Rmkh zm$fSP{oTc~F#~Hrr5QR@afUGiYUC<;r&|@uSDMGG;w=$Y^O38peM+5LzYlW1`b`nM Q<^TWy07*qoM6N<$f=qeK^Z)<= literal 0 HcmV?d00001 diff --git a/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/ai_expand.imageset/ai_expand@3x.png b/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/ai_expand.imageset/ai_expand@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..801b0b7f8d92320e5cf16ff51c65e954a6e7702d GIT binary patch literal 585 zcmV-P0=E5$P)Px#1am@3R0s$N2z&@+hyVZq^GQTORA_DgDxy# zC)_|!pqzjsfIA}_JOSK54q!|#pyQH6>gPfYjhU92wlFoBCt*Qn`pxr(lFkp{a5x+` zp{#~ZUrw0$%BUFe_~qf`Hm^^o|9Zz7JL=g|mnsf(3xZ z-|HfjKDBshT2J{19h;PV*YsZN0kY%?G}F1?YJe!t&x4t69V-E< zk_RjVs8Sv{a)7Gkfg=a-T9}|hc_8COwoUL(z$*Q{=m2RxKX6Sgbnz1+PySF2r4ga@ zPouXd5y_!#7l*^)s1afyg^h8QyAmRlezU!MV{SKC$BQ8IMbe4nl))i`dhtBiyR6hf z*Fn9gZ=;(ey!`#73uVX;0r&)7ibxu-s>(Q&nhT}mfALXZSy+a`0TlA3CuT(`6re8h zK!E^tk_QX})J+~R2vA3PKyzm#D%&bN z;7AhI^0B@*S{7x1t;SivvDt2*oFpnAq4++C#f9v7giDh_N*{1!-9vcHn+-c0j(^M- XSjYq!IF==)00000NkvXXu0mjf!dm=X literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_ai_word.imageset/Contents.json b/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/op_ai_word.imageset/Contents.json similarity index 100% rename from NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_ai_word.imageset/Contents.json rename to NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/op_ai_word.imageset/Contents.json diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_ai_word.imageset/op_ai_word@2x.png b/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/op_ai_word.imageset/op_ai_word@2x.png similarity index 100% rename from NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_ai_word.imageset/op_ai_word@2x.png rename to NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/op_ai_word.imageset/op_ai_word@2x.png diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_ai_word.imageset/op_ai_word@3x.png b/NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/op_ai_word.imageset/op_ai_word@3x.png similarity index 100% rename from NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_ai_word.imageset/op_ai_word@3x.png rename to NEAISearchKit/NEAISearchKit/Assets/NEAISearchKit.xcassets/op_ai_word.imageset/op_ai_word@3x.png diff --git a/NEAISearchKit/NEAISearchKit/Assets/en.lproj/Localizable.strings b/NEAISearchKit/NEAISearchKit/Assets/en.lproj/Localizable.strings new file mode 100644 index 00000000..aaf458eb --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Assets/en.lproj/Localizable.strings @@ -0,0 +1,18 @@ +// 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. + + +// MARK: AI 划词搜 +"operation_ai_word_search"="AI Search"; + +"ok"="OK"; +"complete"="Complete"; +"cancel"="Cancel"; + +"ai_word_search" = "AI Word Search"; +"ai_word_searching" = "AI Word Searching"; +"input_more_button" = "Additional information"; +"input_more_placeholder" = "Please provide more information."; +"not_supported" = "Not supported"; +"request_exception" = "Request Exception"; diff --git a/NEAISearchKit/NEAISearchKit/Assets/ne_loading_data.json b/NEAISearchKit/NEAISearchKit/Assets/ne_loading_data.json new file mode 100644 index 00000000..b0f3148a --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Assets/ne_loading_data.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":24,"ip":0,"op":32,"w":45,"h":45,"nm":"合成 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"形状图层 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[22.5,22.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[40,40],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.129411764706,0.333333333333,0.933333333333,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[99]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":14,"s":[75]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":21,"s":[50]},{"t":30,"s":[1]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":7,"s":[75]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":14,"s":[50]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":21,"s":[25]},{"t":28,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":32,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}} \ No newline at end of file diff --git a/NEAISearchKit/NEAISearchKit/Assets/zh-Hans.lproj/Localizable.strings b/NEAISearchKit/NEAISearchKit/Assets/zh-Hans.lproj/Localizable.strings new file mode 100644 index 00000000..95523e9b --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Assets/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,18 @@ +// 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. + + +// MARK: AI 划词搜 +"operation_ai_word_search"="AI划词搜"; + +"ok"="确认"; +"complete"="完成"; +"cancel"="取消"; + +"ai_word_search" = "AI划词搜"; +"ai_word_searching" = "AI划词搜索中..."; +"input_more_button" = "补充信息"; +"input_more_placeholder" = "补充输入更多信息"; +"not_supported" = "暂不支持"; +"request_exception" = "模型请求异常"; diff --git a/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchConstant.swift b/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchConstant.swift new file mode 100644 index 00000000..16e889fa --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchConstant.swift @@ -0,0 +1,26 @@ + +// 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 NECommonKit +import NECoreKit + +let ModuleName = "NEAISearchKit" + +let coreLoader = CoreLoader() +func localizable(_ key: String) -> String { + coreLoader.localizable(key) +} + +let textFont = UIFont.systemFont(ofSize: 14) +let textMaxSize = CGSize(width: NEConstant.screenWidth - 20 * 2, height: CGFloat.greatestFiniteMagnitude) + +public extension UIImage { + class func ne_imageNamed(name: String?) -> UIImage? { + guard let imageName = name, !imageName.isEmpty else { + return nil + } + return coreLoader.loadImage(imageName) + } +} diff --git a/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchModel.swift b/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchModel.swift new file mode 100644 index 00000000..66015453 --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchModel.swift @@ -0,0 +1,16 @@ + +// 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. + +@objcMembers +open class NEAIWordSearchModel: NSObject { + var content: NSAttributedString? + var height: CGFloat = 0 + + init(_ content: NSAttributedString?) { + self.content = content + let textSize = NSAttributedString.getRealSize(content, textFont, textMaxSize) + height = ceil(textSize.height) + } +} diff --git a/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchTextCell.swift b/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchTextCell.swift new file mode 100644 index 00000000..d0cf6fab --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchTextCell.swift @@ -0,0 +1,70 @@ + +// 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. + +@objcMembers +open class NEAIWordSearchTextCell: UITableViewCell { + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + commonUI() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + commonUI() + } + + /// UI 布局方法 + func commonUI() { + isUserInteractionEnabled = false + contentView.addSubview(contentLabel) + NSLayoutConstraint.activate([ + contentLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + contentLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 20), + contentLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -20), + contentLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -16), + ]) + + contentView.addSubview(bottomLineView) + NSLayoutConstraint.activate([ + bottomLineView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + bottomLineView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + bottomLineView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + bottomLineView.heightAnchor.constraint(equalToConstant: 4), + ]) + } + + /// 是否显示 cell 底部分隔线 + /// - Parameter show: 是否显示 + func showBottomLine(_ show: Bool) { + bottomLineView.isHidden = !show + } + + /// 数据绑定 + /// - Parameter model: 数据模型 + func setModel(_ model: NEAIWordSearchModel) { + contentLabel.attributedText = model.content + } + + /// 文本内容 + lazy var contentLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = textFont + label.textColor = .ne_darkText + label.numberOfLines = 0 + label.textAlignment = .justified + label.accessibilityIdentifier = "id.content" + + return label + }() + + /// 分隔线 + lazy var bottomLineView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor(hexString: "#EDEDED") + return view + }() +} diff --git a/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchViewController.swift b/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchViewController.swift new file mode 100644 index 00000000..eeb38657 --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchViewController.swift @@ -0,0 +1,535 @@ + +// 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 Lottie +import NECommonKit +import NECommonUIKit +import NECoreIM2Kit +import UIKit + +@objcMembers +open class NEAIWordSearchViewController: UIViewController, NEAIWordSearchViewModelDelegate { + let viewModel = NEAIWordSearchViewModel() + var searchText: String? + public var margin: CGFloat = 16 + public var titleBarBottomLineHeight: CGFloat = 1.0 + public var backButtonWidth: CGFloat = 33 + public var moreButtonWidth: CGFloat = 85 + public var themeColor: UIColor = .ne_normalTheme + + // 父视图添加单击手势,点击背景区域 dismiss + var tap: UITapGestureRecognizer? + var contentTableViewTopAnchor: NSLayoutConstraint? + + var viewHeight: CGFloat = 0 + + override open var title: String? { + get { + super.title + } + + set { + super.title = newValue + navTitle.text = title + } + } + + public init(_ searchText: String?) { + super.init(nibName: nil, bundle: nil) + self.searchText = searchText + setupUI() + viewModel.delegate = self + loadData(searchText) + } + + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + setupUI() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + } + + deinit { + if let tap = tap { + view.superview?.removeGestureRecognizer(tap) + } + } + + override open func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + view.layer.cornerRadius = 8 + view.layer.borderWidth = 0.5 + view.layer.borderColor = UIColor.lightGray.cgColor + + if viewHeight == 0 { + viewHeight = 406 + view.frame = CGRect(x: 0, y: NEConstant.screenHeight - viewHeight, width: NEConstant.screenWidth, height: viewHeight) + } + + tap = UITapGestureRecognizer(target: self, action: #selector(tapAction)) + tap?.cancelsTouchesInView = false + view.superview?.addGestureRecognizer(tap!) + } + +// override open func viewWillAppear(_ animated: Bool) { +// super.viewWillAppear(animated) +// if let parent = parent as? ChatViewController { +// parent.isCurrentPage = false +// } +// } +// +// override open func viewWillDisappear(_ animated: Bool) { +// super.viewWillDisappear(animated) +// if let parent = parent as? ChatViewController { +// parent.isCurrentPage = true +// } +// } + + /// 展示错误弹窗 + /// - Parameter error: 错误信息 + func showErrorToast(_ error: Error?) { + if let err = error as? NSError { + switch err.code { + case failedOperation: + showToast(commonLocalizable("parameter_setting_error")) + case rateLimitExceeded: + showToast(commonLocalizable("rate_limit_exceeded")) + case userNotExistCode: + showToast(commonLocalizable("user_not_exist")) + case userBannedCode: + showToast(commonLocalizable("user_banned")) + case userChatBannedCode: + showToast(commonLocalizable("user_chat_banned")) + case noFriendCode: + showToast(commonLocalizable("friend_not_exist")) + case messageHitAntispam1, messageHitAntispam2: + showToast(commonLocalizable("message_hit_antispam")) + case teamMemberNotExist: + showToast(commonLocalizable("team_member_not_exist")) + case teamNormalMemberChatBanned: + showToast(commonLocalizable("team_normal_member_chat_banned")) + case teamMemberChatBanned: + showToast(commonLocalizable("team_member_chat_banned")) + case notAIAccount: + showToast(commonLocalizable("not_ai_account")) + case cannotBlockAIAccount: + showToast(commonLocalizable("cannot_blocklist_ai_account")) + case aiMessagesDisabled: + showToast(commonLocalizable("ai_messages_function_disabled")) + case aiMessageRequestFailed: + showToast(commonLocalizable("failed_request_to_the_LLM")) + default: + showToast(localizable("request_exception")) + } + } + } + + /// 首次加载数据 + /// - Parameter searchText: 搜索内容 + func loadData(_ searchText: String?) { + setTitleText(true) + viewModel.loadData(searchText) { [weak self] error in + self?.showErrorToast(error) + self?.tableViewReload(false) + } + } + + /// 补充加载数据 + /// - Parameter searchText: 搜索内容 + func loadMoreData(_ searchText: String?) { + setTitleText() + inputTextView.resignFirstResponder() + viewModel.loadData(searchText) { [weak self] error in + self?.showErrorToast(error) + self?.tableViewReload(false) + } + } + + /// UI 布局 + open func setupUI() { + title = localizable("ai_word_searching") + view.backgroundColor = .white + + view.addSubview(titleBarView) + NSLayoutConstraint.activate([ + titleBarView.topAnchor.constraint(equalTo: view.topAnchor), + titleBarView.leftAnchor.constraint(equalTo: view.leftAnchor), + titleBarView.rightAnchor.constraint(equalTo: view.rightAnchor), + titleBarView.heightAnchor.constraint(equalToConstant: 50), + ]) + + view.addSubview(contentTableView) + contentTableViewTopAnchor = contentTableView.topAnchor.constraint(equalTo: titleBarBottomLine.bottomAnchor, constant: 0) + contentTableViewTopAnchor?.isActive = true + NSLayoutConstraint.activate([ + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + contentTableView.register(NEAIWordSearchTextCell.self, forCellReuseIdentifier: "\(NEAIWordSearchTextCell.self)") + } + + /// 设置标题 + /// - Parameter isLoading: loading 状态 + func setTitleText(_ isLoading: Bool = true) { + if isLoading { + title = localizable("ai_word_searching") + loadingView.isHidden = false + } else { + title = localizable("ai_word_search") + loadingView.isHidden = true + } + } + + /// 取消按钮点击事件 + func cancelButtonAction() { + if let tap = tap { + view.superview?.removeGestureRecognizer(tap) + } + view.removeFromSuperview() + removeFromParent() + dismiss(animated: true, completion: nil) + } + + /// 单击手势点击事件 + /// - Parameter tap: 单击手势 + func tapAction(_ tap: UITapGestureRecognizer) { + // 判断手势位置位于背景区域 + if tap.location(in: view).y < 0 { + cancelButtonAction() + } + } + + /// 补充信息按钮点击事件 + func moreButtonAction() { + viewHeight = 700 + UIView.animate(withDuration: 0.25) { + self.view.frame = CGRect(x: 0, y: NEConstant.screenHeight - self.viewHeight, width: NEConstant.screenWidth, height: self.viewHeight) + self.view.layoutIfNeeded() + } + + // 移除补充信息按钮,添加输入框 + moreButton.removeFromSuperview() + view.addSubview(inputBackView) + NSLayoutConstraint.activate([ + inputBackView.topAnchor.constraint(equalTo: titleBarBottomLine.bottomAnchor, constant: 16), + inputBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + inputBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + inputBackView.heightAnchor.constraint(equalToConstant: 120), + ]) + + contentTableViewTopAnchor?.constant = 16 + 120 + } + + /// 确认按钮点击事件 + func sureButtonAction() { + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + if let text = inputTextView.text, !text.isEmpty { + loadMoreData(text) + inputTextView.text = "" + placeHolderLabel.isHidden = false + sureButton.isEnabled = false + } + } + + /// 键盘完成按钮点击事件 + func doneButtonAction() { + inputTextView.resignFirstResponder() + } + + // MARK: lazy + + /// 取消按钮 + public lazy var cancelButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(localizable("cancel"), for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 16) + button.setTitleColor(.ne_darkText, for: .normal) + button.contentHorizontalAlignment = .left + button.addTarget(self, action: #selector(cancelButtonAction), for: .touchUpInside) + button.accessibilityIdentifier = "id.cancel" + return button + }() + + /// 标题栏 loading 动画 + public lazy var loadingView: LottieAnimationView = { + let view = LottieAnimationView(name: "ne_loading_data", bundle: coreLoader.bundle) + view.translatesAutoresizingMaskIntoConstraints = false + view.loopMode = .loop + view.contentMode = .scaleAspectFill + view.accessibilityIdentifier = "id.loadingView" + return view + }() + + /// 标题 + public lazy var navTitle: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .systemFont(ofSize: 17, weight: .semibold) + label.textAlignment = .center + label.text = "" + label.accessibilityIdentifier = "id.title" + return label + }() + + /// 补充信息按钮 + public lazy var moreButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(.ne_imageNamed(name: "ai_expand"), for: .normal) + button.setTitle(localizable("input_more_button"), for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 16) + button.setTitleColor(themeColor, for: .normal) + button.accessibilityIdentifier = "id.threePoint" + button.addTarget(self, action: #selector(moreButtonAction), for: .touchUpInside) + button.accessibilityIdentifier = "id.moreButton" + return button + }() + + /// 标题栏分割线 + public lazy var titleBarBottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor(hexString: "#E9EFF5") + view.isHidden = false + return view + }() + + /// 标题栏视图 + public lazy var titleBarView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + view.addSubview(cancelButton) + view.addSubview(moreButton) + view.addSubview(navTitle) + view.addSubview(loadingView) + view.addSubview(titleBarBottomLine) + + NSLayoutConstraint.activate([ + cancelButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: margin), + cancelButton.centerYAnchor.constraint(equalTo: view.centerYAnchor), + cancelButton.widthAnchor.constraint(equalToConstant: backButtonWidth), + cancelButton.heightAnchor.constraint(equalToConstant: 18), + ]) + + NSLayoutConstraint.activate([ + moreButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -margin), + moreButton.centerYAnchor.constraint(equalTo: view.centerYAnchor), + moreButton.widthAnchor.constraint(equalToConstant: moreButtonWidth), + moreButton.heightAnchor.constraint(equalToConstant: 18), + ]) + + NSLayoutConstraint.activate([ + navTitle.centerXAnchor.constraint(equalTo: view.centerXAnchor), + navTitle.centerYAnchor.constraint(equalTo: view.centerYAnchor), + navTitle.widthAnchor.constraint(lessThanOrEqualToConstant: NEConstant.screenWidth - margin * 4 - backButtonWidth - moreButtonWidth - 16), // 16 为补充按钮 icon 宽度 + ]) + + loadingView.play() + NSLayoutConstraint.activate([ + loadingView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + loadingView.widthAnchor.constraint(equalToConstant: 16), + loadingView.heightAnchor.constraint(equalToConstant: 16), + loadingView.rightAnchor.constraint(equalTo: navTitle.leftAnchor, constant: -2), + ]) + + NSLayoutConstraint.activate([ + titleBarBottomLine.leftAnchor.constraint(equalTo: view.leftAnchor), + titleBarBottomLine.rightAnchor.constraint(equalTo: view.rightAnchor), + titleBarBottomLine.bottomAnchor.constraint(equalTo: view.bottomAnchor), + titleBarBottomLine.heightAnchor.constraint(equalToConstant: titleBarBottomLineHeight), + ]) + + return view + }() + + /// 输入框视图 + lazy var inputBackView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor(hexString: "#F5F5F5") + view.layer.cornerRadius = 4 + + view.addSubview(inputTextView) + view.addSubview(sureButton) + + NSLayoutConstraint.activate([ + inputTextView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 12), + inputTextView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -12), + inputTextView.topAnchor.constraint(equalTo: view.topAnchor, constant: 12), + inputTextView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -30), + ]) + + NSLayoutConstraint.activate([ + sureButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -12), + sureButton.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -12), + sureButton.widthAnchor.constraint(equalToConstant: 40), + sureButton.heightAnchor.constraint(equalToConstant: 18), + ]) + + return view + }() + + /// 输入框占位符 + lazy var placeHolderLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = localizable("input_more_placeholder") + label.font = .systemFont(ofSize: 16) + label.textColor = UIColor(hexString: "#AAAAAA") + return label + }() + + /// 输入框 + lazy var inputTextView: UITextView = { + let textView = UITextView() + textView.translatesAutoresizingMaskIntoConstraints = false + textView.backgroundColor = .clear + textView.font = .systemFont(ofSize: 16) + textView.textColor = .ne_darkText + textView.delegate = self + + textView.addSubview(placeHolderLabel) + NSLayoutConstraint.activate([ + placeHolderLabel.leftAnchor.constraint(equalTo: textView.leftAnchor, constant: 6), + placeHolderLabel.topAnchor.constraint(equalTo: textView.topAnchor, constant: 8), + placeHolderLabel.widthAnchor.constraint(equalToConstant: 150), + placeHolderLabel.heightAnchor.constraint(equalToConstant: 18), + ]) + + let accessoryView = UIToolbar(frame: CGRect(x: 0, y: 0, width: NEConstant.screenWidth, height: 40)) + accessoryView.barStyle = .default + + // 完成按钮 + let accessoryDoneButton = UIButton(frame: CGRect(x: 0, y: 0, width: 60, height: 20)) + accessoryDoneButton.translatesAutoresizingMaskIntoConstraints = false + accessoryDoneButton.setTitle(localizable("complete"), for: .normal) + accessoryDoneButton.setTitleColor(.ne_normalTheme, for: .normal) + accessoryDoneButton.addTarget(self, action: #selector(doneButtonAction), for: .touchUpInside) + + accessoryView.addSubview(accessoryDoneButton) + NSLayoutConstraint.activate([ + accessoryDoneButton.rightAnchor.constraint(equalTo: accessoryView.rightAnchor, constant: -12), + accessoryDoneButton.centerYAnchor.constraint(equalTo: accessoryView.centerYAnchor), + accessoryDoneButton.widthAnchor.constraint(equalToConstant: 44), + accessoryDoneButton.heightAnchor.constraint(equalToConstant: 20), + ]) + + textView.inputAccessoryView = accessoryView + + return textView + }() + + /// 确认按钮 + lazy var sureButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(localizable("ok"), for: .normal) + button.setTitleColor(UIColor(hexString: "#2155EE"), for: .normal) + button.setTitleColor(UIColor(hexString: "#2155EE", 0.5), for: .disabled) + button.titleLabel?.font = .systemFont(ofSize: 16) + button.addTarget(self, action: #selector(sureButtonAction), for: .touchUpInside) + button.accessibilityIdentifier = "id.sureButton" + button.isEnabled = false + return button + }() + + /// 内容列表 + lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.separatorStyle = .none + tableView.keyboardDismissMode = .onDrag + tableView.delegate = self + tableView.dataSource = self + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() +} + +// MARK: - UITableViewDelegate, UITableViewDataSource + +extension NEAIWordSearchViewController: UITableViewDelegate, UITableViewDataSource { + public func numberOfSections(in tableView: UITableView) -> Int { + 1 + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + viewModel.data.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let model = viewModel.data[indexPath.row] + let cell = tableView.dequeueReusableCell(withIdentifier: "\(NEAIWordSearchTextCell.self)", for: indexPath) as! NEAIWordSearchTextCell + cell.setModel(model) + + if viewModel.data.count == 1, !moreButton.isHidden { + cell.showBottomLine(false) + } else { + cell.showBottomLine(true) + } + + return cell + } + + public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let model = viewModel.data[indexPath.row] + return model.height + 16 + 20 + } +} + +// MARK: - UITextViewDelegate + +extension NEAIWordSearchViewController: UITextViewDelegate { + public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + placeHolderLabel.isHidden = true + if textView.text.count <= 0 { + sureButton.isEnabled = false + } + return true + } + + public func textViewDidChange(_ textView: UITextView) { + if textView.text.count > 0 { + placeHolderLabel.isHidden = true + sureButton.isEnabled = true + } else { + placeHolderLabel.isHidden = false + sureButton.isEnabled = false + } + } + + public func textViewDidEndEditing(_ textView: UITextView) { + if textView.text.count <= 0 { + placeHolderLabel.isHidden = false + sureButton.isEnabled = false + } + } + + public func tableViewReload(_ isLoading: Bool) { + setTitleText(isLoading) + contentTableView.reloadData() + } +} diff --git a/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchViewModel.swift b/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchViewModel.swift new file mode 100644 index 00000000..af5d757c --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Classes/AIWordSearch/NEAIWordSearchViewModel.swift @@ -0,0 +1,181 @@ + +// 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 NECommonUIKit +import NECoreIM2Kit +import NIMSDK + +@objc +public protocol NEAIWordSearchViewModelDelegate { + func tableViewReload(_ isLoading: Bool) +} + +@objcMembers +open class NEAIWordSearchViewModel: NSObject { + let repo = AIRepo.shared + public weak var delegate: NEAIWordSearchViewModelDelegate? + public var data: [NEAIWordSearchModel] = [] + var searchTexts: [String] = [] + let aiModelTemperature: CGFloat = 0.8 + var requestIds = [String]() + + override public init() { + super.init() + repo.addAIListener(self) + } + + deinit { + repo.removeAIListener(self) + } + + /// 获取大模型请求内容类型 + /// - Parameters: + /// - msg: 请求/响应的文本内容 + /// - type: 类型 + /// - Returns: 大模型请求内容类型 + func getAIModelCallContent(_ msg: String, + _ type: V2NIMAIModelCallContentType) -> V2NIMAIModelCallContent { + let content = V2NIMAIModelCallContent() + content.msg = msg + content.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT + return content + } + + /// 获取 AI 大模型配置 + /// - Parameter temperature: 控制随机性和多样性的程度 + /// - Returns: AI 大模型配置 + func getAIModelConfigParams(_ temperature: CGFloat) -> V2NIMAIModelConfigParams { + let aiConfig = V2NIMAIModelConfigParams() + aiConfig.temperature = temperature + return aiConfig + } + + /// 获取请求调用上下文内容 + /// - Returns: 请求调用上下文内容 + func getAIMessages() -> [V2NIMAIModelCallMessage]? { + var aiMessages = [V2NIMAIModelCallMessage]() + for (i, text) in searchTexts.enumerated() { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "[AISearch], message text\(i + 1): \(String(describing: text))") + let message = V2NIMAIModelCallMessage() + message.msg = text + message.role = .NIM_AI_MODEL_ROLE_TYPE_USER + message.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT + aiMessages.append(message) + } + return aiMessages.isEmpty ? nil : aiMessages + } + + /// 获取 Al 数字人请求参数 + /// - Parameters: + /// - accid: 机器人账号ID + /// - requestId: 请求 id + /// - content: 请求大模型的内容 + /// - modelConfigParams: 请求接口模型相关参数配置 + /// - Returns: Al 数字人请求参数 + func getProxyAIModelCallParams(_ accid: String, + _ requestId: String, + _ content: V2NIMAIModelCallContent, + _ modelConfigParams: V2NIMAIModelConfigParams) -> V2NIMProxyAIModelCallParams { + let params = V2NIMProxyAIModelCallParams() + params.accountId = accid + params.requestId = requestId + params.content = content + params.modelConfigParams = modelConfigParams + params.messages = getAIMessages() + return params + } + + /// 请求 AI 大模型 + /// - Parameters: + /// - searchText: 搜索文本 + /// - completion: 完成回调 + func loadData(_ searchText: String?, _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "[AISearch], searchText: \(String(describing: searchText))") + guard let text = searchText else { + completion(nil) + return + } + + guard let aiUser = NEAIUserManager.shared.getAISearchUser() else { return } + if let accid = aiUser.accountId { + let requestId = UUID().uuidString + let content = getAIModelCallContent(text, .NIM_AI_MODEL_CONTENT_TYPE_TEXT) + let modelConfigParams = getAIModelConfigParams(aiModelTemperature) + let params = getProxyAIModelCallParams(accid, requestId, content, modelConfigParams) + + repo.proxyAIModelCall(params) { [weak self] error in + if let err = error { + print("proxyAIModelCall error: \(err.localizedDescription)") + completion(err) + } else { + self?.searchTexts.append(text) + self?.requestIds.append(requestId) + } + } + } + } +} + +// MARK: - V2NIMAIListener + +extension NEAIWordSearchViewModel: V2NIMAIListener { + /// 展示错误弹窗 + /// - Parameter error: 错误信息 + func getErrorText(_ data: V2NIMAIModelCallResult) -> String { + switch data.code { + case operationSuccess: + return data.content.msg + case failedOperation: + return commonLocalizable("parameter_setting_error") + case rateLimitExceeded: + return commonLocalizable("rate_limit_exceeded") + case userNotExistCode: + return commonLocalizable("user_not_exist") + case userBannedCode: + return commonLocalizable("user_banned") + case userChatBannedCode: + return commonLocalizable("user_chat_banned") + case noFriendCode: + return commonLocalizable("friend_not_exist") + case messageHitAntispam1, messageHitAntispam2: + return commonLocalizable("message_hit_antispam") + case teamMemberNotExist: + return commonLocalizable("team_member_not_exist") + case teamNormalMemberChatBanned: + return commonLocalizable("team_normal_member_chat_banned") + case teamMemberChatBanned: + return commonLocalizable("team_member_chat_banned") + case notAIAccount: + return commonLocalizable("not_ai_account") + case cannotBlockAIAccount: + return commonLocalizable("cannot_blocklist_ai_account") + case aiMessagesDisabled: + return commonLocalizable("ai_messages_function_disabled") + case aiMessageRequestFailed: + return commonLocalizable("failed_request_to_the_LLM") + default: + return localizable("request_exception") + } + } + + /// AI 透传接口的响应的回调 + /// - Parameter data: 响应内容 + public func onProxyAIModelCall(_ data: V2NIMAIModelCallResult) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "[aiSearch], data: \(data.content.msg)") + + guard requestIds.contains(data.requestId) else { return } + + let showText = getErrorText(data) + let arr = NSAttributedString(string: showText) + let model = NEAIWordSearchModel(arr) + self.data.insert(model, at: 0) + requestIds.removeAll { $0 == data.requestId } + + let isLoading = !requestIds.isEmpty + delegate?.tableViewReload(isLoading) + } +} diff --git a/NEAISearchKit/NEAISearchKit/Classes/NEAISearchLoader.h b/NEAISearchKit/NEAISearchKit/Classes/NEAISearchLoader.h new file mode 100644 index 00000000..f3e15a2c --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Classes/NEAISearchLoader.h @@ -0,0 +1,12 @@ +// 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. + +#ifndef Loader_h +#define Loader_h + +@interface NEAISearchLoader : NSObject + +@end + +#endif /* Loader_h */ diff --git a/NEAISearchKit/NEAISearchKit/Classes/NEAISearchLoader.m b/NEAISearchKit/NEAISearchKit/Classes/NEAISearchLoader.m new file mode 100644 index 00000000..f50d48a2 --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Classes/NEAISearchLoader.m @@ -0,0 +1,20 @@ +// 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 "NEAISearchLoader.h" +#import + +#if __has_include() +#import +#else +#import "NEAISearchKit-Swift.h" +#endif + +@implementation NEAISearchLoader + ++ (void)load { + [NEAISearchManager.shared setupInit]; +} + +@end diff --git a/NEAISearchKit/NEAISearchKit/Classes/NEAISearchManager.swift b/NEAISearchKit/NEAISearchKit/Classes/NEAISearchManager.swift new file mode 100644 index 00000000..8534c484 --- /dev/null +++ b/NEAISearchKit/NEAISearchKit/Classes/NEAISearchManager.swift @@ -0,0 +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 NEChatKit +import NECommonKit +import NECommonUIKit +import NECoreIM2Kit +import NECoreKit +import UIKit + +@objcMembers +open class NEAISearchManager: NSObject, XKitService, IMKitPluginService { + public static let shared = NEAISearchManager() + public var serviceName: String = NEAISearchPlugin + public var versionName: String = "1.0.0" + public var appKey: String = IMKitClient.instance.appKey() + + override private init() { + super.init() + } + + /// 插件初始化 + public func setupInit() { + XKit.instance().register(self) + IMKitPluginManager.shared.registerPlugin(serviceName, self) + } + + // MARK: - IMKitPluginService + + /// 注册插件模型 + /// - Parameter text: 文本内容 + /// - Returns: 插件模型 + public func registerPlugin(_ text: String) -> OperationItem? { + let item = OperationItem() + item.text = localizable("operation_ai_word_search") + item.image = UIImage.ne_imageNamed(name: "op_ai_word") + item.type = .plugin + item.onClick = { viewController in + + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + viewController?.showToast(commonLocalizable("network_error")) + return + } + + // 不支持空文本 + if text.isEmpty { + viewController?.showToast(localizable("not_supported")) + return + } + + let aisearchViewController = NEAIWordSearchViewController(text) + viewController?.navigationController?.present(aisearchViewController, animated: true) + } + return item + } +} diff --git a/NEChatUIKit/NEChatUIKit.podspec b/NEChatUIKit/NEChatUIKit.podspec index 6fcdcabd..133b6af4 100644 --- a/NEChatUIKit/NEChatUIKit.podspec +++ b/NEChatUIKit/NEChatUIKit.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = 'NEChatUIKit' - s.version = '10.2.1' + s.version = '10.3.0' s.summary = 'Chat Module of IM.' # This description is used to generate tags and improve search results. @@ -32,7 +32,7 @@ TODO: Add long description of the pod here. s.author = 'yunxin engineering department' s.source = { :git => 'ssh://git@g.hz.netease.com:22222/yunxin-app/xkit-ios.git', :tag => s.version.to_s } - s.ios.deployment_target = '11.0' + s.ios.deployment_target = '12.0' s.swift_version = '5.0' s.source_files = 'NEChatUIKit/Classes/**/*' diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/chat_translation.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/chat_translation.imageset/Contents.json new file mode 100644 index 00000000..02a88a01 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/chat_translation.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Group 1817.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Group 1817-2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/chat_translation.imageset/Group 1817-2.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/chat_translation.imageset/Group 1817-2.png new file mode 100644 index 0000000000000000000000000000000000000000..eeec71ff5f7b377b9849f9a56b9b6df730783424 GIT binary patch literal 3995 zcmZ`+c|26@7r!&cPE&ZTp){7P$s3Yk5}F};gD}`D$QpGlADbCa0$W0N283UcVibvo!Ag)O0)d%i8uz z-?OnZS;|9^^SAa;xz8?V^T~@*?h2S2Wf|4MBkxzaUyx=-XBWjen`fx~kj>5*7R{E% zyU_jk93}XfQR?Vo?A@atO)NrT!Shj~+G~Yxs3u3_)d@72s-`@bsNmaubvq}t3pv-7 z)4hoK+Htt?S2%Sytb65Mfu1Fn1W{!w1NLzH_|Ie!nwmYtN6G$utPxO2mZ6EQUiA@t z4pacGWQo;M`Pu3Y5w;%1gfCW&TtQsJ(6W#22Z7G~X^0sqnh8zS2d0dTCuVt}6K`>n zt@icof#InBot3uBLYPhh*^&nM4DfEul|eB`NLg(lT@qb))B$W>Z!y$apATY7$B-Z&ftg~p&I1NV=*g#OuH<#Ii2M##0*-*M zPmou#-qn)P1F%cmID(J%ea)Br3|qX8;X`la+%QlJY$M(dVgPw!_Aq*y;$^SC3a31; zDpFTn0~cBbwkG3$#37ua!(Q>h*V^EE(x7pw;RHE3#?_aN4^u;cT(e4cFh$5x%f#Y>Nx@Dl^BfwSB;7?1Dm%IyaK-1ytJ&@dmaj%qT=uC|+r-ZaA$t4>Bfq5Sa7*xrk*WeUp9suHJx>wmwkb9!w{p z9rg~*YKnJFkS8Uq7o(YJYJO)?&Pg>TayT!_b4~~9X%)y=L04ce-MgB4W?G{|H2MY- z)|hJZ==GZJzBLK32UQWv>SPJ8)XwgpZJyJz;l-qtau`ozoUc4j9;RKC(>g zuXBCgOZOq#EbMXDQu3}lEGTI2r*(BG?u*eBVfQUhNrFLUF3?@V9g9|UsaY%K_=#sl<3_;d+bS33<3Y_-VdsR=J?!GA(c;1jR$)S>OxBgSto+v-- zQO?*i!<8bwO{>{0<#5vtIALQQ!{AK$7(`N@m;#jMgf+>AXZq_iQugkUaf-yQAJ3{i zWKPI7S=Gu2wRj{-+G9~xs*IcNJ*2N$xU-JE53xw|=Mv$wybUb3tFR=;kC_A9PJ&O+ z+T6U@B`?3<1=FcAFDP=m&VIkLr?WUv5A>MN5sP8ZY)qiⅈ4$P#Tbe)dAYBh~oYw zFJUbidrryPG<}M8Rlnaw5;0Q#vQFD^jcKs+(_4JB z)!wO%_iJ=%;@+!+E&r6YKIeRp38Z_Zo0L$yRpK)mltVIEdE}f0wMW~Xp$~&*Z3fR5 z-JMT8ZdZ9+M`(vVe|rIGl-{0d8Qrr53J%S=FR5F<%1Y8qxWx8{X`{J zYi~c~B6l1c`|{-wWa8<=gf=+H;v-Vkm!FT<1 z0WcjL%#SNB??yI{ww%FI`yU+u$G*5Y#CGNF0I_HLs%Fzg2nS7R^J?=ZQ90ztMp_hN z4wkq3llo>k3B3>D=vk59H7zw;>=JIc6p~q6?Xh88ntBu+;+SvH;Bcryb&@2A2V<${ zcy7wk=@aYz#OQ_SqmU@i@R{!6o!SOR%qd~y@-E+f?ACIHsOq2TZDfSGtik*3^q~@d zb>*4i33FRl&jXLLm0WllAitZgZhIvncsVieFZ+LDhz4+Su!sfF4FBl~)vg8!M05x!=(&*1xAKmu}i_06%=cgd-#At-f@ zaL$ObTF7%5< z1Z`TCq+X!S+=C!d_NuyDS3G%Ch4foJ37)t2Zsp$O@_n_?vnuS^98uAuX_4BLh=hpC zPj)O<+^JUanV!uosaMJ&|5~>Cw#*awxdEswydbRbO$R3iTiQ=(mgqIBw=_QY=nLOEUb1%l?Qt|vrG zqc<=&jkm`tYFqQI{_3n&^8ug~*(loz!xbRllBd`&0TE&G{f)7yPBLV(n&0{-)Ay`e zz{{VSX5|Qv8fJ0tIM4}aUYYWH`U$^(G`K@(Pc^{?dvIFr!;idamplzIXD(WWGJ9b+@bmVZd zqWGBIH$4Q$bdZQ7@@{B&kgnoEAY+pbJ8(&BEnCXG!!c97S!x77jkgOvU9%+~5AVv^ zkS;%nQY|5*TSiMA_v@&+9 z-#$&^UE06Il*g4=Z~eS*xd%z9b2D8D5)cg*l`RCb!{CIb<&^s9rjZEo2J>l+=Z*%G zpmSHprA;{f6G(&;g#2@=@&~9TO=qH=DFsB4$X|QP*n{SlFWBjeNhgH%KkVI$!>P(3 zMs3Jcf17C=#vYVdH@f)W8{GT*C5)-SxO|ZPnvhzEYSp@U#}=gs4Dh<&xB7_a4dA?> zT0kTiUlRTSmleaW^s>IQu5IpZ2RBH}SzRxLJ+=2d;CE!-+SmR*?-Y&D6&B2~HV`$8 ziqy&1sm*%0kXrrZo`VGBa*e}b!+=iyo$jTMPX1$wG=T5d+SL>M`Lqoqx_E|tJl_1! zWCC?6YQ)COO~mdt~BOE7p=Un4cx@hgE3DHr9kRH2wF( z-8f;i>E7c)n41D7eqf*ilTHEiK%7<3b$<)fd}prDlgKLdYi=%^wbO_`0v6x#9Wvq(q{g%nJ*J9RjaN_AZ%yQr!9CQ3~W z8IMXWDvj~^^VWbzHeF4g7hB$`!bF@JSlAwpST&?!L&kKKg{-SbHnPN+<+hQi#vheO z*K9rn5tm(fuIvqhNugLx**CFj4J$1vQ}h<=UWO%n$_KNz%updpx zrO>911tnJUoaN2)DaqZe0Q2`JEaog^ zpFyLiPEH?yc$bqxLOY>$OMq-jt0*fxgXuLV_eILjD$x2*W(LllCstHCRr5{{$eqrZ?+S6Zir zgd{@KHyzJlOzFYmsh@+)yWvX9WrK2H`mf!yX8cvJ*^>3(l8AeDFQs$yxN2~M{UL0# zm@gTvqm7GBkT+>O=eh|j%h~E)lT1!)QfyvpEfWI3GT%SP^8JjvmS{?(E?LsC!>;(a za@m4wn_jNuuXMDIZQl6KKZT4y<)ADO4&SU*kxb@dqoXQP1_Xhh!Iv`jKO)IX+>By< z9`$&nZGtdVKT3geaB7(P=C!8rd#YD^o~rH(eyo?$Dx>}VKBJf-duY%nUWM81NOJY{ zaiwMLFXG?E9P#7V6NEL+CSv1*1%ebxLDBW%(EvL74Ahmu5q%n>^76ggA{Tdfhuz{c zJvToZG6VMIOOOezZo0k+G29Bjqij_0{rP>!82sSOs#YpKre=hPob*vK{AtkjDEV2~ zs^bfO`Af-WZ=vb|qi$}=-cRKz2i!Xs8_ZE7Uu0+{_eSH3N~KcbK4^*c^>lcn>uGF&TPUlg!WeK~#A|i zIx>iTH=x{;$<@4IjK)c39?w8a9-bs&D0X=hxO@a6iUIrG#N-w0M#6B!&7)rVnrn zU`$WoSm43O$ude4^TSg%U-+s?kqWU2v7zVhxSV0wi73p-rvb(3`Cx+$V6^l#D>UpQ F{{wK7rd9v| literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/chat_translation.imageset/Group 1817.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/chat_translation.imageset/Group 1817.png new file mode 100644 index 0000000000000000000000000000000000000000..3b9ede3b2d4e850f648a7264467e2b5866a4459b GIT binary patch literal 2696 zcmV;33U~F1P)Px#1am@3R0s$N2z&@+hyVZzGD$>1RCt{2olk5O*B!?{Z`DYhJ!N;J1X26s+7Kq6RJ!m*NrRnh|$1T`pbB~U?;s6sE$B2|Gp7c3W1Tjfv(`4EeO1hQ+%mWYK9 z!4wDCjxV)|AkwaL+Np%f>tS}*JF~O@y`F#Xb7I#sZ+_zUo!@)!_kQoKpw37sn}9Z; z9oPmm0&f8^Km#K6w!!1P0(4*vcmb>cPk~wBu@GXpo(~rr-Jq26HDC|W3v>e8>+zhB zAc1*c92f;23L#!@*Yqc3=S5U(NlB4QCqo zSO_s+>5UbSz@?NYfhm;py~KWCD&w1qC|dwh%BYm`DsUZ$SNb4D-H8L&rIc5tl+ki; zDys{SQofE6@;|83IK}2Ng3%@XQ0l^x0?0Uj7yy0pxOW6GzRz(mE;K``!RfL!O{g}7Ix-Px&5H>9N3w=fMkyV zr%}!iGFvcwoFy9pGH?Gks3qN?lLX#2qd;y1IEHe57)W6F8puR|Oc~eLs7ju&Fc0h& zLcBC1z#f$I!^CzBpJ%&(UiWNKxI{0k3!u|QLlo}N2_Q0c`%9>tf7p;fa|C14hH`%R z5I|c5W5VQyhd8r8K0bX+l-p=g#Z?p>Uvsg)hsT3>$V9s;|l?Jm2}!0 zT~aM50fZbSfRGa!0Yo*8U2Qu^#8v%EF22bF)IuU2{^eg#dQ7?XY}4^v(g7Eb$(J7eI?c-oCZDDH{>OPT&Qw zufuuc)1iav94rWd09p(5_bn|y-DFF=NuUMb6#h+Go11DL47LV9M1dB7J^bIkbIETKP{OmS^+$ReKGckgrl!X@8CgJ254 zmiit4*OcY+gsqgzE*K~Fb+qTtD(>DVnOe)cHkn%E;^nKJEM>D3H~}Q$F^fZ2R-ffL zDXr@)o;aeK#?jvQ z%PubQ@^~Onvw%cAmKOmoU;MPJd(E6cZUu589^+{5`;OP|+PQri%vQP3_A z=L`ZB0cy?7+voS^&h%M6+xzlFBF=^LXYR5)$#4=aWLdI3(($-<&OZ&7p$_IGVFMH+*xX zta*4;)9C+f{l_&ImX>)V9?MJkod3fg^X_}M`*2>j0miE1#N;#=&i~HSZ$AVm0(9EK zzdhSm2s0;Oj`W?TgP-+ZbG+}?=oralik|K+%feGncNcp*+CA@l2*?H~DtWEf2!sG~)41l+Y$c?1oyEoc9mr0hzgQc$M#l;!p1evR1dyAEsb&FYUq^f1 zf>5c@U#K0w)d>b-5O`~JEGvSE3d69|LeIj|a#^9jR6Bkk5CTZ+dc}@6v9Ix`b)DhA z|D)!1{6ZkQfNDv^V~&->m4^O92lp4;fZ}yREdYC#?!nA#!R6yZ_#xX@j8F@pv0-cW z-^paE>^1&e`c9qjb$NJb1z;TdxW0C2{)d+0X_ ztpF;0+wItE1^kLH1W>ONJ*q9%^!gfqCHO)B^$NAsobwly&~M@k0kk$Z6|A_a1wAJM zM5Cog^5&+{AmN)#pjmOVvicn>tEDS_${~09RxG4PHI0Ubt-j6n6~h-5L7T-FqhDGQ zK`qTq?B2P9y&dhop6eGw3Bbm};erQn#Wc_*gs{|qF1)0G144+rslH=PVCKS4P-_H1 zKmwmQIUkA?g$f~V0R8o{O0jA5XME|1V;5i|rHleM0Pp_hiJ}-Iz)2xQy3hqh1ppwW zd>t4D{Qq0&;bIIpE`<1@*hMAx1!NjHj-e)nKNz75=R=trLoyAW1pa~=7bYh5M!=DopB!<(+F>3f8nw2~oMco}ErECJ)fOcRT z&siC^(-V?r_;~?0000Px#1am@3R0s$N2z&@+hyVZqjY&j7R7i=n)V)r^P!tF7|7|Z+1E>)N*<6(H1SAYH z7=ro)`U1ugaUki$0to}|#>CYw%os5U3&zb2Ypd{k#Y#b%c5$d_GxE zlBrUt=Q(_q*!A$JaFqN1kSO0x$n4sKN~j^VoYWB-`GD!S%(v&Dq9Fhv(%@-~|FWDc z%6AhA>n+cM001Ma%q61^W&KN$Zku8nq7b*?zU&a-FLk2^zOq7GmJJp`#*iH*gS}e> zF+dt7(~U11b|5=0QExT+H5ka^sAi9e>^J!*z7v}WK`)_SLu4PmL-zokcR!xzzOHtJen5) ZfL~ay-@cJu3}pZS002ovPDHLkV1jPK&J+Lu literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/language_selected.imageset/language_selected@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/language_selected.imageset/language_selected@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8ab0a264ba58397e533b0d4593311f4963b8f8f6 GIT binary patch literal 638 zcmV-^0)hRBP)Px#1am@3R0s$N2z&@+hyVZrC`m*?R9J=0*Uf9wU>pbVZ=NLUrmGF3h-Dsyp$0Ds zonzqQHae)_!INiC-l`S^JH`&OO&B7c{14ovsD~lJY3gCai+Cwa51w_Jjm{QZWAem< zX0$a)laiNxk5Aq{{P;aD0S-ZOg>q)1v{N0QS7+MCnYch!a)olHHaxB9BsE5;X0WKw zy}O^@h#i+N%Az_n`AlbyoU+E+fwAH8MOjn_FDg1orn|2Zsu>&_SJ=Y_0|1j@2}_%q z$ScVpk?`vC2$FDnGKd7A57cG9YFmdne)3{;4zslu{il`2r z?`YVBzO%Nqbi(!FFkt||*uks}7<;c1{K%YLKZFef08$$`{laOAYwXB$^Y^|ywP%jR1HtC)eChR|)S0FKeZ31LGOqeas zE6^=LzyOE6b$wN;3XNCiIQrvLTrj|8Px#1am@3R0s$N2z&@+hyVZq0ZBwbR7i>Kk}*!hKoCXWx24qdq{tC0VNoYXh>)Pp z4M-1SO+t-N!WD2G8ORZQ5%16#ln^_%aqHjh&g|Pi`XgEtJG=HJZxW`GoBTz)bNxXZ z(2P5YOJHgtw{89HK+ojhIB9I`3hb*9I08pt7YQJKvR5kwe?RLHaJYfYZj59|mCkZ+ZlvF;3m zB;P7KygON&&y7-vv=i)EuI9d8SC96NnAaAK>|>)vU8!-g&m>=zA*-f$jSitU`J+U8 pIfT>X|0eoxMb2jNgU!42egpXzRuztL1xWw^002ovPDHLkV1fwnjMe}E literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/use_arrow.imageset/use_arrow@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/use_arrow.imageset/use_arrow@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..3f882e0fa443ba83af4795c6be207e0c30495e58 GIT binary patch literal 447 zcmV;w0YLtVP)Px#1am@3R0s$N2z&@+hyVZqX-PyuR9J=W)lE*qKp4jH|J&gJT)@PI2f$RvS@|PWp5zRLteAo>;?^ zKwsh2Sgzmh<+I8fPF2)8%mn%X0)6W+tGY&IRD`p<>qgKz(xNTeqAl8@E!v|06cvD; zuU)g{1?V*p;v`e*z_$jE;$meMCz(<^g~DR3t;2cbOVNV?6@G1bCzdK+S+Eicunc6LESA8u ps5rI69qXezQN6g+hsLqG@&&n^e&D$o1SS9g002ovPDHLkV1nIL!BPMK literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/Contents.json index 0c131fc5..523ee4fe 100644 --- a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/Contents.json +++ b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/Contents.json @@ -5,12 +5,12 @@ "scale" : "1x" }, { - "filename" : "Frame 45@2x.png", + "filename" : "op_delete@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "Frame 45@3x.png", + "filename" : "op_delete@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/Frame 45@2x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/Frame 45@2x.png deleted file mode 100644 index 77fc669706e47dc7c2cab8289ccfb1406db63585..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 647 zcmeAS@N?(olHy`uVBq!ia0vp^Dj>|k1|%Oc%$NbBI14-?iy0WiR6&^0Gf3qFP>``W z$lZxy-8q?;Kn_c~qpu?a!^VE@KZ&di3`{bfE{-7;jBjVydK`8TIUaxXV98~xn^v8> zR&?xclGpgr+T7d?M3Y}E>fl>s!Rgn<$EC+*)gj-!K(tiAF=MT4PLBBHlB`Q7kLccO zHB{KcPio$))qx z4_^D#l^X9oePfP~u(6d9$K%7!6Zn@u7k^M+5qA91BbzAI^_lmzHh5+i3qSD=ci;VO z_BQrhZY@!^=|>-P9_F!_XMQ8*#bo|1g;6D|*XO=EP%ZI|uWy0OgBPcBIM1e9=$vj{ z^SMj$jdG5NSMTk!5{u4-X0GSEtZ`Zu(jzm-jriDL(H0s&9uM3qQJXM3k$|?uwXu+_yI_Z@-+|QuV=e z&c>51-{pUc`ffK=2rOh@tlZItiPk1H%gy2Y{aAu)Fk zc)0+a3{uqDcp3Ql>o@SiWs3Di12+F|8NTysmmnA%1(6hsB^>LxeN8u$%+jlAw)@}` zwwFIdM^S5E`+Xy@n0x_Vul)h-P{A=$Usq6Ixp_@Dl-Bgb_*L6|aG7EQ2hejj9uqE# z2>5+)$K*8X^#<_3y~gxtns`y7`tD2Ed1h#ZbvO_5MmxFxT^pN&ao(li%rh2w%1~&u z>B#hS1nA_ew+7EA1AcTi?_Mt$zB8C236f0|(QV!aVWiyrNM(TLOmo-MVTu$S3mEy$ zG3z`tF@CKho_dD$Jf`B9Y*=oBw{yFY&2B*?as_6lrnIkeHk8+4m;)OJ}oI-yh#EF>eIZ6l)b@Y$z1U zz-+IJB{cB{V@FQE>0=KaiCEB+GRy@%Hw)!FWJ3v)4JAxAlrY&)!em1UlMN+IHk2^g zP{L$G36l*aOg4lHb2BH+i%?;cx=ep$EZmXW(L=vcn-;7Wu}%cw(8S^Jbu>a3(fd^E^xz`SWh zgDBARLqOaBXnECr!tRA^1L971%1Ofg@8ofR`2&A zlR>QjA@zbGfc*%8_0It^)XaXx3}Zuv(4M!=CV*^`tmuGxGBgKkC$n&scgE$B1CMuf=ga7Wo*b;sq>sv zv1N*}sXVM_yD}zTg&lPOLBEca5{FljJTr-u_L{2WW{2(Z9l!ucU{iNC9Va?cI#2G7 zlz~s~LsKuKBV};(7)asAV95^|++{i$*o{(^`68|)<1IQ@2p-^YzTWD{k(Z4GcmISYoyX-Pqa`;RBf_J%L`2mt$|^FQ#=f%u0A?e;R*7zd@jbKH6Ts8cwmEps{G z-C~mT`okFDP%y7-xd5+H;Fdt^cyX1Kgug58uV?Nn^*keS?wnjxBz=Uw`}Pw{!3tRaYxpqN=1&!q%y*r@2sv| z>-xWb^ko|mmkbVzLBmUhhjLQPWzuAj%G8qkRK1C>t=M|V_z7!V1In5boFV`K002ov JPDHLkV1lDlW|jZ| literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/op_delete@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_delete.imageset/op_delete@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..8e2b27c2edb3dd1a944acae31b65bd0496fa32c5 GIT binary patch literal 1268 zcmV*>xCpsvaDDVA`PwI8|fTexf5xc*$ zw{P%E9o?!BJbBSS*sq~i4cj|=ZQ%E+dRIYUy7%hY)7MpW>JWPmc&Vxp*bw8&<06Sl z>twt4=wc;Ut1|YMtN7b~;ICsTQet?q65MHGu&2@O`rf5;e~`Su)~zufa8}kvC1Mz! zWorgalL9z8>J7Jd_IhBwxK74*TpKZT8vzCgoZ0(o`V~A`Ff!F&jVcn0z1H^k*_>4E zik}UJVJP8O(Rc2(Z0-83m2>@AApX5UCoVbhQsS>vG$lMFawfxM`DcoJiB2pm<0Yhg z0Wtyh_jr}l>9L+`v0ES9TT^B_&jpk!c91&RfJz{eGm#>u%w^hHNKzegaNHW}47fAH zMTqv#zPBt_JRw+e5g>t_GS8|YY_VWgCn?&|?c}I8oGl4-x{u$2txvhvmp`n0U;}i!(0i79t&Wd{3Aa$j(|rOQ6v)RuA*%uR1f_H4!Txip5P( zq@uXTM8%S(zkn9I9xRC6G%SeSG%SeSG%SeSG%Sc!LzP3uluDWUo09}z4@wb}19O_& z5XtUlertv2@#SjOVO1}&DSg|Z5b^}9-2%CkZ z)6Zk2fKvE!x}pjTQ}s4~4@+rcGrov44Vx|~cRSgbmGJ-Nv{)f(%&^=DV)f80F{%E6 zXQ6JrGu=js1*Ur9o0y7rktmf?nZk@i(fvdvlTV2cm-Y@R_d=&pZpsmCKYTp-l}}ORW?(_~b8G%^ zQ&Z)Tm>IYw$5PZ{la?ZRQA#TTGI}ri2ahZ|9&dygh@W=$e3|V^@+KrKO=bo$Dd%L; zV$R{>t{4%^eIW>m4F!W2{e%6)UlYWpB-?Gj#cKH5-E^W`qYkIyp%` e2id emoticon_emoji_13 tag - [Superise] + [Surprise] file @@ -163,7 +163,7 @@ id emoticon_emoji_19 tag - [Thrille] + [Thrill] file diff --git a/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings b/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings index 9fa026ea..6436abcf 100644 --- a/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings +++ b/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings @@ -3,9 +3,6 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -//MAKR:common -"ok"="OK"; - //MAKR:message "send_picture"="get a picture"; "send_voice"="get a audio"; @@ -131,7 +128,6 @@ "cancel"="cancel"; "send_to"="send to"; "send"="send"; -"confirm"="yes"; "session_record"="session record"; "collection_message"=" collection message"; @@ -223,3 +219,14 @@ "chat_collection_team_tip"="From %@ "; "collection_limit"="The number of favorites has reached the limit value"; +"language_title"="Translate input content into"; + +"translate_use"="Use"; +"translate_sure"="AI Translate"; +"ai_translating"="AI Translating..."; +"translate_default"="Translate as"; +"format_not_supported"="This format is not supported."; +"chat_translate"="Translate"; +"ai_user_pin_top"="Pin to top"; +"request_exception" = "Request Exception"; +"ai_request_ing" = "Model request responsing"; diff --git a/NEChatUIKit/NEChatUIKit/Assets/ne_loading_data.json b/NEChatUIKit/NEChatUIKit/Assets/ne_loading_data.json new file mode 100644 index 00000000..f62c71ea --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/ne_loading_data.json @@ -0,0 +1 @@ +{"v":"5.12.1","fr":24,"ip":0,"op":32,"w":45,"h":45,"nm":"合成 3","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"形状图层 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[22.5,22.5,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[40,40],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"椭圆路径 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.20,0.49,1.00,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"描边 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"变换"}],"nm":"椭圆 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[99]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":14,"s":[75]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":21,"s":[50]},{"t":30,"s":[1]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":7,"s":[75]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":14,"s":[50]},{"i":{"x":[0.833],"y":[1]},"o":{"x":[0.167],"y":[0]},"t":21,"s":[25]},{"t":28,"s":[0]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"修剪路径 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":32,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}} diff --git a/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings index 90c404b2..01999870 100644 --- a/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -3,9 +3,6 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -//MAKR:common -"ok"="确认"; - //MAKR:message "send_picture"="发来了一张图片"; "send_voice"="发来了一段语音"; @@ -92,7 +89,7 @@ "operation_select"="多选"; "operation_collection"="收藏"; "operation_top"="置顶"; -"operation_untop"="移除置顶"; +"operation_untop"="取消置顶"; "operation_delete"="删除"; "operation_recall"="撤回"; "withdrew_message" = "撤回了一条消息"; @@ -127,7 +124,6 @@ "cancel"="取消"; "send_to"="发送给"; "send"="发送"; -"confirm"="确定"; "session_record"="的会话记录"; "collection_message"="的收藏消息"; @@ -220,3 +216,14 @@ "chat_collection_team_tip"="来自于 %@"; "collection_limit"="收藏数已达到限制值"; +"language_title"="将输入内容翻译为"; + +"translate_use"="使用"; +"translate_sure"="AI处理"; +"ai_translating"="AI处理中..."; +"translate_default"="翻译为..."; +"chat_translate"="翻译"; +"ai_user_pin_top"="PIN置顶"; +"format_not_supported"="暂不支持该格式"; +"request_exception" = "模型请求异常"; +"ai_request_ing" = "大模型请求响应中..."; diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift index 6ae1366d..c80b5724 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift @@ -17,9 +17,7 @@ open class NEChatBaseCell: UITableViewCell { super.init(coder: coder) } - open func uploadProgress(byRight: Bool, _ progress: UInt) { - fatalError("override in sub class") - } + open func uploadProgress(_ progress: UInt) {} open func setModel(_ model: MessageContentModel) {} open func setModel(_ model: MessageContentModel, _ isSend: Bool = false) {} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/NEChatBaseViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/NEChatBaseViewController.swift index 1861dee8..1f8e019f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/NEChatBaseViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/NEChatBaseViewController.swift @@ -9,7 +9,16 @@ import UIKit @objcMembers open class NEChatBaseViewController: UIViewController, UIGestureRecognizerDelegate { - var topConstant: CGFloat = 0 + public var topConstant: CGFloat = 0 { + didSet { + navigationViewHeightAnchor?.constant = topConstant + } + } + + // 自定义导航栏高度布局约束 + public var navigationViewHeightAnchor: NSLayoutConstraint? + + // 自定义导航栏 public let navigationView = NENavigationView() override open var title: String? { @@ -23,32 +32,52 @@ open class NEChatBaseViewController: UIViewController, UIGestureRecognizerDelega } } - override open func viewDidLoad() { - super.viewDidLoad() - navigationController?.interactivePopGestureRecognizer?.delegate = self - view.backgroundColor = NEKitChatConfig.shared.ui.messageProperties.chatViewBackground ?? .white + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + // 配置项:会话界面是否展示标题栏 if !NEKitChatConfig.shared.ui.messageProperties.showTitleBar { navigationController?.isNavigationBarHidden = true + navigationView.removeFromSuperview() return } if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { navigationController?.isNavigationBarHidden = false + navigationView.removeFromSuperview() setupBackUI() - topConstant = NEConstant.navigationAndStatusHeight } else { navigationController?.isNavigationBarHidden = true + } + } + + override open func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = NEKitChatConfig.shared.ui.messageProperties.chatViewBackground ?? .white + + if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { + topConstant = NEConstant.navigationAndStatusHeight + } else { topConstant = NEConstant.navigationAndStatusHeight navigationView.translatesAutoresizingMaskIntoConstraints = false navigationView.addBackButtonTarget(target: self, selector: #selector(backEvent)) navigationView.addMoreButtonTarget(target: self, selector: #selector(toSetting)) + view.addSubview(navigationView) + navigationViewHeightAnchor = navigationView.heightAnchor.constraint(equalToConstant: topConstant) + navigationViewHeightAnchor?.isActive = true NSLayoutConstraint.activate([ navigationView.leftAnchor.constraint(equalTo: view.leftAnchor), navigationView.rightAnchor.constraint(equalTo: view.rightAnchor), navigationView.topAnchor.constraint(equalTo: view.topAnchor), - navigationView.heightAnchor.constraint(equalToConstant: topConstant), ]) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoader.h b/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoader.h new file mode 100644 index 00000000..d81753cc --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoader.h @@ -0,0 +1,12 @@ +//// 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. + +#ifndef NEChatLoader_h +#define NEChatLoader_h + +@interface NEChatLoader : NSObject + +@end + +#endif /* NEChatLoader_h */ diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoader.m b/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoader.m new file mode 100644 index 00000000..2929ff84 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoader.m @@ -0,0 +1,35 @@ +//// 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 "NEChatLoader.h" +#import + +#if __has_include() +#import +#else +#import "NEChatUIKit-Swift.h" +#endif + +@interface NEChatLoader () + +@end + +@implementation NEChatLoader + +static id gShareInstance = nil; + ++ (instancetype)shareInstance { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + gShareInstance = [[self alloc] init]; + }); + return gShareInstance; +} + ++ (void)load { + NSLog(@"NEChatLoader load"); + [NEChatLoaderService.shared setupInit]; +} + +@end diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoaderService.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoaderService.swift new file mode 100644 index 00000000..6a995798 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatLoaderService.swift @@ -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 Foundation +import NEChatKit + +@objcMembers +public class NEChatLoaderService: NSObject { + public static let shared = NEChatLoaderService() + + override private init() { + super.init() + } + + /// 初始化方法 + /// 此方法会在模块被加载时调用 + public func setupInit() { + ChatKitClient.shared.registerInit(NEChatService.shared) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatService.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatService.swift new file mode 100644 index 00000000..517a3b25 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/NEChatService.swift @@ -0,0 +1,52 @@ +//// 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 NIMSDK +import SDWebImage +import SDWebImageSVGKitPlugin +import SDWebImageWebPCoder + +@objcMembers +public class NEChatService: NSObject, ChatServiceDelegate, NEChatEmojProtocol { + public static let shared = NEChatService() + + override private init() { + super.init() + } + + /// 注册 NEChatUIKit 初始化协议 + /// - Parameter params: 初始化参数 + public func setupInit(_ params: [String: Any]?) { + registerRouter(params) + } + + /// 注册路由 + /// - Parameter param: 参数 + public func registerRouter(_ param: [String: Any]?) { + if let isFun = param?["isFun"] as? Bool, isFun { + ChatRouter.registerFun() + } else { + ChatRouter.register() + } + + NIMKitFileLocationHelper.setStaticAppkey(NIMSDK.shared().appKey()) + NIMKitFileLocationHelper.setStaticUserId(IMKitClient.instance.account()) + let webpCoder = SDImageWebPCoder() + SDImageCodersManager.shared.addCoder(webpCoder) + let svgCoder = SDImageSVGKCoder.shared + SDImageCodersManager.shared.addCoder(svgCoder) + + NEChatKitClient.instance.addEmojDelegate(self) + } + + public func getEmojAttributeString(_ content: String, _ font: CGFloat) -> NSAttributedString? { + let attributeStr = NEEmotionTool.getAttWithStr( + str: content, + font: UIFont.systemFont(ofSize: font) + ) + return attributeStr + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift index bfa4bb29..aaa907ca 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift @@ -16,7 +16,7 @@ import UIKit import WebKit @objcMembers -open class ChatViewController: NEChatBaseViewController, UINavigationControllerDelegate, UITableViewDataSource, UITableViewDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate, NIMMediaManagerDelegate, CLLocationManagerDelegate, UITextViewDelegate, ChatInputViewDelegate, ChatInputMultilineDelegate, ChatViewModelDelegate, MessageOperationViewDelegate, NEContactListener { +open class ChatViewController: NEChatBaseViewController, UINavigationControllerDelegate, UITableViewDataSource, UITableViewDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate, NIMMediaManagerDelegate, CLLocationManagerDelegate, UITextViewDelegate, UIImagePickerControllerDelegate, ChatInputViewDelegate, ChatInputMultilineDelegate, ChatViewModelDelegate, MessageOperationViewDelegate, NETranslateViewDelegate, SelectLanguageDelegate { private let kCallKitDismissNoti = "kCallKitDismissNoti" private let kCallKitShowNoti = "kCallKitShowNoti" public var titleContent = "" @@ -34,7 +34,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD public var isCurrentPage = true public var isMute = false // 是否禁言 private var isMutilSelect = false // 是否多选模式 - private var isUploadingData = false // 是否正在加载数据(上拉) + private var isLoadingData = false // 是否正在加载数据 private var hasFirstLoadData = false // 是否完成第一次加载数据 private var uploadHasNoMore = false // 上拉无更多数据 private var networkBroken = false // 网络断开标志 @@ -44,13 +44,27 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD private var needMarkReadMsgs = [V2NIMMessage]() private var atUsers = [NSRange]() - var replyView = ReplyView() - public var operationView: MessageOperationView? + lazy var replyView: ReplyView = { + let view = ReplyView() + view.translatesAutoresizingMaskIntoConstraints = false + view.closeButton.addTarget(self, action: #selector(closeReply), for: .touchUpInside) + return view + }() public var normalOffset: CGFloat = 0 public var bottomExanpndHeight: CGFloat = 204 // 底部展开高度 public var normalInputHeight: CGFloat = 100 public var brokenNetworkViewHeight: CGFloat = 36 + public var currentKeyboardHeight: CGFloat = 0 + + // 顶部扩展视图距 view 顶部的间距 + public var bodyTopViewTopConstant: CGFloat = 0 { + didSet { + bodyTopViewTopAnchor?.constant = bodyTopViewTopConstant + } + } + + // 顶部扩展视图的高度 public lazy var bodyTopViewHeight: CGFloat = 0 { didSet { bodyTopViewHeightAnchor?.constant = bodyTopViewHeight @@ -58,6 +72,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD } } + // 底部扩展视图的高度 public lazy var bodyBottomViewHeight: CGFloat = 0 { didSet { bodyBottomViewHeightAnchor?.constant = bodyBottomViewHeight @@ -65,20 +80,35 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD } } + // 底部内容视图(包含输入框)的高度 public lazy var bottomViewHeight: CGFloat = 404 { didSet { bottomViewHeightAnchor?.constant = bottomViewHeight } } - public var currentKeyboardHeight: CGFloat = 0 + // 顶部扩展视图顶部布局约束 + public var bodyTopViewTopAnchor: NSLayoutConstraint? + // 顶部扩展视图高度布局约束 public var bodyTopViewHeightAnchor: NSLayoutConstraint? + + // 底部扩展视图高度布局约束 public var bodyBottomViewHeightAnchor: NSLayoutConstraint? + + // 内容视图顶部布局约束 public var contentViewTopAnchor: NSLayoutConstraint? + + // 底部扩展视图顶部布局约束 public var bottomViewTopAnchor: NSLayoutConstraint? + + // 底部扩展视图高度布局约束 public var bottomViewHeightAnchor: NSLayoutConstraint? + // 翻译视图高度布局约束 + public var translateLanguageViewHeightAnchor: NSLayoutConstraint? + + // 顶部扩展视图 public lazy var bodyTopView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -86,6 +116,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD return view }() + // 中部视图(包含断网横幅和内容视图) public lazy var bodyView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -111,6 +142,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD return view }() + // 断网横幅 public lazy var brokenNetworkView: NEBrokenNetworkView = { let view = NEBrokenNetworkView() view.translatesAutoresizingMaskIntoConstraints = false @@ -118,6 +150,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD return view }() + // 内容视图 public lazy var contentView: UIView = { let contentView = UIView() contentView.translatesAutoresizingMaskIntoConstraints = false @@ -153,9 +186,13 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD tableView.estimatedSectionHeaderHeight = 0 tableView.estimatedSectionFooterHeight = 0 } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() + // 底部扩展视图 public lazy var bodyBottomView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -163,6 +200,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD return view }() + // 底部内容视图(包含输入框等) public lazy var bottomView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -211,34 +249,54 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD return topMessageView }() + /// 翻译框 + public lazy var translateLanguageView: NEAITranslateView = { + let translateView = NEAITranslateView() + translateView.translatesAutoresizingMaskIntoConstraints = false + translateView.delegate = self + return translateView + }() + + /// 长按操作菜单 + public lazy var operationView: MessageOperationView = { + let operationView = MessageOperationView(frame: .zero) + operationView.isHidden = true + operationView.delegate = self + view.addSubview(operationView) + return operationView + }() + 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) - IMKitClient.instance.addLoginListener(self) + addListener() } public required init?(coder: NSCoder) { super.init(coder: coder) } - deinit { - NEALog.infoLog(className(), desc: "deinit") - viewModel.clearUnreadCount() - cleanDelegate() + /// 添加监听 + open func addListener() { + NIMSDK.shared().mediaManager.add(self) + IMKitClient.instance.addLoginListener(self) } - func cleanDelegate() { + /// 移除监听 + open func removeListener() { NIMSDK.shared().mediaManager.remove(self) - ContactRepo.shared.removeContactListener(self) IMKitClient.instance.removeLoginListener(self) viewModel.delegate = nil } + deinit { + NEALog.infoLog(className(), desc: "deinit") + viewModel.clearUnreadCount() + removeListener() + } + override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) NEKeyboardManager.shared.enable = false @@ -246,6 +304,10 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD isCurrentPage = true markNeedReadMsg() + if NEKitChatConfig.shared.ui.messageProperties.showTitleBar { + bodyTopViewTopConstant = topConstant + } + NEChatDetectNetworkTool.shareInstance.netWorkReachability { [weak self] status in if status == .notReachable { self?.brokenNetworkView.isHidden = false @@ -259,6 +321,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD override open func viewDidLoad() { super.viewDidLoad() + navigationController?.interactivePopGestureRecognizer?.delegate = self viewModel.delegate = self commonUI() addObseve() @@ -273,14 +336,17 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD } } } + + weakSelf?.viewModel.translationAIUser = NEAIUserManager.shared.getAITranslateUser() } override open func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) + navigationController?.interactivePopGestureRecognizer?.delegate = nil NEKeyboardManager.shared.enable = true NEKeyboardManager.shared.shouldResignOnTouchOutside = true isCurrentPage = false - operationView?.removeFromSuperview() + removeOperationView() if audioPlayer?.isPlaying == true { audioPlayer?.stop() } @@ -299,6 +365,8 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD if parent == nil { let param = ["sessionId": viewModel.conversationId] Router.shared.use("ClearAtMessageRemind", parameters: param, closure: nil) + + NETeamUserManager.shared.removeAllTeamInfo() } } @@ -322,18 +390,16 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD view.addSubview(bodyView) view.addSubview(bodyBottomView) view.addSubview(bottomView) - - var bodyTopViewTopConstant: CGFloat = 0 - if #available(iOS 10, *) { - bodyTopViewTopConstant += KStatusBarHeight - } - if NEKitChatConfig.shared.ui.messageProperties.showTitleBar { - bodyTopViewTopConstant += kNavigationHeight + if IMKitConfigCenter.shared.enableAIUser == true { + view.addSubview(translateLanguageView) + translateLanguageView.chatInputText = chatInputView.textView } + bodyTopViewHeightAnchor = bodyTopView.heightAnchor.constraint(equalToConstant: bodyTopViewHeight) bodyTopViewHeightAnchor?.isActive = true + bodyTopViewTopAnchor = bodyTopView.topAnchor.constraint(equalTo: view.topAnchor, constant: bodyTopViewTopConstant) + bodyTopViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ - bodyTopView.topAnchor.constraint(equalTo: view.topAnchor, constant: bodyTopViewTopConstant), bodyTopView.leftAnchor.constraint(equalTo: view.leftAnchor), bodyTopView.rightAnchor.constraint(equalTo: view.rightAnchor), ]) @@ -355,12 +421,29 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD bodyBottomView.rightAnchor.constraint(equalTo: view.rightAnchor), ]) - NSLayoutConstraint.activate([ - bodyView.topAnchor.constraint(equalTo: bodyTopView.bottomAnchor), - bodyView.leftAnchor.constraint(equalTo: view.leftAnchor), - bodyView.rightAnchor.constraint(equalTo: view.rightAnchor), - bodyView.bottomAnchor.constraint(equalTo: bodyBottomView.topAnchor), - ]) + if IMKitConfigCenter.shared.enableAIUser { + translateLanguageViewHeightAnchor = translateLanguageView.heightAnchor.constraint(equalToConstant: 0) + NSLayoutConstraint.activate([ + translateLanguageView.leftAnchor.constraint(equalTo: view.leftAnchor), + translateLanguageView.rightAnchor.constraint(equalTo: view.rightAnchor), + translateLanguageView.bottomAnchor.constraint(equalTo: bodyBottomView.topAnchor), + translateLanguageViewHeightAnchor!, + ]) + + NSLayoutConstraint.activate([ + bodyView.topAnchor.constraint(equalTo: bodyTopView.bottomAnchor), + bodyView.leftAnchor.constraint(equalTo: view.leftAnchor), + bodyView.rightAnchor.constraint(equalTo: view.rightAnchor), + bodyView.bottomAnchor.constraint(equalTo: translateLanguageView.topAnchor), + ]) + } else { + NSLayoutConstraint.activate([ + bodyView.topAnchor.constraint(equalTo: bodyTopView.bottomAnchor), + bodyView.leftAnchor.constraint(equalTo: view.leftAnchor), + bodyView.rightAnchor.constraint(equalTo: view.rightAnchor), + bodyView.bottomAnchor.constraint(equalTo: bodyBottomView.topAnchor), + ]) + } tableView.register( NEBaseChatMessageCell.self, @@ -388,13 +471,13 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD override open func backEvent() { super.backEvent() - cleanDelegate() + removeListener() } // load data的时候会调用 open func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { if NEFriendUserCache.shared.getFriendInfo(IMKitClient.instance.account()) == nil { - ContactRepo.shared.getUserList(accountIds: [IMKitClient.instance.account()]) { users, error in + ContactRepo.shared.getUserListFromCloud(accountIds: [IMKitClient.instance.account()]) { users, error in completion() } } else { @@ -405,7 +488,8 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD /// 点击头像回调 /// - Parameter model: cell模型 open func didTapHeadPortrait(model: MessageContentModel?) { - if let isOut = model?.message?.isSelf, isOut { + if !ChatMessageHelper.isAISender(model?.message), + let isOut = model?.message?.isSelf, isOut { Router.shared.use( MeSettingRouter, parameters: ["nav": navigationController as Any], @@ -413,7 +497,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD ) return } - if let uid = model?.message?.senderId { + if let uid = ChatMessageHelper.getSenderId(model?.message) { Router.shared.use( ContactUserInfoPageRouter, parameters: ["nav": navigationController as Any, "uid": uid], @@ -424,6 +508,32 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD open func setOperationItems(items: inout [OperationItem], model: MessageContentModel?) {} + func removeOperationView(_ cellEndEditing: Bool = true) { + if operationView.isHidden == false { + operationView.isHidden = true + } + + // 取消划词选中 + if cellEndEditing { + viewModel.operationModel?.cell?.contentView.endEditing(true) + } + } + + /// 好友(用户)信息变更回调 + /// - Parameter accountId: 用户 id + open 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 = NETeamUserManager.shared.getShowName(sessionId) + self?.titleContent = showName + self?.title = showName + })) + } + viewModel.updateMessageInfo(accountId) + } + /// 长按消息内容 /// - Parameters: /// - cell: 长按cell @@ -445,16 +555,52 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD layoutInputView(offset: 0) } - operationView?.removeFromSuperview() - // get operations - guard let items = viewModel.avalibleOperationsForMessage(model) else { + guard var items = viewModel.avalibleOperationsForMessage(model) else { return } - var filterItems = items + // 插件导入 + var pluginItems: [OperationItem] = [] + if let text = model?.selectText() { + // 划词 + if NEAIUserManager.shared.getAISearchUser() == nil { + // 未配置划词数字人 + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: "AI Search User is nil") + } else if IMKitConfigCenter.shared.enableAIUser == false { + // 未开启全局数字人开关 + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: "IMKitConfigCenter enableAIUser is false") + } else { + pluginItems.append(contentsOf: IMKitPluginManager.shared.getPlugins(NEAISearchPlugin, text)) + } + } + + // 划词(非全选)只展示【复制】、【AI划词搜】 + if let model = model as? MessageTextModel { + if let selectRange = model.selectRange, + selectRange.length > 0 { + if model.attributeStr == nil, let model = model as? MessageRichTextModel { + if selectRange.length != model.titleAttributeStr?.string.utf16.count { + items = items.filter { item in + item.type == .copy + } + } + } else if selectRange.length != model.attributeStr?.string.utf16.count { + items = items.filter { item in + item.type == .copy + } + } + } else { + removeOperationView() + return + } + } + + items.append(contentsOf: pluginItems) + + // 全局过滤 if let filter = operationCellFilter { - filterItems = items.filter { item in + items = items.filter { item in if let type = item.type { return !filter.contains(type) } @@ -464,46 +610,59 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD // 配置项自定义 items if let chatPopMenu = NEKitChatConfig.shared.ui.chatPopMenu { - chatPopMenu(&filterItems, model) + chatPopMenu(&items, model) } // 供用户自定义 items - setOperationItems(items: &filterItems, model: 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 - let w = filterItems.count <= 5 ? 60.0 * Double(filterItems.count) + 16.0 : 60.0 * 5 + 16.0 - let h = filterItems.count <= 5 ? 56.0 + 16.0 : 56.0 * 2 + 16.0 - - let rectInTableView = tableView.rectForRow(at: index) - let rectInView = tableView.convert(rectInTableView, to: view) - let topOffset = NEConstant.navigationAndStatusHeight - var operationY = 0.0 - if topOffset + h + bodyTopViewHeight > rectInView.origin.y { - // under the cell - operationY = rectInView.origin.y + rectInView.size.height - } else { - operationY = rectInView.origin.y - h - } - var frameX = 0.0 - if let msg = model?.message, - msg.isSelf { - frameX = kScreenWidth - w - } - var frame = CGRect(x: frameX, y: operationY, width: w, height: h) - if frame.origin.y + h < tableView.frame.origin.y { - frame.origin.y = tableView.frame.origin.y - } else if frame.origin.y + h > view.frame.size.height { - frame.origin.y = tableView.frame.origin.y + tableView.frame.size.height - h + setOperationItems(items: &items, model: model) + + guard let index = tableView.indexPath(for: cell) else { + tableViewReload() + return + } + + removeOperationView() + if viewModel.operationModel != model { + viewModel.operationModel?.cell?.resetSelectRange() + viewModel.operationModel = model + viewModel.operationModel?.cell?.selectAllRange() + } + + // 计算宽高 + let w = items.count <= 5 ? 60.0 * Double(items.count) + 16.0 : 60.0 * 5 + 16.0 + let h = items.count <= 5 ? 56.0 + 16.0 : 56.0 * 2 + 16.0 + + let rectInTableView = tableView.rectForRow(at: index) + let rectInView = tableView.convert(rectInTableView, to: view) + let topOffset = NEConstant.navigationAndStatusHeight + + var operationY = 0.0 + if topOffset + h + bodyTopViewHeight > rectInView.origin.y { + operationY = rectInView.origin.y + rectInView.size.height - chat_timeCellH + } else { + // 位于消息上方 + operationY = rectInView.origin.y - h + if model?.timeContent != nil { + operationY += chat_timeCellH } + } - operationView = MessageOperationView(frame: frame) - operationView!.delegate = self - operationView!.items = filterItems - view.addSubview(operationView!) - })) + var frameX = 56.0 + if let msg = model?.message, + msg.isSelf { + frameX = kScreenWidth - w - frameX + } + + var frame = CGRect(x: frameX, y: operationY, width: w, height: h) + if frame.origin.y + h < tableView.frame.origin.y { + frame.origin.y = tableView.frame.origin.y + } else if frame.origin.y + h > view.frame.size.height { + frame.origin.y = tableView.frame.origin.y + tableView.frame.size.height - h + } + + operationView.frame = frame + operationView.items = items + operationView.isHidden = false } // MARK: UIGestureRecognizerDelegate @@ -600,10 +759,8 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD return } - if let opeView = operationView, - view.subviews.contains(opeView) { - opeView.removeFromSuperview() - + if operationView.isHidden == false { + removeOperationView() } else { if chatInputView.textView.isFirstResponder || chatInputView.titleField.isFirstResponder { chatInputView.textView.resignFirstResponder() @@ -624,12 +781,14 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD // 多端登录清空未读数 viewModel.clearUnreadCount() + isLoadingData = true viewModel.loadData { error, historyEnd, newEnd, index in NEALog.infoLog( ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK loadData " + (error?.localizedDescription ?? "no error") ) + weakSelf?.isLoadingData = false if let ms = weakSelf?.viewModel.messages, ms.count > 0 { weakSelf?.tableViewReload() if weakSelf?.viewModel.isHistoryChat == true, @@ -705,6 +864,8 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD NotificationCenter.default.addObserver(self, selector: #selector(didShowCallView), name: Notification.Name(kCallKitShowNoti), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didTapHeader), name: NENotificationName.didTapHeader, object: nil) + let tap = UITapGestureRecognizer(target: self, action: #selector(viewTap)) tap.delegate = self tap.cancelsTouchesInView = false @@ -758,10 +919,13 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD // MARK: - 键盘通知相关操作 open func keyBoardWillShow(_ notification: Notification) { - if !isCurrentPage { + if !chatInputView.textView.isFirstResponder, + !chatInputView.titleField.isFirstResponder { return } - operationView?.removeFromSuperview() + + removeOperationView() + if chatInputView.currentType != .text { return } @@ -785,9 +949,15 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD } open func keyBoardWillHide(_ notification: Notification) { + if !chatInputView.textView.isFirstResponder, + !chatInputView.titleField.isFirstResponder { + return + } + if chatInputView.currentType != .text { return } + chatInputView.currentButton?.isSelected = false var animationDuration: TimeInterval = 0.1 @@ -841,6 +1011,10 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD // MARK: - ChatInputViewDelegate + public func didTranslateResult(_ content: String) { + translateLanguageView.setTranslateContent(content) + } + open func sendText(text: String?, attribute: NSAttributedString?) { if let title = chatInputView.titleField.text, title.trimmingCharacters(in: .whitespaces).isEmpty == false { // 换行消息 @@ -860,25 +1034,44 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD if let remoteExt = chatInputView.getAtRemoteExtension(attribute) { customMessage.serverExtension = getJSONStringFromDictionary(remoteExt) } - + var firstAIUserAccid = chatInputView.nickAccidList.first(where: { NEAIUserManager.shared.isAIUser($0) }) + if NEAIUserManager.shared.isAIUser(viewModel.sessionId) { + firstAIUserAccid = viewModel.sessionId + } + chatInputView.clearAtCache() + translateLanguageView.changeToIdleState(true) if viewModel.isReplying, let msg = viewModel.operationModel?.message { viewModel.replyMessageWithoutThread(message: customMessage, - replyMessage: msg) { [weak self] message, error in + replyMessage: msg, + aiUserAccid: firstAIUserAccid) { [weak self] message, 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.sendMessage(message: customMessage) { [weak self] message, error in - self?.showErrorToast(error) + viewModel.sendRichTextMessage(message: customMessage, + title: title, + body: text, + aiUserAccid: firstAIUserAccid) { [weak self] message, error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK sendRichTextMessage " + (error?.localizedDescription ?? "no error") + ) + + if error != nil { + self?.showErrorToast(error) + } + self?.chatInputView.titleField.text = nil self?.chatInputView.textView.text = nil self?.didSendFinishAndCheckoutInput() @@ -899,28 +1092,43 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD guard let content = text, content.count > 0 else { return } + translateLanguageView.changeToIdleState(true) let remoteExt = chatInputView.getAtRemoteExtension(attribute) + var firstAIUserAccid = chatInputView.nickAccidList.first(where: { NEAIUserManager.shared.isAIUser($0) }) + if NEAIUserManager.shared.isAIUser(viewModel.sessionId) { + firstAIUserAccid = viewModel.sessionId + } chatInputView.clearAtCache() if viewModel.isReplying, let msg = viewModel.operationModel?.message { - viewModel.replyMessageWithoutThread(message: MessageUtils.textMessage(text: content, remoteExt: remoteExt), replyMessage: msg) { [weak self] message, error in + viewModel.replyMessageWithoutThread(message: MessageUtils.textMessage(text: content, remoteExt: remoteExt), + replyMessage: msg, + aiUserAccid: firstAIUserAccid) { [weak self] message, error in NEALog.infoLog( ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK replyMessage " + (error?.localizedDescription ?? "no error") ) + if error != nil { self?.showErrorToast(error) } + self?.didSendFinishAndCheckoutInput() } closeReply(button: nil) } else { - viewModel.sendTextMessage(text: content, remoteExt: remoteExt) { [weak self] message, error in + viewModel.sendTextMessage(text: content, + remoteExt: remoteExt, + aiUserAccid: firstAIUserAccid) { [weak self] message, error in NEALog.infoLog( ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendTextMessage " + (error?.localizedDescription ?? "no error") ) - self?.showErrorToast(error) + + if error != nil { + self?.showErrorToast(error) + } + self?.chatInputView.titleField.text = nil self?.chatInputView.textView.text = nil self?.didSendFinishAndCheckoutInput() @@ -968,9 +1176,15 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD showFileAction() } else if let type = cell.cellData?.type, type == .rtc { showRtcCallAction() - } else {} + } else if cell.cellData?.type == .translate { + showTranslateView() + } else if cell.cellData?.type == .photo { + openPhoto() + } } + open func openPhoto() {} + open func showTakePicture() { showBottomVideoAction(self, false) } @@ -994,13 +1208,24 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD showActionSheet([videoCallAction, audioCallAction, cancelAction]) } + /// 显示翻译UI + open func showTranslateView() { + if translateLanguageViewHeightAnchor?.constant == 0 { + translateLanguageViewHeightAnchor?.constant = 70 +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, +// execute: DispatchWorkItem(block: { [weak self] in +// self?.scrollTableViewToBottom() +// })) + } + } + /// 跳转音视频呼叫页面 /// - Parameter type: 呼叫类型,1 - 音频;2 - 视频 func useToCallViewRouter(_ type: Int) { // 校验配置项 - if !IMKitConfigCenter.shared.strangerCallEnable, + if !IMKitConfigCenter.shared.enableOnlyFriendCall, !NEFriendUserCache.shared.isFriend(viewModel.sessionId) { - viewModel.insertTipMessage(chatLocalizable("disable_stranger_call"), viewModel.conversationId) + viewModel.insertTipMessage(chatLocalizable("disable_stranger_call")) return } @@ -1010,7 +1235,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD param["remoteShowName"] = titleContent param["type"] = NSNumber(integerLiteral: type) - if let user = NEFriendUserCache.shared.getFriendInfo(viewModel.sessionId) ?? ChatUserCache.shared.getUserInfo(viewModel.sessionId) { + if let user = ChatMessageHelper.getUserFromCache(viewModel.sessionId) { param["remoteAvatar"] = user.user?.avatar } @@ -1026,15 +1251,26 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD open func textChanged(text: String) -> Bool { if text == "@" { + // 校验配置项 + if !IMKitConfigCenter.shared.enableAtMessage { + return true + } + // 做p2p类型判断 if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) == .CONVERSATION_TYPE_P2P { - return true + // 非数字人会话可以 @ 数字人 + if IMKitConfigCenter.shared.enableAIUser, + !NEAIUserManager.shared.isAIUser(viewModel.sessionId) { + DispatchQueue.main.async { + self.showUserSelectVC(showTeamMembers: false) + } + } } else { DispatchQueue.main.async { - self.showUserSelectVC(text: text) + self.showUserSelectVC(showTeamMembers: true) } - return true } + return true } else { return true @@ -1093,6 +1329,10 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD return true } + public func textViewDidChange() { + translateLanguageView.changeToIdleState() + } + open func textFieldDidEndEditing(_ text: String?) { checkAndSendTypingState(endEdit: true) } @@ -1115,7 +1355,8 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD } open func willSelectItem(button: UIButton?, index: Int) { - operationView?.removeFromSuperview() + removeOperationView() + if index == 2 || button?.isSelected == true { if index == 0 { // 语音 @@ -1234,12 +1475,20 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD if isFile == true { copyFileToSend(url: videoUrl, displayName: imageName) } else { - viewModel.sendVideoMessage(url: videoUrl, name: imageName, width: imageWidth, height: imageHeight, duration: videoDuration) { error in + viewModel.sendVideoMessage(url: videoUrl, + name: imageName, + width: imageWidth, + height: imageHeight, + duration: videoDuration) { [weak self] message, error, progress in NEALog.infoLog( ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendVideoMessage " + (error?.localizedDescription ?? "no error") ) weakSelf?.showErrorToast(error) + + if progress > 0, progress <= 100 { + self?.setModelProgress(message, progress) + } } } return @@ -1288,12 +1537,17 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD 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 + viewModel.sendFileMessage(filePath: imageUrl.relativePath, + displayName: imageName) { [weak self] message, error, progress in NEALog.infoLog( ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendFileMessage" + (error?.localizedDescription ?? "no error") ) self?.showErrorToast(error) + + if progress > 0, progress <= 100 { + self?.setModelProgress(message, progress) + } } } } else { @@ -1390,12 +1644,17 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD 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 + viewModel.sendFileMessage(filePath: desPath, + displayName: displayName) { [weak self] message, error, progress in NEALog.infoLog( ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendFileMessage " + (error?.localizedDescription ?? "no error") ) self?.showErrorToast(error) + + if progress > 0, progress <= 100 { + self?.setModelProgress(message, progress) + } } } } @@ -1433,47 +1692,6 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD controller.dismiss(animated: true) } - // 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) - self?.titleContent = showName - self?.title = showName - })) - } - } - - /// 用户信息变更回调 - /// - Parameter users: 用户列表 - public func onUserProfileChanged(_ users: [V2NIMUser]) { - for user in users { - guard let accountId = user.accountId else { - return - } - - if !NEFriendUserCache.shared.isFriend(accountId) { - ChatUserCache.shared.updateUserInfo(user) - } - - onUserOrFriendInfoChanged(accountId) - } - } - - /// 好友信息变更回调 - /// - Parameter friendInfo: 好友信息 - public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { - guard let accountId = friendInfo.accountId else { - return - } - onUserOrFriendInfoChanged(accountId) - } - // MARK: - ChatviewModelDelegate /// 本端即将发送消息状态回调,此时消息还未发送,可对消息进行修改或者拦截发送 @@ -1491,7 +1709,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD /// 收到消息 /// - Parameter messages: 消息列表 open func onRecvMessages(_ messages: [V2NIMMessage], _ indexs: [IndexPath]) { - operationView?.removeFromSuperview() + removeOperationView() insertRows(indexs) // 如果当前页面是活跃状态,发送已读回执 @@ -1519,6 +1737,11 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD /// - Parameter message: 消息 public func sendSuccess(_ message: V2NIMMessage, _ index: IndexPath) { tableViewReloadIndexs([index]) + + // 提示【大模型请求响应中...】 + if message.aiConfig != nil, message.aiConfig?.aiStatus == .MESSAGE_AI_STATUS_AT { + showToast(chatLocalizable("ai_request_ing")) + } } public func onLoadMoreWithMessage(_ indexs: [IndexPath]) { @@ -1530,7 +1753,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD return } - operationView?.removeFromSuperview() + removeOperationView() tableViewReloadIndexs(reloadIndex) { [weak self] in for index in reloadIndex { if let numberOfRows = self?.tableView.numberOfRows(inSection: 0), index.row == numberOfRows - 1 { @@ -1570,7 +1793,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD return } viewModel.selectedMessages.removeAll(where: { $0.messageClientId == message.messageClientId }) - operationView?.removeFromSuperview() + removeOperationView() NEALog.infoLog(className(), desc: "on revoke message at indexs \(atIndexs)") tableViewReloadIndexs(atIndexs) { [weak self] in for index in atIndexs { @@ -1599,11 +1822,15 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD } open func tableViewDeleteIndexs(_ indexs: [IndexPath]) { + if isLoadingData { + return + } + tableView.deleteData(indexs) } open func tableViewReloadIndexs(_ indexs: [IndexPath], _ completion: (() -> Void)? = nil) { - if isUploadingData { + if isLoadingData { return } @@ -1856,6 +2083,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD temMutaString.append(spaceStr) mutaString.insert(temMutaString, at: location) + chatInputView.nickAccidList.append(accid.count > 0 ? accid : "ait_all") chatInputView.nickAccidDic[addText] = accid.count > 0 ? accid : "ait_all" chatInputView.textView.attributedText = mutaString chatInputView.textView.selectedRange = NSMakeRange(selectRange.location + temMutaString.length, 0) @@ -1871,6 +2099,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD selectRange = NSMakeRange(selectRange.location - 1, selectRange.length) } + chatInputView.nickAccidList.append(accid.count > 0 ? accid : "ait_all") chatInputView.nickAccidDic[addText] = accid.count > 0 ? accid : "ait_all" chatInputView.textView.attributedText = mutaString @@ -1879,18 +2108,18 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD } /// 获取@列表视图控制器 - 基类 - func getUserSelectVC() -> NEBaseSelectUserViewController { - NEBaseSelectUserViewController(sessionId: viewModel.sessionId, showSelf: false) + func getUserSelectVC(showTeamMembers: Bool) -> NEBaseSelectUserViewController { + NEBaseSelectUserViewController(conversationId: viewModel.conversationId, showSelf: false, showTeamMembers: showTeamMembers) } - private func showUserSelectVC(text: String) { - let selectVC = getUserSelectVC() + private func showUserSelectVC(showTeamMembers: Bool) { + let selectVC = getUserSelectVC(showTeamMembers: showTeamMembers) selectVC.modalPresentationStyle = .formSheet selectVC.selectedBlock = { [weak self] index, model in var addText = "" var accid = "" - if model == nil { + if model == nil, showTeamMembers { addText += chatLocalizable("user_select_all") } else { if let m = model { @@ -1920,6 +2149,9 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD // MARK: - MessageOperationViewDelegate open func didSelectedItem(item: OperationItem) { + removeOperationView() + + // 配置项拦截 if let popMenuClick = NEKitChatConfig.shared.ui.popMenuClick { popMenuClick(item) return @@ -1951,6 +2183,10 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD case .collection: toCollectMessage() default: + if let onClick = item.onClick { + onClick(self) + } + customOperation() } } @@ -1959,6 +2195,13 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD open func copyMessage() { if let model = viewModel.operationModel as? MessageTextModel { + // 划词 + if let text = model.selectText() { + UIPasteboard.general.string = text + showToast(commonLocalizable("copy_success")) + return + } + if let text = model.message?.text, !text.isEmpty { UIPasteboard.general.string = text showToast(commonLocalizable("copy_success")) @@ -1990,16 +2233,24 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD open func showReplyMessageView(isReEdit: Bool = false) { viewModel.isReplying = true + if chatInputView.chatInpuMode != .multipleReturn { view.addSubview(replyView) - replyView.closeButton.addTarget(self, action: #selector(closeReply), for: .touchUpInside) - replyView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - replyView.leadingAnchor.constraint(equalTo: chatInputView.leadingAnchor), - replyView.trailingAnchor.constraint(equalTo: chatInputView.trailingAnchor), - replyView.bottomAnchor.constraint(equalTo: chatInputView.topAnchor), - replyView.heightAnchor.constraint(equalToConstant: 36), - ]) + if IMKitConfigCenter.shared.enableAIUser { + NSLayoutConstraint.activate([ + replyView.leadingAnchor.constraint(equalTo: translateLanguageView.leadingAnchor), + replyView.trailingAnchor.constraint(equalTo: translateLanguageView.trailingAnchor), + replyView.bottomAnchor.constraint(equalTo: translateLanguageView.topAnchor), + replyView.heightAnchor.constraint(equalToConstant: 36), + ]) + } else { + NSLayoutConstraint.activate([ + replyView.leadingAnchor.constraint(equalTo: chatInputView.leadingAnchor), + replyView.trailingAnchor.constraint(equalTo: chatInputView.trailingAnchor), + replyView.bottomAnchor.constraint(equalTo: chatInputView.topAnchor), + replyView.heightAnchor.constraint(equalToConstant: 36), + ]) + } } if let message = viewModel.operationModel?.message { @@ -2014,13 +2265,13 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD } } else { var text = chatLocalizable("msg_reply") - if let uid = message.senderId { - var showName = ChatTeamCache.shared.getShowName(uid, false) + if let uid = ChatMessageHelper.getSenderId(message) { + var showName = NETeamUserManager.shared.getShowName(uid, false) if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) != .CONVERSATION_TYPE_P2P, !IMKitClient.instance.isMe(uid) { addToAtUsers(addText: "@" + showName + "", isReply: true, accid: uid) } - showName = ChatTeamCache.shared.getShowName(uid) + showName = NETeamUserManager.shared.getShowName(uid) text += " " + showName text += ": \(ChatMessageHelper.contentOfMessage(message))" replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, @@ -2328,7 +2579,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard indexPath.row < viewModel.messages.count else { return NEBaseChatMessageCell() } - var model = viewModel.messages[indexPath.row] + let model = viewModel.messages[indexPath.row] var reuseId = "\(NEBaseChatMessageCell.self)" if model.replyedModel?.isReplay == true, model.isRevoked == false { @@ -2358,6 +2609,12 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD } } + var isSend = model.message?.isSelf ?? false + // 数字人回复的消息 + if ChatMessageHelper.isAISender(model.message) { + isSend = false + } + let cell = tableView.dequeueReusableCell(withIdentifier: reuseId, for: indexPath) if let c = cell as? NEBaseChatMessageTipCell { if let m = model as? MessageTipsModel { @@ -2385,13 +2642,13 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD } if let m = model as? MessageContentModel { - c.setModel(m, m.message?.isSelf ?? false) + c.setModel(m, isSend) c.setSelect(m, isMutilSelect) } return c } else if let c = cell as? NEChatBaseCell, let m = model as? MessageContentModel { - c.setModel(m, m.message?.isSelf ?? false) + c.setModel(m, isSend) return cell } else { return NEBaseChatMessageCell() @@ -2416,7 +2673,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD return } - operationView?.removeFromSuperview() + removeOperationView() if chatInputView.textView.isFirstResponder { chatInputView.textView.resignFirstResponder() } else { @@ -2448,10 +2705,10 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD // 预加载 let leaveCount = 10 // 剩余多少行开始预加载 weak var weakSelf = self - if indexPath.row <= leaveCount, !isUploadingData, !uploadHasNoMore { + if indexPath.row <= leaveCount, !isLoadingData, !uploadHasNoMore { // 上拉预加载更多 - if !isUploadingData { - isUploadingData = true + if !isLoadingData { + isLoadingData = true viewModel.dropDownRemoteRefresh { error, count, messages in if let err = error { NEALog.errorLog( @@ -2481,7 +2738,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD animated: false ) } - weakSelf?.isUploadingData = false + weakSelf?.isLoadingData = false } } } @@ -2492,7 +2749,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD // MARK: - UIScrollViewDelegate open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - operationView?.removeFromSuperview() + removeOperationView(false) } // MARK: CLLocationManagerDelegate @@ -2556,17 +2813,35 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD MultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) } - open func expandMoreAction() { - var data = NEChatUIKitClient.instance.getMoreActionData(sessionType: V2NIMConversationIdUtil.conversationType(viewModel.conversationId)) + /// 输入框【更多】按钮点击事件,子类重写 + @discardableResult + open func expandMoreAction() -> [NEMoreItemModel] { + var items = NEChatUIKitClient.instance.getMoreActionData(sessionType: V2NIMConversationIdUtil.conversationType(viewModel.conversationId)) if NEChatKitClient.instance.delegate == nil { - data = data.filter { item in + items = items.filter { item in if item.type == .location { return false } return true } } - chatInputView.chatAddMoreView.configData(data: NEChatUIKitClient.instance.getMoreActionData(sessionType: V2NIMConversationIdUtil.conversationType(viewModel.conversationId))) + + if NEAIUserManager.shared.isAIUser(viewModel.sessionId) { + items = items.filter { item in + if item.type == .rtc { + return false + } + return true + } + } + + if NEAIUserManager.shared.getAITranslateUser()?.accountId?.count ?? 0 <= 0 { + items.removeAll { model in + model.type == .translate + } + } + + return items } open func showTextViewController(_ model: MessageContentModel?) { @@ -2608,7 +2883,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD case .custom: didTapCustomMessage(model, replyIndex) default: - if replyIndex != nil, model?.type == .text || model?.type == .reply { + if (replyIndex ?? -1) > -1, model?.type == .text || model?.type == .reply { showTextViewController(model) } else { print(#function + "message did tap, type:\(String(describing: model?.type.rawValue))") @@ -2828,7 +3103,13 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD return } - fileCell?.setModel(fileModel, fileModel.message?.isSelf ?? false) + var isSend = fileModel.message?.isSelf ?? false + // 数字人回复的消息 + if ChatMessageHelper.isAISender(fileModel.message) { + isSend = false + } + + fileCell?.setModel(fileModel, isSend) viewModel.downLoad(urlString, path) { [weak self] progress in NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "downLoad file progress: \(progress)") @@ -2839,7 +3120,7 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD fileModel.state = .Downalod fileModel.progress = progress - fileCell?.uploadProgress(byRight: fileModel.message?.isSelf ?? true, progress) + fileCell?.uploadProgress(progress) } _: { [weak self] localPath, error in self?.showErrorToast(error) @@ -2848,6 +3129,75 @@ open class ChatViewController: NEChatBaseViewController, UINavigationControllerD } } } + + /// 设置(视频、文件)消息模型(上传、下载)进度 + /// - Parameters: + /// - message: 消息 + /// - progress:(上传、下载)进度 + func setModelProgress(_ message: V2NIMMessage?, _ progress: UInt) { + for (i, model) in viewModel.messages.enumerated() { + if model.message?.messageClientId == message?.messageClientId, + let model = model as? MessageVideoModel { + if let cell = tableView.cellForRow(at: IndexPath(row: i, section: 0)) as? NEBaseChatMessageCell { + model.cell = cell + } + model.setModelProgress(progress) + } + } + } + + /// 弹出语言选择 + open func showLanguageContentController(_ controller: NEBaseSelectLanguageViewController) { + controller.modalPresentationStyle = .custom + controller.transitioningDelegate = self + controller.delegate = self + present(controller, animated: true, completion: nil) + } + + // MARK: - NEBaseTranslateLanguageView Delegate + + public func didUseTranslate(_ content: String) { + chatInputView.textView.text = content + } + + /// 开始翻译回调 + public func didStartClick() { + if let contentString = chatInputView.getRealSendText(chatInputView.textView.attributedText) { + viewModel.translateLanguage(contentString, targetLanguage: translateLanguageView.currentLanguage) { [weak self] error in + if error != nil { + if let code = error?.code { + let content = ChatMessageHelper.getAIErrorMsage(code) + if let content = content, !content.isEmpty { + self?.showToast(content) + } else if let errMsg = error?.localizedDescription { + self?.showToast(errMsg) + } + } + self?.translateLanguageView.changeToIdleState() + } + } + } + } + + public func didChangeViewHeight(_ translateView: NEAITranslateView, _ changeHeight: CGFloat) { + translateLanguageViewHeightAnchor?.constant = changeHeight + } + + public func didSwitchLanguageClick(_ currentLanguage: String?) { + print("translateLanguageViewDidClickSwitchLanguage ", currentLanguage as Any) + } + + public func didCloseClick(_ view: NEAITranslateView) { + translateLanguageViewHeightAnchor?.constant = 0 + } + + // MARK: - Select Language Delegate + + public func didSelectLanguage(_ language: String?, _ controller: UIViewController?) { + if let currentLanguage = language { + translateLanguageView.currentLanguage = currentLanguage + } + } } // MARK: - TopMessageViewDelegate @@ -2897,6 +3247,12 @@ extension ChatViewController: TopMessageViewDelegate { } } +extension ChatViewController: UIViewControllerTransitioningDelegate { + public func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + CustomPresentationController(presentedViewController: presented, presenting: presenting) + } +} + // MARK: - NEMutilSelectBottomViewDelegate extension ChatViewController: NEMutilSelectBottomViewDelegate { @@ -3103,13 +3459,33 @@ extension ChatViewController: ChatBaseCellDelegate { var addText = "" var accid = "" - if let m = model, let senderId = m.message?.senderId { - accid = senderId - let name = ChatTeamCache.shared.getShowName(senderId, false) - addText += name - addText = "@" + addText + "" + if let m = model { + if let senderId = ChatMessageHelper.getSenderId(m.message) { + accid = senderId + let name = NETeamUserManager.shared.getShowName(senderId, false) + addText += name + addText = "@" + addText + "" + + addToAtUsers(addText: addText, accid: accid, true) + } + } + } + + /// 点击消息发送者头像 + /// 拉取最新用户信息后刷新消息发送者信息 + /// - Parameter noti: 通知对象 + func didTapHeader(_ noti: Notification) { + if let user = noti.object as? NEUserWithFriend, + let accid = user.user?.accountId { + if NEFriendUserCache.shared.isFriend(accid) { + NEFriendUserCache.shared.updateFriendInfo(user) + } else if NETeamUserManager.shared.getUserInfo(accid) != nil { + NETeamUserManager.shared.updateUserInfo(userWithFriend: user) + } else if NEP2PChatUserCache.shared.getUserInfo(accid) != nil { + NEP2PChatUserCache.shared.updateUserInfo(user) + } - addToAtUsers(addText: addText, accid: accid, true) + onUserOrFriendInfoChanged(accid) } } @@ -3184,7 +3560,7 @@ extension ChatViewController: ChatBaseCellDelegate { if index >= 0 { ChatDeduplicationHelper.instance.removeBlackTipSendedId(messageId: msg.messageClientId) - viewModel.sendMessage(message: msg) { _, error in + viewModel.sendMessage(message: msg) { _, error, pro in if let err = error { print("resend message error: \(err.localizedDescription)") } @@ -3247,6 +3623,7 @@ extension ChatViewController: ChatBaseCellDelegate { if text.last == " " { text = String(text.prefix(text.count - 1)) } + chatInputView.nickAccidList.append(key) chatInputView.nickAccidDic[text] = key } @@ -3301,6 +3678,16 @@ extension ChatViewController: ChatBaseCellDelegate { } } + /// 文本消息划词选中失去焦点 + /// - Parameters: + /// - cell: 所处位置的 cell + /// - model: 消息模型 + public func didTextViewLoseFocus(_ cell: UITableViewCell, _ model: MessageContentModel?) { + if viewModel.operationModel == model { + removeOperationView() + } + } + open func getReadView(_ message: V2NIMMessage, _ teamId: String) -> NEBaseReadViewController { ReadViewController(message: message, teamId: teamId) } @@ -3325,7 +3712,7 @@ extension ChatViewController: ChatBaseCellDelegate { execute: DispatchWorkItem(block: { [weak self] in self?.scrollTableViewToBottom() })) - operationView?.removeFromSuperview() + removeOperationView() } open func didHideMultipleButtonClick() { @@ -3351,7 +3738,7 @@ extension ChatViewController: ChatBaseCellDelegate { /// 显示被离开群弹框 open func showLeaveTeamAlert() { - if IMKitConfigCenter.shared.dismissTeamDeleteConversation == false { + if IMKitConfigCenter.shared.enabledismissTeamDeleteConversation == false { return } showSingleAlert(message: chatLocalizable("team_has_been_quit")) { [weak self] in @@ -3361,7 +3748,7 @@ extension ChatViewController: ChatBaseCellDelegate { /// 显示群被解散弹框 open func showDismissTeamAlert() { - if IMKitConfigCenter.shared.dismissTeamDeleteConversation == false { + if IMKitConfigCenter.shared.enabledismissTeamDeleteConversation == false { return } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/CustomPresentationController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/CustomPresentationController.swift new file mode 100644 index 00000000..cc302e1c --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/CustomPresentationController.swift @@ -0,0 +1,78 @@ +//// 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 + +final class CustomPresentationController: UIPresentationController { + // MARK: - Properties + + private lazy var dimmingView: UIView = { + let dimmingView = UIView() + dimmingView.translatesAutoresizingMaskIntoConstraints = false + dimmingView.backgroundColor = UIColor.black.withAlphaComponent(0.5) + let recognizer = UITapGestureRecognizer(target: self, + action: #selector(handleTap(recognizer:))) + dimmingView.addGestureRecognizer(recognizer) + return dimmingView + }() + + override var frameOfPresentedViewInContainerView: CGRect { + var frame: CGRect = .zero + frame.size = size(forChildContentContainer: presentedViewController, + withParentContainerSize: containerView!.bounds.size) + frame.origin.y = frame.size.height - 404 + return frame + } + + /// 重写开始动画 + override func presentationTransitionWillBegin() { + guard let containerView = containerView else { + return + } + containerView.insertSubview(dimmingView, at: 0) + NSLayoutConstraint.activate([ + dimmingView.topAnchor.constraint(equalTo: containerView.topAnchor), + dimmingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor), + dimmingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor), + dimmingView.bottomAnchor.constraint(equalTo: containerView.bottomAnchor), + ]) + + guard let coordinator = presentedViewController.transitionCoordinator else { + dimmingView.alpha = 1.0 + return + } + + coordinator.animate(alongsideTransition: { _ in + self.dimmingView.alpha = 1.0 + }) + } + + /// 重写结束动画 + override func dismissalTransitionWillBegin() { + guard let coordinator = presentedViewController.transitionCoordinator else { + dimmingView.alpha = 0.0 + return + } + coordinator.animate(alongsideTransition: { _ in + self.dimmingView.alpha = 0.0 + }) + } + + override func containerViewWillLayoutSubviews() { + presentedView?.frame = frameOfPresentedViewInContainerView + } + + override func size(forChildContentContainer container: UIContentContainer, + withParentContainerSize parentSize: CGSize) -> CGSize { + CGSize(width: parentSize.width, height: parentSize.height) + } +} + +// MARK: - Private + +private extension CustomPresentationController { + @objc func handleTap(recognizer: UITapGestureRecognizer) { + presentingViewController.dismiss(animated: true) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift index 24f3c8d2..a8bd6a86 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift @@ -33,6 +33,15 @@ open class MultiForwardViewController: NEChatBaseViewController, UINavigationCon tableView.dataSource = self tableView.backgroundColor = .clear tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() @@ -353,7 +362,7 @@ open class MultiForwardViewController: NEChatBaseViewController, UINavigationCon viewModel.downLoad(urlString, path) { progress in NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "downLoad file progress: \(progress)") fileModel.progress = progress - fileModel.cell?.uploadProgress(byRight: false, progress) + fileModel.cell?.uploadProgress(progress) } _: { [weak self] localPath, error in self?.showErrorToast(error) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseCollectionMessageController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseCollectionMessageController.swift index a166aeb7..ddb2b136 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseCollectionMessageController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseCollectionMessageController.swift @@ -12,6 +12,8 @@ import UIKit open class NEBaseCollectionMessageController: NEChatBaseViewController, UITableViewDelegate, UITableViewDataSource, CollectionMessageCellDelegate, UIDocumentInteractionControllerDelegate { var audioPlayer: AVAudioPlayer? // 仅用于语音消息的播放 + /// 收藏列表顶部约束 + public var contentTableTopAnchor: NSLayoutConstraint? /// 收藏列表 public lazy var contentTable: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) @@ -23,6 +25,15 @@ open class NEBaseCollectionMessageController: NEChatBaseViewController, UITableV tableView.backgroundColor = .clear tableView.mj_footer = MJRefreshAutoFooter(refreshingTarget: self, refreshingAction: #selector(loadMoreData)) tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() @@ -60,6 +71,11 @@ open class NEBaseCollectionMessageController: NEChatBaseViewController, UITableV super.init(coder: coder) } + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + contentTableTopAnchor?.constant = topConstant + } + override open func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) stopPlay() @@ -104,11 +120,12 @@ open class NEBaseCollectionMessageController: NEChatBaseViewController, UITableV /// UI初始化 open func setupUI() { title = chatLocalizable("operation_collection") - navigationView.navTitle.text = chatLocalizable("operation_collection") navigationView.moreButton.isHidden = true + view.addSubview(contentTable) + contentTableTopAnchor = contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + contentTableTopAnchor?.isActive = true NSLayoutConstraint.activate([ - contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight), contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -241,7 +258,7 @@ open class NEBaseCollectionMessageController: NEChatBaseViewController, UITableV /// 转发 /// - Parameter message: 消息 - open func forwardCollectionMessage(_ message: V2NIMMessage, _ senderName: String) { + open func forwardCollectionMessage(_ message: V2NIMMessage, _ conversationName: String) { weak var weakSelf = self Router.shared.register(ForwardMultiSelectedRouter) { param in var items = [ForwardItem]() @@ -262,7 +279,7 @@ open class NEBaseCollectionMessageController: NEChatBaseViewController, UITableV } let type = chatLocalizable("operation_forward") - weakSelf?.showForwardAlertController(items: items, type: type, conversationId: message.conversationId, senderName: senderName) { comment in + weakSelf?.showForwardAlertController(items: items, type: type, conversationId: message.conversationId, conversationName: conversationName) { comment in if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { weakSelf?.showToast(commonLocalizable("network_error")) return @@ -284,17 +301,18 @@ open class NEBaseCollectionMessageController: NEChatBaseViewController, UITableV /// - Parameters: /// - items: 转发对象 /// - type: 转发类型(合并转发/逐条转发/转发) + /// - conversationName: 会话名称 /// - sureBlock: 确认按钮点击回调 func showForwardAlertController(items: [ForwardItem], type: String, conversationId: String?, - senderName: String?, + conversationName: String?, _ sureBlock: ((String?) -> Void)? = nil) { let forwardAlert = getCollectionForwardAlertController() forwardAlert.setItems(items) forwardAlert.forwardType = type forwardAlert.sureBlock = sureBlock - if let name = senderName { + if let name = conversationName { forwardAlert.senderName = name } @@ -314,7 +332,7 @@ open class NEBaseCollectionMessageController: NEChatBaseViewController, UITableV return } if let message = model.message { - forwardCollectionMessage(message, model.senderName ?? "") + forwardCollectionMessage(message, model.conversationName ?? "") } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift index f85deef6..2fe1b0b3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift @@ -272,7 +272,7 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD commentTextFeild.heightAnchor.constraint(equalToConstant: 32), ]) - // 水平分割线 + // 水平分隔线 let verticalLine = UIView() verticalLine.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(verticalLine) @@ -284,7 +284,7 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD verticalLine.topAnchor.constraint(equalTo: commentTextFeild.bottomAnchor, constant: 24.0), ]) - // 竖直分割线 + // 竖直分隔线 let horizontalLine = UIView() horizontalLine.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(horizontalLine) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift index d19bd24f..48d7c186 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift @@ -31,6 +31,8 @@ open class NEBasePinMessageViewController: NEChatBaseViewController, UITableView /// 数据拉取标志 var isLoadingData = false + /// 内容列表 顶部布局约束 + public var tableViewTopAnchor: NSLayoutConstraint? /// 内容列表 public lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) @@ -41,6 +43,15 @@ open class NEBasePinMessageViewController: NEChatBaseViewController, UITableView tableView.dataSource = self tableView.backgroundColor = .clear tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() @@ -74,6 +85,11 @@ open class NEBasePinMessageViewController: NEChatBaseViewController, UITableView IMKitClient.instance.removeLoginListener(self) } + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableViewTopAnchor?.constant = topConstant + } + override open func viewDidLoad() { super.viewDidLoad() @@ -114,11 +130,12 @@ open class NEBasePinMessageViewController: NEChatBaseViewController, UITableView /// UI 初始化 func setupUI() { title = chatLocalizable("operation_pin") - navigationView.navTitle.text = chatLocalizable("operation_pin") navigationView.moreButton.isHidden = true + view.addSubview(tableView) + tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + tableViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight), tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -232,7 +249,6 @@ open class NEBasePinMessageViewController: NEChatBaseViewController, UITableView if let err = error { print(err.localizedDescription) } else { - weakSelf?.emptyView.isHidden = (weakSelf?.viewModel.items.count ?? 0) > 0 weakSelf?.showToast(chatLocalizable("cancel_pin_success")) } } @@ -394,11 +410,12 @@ open class NEBasePinMessageViewController: NEChatBaseViewController, UITableView return } - let indexs = indexs.filter { $0.row >= 0 && $0.row < viewModel.items.count } + let indexs = indexs.filter { $0.row >= 0 && $0.row < tableView.numberOfRows(inSection: 0) } if !indexs.isEmpty { - tableView.deleteData(indexs) - emptyView.isHidden = viewModel.items.count > 0 + tableView.deleteData(indexs) { [weak self] _ in + self?.emptyView.isHidden = (self?.viewModel.items.count ?? 0) > 0 + } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift index 85ced6a5..eb5aad5e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift @@ -17,17 +17,75 @@ open class NEBaseReadViewController: NEChatBaseViewController, UITableViewDelega private var message: V2NIMMessage private var teamId: String + /// 已读按钮 + public lazy var readButton: UIButton = { + let button = UIButton(type: .custom) + button.titleLabel?.font = UIFont.systemFont(ofSize: 14) + button.setTitle(chatLocalizable("read"), for: .normal) + button.setTitleColor(UIColor.ne_darkText, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(readButtonEvent), for: .touchUpInside) + button.accessibilityIdentifier = "id.tabHasRead" + return button + }() + + /// 未读按钮 + public lazy var unreadButton: UIButton = { + let button = UIButton(type: .custom) + button.titleLabel?.font = UIFont.systemFont(ofSize: 14) + button.setTitle(chatLocalizable("unread"), for: .normal) + button.setTitleColor(UIColor.ne_darkText, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(unreadButtonEvent), for: .touchUpInside) + button.accessibilityIdentifier = "id.tabUnRead" + return button + }() + /// 已读/未读 按钮下方横线的左侧布局约束 public var bottonBottomLineLeftAnchor: NSLayoutConstraint? /// 已读/未读 按钮下方横线 - lazy var bottonBottomLine: UIView = { + public lazy var bottonBottomLine: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false return view }() + /// 已读/未读 tab 视图顶部布局约束 + public var tabViewTopAnchor: NSLayoutConstraint? + /// 已读未读 tab 视图 + public lazy var tabView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(readButton) + NSLayoutConstraint.activate([ + readButton.topAnchor.constraint(equalTo: view.topAnchor), + readButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), + readButton.heightAnchor.constraint(equalToConstant: 48), + readButton.widthAnchor.constraint(equalToConstant: kScreenWidth / 2.0), + ]) + + view.addSubview(unreadButton) + NSLayoutConstraint.activate([ + unreadButton.topAnchor.constraint(equalTo: readButton.topAnchor), + unreadButton.leadingAnchor.constraint(equalTo: readButton.trailingAnchor), + unreadButton.trailingAnchor.constraint(equalTo: view.trailingAnchor), + unreadButton.heightAnchor.constraint(equalToConstant: 48), + ]) + + view.addSubview(bottonBottomLine) + bottonBottomLineLeftAnchor = bottonBottomLine.leadingAnchor.constraint(equalTo: view.leadingAnchor) + NSLayoutConstraint.activate([ + bottonBottomLine.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 0), + bottonBottomLine.heightAnchor.constraint(equalToConstant: 1), + bottonBottomLine.widthAnchor.constraint(equalTo: readButton.widthAnchor), + bottonBottomLineLeftAnchor!, + ]) + return view + }() + /// 已读 tableView - lazy var readTableView: UITableView = { + public lazy var readTableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) tableView.delegate = self tableView.dataSource = self @@ -35,11 +93,21 @@ open class NEBaseReadViewController: NEChatBaseViewController, UITableViewDelega tableView.sectionFooterHeight = 0 tableView.translatesAutoresizingMaskIntoConstraints = false tableView.separatorStyle = .none + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() - /// 已读 tableView - lazy var unreadTableView: UITableView = { + /// 未读 tableView + public lazy var unreadTableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) tableView.delegate = self tableView.dataSource = self @@ -48,31 +116,17 @@ open class NEBaseReadViewController: NEChatBaseViewController, UITableViewDelega tableView.translatesAutoresizingMaskIntoConstraints = false tableView.separatorStyle = .none tableView.isHidden = true - return tableView - }() + tableView.keyboardDismissMode = .onDrag - /// 已读按钮 - lazy var readButton: UIButton = { - let button = UIButton(type: .custom) - button.titleLabel?.font = UIFont.systemFont(ofSize: 14) - button.setTitle(chatLocalizable("read"), for: .normal) - button.setTitleColor(UIColor.ne_darkText, for: .normal) - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget(self, action: #selector(readButtonEvent), for: .touchUpInside) - button.accessibilityIdentifier = "id.tabHasRead" - return button - }() - - /// 未读按钮 - lazy var unreadButton: UIButton = { - let button = UIButton(type: .custom) - button.titleLabel?.font = UIFont.systemFont(ofSize: 14) - button.setTitle(chatLocalizable("unread"), for: .normal) - button.setTitleColor(UIColor.ne_darkText, for: .normal) - button.translatesAutoresizingMaskIntoConstraints = false - button.addTarget(self, action: #selector(unreadButtonEvent), for: .touchUpInside) - button.accessibilityIdentifier = "id.tabUnRead" - return button + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView }() /// 空视图 @@ -100,6 +154,11 @@ open class NEBaseReadViewController: NEChatBaseViewController, UITableViewDelega super.init(coder: coder) } + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tabViewTopAnchor?.constant = topConstant + } + override open func viewDidLoad() { super.viewDidLoad() commonUI() @@ -110,81 +169,38 @@ open class NEBaseReadViewController: NEChatBaseViewController, UITableViewDelega title = chatLocalizable("message_read") navigationView.moreButton.isHidden = true - view.addSubview(readButton) + view.addSubview(tabView) + tabViewTopAnchor = tabView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + tabViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ - readButton.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - readButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), - readButton.heightAnchor.constraint(equalToConstant: 48), - readButton.widthAnchor.constraint(equalToConstant: kScreenWidth / 2.0), + tabView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tabView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tabView.heightAnchor.constraint(equalToConstant: 48), ]) - view.addSubview(unreadButton) + view.addSubview(readTableView) NSLayoutConstraint.activate([ - unreadButton.topAnchor.constraint(equalTo: readButton.topAnchor), - unreadButton.leadingAnchor.constraint(equalTo: readButton.trailingAnchor), - unreadButton.trailingAnchor.constraint(equalTo: view.trailingAnchor), - unreadButton.heightAnchor.constraint(equalToConstant: 48), + readTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), + readTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + readTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + readTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - view.addSubview(bottonBottomLine) - bottonBottomLineLeftAnchor = bottonBottomLine.leadingAnchor.constraint(equalTo: view.leadingAnchor) + view.addSubview(unreadTableView) NSLayoutConstraint.activate([ - bottonBottomLine.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 0), - bottonBottomLine.heightAnchor.constraint(equalToConstant: 1), - bottonBottomLine.widthAnchor.constraint(equalTo: readButton.widthAnchor), - bottonBottomLineLeftAnchor!, + unreadTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), + unreadTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + unreadTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + unreadTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - view.addSubview(readTableView) - if #available(iOS 11.0, *) { - NSLayoutConstraint.activate([ - readTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), - readTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - readTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - readTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - ]) - } else { - NSLayoutConstraint.activate([ - readTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), - readTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - readTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - readTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - } - - view.addSubview(unreadTableView) - if #available(iOS 11.0, *) { - NSLayoutConstraint.activate([ - unreadTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), - unreadTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - unreadTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - unreadTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - ]) - } else { - NSLayoutConstraint.activate([ - unreadTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), - unreadTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - unreadTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - unreadTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - } - view.addSubview(emptyView) - if #available(iOS 11.0, *) { - NSLayoutConstraint.activate([ - emptyView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), - emptyView.leftAnchor.constraint(equalTo: view.leftAnchor), - emptyView.rightAnchor.constraint(equalTo: view.rightAnchor), - emptyView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), - ]) - } else { - NSLayoutConstraint.activate([ - emptyView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), - emptyView.leftAnchor.constraint(equalTo: view.leftAnchor), - emptyView.rightAnchor.constraint(equalTo: view.rightAnchor), - emptyView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - } + NSLayoutConstraint.activate([ + emptyView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), + emptyView.leftAnchor.constraint(equalTo: view.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: view.rightAnchor), + emptyView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) } /// 加载数据 diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectLanguageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectLanguageViewController.swift new file mode 100644 index 00000000..05c1bb80 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectLanguageViewController.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 SelectLanguageDelegate: NSObjectProtocol { + /// 语言选择回调 + /// - Parameter language: 语言 + /// - Parameter controller: 控制器 + @objc optional func didSelectLanguage(_ language: String?, _ controller: UIViewController?) +} + +@objcMembers +open class NEBaseSelectLanguageViewController: NEChatBaseViewController, UITableViewDelegate, UITableViewDataSource { + /// 数据模型 + public let viewModel = SelectLanguageViewModel() + + /// 代理协议 + public weak var delegate: SelectLanguageDelegate? + + /// 当前选择语言,从外部传入 + public var currentContent = "" + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + viewModel.datas.count + } + + open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 48 + } + + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if let cell = tableView.dequeueReusableCell(withIdentifier: "\(NEBaseLanguageCell.self)", for: indexPath) as? NEBaseLanguageCell { + let model = viewModel.datas[indexPath.row] + model.isSelect = (model.language == currentContent) + cell.configureData(model) + return cell + } + let cell = UITableViewCell() + return cell + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let model = viewModel.datas[indexPath.row] + delegate?.didSelectLanguage?(model.language, self) + dismiss(animated: true) + } + + override open func viewDidLoad() { + super.viewDidLoad() + setupLanguageUI() + } + + /// 语言列表 + public lazy var languageTableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.backgroundColor = .clear + tableView.delegate = self + tableView.dataSource = self + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + + /// UI 初始化 + open func setupLanguageUI() { + navigationController?.isNavigationBarHidden = true + navigationView.isHidden = true + } + + open func cancelClick() { + dismiss(animated: true) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift index 09052f37..853092b4 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift @@ -5,6 +5,7 @@ import NEChatKit import NECoreIM2Kit +import NIMSDK import UIKit public typealias DidSelectedAtRow = (_ index: Int, _ model: NETeamMemberInfoModel?) -> Void @@ -13,24 +14,27 @@ public typealias DidSelectedAtRow = (_ index: Int, _ model: NETeamMemberInfoMode open class NEBaseSelectUserViewController: NEChatBaseViewController, UITableViewDelegate, UITableViewDataSource { public var tableView = UITableView(frame: .zero, style: .plain) - public var sessionId: String + public var conversationId: String public var viewModel = TeamMemberSelectVM() public var selectedBlock: DidSelectedAtRow? var teamInfo: NETeamInfoModel? //// 是否展示自己 private var showSelf = true + private var showTeamMembers: Bool = false var className = "SelectUserViewController" var isShowAtAll = true - init(sessionId: String, showSelf: Bool = true) { - self.sessionId = sessionId + init(conversationId: String, showSelf: Bool = true, showTeamMembers: Bool = false) { + self.conversationId = conversationId self.showSelf = showSelf + self.showTeamMembers = showTeamMembers super.init(nibName: nil, bundle: nil) } public required init?(coder: NSCoder) { - sessionId = "" + conversationId = "" showSelf = true + showTeamMembers = false super.init(coder: coder) } @@ -90,6 +94,17 @@ open class NEBaseSelectUserViewController: NEChatBaseViewController, UITableView tableView.translatesAutoresizingMaskIntoConstraints = false tableView.separatorStyle = .none tableView.tableFooterView = UIView() + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + view.addSubview(tableView) if #available(iOS 11.0, *) { @@ -112,7 +127,33 @@ open class NEBaseSelectUserViewController: NEChatBaseViewController, UITableView } func loadData() { - viewModel.fetchTeamMembers(sessionId) { [weak self] error, team in + // 数字人列表 + var aiUserMembers = [NETeamMemberInfoModel]() + if IMKitConfigCenter.shared.enableAIUser { + let aiUsers = NEAIUserManager.shared.getAIChatUserList() + aiUserMembers = aiUsers.map { user in + let teamMember = NETeamMemberInfoModel() + teamMember.nimUser = NEUserWithFriend(user: user) + return teamMember + } + } + + if !showTeamMembers { + let team = NETeamInfoModel() + team.users = aiUserMembers + teamInfo = team + isShowAtAll = false + tableView.reloadData() + return + } + + guard V2NIMConversationIdUtil.conversationType(conversationId) == .CONVERSATION_TYPE_TEAM, + let teamId = V2NIMConversationIdUtil.conversationTargetId(conversationId) else { + return + } + + // 获取群成员列表 + viewModel.getTeamMembers(teamId) { [weak self] error, team in NEALog.infoLog( ModuleName + " " + (self?.className ?? "SelectUserViewController"), desc: "CALLBACK fetchTeamMembers " + (error?.localizedDescription ?? "no error") @@ -165,10 +206,30 @@ open class NEBaseSelectUserViewController: NEChatBaseViewController, UITableView (m1.teamMember?.joinTime ?? 0) < (m2.teamMember?.joinTime ?? 0) }) + // 管理员列表过滤重复的数字人 + managers = managers.filter { member in + if aiUserMembers.contains(where: { $0.nimUser?.user?.accountId == member.nimUser?.user?.accountId + }) { + return false + } else { + return true + } + } + + // 普通成员列表过滤重复的数字人 + normals = normals.filter { member in + if aiUserMembers.contains(where: { $0.nimUser?.user?.accountId == member.nimUser?.user?.accountId + }) { + return false + } else { + return true + } + } + if let owner = owner { - team?.users = [owner] + managers + normals + team?.users = aiUserMembers + [owner] + managers + normals } else { - team?.users = managers + normals + team?.users = aiUserMembers + managers + normals } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift index a932f27d..248179cd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift @@ -42,6 +42,7 @@ open class NEBaseUserSettingViewController: NEChatBaseViewController, UserSettin return label }() + public var contentTableTopAnchor: NSLayoutConstraint? public lazy var contentTable: UITableView = { let contentTable = UITableView() contentTable.translatesAutoresizingMaskIntoConstraints = false @@ -71,6 +72,11 @@ open class NEBaseUserSettingViewController: NEChatBaseViewController, UserSettin super.init(coder: coder) } + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + contentTableTopAnchor?.constant = topConstant + } + override open func viewDidLoad() { super.viewDidLoad() viewModel.delegate = self @@ -96,10 +102,11 @@ open class NEBaseUserSettingViewController: NEChatBaseViewController, UserSettin navigationView.backgroundColor = .ne_lightBackgroundColor view.addSubview(contentTable) + contentTableTopAnchor = contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + contentTableTopAnchor?.isActive = true 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), ]) @@ -144,12 +151,12 @@ open class NEBaseUserSettingViewController: NEChatBaseViewController, UserSettin nameLabel.text = viewModel.userInfo?.showName() cornerBackView.addSubview(nameLabel) - if IMKitConfigCenter.shared.teamEnable { + if IMKitConfigCenter.shared.enableTeam { NSLayoutConstraint.activate([ 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), + userHeaderView.widthAnchor.constraint(equalToConstant: fun_chat_min_h), + userHeaderView.heightAnchor.constraint(equalToConstant: fun_chat_min_h), ]) nameLabel.font = NEConstant.defaultTextFont(12) @@ -168,6 +175,7 @@ open class NEBaseUserSettingViewController: NEChatBaseViewController, UserSettin addButton.widthAnchor.constraint(equalToConstant: 42.0), addButton.heightAnchor.constraint(equalToConstant: 42.0), ]) + } else { NSLayoutConstraint.activate([ userHeaderView.leftAnchor.constraint(equalTo: cornerBackView.leftAnchor, constant: 16), @@ -223,16 +231,29 @@ open class NEBaseUserSettingViewController: NEChatBaseViewController, UserSettin filters.insert(uid) } - Router.shared.use( - ContactUserSelectRouter, - parameters: [ - "nav": navigationController as Any, - "filters": filters, - "limit": inviteNumberLimit, - "uid": userId ?? "", - ], - closure: nil - ) + if IMKitConfigCenter.shared.enableAIUser { + Router.shared.use( + ContactFusionSelectRouter, + parameters: [ + "nav": navigationController as Any, + "filters": filters, + "limit": inviteNumberLimit, + "uid": userId ?? "", + ], + closure: nil + ) + } else { + Router.shared.use( + ContactUserSelectRouter, + parameters: [ + "nav": navigationController as Any, + "filters": filters, + "limit": inviteNumberLimit, + "uid": userId ?? "", + ], + closure: nil + ) + } Router.shared.register(TeamCreateDiscussResult) { param in print("create discuss ", param) @@ -277,6 +298,10 @@ open class NEBaseUserSettingViewController: NEChatBaseViewController, UserSettin showToast(error.localizedDescription) } + func didShowErrorMsg(_ msg: String) { + showToast(msg) + } + // MARK: UITableViewDataSource, UITableViewDelegate open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift index 0eef48d7..39df506d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift @@ -89,7 +89,7 @@ open class EmojiPageView: UIView { func reloadPage() { // reload时候记录上次位置 // guard let cPage = currentPage else { -// NEALog.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 2aad5db6..c1897036 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift @@ -142,7 +142,7 @@ open class InputEmoticonContainerView: UIView { return emotionsCount / layoutCount + 1 } } else { - NEALog.errorLog(classTag, desc: "❌count maybe nil") + NEALog.errorLog(classTag, desc: "count maybe nil") return 0 } } @@ -251,7 +251,7 @@ extension InputEmoticonContainerView { startX: CGFloat, startY: CGFloat, iconWidth: CGFloat, iconHeight: CGFloat, emotion: NIMInputEmoticonCatalog) { guard let layout = emotion.layout else { - NEALog.errorLog(classTag, desc: "❌emotion is nill") + NEALog.errorLog(classTag, desc: "emotion is nill") return } @@ -298,7 +298,7 @@ extension InputEmoticonContainerView: EmojiPageViewDelegate, EmojiPageViewDataSo var resultEmotion = NIMInputEmoticonCatalog() guard let totalData = totalCatalogData, let targetView = pageView else { - NEALog.errorLog(classTag, desc: "❌totalCatalogData is nil") + NEALog.errorLog(classTag, desc: "totalCatalogData is nil") return UIView() } @@ -336,7 +336,7 @@ extension InputEmoticonContainerView: InputEmoticonTabViewDelegate { extension InputEmoticonContainerView: NIMInputEmoticonButtonDelegate { open func selectedEmoticon(emotion: NIMInputEmoticon, catalogID: String) { guard let emotionId = emotion.emoticonID else { - NEALog.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 e18ce932..737adafa 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift @@ -65,7 +65,7 @@ open class InputEmoticonTabView: UIControl { seps.removeAll() guard let catalogs = emoticonCatalogs else { - NEALog.errorLog(className, desc: "❌emoticonCatalogs is nil") + NEALog.errorLog(className, desc: "emoticonCatalogs is nil") return } for catelog in catalogs { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionAttachment.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionAttachment.swift index 76afc50c..cb59675b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionAttachment.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionAttachment.swift @@ -11,7 +11,11 @@ open class NEEmotionAttachment: NSTextAttachment { public var emotion: NIMInputEmoticon? { set { _emotion = newValue - image = UIImage.ne_bundleImage(name: emotion?.fileName ?? "") + if NIMInputEmoticonManager.shared.isCustomEmojResource == false { + image = UIImage.ne_bundleImage(name: emotion?.fileName ?? "") + } else { + image = UIImage(named: emotion?.fileName ?? "") + } } get { _emotion diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift index da31610a..bb5089d0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift @@ -33,7 +33,7 @@ open class NEEmotionTool: NSObject { /// - offset: 偏移量 /// - Returns: 替换表情后的富文本 class func getAttWithStr(str: String, font: UIFont, - _ offset: CGPoint = CGPoint(x: 0, y: -3)) -> NSMutableAttributedString { + _ offset: CGPoint = CGPoint(x: 0, y: -4)) -> NSMutableAttributedString { let regularArr = getRegularArray(str: str) let emoticons = NIMInputEmoticonManager.shared .emoticonCatalog(catalogID: NIMKit_EmojiCatalog)?.emoticons @@ -75,4 +75,28 @@ open class NEEmotionTool: NSObject { textAttachment.bounds = CGRect(x: offset.x, y: offset.y, width: height, height: height) return NSAttributedString(attachment: textAttachment) } + + /// 将富文本中的表情转换为文本 + /// - Parameters: + /// - att: 富文本 + /// - range: 范围 + /// - Returns: 文本 + class func getTextWithAtt(_ att: NSMutableAttributedString?, _ range: NSRange) -> String? { + guard let att = att else { + return nil + } + + var text = "" + att.enumerateAttributes(in: range, using: { attrs, range, stop in + if let attachment = attrs[NSAttributedString.Key.attachment] as? NEEmotionAttachment { + text += attachment.emotion?.tag ?? "" + } else { + let subStr = (att.string as NSString).substring(with: range) + text += subStr + } + + }) + + return text.isEmpty ? nil : text + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift index 9fce7a97..ef377602 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift @@ -71,12 +71,31 @@ open class NIMInputEmoticonManager: NSObject { private var catalogs: [NIMInputEmoticonCatalog]? private var classTag = "NIMInputEmoticonManager" + /// 是否是外部资源 + private(set) var isCustomEmojResource = false + override public init() { super.init() parsePlist() preloadEmoticonResource() } + public func setCustomEmojConfig(_ array: NSArray) { + var catalogs = [NIMInputEmoticonCatalog]() + for dict in array { + if let convertDict = dict as? NSDictionary { + let info = convertDict["info"] as? [String: Any] + let emotions = convertDict["data"] as? NSArray + let cataLog = catalogByInfo( + info: info as NSDictionary?, + emoticonsArray: emotions + ) + } + } + self.catalogs = catalogs + isCustomEmojResource = true + } + func parsePlist() { var catalogs = [NIMInputEmoticonCatalog]() let filePath = Bundle.nim_EmojiPlistFile() @@ -101,7 +120,7 @@ open class NIMInputEmoticonManager: NSObject { let cataLog = NIMInputEmoticonCatalog() guard let infoDict = info, let emotions = emoticonsArray else { - NEALog.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 @@ -152,7 +171,7 @@ open class NIMInputEmoticonManager: NSObject { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { - NEALog.errorLog(classTag, desc: "❌catalogs is nil") + NEALog.errorLog(classTag, desc: "catalogs is nil") return emotion } @@ -172,7 +191,7 @@ open class NIMInputEmoticonManager: NSObject { open func emoticonByID(emoticonID: String) -> NIMInputEmoticon? { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { - NEALog.errorLog(classTag, desc: "❌catalogs is nil") + NEALog.errorLog(classTag, desc: "catalogs is nil") return emotion } @@ -192,7 +211,7 @@ open class NIMInputEmoticonManager: NSObject { open func emoticonByCatalogID(catalogID: String, emoticonID: String) -> NIMInputEmoticon? { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { - NEALog.errorLog(classTag, desc: "❌catalogs is nil") + NEALog.errorLog(classTag, desc: "catalogs is nil") return emotion } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift index 4af92d43..8c70f13b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift @@ -58,9 +58,12 @@ public class ChatMessageHelper: NSObject { return "" } if V2NIMConversationIdUtil.conversationType(conversationId) == .CONVERSATION_TYPE_P2P { + if NEAIUserManager.shared.isAIUser(sessionId) { + return NEAIUserManager.shared.getShowName(sessionId) ?? sessionId + } return NEFriendUserCache.shared.getShowName(sessionId) } else { - return ChatTeamCache.shared.getTeamInfo()?.name ?? "" + return NETeamUserManager.shared.getTeamInfo()?.name ?? sessionId } } @@ -225,11 +228,7 @@ public class ChatMessageHelper: NSObject { 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 { + NETeamUserManager.shared.getTeamMembers(accountIds: accIds) { completion(MessageTipsModel(message: message)) } } else { @@ -359,6 +358,45 @@ public class ChatMessageHelper: NSObject { } } + public static func getAIErrorMsage(_ errorCode: NSInteger) -> String? { + var content: String? + switch errorCode { + case failedOperation: + content = commonLocalizable("parameter_setting_error") + case rateLimitExceeded: + content = commonLocalizable("rate_limit_exceeded") + case userNotExistCode: + content = commonLocalizable("user_not_exist") + case userBannedCode: + content = commonLocalizable("user_banned") + case userChatBannedCode: + content = commonLocalizable("user_chat_banned") + case noFriendCode: + content = commonLocalizable("friend_not_exist") + case messageHitAntispam1, messageHitAntispam2: + content = commonLocalizable("message_hit_antispam") + case teamMemberNotExist: + content = commonLocalizable("team_member_not_exist") + case teamNormalMemberChatBanned: + content = commonLocalizable("team_normal_member_chat_banned") + case teamMemberChatBanned: + content = commonLocalizable("team_member_chat_banned") + case notAIAccount: + content = commonLocalizable("not_ai_account") + case cannotBlockAIAccount: + content = commonLocalizable("cannot_blocklist_ai_account") + case aiMessagesDisabled: + content = commonLocalizable("ai_messages_function_disabled") + case aiMessageRequestFailed: + content = commonLocalizable("failed_request_to_the_LLM") + case aiMessageNotSupport: + content = chatLocalizable("format_not_supported") + default: + break + } + return content + } + /// 移除消息扩展字段中的 回复、@ /// - Parameter forwardMessage: 消息 public static func clearForwardAtMark(_ forwardMessage: V2NIMMessage) { @@ -402,8 +440,8 @@ public class ChatMessageHelper: NSObject { clearForwardAtMark(msg) // 保存消息昵称和头像 - if let from = msg.senderId { - let user = NEFriendUserCache.shared.getFriendInfo(from) ?? ChatUserCache.shared.getUserInfo(from) + if let from = ChatMessageHelper.getSenderId(msg) { + let user = getUserFromCache(from) if let user = user { let senderNick = user.showName(false) if var remoteExt = getDictionaryFromJSONString(msg.serverExtension ?? "") as? [String: Any] { @@ -487,6 +525,38 @@ public class ChatMessageHelper: NSObject { return nil } + /// 判断是否是数字人发送的消息 + /// - Parameter message: 消息 + /// - Returns: 是否是数字人发送的消息 + public static func isAISender(_ message: V2NIMMessage?) -> Bool { + if message?.aiConfig != nil, message?.aiConfig?.aiStatus == .MESSAGE_AI_STATUS_RESPONSE { + return true + } + return false + } + + /// 获取消息发送者实际 id + /// - Parameter message: 消息 + /// - Returns: 实际发送者的 id + public static func getSenderId(_ message: V2NIMMessage?) -> String? { + var senderId = message?.senderId + // 数字人回复的消息 + if IMKitConfigCenter.shared.enableAIUser, + message?.aiConfig != nil, + message?.aiConfig?.aiStatus == .MESSAGE_AI_STATUS_RESPONSE { + senderId = message?.aiConfig?.accountId + } + + return senderId + } + + /// 从缓存中获取用户信息 + /// - Parameter accountId: 用户 id + /// - Returns: 用户信息 + public static func getUserFromCache(_ accountId: String) -> NEUserWithFriend? { + NEAIUserManager.shared.getAIUserById(accountId) ?? NEFriendUserCache.shared.getFriendInfo(accountId) ?? NEP2PChatUserCache.shared.getUserInfo(accountId) ?? NETeamUserManager.shared.getUserInfo(accountId) + } + /// 查找回复信息键值对 /// - Parameter message: 消息 /// - Returns: 回复消息的 id @@ -505,6 +575,111 @@ public class ChatMessageHelper: NSObject { return refer } + /// 计算减少的 + /// - Parameter attribute: at 文本前的文本 + static func getReduceIndexCount(_ attribute: NSAttributedString) -> Int { + var count = 0 + attribute.enumerateAttributes( + in: NSMakeRange(0, attribute.length), + options: NSAttributedString.EnumerationOptions(rawValue: 0) + ) { dics, range, stop in + if let neAttachment = dics[NSAttributedString.Key.attachment] as? NEEmotionAttachment { + if let tagCount = neAttachment.emotion?.tag?.count { + count = count + tagCount - 1 + } + } + } + return count + } + + /// 解析消息中的 @ + /// - Parameters: + /// - message: 消息 + /// - attributeStr: 消息富文本 + /// - Returns: 高亮 @ 后的消息富文本 + public static func loadAtInMessage(_ message: V2NIMMessage?, _ attributeStr: NSMutableAttributedString?) -> NSMutableAttributedString? { + // 数字人回复的消息不展示高亮(serverExtension 会被带回) + if message?.aiConfig != nil, message?.aiConfig?.aiStatus == .MESSAGE_AI_STATUS_RESPONSE { + return nil + } + + let text = message?.text ?? "" + let messageTextFont = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + + // 兼容老的表情消息,如果前面有表情而位置计算异常则回退回老的解析 + var notFound = false + + // 计算表情(根据转码后的index) + 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] { + for model in models { + // 前面因为表情增加的索引数量 + var count = 0 + if text.count > model.start { + let frontAttributeStr = NEEmotionTool.getAttWithStr( + str: String(text.prefix(model.start)), + font: messageTextFont + ) + count = getReduceIndexCount(frontAttributeStr) + } + let start = model.start - count + if start < 0 { + notFound = true + break + } + var end = model.end - count + + if model.end + atRangeOffset > text.count { + notFound = true + break + } + // 获取起始索引 + let startIndex = text.index(text.startIndex, offsetBy: model.start) + // 获取结束索引 + let endIndex = text.index(text.startIndex, offsetBy: model.end + atRangeOffset) + let frontAttributeStr = NEEmotionTool.getAttWithStr( + str: String(text[startIndex ..< endIndex]), + font: messageTextFont + ) + let innerCount = getReduceIndexCount(frontAttributeStr) + end = end - innerCount + if end <= start { + notFound = true + break + } + + if attributeStr?.length ?? 0 > end { + attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(start, end - start + atRangeOffset)) + } + } + } + } + } + } + } + + if notFound == true, 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] { + for model in models { + if attributeStr?.length ?? 0 > model.end { + attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(model.start, model.end - model.start + atRangeOffset)) + } + } + } + } + } + } + } + + return attributeStr + } + /// 获取文件 MD5 值 /// - Parameter fileURL: 文件 URL /// - Returns: md5 值 diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatTeamCache.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatTeamCache.swift deleted file mode 100644 index 97ba3a55..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatTeamCache.swift +++ /dev/null @@ -1,129 +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 Foundation -import NEChatKit -import NECoreIM2Kit -import NIMSDK - -public class ChatTeamCache: NSObject { - public static let shared = ChatTeamCache() - private var teamInfo: V2NIMTeam? - private var cacheTeamMemberInfoDic = [String: V2NIMTeamMember]() - - 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(_ teamMember: V2NIMTeamMember?) { - guard let teamMember = teamMember else { - return - } - - let accountId = teamMember.accountId - NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:\(accountId)") - cacheTeamMemberInfoDic[accountId] = teamMember - } - - /// 获取缓存的群聊信息 - public func getTeamInfo() -> V2NIMTeam? { - teamInfo - } - - /// 获取缓存的群成员信息 - public func getTeamMemberInfo(accountId: String) -> V2NIMTeamMember? { - 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) -> String { - NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", userId: " + accountId) - // 好友缓存 - var fullName = NEFriendUserCache.shared.getShowName(accountId, showAlias) - - // 非好友缓存 - if !NEFriendUserCache.shared.isFriend(accountId) { - fullName = ChatUserCache.shared.getShowName(accountId) - } - - // 群成员缓存 - if let teamMember = cacheTeamMemberInfoDic[accountId] { - if teamMember.accountId == accountId { - if let teamNick = teamMember.teamNick, !teamNick.isEmpty { - fullName = teamNick - } - - if showAlias, - let friend = NEFriendUserCache.shared.getFriendInfo(accountId), - let alias = friend.friend?.alias, - !alias.isEmpty { - fullName = alias - } - - return fullName - } - } - return fullName - } - - // 获取展示的群成员名字, 备注 > 群昵称 > 昵称 > 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) - } - } - - // 先查询用户信息(陌生人) - if !loadUserIds.isEmpty { - group.enter() - ContactRepo.shared.getUserList(accountIds: Array(loadUserIds)) { users, error in - users?.forEach { ChatUserCache.shared.updateUserInfo($0) } - group.leave() - } - } - - // 再查询群成员信息 - if !loadMemberIds.isEmpty { - group.enter() - TeamRepo.shared.getTeamMemberListByIds(teamId, .TEAM_TYPE_NORMAL, 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 deleted file mode 100644 index 3b5786a3..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatUserCache.swift +++ /dev/null @@ -1,56 +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 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 { - NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", userId: " + userId) - let user = getUserInfo(userId) - return user?.showName(showAlias) ?? userId - } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NEP2PChatUserCache.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NEP2PChatUserCache.swift new file mode 100644 index 00000000..27302680 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NEP2PChatUserCache.swift @@ -0,0 +1,104 @@ +// 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 + +@objc +public protocol NEP2PChatUserCacheListener: NSObjectProtocol { + /// 非好友单聊信息缓存更新 + /// - Parameter accountId: 用户 id + @objc optional func onUserInfoUpdate(_ accountId: String) +} + +/// 陌生人用户信息缓存,只缓存非好友单聊用户 +public class NEP2PChatUserCache: NSObject { + public static let shared = NEP2PChatUserCache() + + /// 多代理容器 + private let multiDelegate = MultiDelegate(strongReferences: false) + + // 非好友列表,聊天页面销毁时同步清空 + public var noUserCache = [String: NEUserWithFriend]() + + override private init() { + super.init() + ContactRepo.shared.addContactListener(self) + } + + deinit { + ContactRepo.shared.removeContactListener(self) + } + + /// 添加代理 + /// - Parameter listener: 代理 + public func addListener(_ listener: NEP2PChatUserCacheListener) { + multiDelegate.addDelegate(listener) + } + + /// 移除代理 + /// - Parameter listener: 代理 + public func removeListener(_ listener: NEP2PChatUserCacheListener) { + multiDelegate.removeDelegate(listener) + } + + // 添加(更新)非好友信息 + public func updateUserInfo(_ user: V2NIMUser?) { + guard let accid = user?.accountId else { return } + noUserCache[accid]?.user = user + + multiDelegate |> { delegate in + delegate.onUserInfoUpdate?(accid) + } + } + + // 添加(更新)非好友信息 + public func updateUserInfo(_ user: NEUserWithFriend?) { + guard let accid = user?.user?.accountId else { return } + noUserCache[accid] = 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 { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", userId: " + userId) + let user = getUserInfo(userId) + return user?.showName(showAlias) ?? userId + } +} + +// MARK: - NEContactListener + +extension NEP2PChatUserCache: NEContactListener { + /// 用户信息变更回调 + /// - Parameter users: 用户列表 + public func onUserProfileChanged(_ users: [V2NIMUser]) { + for user in users { + guard let accid = user.accountId else { break } + + if noUserCache[accid] == nil { + updateUserInfo(user) + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NETeamUserManager.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NETeamUserManager.swift new file mode 100644 index 00000000..d86a24cc --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NETeamUserManager.swift @@ -0,0 +1,519 @@ +// 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 + +/// 群聊缓存代理,包含群信息更新和群成员更新 +/// 群成员更新包含自己 +@objc +public protocol NETeamChatUserCacheListener: NETeamListener { + /// 群信息更新 + /// - Parameter teamId: 群 id + @objc optional func onTeamInfoUpdate(_ teamId: String) + + /// 群成员更新 + /// - Parameter accountId: 用户 id + @objc optional func onTeamMemberUpdate(_ accountId: String) +} + +/// 群聊缓存类,缓存群成员信息和非好友用户信息 +@objcMembers +public class NETeamUserManager: NSObject { + public static let shared = NETeamUserManager() + let teamRepo = TeamRepo.shared + let contactRepo = ContactRepo.shared + + /// 多代理容器 + private let multiDelegate = MultiDelegate(strongReferences: false) + + // 群ID + private var tid: String? + + // 当前群信息,可空 + private var currentTeam: V2NIMTeam? + + // 群成员信息 + private var teamMemberCache = [String: V2NIMTeamMember]() + + // 非好友的用户信息 + private var userInfoCache = [String: NEUserWithFriend]() + + // 是否已经拉取了所有群成员 + private var haveLoadAllMembers = false + + override private init() { + super.init() + IMKitClient.instance.addLoginListener(self) + teamRepo.addTeamListener(self) + contactRepo.addContactListener(self) + } + + deinit { + IMKitClient.instance.removeLoginListener(self) + teamRepo.removeTeamListener(self) + contactRepo.removeContactListener(self) + } + + /// 添加代理 + /// - Parameter listener: 代理 + public func addListener(_ listener: NETeamChatUserCacheListener) { + multiDelegate.addDelegate(listener) + } + + /// 移除代理 + /// - Parameter listener: 代理 + public func removeListener(_ listener: NETeamChatUserCacheListener) { + multiDelegate.removeDelegate(listener) + } + + /// 加载缓存 + /// - Parameter teamId: 群 id + public func loadData(_ teamId: String) { + removeAllTeamInfo() + tid = teamId + + // 获取群信息 + teamRepo.getTeamInfo(teamId) { [weak self] team, error in + self?.updateTeamInfo(team) + } + + // 获取自己的群成员信息 + teamRepo.getTeamMember(teamId, .TEAM_TYPE_NORMAL, IMKitClient.instance.account()) { [weak self] teamMember, error in + self?.updateTeamMemberInfo(teamMember) + } + } + + /// 更新当前群信息 + /// - Parameter team: 群信息 + public func updateTeamInfo(_ team: V2NIMTeam?) { + guard let teamId = team?.teamId, teamId == tid else { return } + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: \(teamId)") + currentTeam = team + + multiDelegate |> { delegate in + delegate.onTeamInfoUpdate?(teamId) + } + } + + /// 更新群成员信息 + /// - Parameter teamMember: 群成员信息 + public func updateTeamMemberInfo(_ teamMember: V2NIMTeamMember?) { + guard let teamMember = teamMember, teamMember.teamId == tid else { + return + } + + let accid = teamMember.accountId + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:\(accid)") + teamMemberCache[accid] = teamMember + + multiDelegate |> { delegate in + delegate.onTeamMemberUpdate?(accid) + } + } + + // 添加(更新)非好友信息 + public func updateUserInfo(user: V2NIMUser?) { + guard let accid = user?.accountId else { return } + userInfoCache[accid]?.user = user + + multiDelegate |> { delegate in + delegate.onTeamMemberUpdate?(accid) + } + } + + // 添加(更新)非好友信息 + public func updateUserInfo(userWithFriend: NEUserWithFriend?) { + guard let accid = userWithFriend?.user?.accountId else { return } + userInfoCache[accid] = userWithFriend + + multiDelegate |> { delegate in + delegate.onTeamMemberUpdate?(accid) + } + } + + /// 获取缓存的群聊信息 + public func getTeamInfo() -> V2NIMTeam? { + currentTeam + } + + /// 获取缓存的群成员信息 + public func getTeamMemberInfo(_ accountId: String) -> V2NIMTeamMember? { + teamMemberCache[accountId] + } + + /// 获取缓存的所有群成员信息 + public func getAllTeamMembers() -> [V2NIMTeamMember]? { + if haveLoadAllMembers { + var teamMemberInfoModels = [NETeamMemberInfoModel]() + for (accid, member) in teamMemberCache { + let model = NETeamMemberInfoModel() + model.teamMember = member + model.nimUser = NEFriendUserCache.shared.getFriendInfo(accid) ?? userInfoCache[accid] + teamMemberInfoModels.append(model) + } + + return teamMemberCache.values.map { $0 } + } + return nil + } + + /// 获取缓存的所有群成员信息 + public func getAllTeamMemberModels() -> [NETeamMemberInfoModel]? { + if haveLoadAllMembers { + var teamMemberInfoModels = [NETeamMemberInfoModel]() + for (accid, member) in teamMemberCache { + let model = NETeamMemberInfoModel() + model.teamMember = member + model.nimUser = NEFriendUserCache.shared.getFriendInfo(accid) ?? userInfoCache[accid] + teamMemberInfoModels.append(model) + } + + return teamMemberInfoModels.isEmpty ? nil : teamMemberInfoModels + } + return nil + } + + /// 获取缓存的非好友用户信息 + public func getUserInfo(_ accountId: String) -> NEUserWithFriend? { + userInfoCache[accountId] + } + + /// 删除群成员信息缓存 + public func removeTeamMemberInfo(_ accountId: String) { + if let _ = teamMemberCache[accountId] { + teamMemberCache.removeValue(forKey: accountId) + } + } + + /// 删除所有信息缓存 + public func removeAllTeamInfo() { + tid = nil + currentTeam = nil + userInfoCache.removeAll() + teamMemberCache.removeAll() + haveLoadAllMembers = false + } + + /// 获取缓存群成员名字,team: 备注 > 群昵称 > 昵称 > ID + public func getShowName(_ accountId: String, + _ showAlias: Bool = true) -> String { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", userId: " + accountId) + + // 数字人直接返回 + if let aiUser: NEUserWithFriend = NEAIUserManager.shared.getAIUserById(accountId) { + return aiUser.showName() ?? accountId + } + + // 非好友缓存 + var fullName = userInfoCache[accountId]?.showName() ?? NEP2PChatUserCache.shared.getShowName(accountId) + + // 好友缓存 + if NEFriendUserCache.shared.isFriend(accountId) { + fullName = NEFriendUserCache.shared.getShowName(accountId, showAlias) + } + + // 群成员缓存 + if let teamMember = teamMemberCache[accountId] { + if teamMember.accountId == accountId { + if let teamNick = teamMember.teamNick, !teamNick.isEmpty { + fullName = teamNick + } + + if showAlias, + let friend = NEFriendUserCache.shared.getFriendInfo(accountId), + let alias = friend.friend?.alias, + !alias.isEmpty { + fullName = alias + } + + return fullName + } + } + return fullName + } + + // 获取群成员信息和用户信息 + public func getTeamMembers(accountIds: [String], _ completion: @escaping () -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: \(String(describing: tid))") + + var loadUserIds = Set() // 需要查询用户信息的ID + var loadMemberIds = Set() // 需要查询群成员信息的ID + let group = DispatchGroup() + + for userId in accountIds { + if !NEAIUserManager.shared.isAIUser(userId), + !NEFriendUserCache.shared.isFriend(userId), + userInfoCache[userId] == nil { + loadUserIds.insert(userId) + } + + if teamMemberCache[userId] == nil, !NEAIUserManager.shared.isAIUser(userId) { + loadMemberIds.insert(userId) + } + } + + // 先查询用户信息(陌生人) + if !loadUserIds.isEmpty { + group.enter() + ContactRepo.shared.getUserListFromCloud(accountIds: Array(loadUserIds)) { [weak self] users, error in + users?.forEach { self?.updateUserInfo(userWithFriend: $0) } + group.leave() + } + } + + guard let tid = tid else { + NEALog.errorLog(ModuleName + " " + className(), desc: #function + ", teamId is nil") + completion() + return + } + + // 再查询群成员信息 + if !loadMemberIds.isEmpty { + group.enter() + TeamRepo.shared.getTeamMemberListByIds(tid, .TEAM_TYPE_NORMAL, Array(loadMemberIds)) { [weak self] teamMembers, error in + for teamMember in teamMembers ?? [] { + if teamMember.inTeam { + self?.updateTeamMemberInfo(teamMember) + } + } + + group.leave() + } + } + + group.notify(queue: .main) { + completion() + } + } + + /// 获取所有群成员信息和用户信息 + /// - Parameter teamId: 群id + /// - Parameter queryType: 查询类型 + /// - Parameter completion: 完成后的回调 + public func getAllTeamMembers(_ teamId: String, + _ queryType: V2NIMTeamMemberRoleQueryType = .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL, + _ completion: @escaping ([NEUserWithFriend]) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamid:\(teamId)") + var memberLists = [V2NIMTeamMember]() + weak var weakSelf = self + getAllTeamMemberWithMaxLimit(teamId, nil, &memberLists, queryType) { members, error in + if let err = error { + NEALog.errorLog(ModuleName + " " + NETeamUserManager.className(), desc: #function + ", err:\(err.localizedDescription)") + } else { + if let teamMembers = members { + var notFriendMembers = [String]() + for member in teamMembers { + weakSelf?.teamMemberCache[member.accountId] = member + if !NEFriendUserCache.shared.isFriend(member.accountId) { + notFriendMembers.append(member.accountId) + } + } + + weakSelf?.splitMembers(notFriendMembers, 150, completion) + } + } + } + } + + /// 获取群成员(使用最大分页参数,防止触发频控) + /// - Parameter teamId: 群ID + /// - Parameter completion: 完成回调 + public func getAllTeamMemberWithMaxLimit(_ teamId: String, _ nextToken: String? = nil, _ memberList: inout [V2NIMTeamMember], _ queryType: V2NIMTeamMemberRoleQueryType, _ completion: @escaping ([V2NIMTeamMember]?, NSError?) -> Void) { + NEALog.infoLog(className(), desc: #function + " teamId : \(teamId)") + let option = V2NIMTeamMemberQueryOption() + option.direction = .QUERY_DIRECTION_ASC + option.limit = 1000 + 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?.getAllTeamMemberWithMaxLimit(teamId, result?.nextToken, &temMemberLists, queryType, completion) + } + } + } + } + } + + /// 分页查询群成员信息 + /// - Parameter members: 要查询的群成员列表 + /// - Parameter model : 群信息 + /// - Parameter maxSizeByPage: 单页最大查询数量 + /// - Parameter completion: 完成后的回调 + private func splitMembers(_ members: [String], + _ maxSizeByPage: Int = 150, + _ completion: @escaping ([NEUserWithFriend]) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", members.count:\(members.count)") + var remaind = [[String]]() + remaind.append(contentsOf: members.chunk(maxSizeByPage)) + let memberUsers = [NEUserWithFriend]() + fetchTeamMemberUserInfos(&remaind, memberUsers, completion) + } + + /// 从云信服务器批量获取用户资料 + /// - Parameter remainUserIds: 用户集合 + /// - Parameter completion: 成功回调 + private func fetchTeamMemberUserInfos(_ remainUserIds: inout [[String]], + _ memberUsers: [NEUserWithFriend], + _ completion: @escaping ([NEUserWithFriend]) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", remainUserIds.count:\(remainUserIds.count)") + guard let members = remainUserIds.first else { + haveLoadAllMembers = true + completion(memberUsers) + return + } + + var temArray = remainUserIds + var memberUsers = memberUsers + weak var weakSelf = self + + contactRepo.getUserListFromCloud(accountIds: members) { [weak self] users, v2Error in + if let err = v2Error { + NEALog.errorLog(ModuleName + " " + NETeamUserManager.className(), desc: #function + "err:\(err.localizedDescription)") + } else { + if let users = users { + for user in users { + if let accid = user.user?.accountId { + self?.userInfoCache[accid] = user + memberUsers.append(user) + } + } + } + temArray.removeFirst() + weakSelf?.fetchTeamMemberUserInfos(&temArray, memberUsers, completion) + } + } + } +} + +// MARK: - NEContactListener + +extension NETeamUserManager: NEContactListener { + /// 好友信息缓存更新 + /// - Parameter accountId: 用户 id + public func onContactChange(_ changeType: NEContactChangeType, _ contacts: [NEUserWithFriend]) { + for contact in contacts { + if let accid = contact.user?.accountId { + // 好友被删除,则信息缓存移至 userInfoCache + if changeType == .deleteFriend, teamMemberCache[accid] != nil { + contact.friend = nil + updateUserInfo(userWithFriend: contact) + continue + } + + if userInfoCache[accid] != nil { + updateUserInfo(user: contact.user) + } else if teamMemberCache[accid] != nil, + NEFriendUserCache.shared.isFriend(accid) { + multiDelegate |> { delegate in + delegate.onTeamMemberUpdate?(accid) + } + } + } + } + } +} + +// MARK: - NETeamListener + +extension NETeamUserManager: NETeamListener { + /// 群组信息更新回调 + /// - Parameter team: 群组对象 + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + if team.teamId == tid { + updateTeamInfo(team) + } + } + + /// 群组成员加入回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { + var notFriendMembers = [String]() + for member in teamMembers { + if member.teamId == tid { + updateTeamMemberInfo(member) + if !NEFriendUserCache.shared.isFriend(member.accountId) { + notFriendMembers.append(member.accountId) + } + } + } + + splitMembers(notFriendMembers) { [weak self] userFirends in + for userFirend in userFirends { + self?.updateUserInfo(userWithFriend: userFirend) + } + } + } + + /// 群组成员信息变更回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + for member in teamMembers { + if member.teamId == tid { + updateTeamMemberInfo(member) + } + } + } + + /// 群组成员退出回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + for member in teamMembers { + if member.teamId == tid { + removeTeamMemberInfo(member.accountId) + } + } + } + + /// 群组成员被踢回调 + /// - Parameter operatorAccountId: 操作者用户id + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + for member in teamMembers { + if member.teamId == tid { + removeTeamMemberInfo(member.accountId) + } + } + } +} + +// MARK: - NEIMKitClientListener + +extension NETeamUserManager: NEIMKitClientListener { + /// 登录连接状态回调 + /// - Parameter status: 连接状态 + public func onDataSync(_ type: V2NIMDataSyncType, state: V2NIMDataSyncState, error: V2NIMError?) { + guard let tid = tid else { return } + + // 断网重连后,重新拉取群信息、自己的群成员信息 + if type == .DATA_SYNC_TYPE_TEAM_MEMBER, state == .DATA_SYNC_STATE_COMPLETED { + // 获取群信息 + teamRepo.getTeamInfo(tid) { [weak self] team, error in + self?.updateTeamInfo(team) + } + + // 获取自己的群成员信息 + teamRepo.getTeamMember(tid, .TEAM_TYPE_NORMAL, IMKitClient.instance.account()) { [weak self] teamMember, error in + self?.updateTeamMemberInfo(teamMember) + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift index 5b3404c7..4bd7a4ae 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift @@ -84,7 +84,6 @@ open class NotificationMessageUtils: NSObject { } else { text = fromName + chatLocalizable("pass") + toNamestext } - case .MESSAGE_NOTIFICATION_TYPE_TEAM_OWNER_TRANSFER: text = fromName + chatLocalizable("transfer") + toFirstName case .MESSAGE_NOTIFICATION_TYPE_TEAM_ADD_MANAGER: @@ -95,7 +94,6 @@ open class NotificationMessageUtils: NSObject { 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") } @@ -110,7 +108,7 @@ open class NotificationMessageUtils: NSObject { if sourceId == IMKitClient.instance.account() { return chatLocalizable("You") + " " } else { - return ChatTeamCache.shared.getShowName(sourceId) + return NETeamUserManager.shared.getShowName(sourceId) } } else { return "" @@ -128,7 +126,7 @@ open class NotificationMessageUtils: NSObject { if targetID == IMKitClient.instance.account() { toNames.append(chatLocalizable("You") + " ") } else { - let name = ChatTeamCache.shared.getShowName(targetID) + let name = NETeamUserManager.shared.getShowName(targetID) toNames.append(name) } } @@ -146,7 +144,7 @@ open class NotificationMessageUtils: NSObject { } open class func teamType(message: V2NIMMessage) -> TeamType { - if let team = ChatTeamCache.shared.getTeamInfo() { + if let team = NETeamUserManager.shared.getTeamInfo() { if team.isDisscuss() == true { return .discussTeam } else { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/CollectionMessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/CollectionMessageModel.swift index d8a427ed..a54f342b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/CollectionMessageModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/CollectionMessageModel.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 NIMSDK import UIKit diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift index e35783d4..f4c4a91a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift @@ -73,9 +73,9 @@ open class MessageCallRecordModel: MessageContentModel { } 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) + let contentSizeWidth = textSize.width + chat_cell_margin * 2 + let contentSizeHeight = textSize.height + (isAuiodRecord ? 20 : 24) + contentSize = CGSize(width: contentSizeWidth, height: contentSizeHeight) 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 0737cdad..9fd3c011 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift @@ -5,6 +5,7 @@ import CoreAudio import Foundation +import NEChatKit import NECoreIM2Kit import NIMSDK @@ -13,14 +14,21 @@ open class MessageContentModel: NSObject, MessageModel { public var type: MessageType = .custom // 消息类型(文本、图片、自定义消息...) public var customType: Int = 0 // 自定义消息的子类型(合并转发、换行消息...) public var message: V2NIMMessage? + public weak var cell: NEBaseChatMessageCell? // 消息对应的cell public var offset: CGFloat = 0 + public var textWidth: 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 selectRange: NSRange? // 划词选择的范围 + open func selectText() -> String? { + nil + } + public var showSelect: Bool = false // 多选按钮是否展示 public var isSelected: Bool = false // 多选是否选中 public var isSelectAll: Bool = false // 是否全选 @@ -50,6 +58,11 @@ open class MessageContentModel: NSObject, MessageModel { public var replyedModel: MessageModel? { didSet { if let reply = replyedModel as? MessageContentModel, reply.isReplay == true { + if type == .tip { + replyedModel?.isReplay = false + return + } + type = .reply replyText = ReplyMessageUtil.textForReplyModel(model: reply) // height 计算移至 getMessageModel(model:) @@ -114,7 +127,8 @@ open class MessageContentModel: NSObject, MessageModel { public required init(message: V2NIMMessage?) { self.message = message if message?.conversationType == .CONVERSATION_TYPE_TEAM, - !IMKitClient.instance.isMe(message?.senderId) { + let senderId = ChatMessageHelper.getSenderId(message), + !IMKitClient.instance.isMe(senderId) { fullNameHeight = NEKitChatConfig.shared.ui.messageProperties.showTeamMessageNick ? 20 : 0 } 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 fbaf89b4..11134a53 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift @@ -22,7 +22,7 @@ open class MessageCustomModel: MessageContentModel { } contentSize = CGSize(width: 0, height: contentHeight) - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height + height = CGFloat(contentHeight) + chat_content_margin * 2 + fullNameHeight + chat_pin_height if let customHeight = NECustomUtils.heightOfCustomMessage(message?.attachment) { height = customHeight } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift index 8557abe3..4d27faaa 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift @@ -19,7 +19,7 @@ open class MessageImageModel: MessageContentModel { urlString = path } else if let url = imageObject.url { if imageObject.ext?.lowercased() != ".gif" { - urlString = ResourceRepo.shared.imageThumbnailURL(url) + urlString = V2NIMStorageUtil.imageThumbUrl(url, thumbSize: 350) } urlString = url } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift deleted file mode 100644 index fcf630aa..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift +++ /dev/null @@ -1,80 +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 Foundation -import NIMSDK - -@objc -public enum MessageType: Int { - case text = 1 - case image - case audio - case video - case location - case notification - case file - case tip - case robot - case rtcCallRecord - case custom - case time - case revoke - case reply - - /// 合并转发消息 - case multiForward - - /// 带标题的文本消息 - case richText -} - -@objc -public protocol MessageModel: NSObjectProtocol { - var message: V2NIMMessage? { get set } - var type: MessageType { get set } - var customType: Int { get set } - - // 宽高 - var contentSize: CGSize { get set } // 气泡区域的大小 不包含气泡上下到cell上下的边距 - var offset: CGFloat { get set } - var height: CGFloat { get set } - func cellHeight() -> CGFloat - - // 名称头像 - var shortName: String? { get set } // 名字后2位 - var fullName: String? { get set } // 名字全长 - var avatar: String? { get set } - - // 标记 - var isPined: Bool { get set } - var pinAccount: String? { get set } - var pinShowName: String? { 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 } // 具体时间 - - // 是否是未知消息 - var unkonwMessage: Bool { get set } - - init(message: V2NIMMessage?) -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift index b7c87d73..e5dc8115 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift @@ -9,6 +9,7 @@ import NIMSDK @objcMembers open class MessageRichTextModel: MessageTextModel { + public var titleText: String? public var titleAttributeStr: NSMutableAttributedString? public var titleTextHeight: CGFloat = 0 @@ -19,6 +20,7 @@ open class MessageRichTextModel: MessageTextModel { return } + titleText = title let body = (data["body"] as? String) ?? "" message?.text = body super.init(message: message) @@ -33,9 +35,27 @@ open class MessageRichTextModel: MessageTextModel { let textSize = NSAttributedString.getRealSize(titleAttributeStr, messageTextFont, messageMaxSize) titleTextHeight = textSize.height - 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 + chat_pin_height + + let contentSizeWidth = max(textWidth, textSize.width) + chat_content_margin * 2 + let contentSizeHeight = contentSize.height + titleTextHeight + (body.isEmpty ? 0 : chat_content_margin) + contentSize = CGSize(width: contentSizeWidth, height: contentSizeHeight) + height = contentSizeHeight + chat_content_margin * 2 + fullNameHeight + chat_pin_height + } + + /// 获取划词选中的文本(替换内置表情) + /// - Returns: 选中的文本 + override open func selectText() -> String? { + if attributeStr == nil, titleText != nil { + if let selectRange = selectRange { + // 内置表情转为文本 + let text = NEEmotionTool.getTextWithAtt(titleAttributeStr, selectRange) + + return text + } + } else { + return super.selectText() + } + + return nil } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift index 0c29ab3c..edfa668c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import Foundation +import NEChatKit import NECommonKit import NIMSDK @@ -11,109 +12,44 @@ import NIMSDK open class MessageTextModel: MessageContentModel { public var attributeStr: NSMutableAttributedString? public var textHeight: CGFloat = 0 - public var textWidght: CGFloat = 0 public required init(message: V2NIMMessage?) { super.init(message: message) type = .text - let text = message?.text ?? "" - attributeStr = NEEmotionTool.getAttWithStr( - str: text, - font: messageTextFont - ) - // 兼容老的表情消息,如果前面有表情而位置计算异常则回退回老的解析 - var notFound = false - - // 计算表情(根据转码后的index) - 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] { - for model in models { - // 前面因为表情增加的索引数量 - var count = 0 - if text.count > model.start { - let frontAttributeStr = NEEmotionTool.getAttWithStr( - str: String(text.prefix(model.start)), - font: messageTextFont - ) - count = getReduceIndexCount(frontAttributeStr) - } - let start = model.start - count - if start < 0 { - notFound = true - break - } - var end = model.end - count - - if model.end + atRangeOffset > text.count { - notFound = true - break - } - // 获取起始索引 - let startIndex = text.index(text.startIndex, offsetBy: model.start) - // 获取结束索引 - let endIndex = text.index(text.startIndex, offsetBy: model.end + atRangeOffset) - let frontAttributeStr = NEEmotionTool.getAttWithStr( - str: String(text[startIndex ..< endIndex]), - font: messageTextFont - ) - let innerCount = getReduceIndexCount(frontAttributeStr) - end = end - innerCount - if end <= start { - notFound = true - break - } - - if attributeStr?.length ?? 0 > end { - attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(start, end - start + atRangeOffset)) - } - } - } - } - } - } + if let text = message?.text, !text.isEmpty { + attributeStr = NEEmotionTool.getAttWithStr( + str: text, + font: messageTextFont + ) } - if notFound == true, 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] { - for model in models { - if attributeStr?.length ?? 0 > model.end { - attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(model.start, model.end - model.start + atRangeOffset)) - } - } - } - } - } + // 解析 @,效果高亮 + if IMKitConfigCenter.shared.enableAtMessage { + if let att = ChatMessageHelper.loadAtInMessage(message, attributeStr) { + attributeStr = att } } 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 + chat_pin_height + textWidth = textSize.width + let contentSizeWidth = textWidth + chat_content_margin * 2 + let contentSizeHeight = textHeight + chat_content_margin * 2 + contentSize = CGSize(width: contentSizeWidth, height: contentSizeHeight) + height = contentSizeHeight + chat_content_margin * 2 + fullNameHeight + chat_pin_height } - /// 计算减少的 - /// - Parameter attribute: at 文本前的文本 - func getReduceIndexCount(_ attribute: NSAttributedString) -> Int { - var count = 0 - attribute.enumerateAttributes( - in: NSMakeRange(0, attribute.length), - options: NSAttributedString.EnumerationOptions(rawValue: 0) - ) { dics, range, stop in - if let neAttachment = dics[NSAttributedString.Key.attachment] as? NEEmotionAttachment { - if let tagCount = neAttachment.emotion?.tag?.count { - count = count + tagCount - 1 - } - } + /// 获取划词选中的文本(替换内置表情) + /// - Returns: 选中的文本 + override open func selectText() -> String? { + if let selectRange = selectRange { + // 内置表情转为文本 + let text = NEEmotionTool.getTextWithAtt(attributeStr, selectRange) + + return text } - return count + + return nil } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift index 4e6b578f..d10a3457 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift @@ -23,9 +23,14 @@ open class MessageTipsModel: MessageContentModel { } } - let font: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.timeTextSize) + var font: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.timeTextSize) + if ChatMessageHelper.isAISender(message) { + font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + } - contentSize = String.getRealSize(text, font, messageMaxSize) + let textSize = String.getRealSize(text, font, messageMaxSize) + textWidth = textSize.width + contentSize = textSize height = contentSize.height + chat_content_margin * 3 // time diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift index 8ed5f362..ca08e54f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift @@ -16,7 +16,6 @@ public enum DownloadState: Int { open class MessageVideoModel: MessageImageModel { public var state = DownloadState.Success public var progress: UInt = 0 - public weak var cell: NEChatBaseCell? public required init(message: V2NIMMessage?) { super.init(message: message) @@ -32,4 +31,17 @@ open class MessageVideoModel: MessageImageModel { } height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } + + /// 设置(视频、文件)消息模型(上传、下载)进度 + /// - Parameters: + /// - progress:(上传、下载)进度 + public func setModelProgress(_ progress: UInt) { + if progress == 100 { + state = .Success + } else { + state = .Downalod + } + + cell?.uploadProgress(progress) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEMoreItemModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEMoreItemModel.swift index b3c666b3..02b1c45e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEMoreItemModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEMoreItemModel.swift @@ -14,6 +14,7 @@ public enum NEMoreActionType: NSInteger { case file case remind case photo + case translate case other = 100 } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEPinMessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEPinMessageModel.swift index 250f6175..1c58064f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEPinMessageModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEPinMessageModel.swift @@ -32,8 +32,10 @@ open class NEPinMessageModel: NSObject { private func modelFromMessage(message: V2NIMMessage) -> MessageModel { let model = ChatMessageHelper.modelFromMessage(message: message) - model.fullName = message.senderId - model.shortName = NEFriendUserCache.getShortName(message.senderId ?? "") + let uid = ChatMessageHelper.getSenderId(message) + + model.fullName = uid ?? "" + model.shortName = NEFriendUserCache.getShortName(uid ?? "''") return model } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItem.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItemExtension.swift similarity index 68% rename from NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItem.swift rename to NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItemExtension.swift index 2e67e575..b4fb9a59 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItem.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItemExtension.swift @@ -3,31 +3,11 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import Foundation - -@objc -public enum OperationType: Int { - case copy = 1 - case reply - case forward - case pin - case removePin - case multiSelect - case collection - case delete - case recall - case top - case untop -} - -@objcMembers -open class OperationItem: NSObject { - public var text: String = "" - public var imageName: String = "" - public var type: OperationType? +import NEChatKit +public extension OperationItem { /// 复制 - public static func copyItem() -> OperationItem { + static func copyItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_copy") item.imageName = "op_copy" @@ -36,7 +16,7 @@ open class OperationItem: NSObject { } /// 回复 - public static func replayItem() -> OperationItem { + static func replayItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_replay") item.imageName = "op_replay" @@ -45,7 +25,7 @@ open class OperationItem: NSObject { } /// 转发 - public static func forwardItem() -> OperationItem { + static func forwardItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_forward") item.imageName = "op_forward" @@ -54,7 +34,7 @@ open class OperationItem: NSObject { } /// 标记 - public static func pinItem() -> OperationItem { + static func pinItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_pin") item.imageName = "op_pin" @@ -63,7 +43,7 @@ open class OperationItem: NSObject { } /// 取消标记 - public static func removePinItem() -> OperationItem { + static func removePinItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_cancel_pin") item.imageName = "op_pin" @@ -72,7 +52,7 @@ open class OperationItem: NSObject { } /// 多选 - public static func selectItem() -> OperationItem { + static func selectItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_select") item.imageName = "op_select" @@ -81,7 +61,7 @@ open class OperationItem: NSObject { } /// 收藏 - public static func collectionItem() -> OperationItem { + static func collectionItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_collection") item.imageName = "op_collect" @@ -90,7 +70,7 @@ open class OperationItem: NSObject { } /// 置顶 - public static func topItem() -> OperationItem { + static func topItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_top") item.imageName = "op_top" @@ -99,7 +79,7 @@ open class OperationItem: NSObject { } /// 移除置顶 - public static func untopItem() -> OperationItem { + static func untopItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_untop") item.imageName = "op_untop" @@ -108,7 +88,7 @@ open class OperationItem: NSObject { } /// 删除 - public static func deleteItem() -> OperationItem { + static func deleteItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_delete") item.imageName = "op_delete" @@ -117,7 +97,7 @@ open class OperationItem: NSObject { } /// 撤回 - public static func recallItem() -> OperationItem { + static func recallItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_recall") item.imageName = "op_recall" diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageCell.swift index a88dff72..2aceee3c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageCell.swift @@ -185,7 +185,7 @@ open class NEBaseCollectionMessageCell: UITableViewCell { collectionModel = model headerView.configHeadData(headUrl: model.chatmodel.avatar, name: model.chatmodel.fullName ?? "", - uid: model.chatmodel.message?.senderId ?? "") + uid: ChatMessageHelper.getSenderId(model.chatmodel.message) ?? "") nameLabel.text = model.chatmodel.fullName if let time = model.collection?.updateTime { timeLabel.text = String.stringFromDate(date: Date(timeIntervalSince1970: time)) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageVideoCell.swift index 2bb419fa..32383d00 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageVideoCell.swift @@ -89,7 +89,7 @@ class NEBaseCollectionMessageVideoCell: NEBaseCollectionMessageImageCell { if let videoObject = model.chatmodel.message?.attachment as? V2NIMMessageVideoAttachment { // 获取首帧 let videoUrl = videoObject.url ?? "" - let thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + let thumbUrl = V2NIMStorageUtil.videoCoverUrl(videoUrl, offset: 0) collectionContentImageView.sd_setImage( with: URL(string: thumbUrl), placeholderImage: nil, diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift index 0f6fff0e..99a19b95 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift @@ -34,6 +34,9 @@ public protocol ChatBaseCellDelegate: NSObjectProtocol { // 单击多选按钮 func didTapSelectButton(_ cell: UITableViewCell, _ model: MessageContentModel?) + + // 划词选中失去焦点 + @objc optional func didTextViewLoseFocus(_ cell: UITableViewCell, _ model: MessageContentModel?) } @objc @@ -373,13 +376,12 @@ open class NEBaseChatMessageCell: NEChatBaseCell { } open func initSubviewsLayout() { - if NEKitChatConfig.shared.ui.messageProperties.avatarType == .rectangle, - NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius > 0 { - avatarImageRight.layer.cornerRadius = NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius - avatarImageLeft.layer.cornerRadius = NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius - } else if NEKitChatConfig.shared.ui.messageProperties.avatarType == .cycle { + if NEKitChatConfig.shared.ui.messageProperties.avatarType == .cycle { avatarImageRight.layer.cornerRadius = 16.0 avatarImageLeft.layer.cornerRadius = 16.0 + } else if NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius > 0 { + avatarImageRight.layer.cornerRadius = NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius + avatarImageLeft.layer.cornerRadius = NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius } else { avatarImageRight.layer.cornerRadius = 16.0 avatarImageLeft.layer.cornerRadius = 16.0 @@ -403,20 +405,8 @@ open class NEBaseChatMessageCell: NEChatBaseCell { } open func longPress(longPress: UILongPressGestureRecognizer) { - switch longPress.state { - case .began: - print("state:begin") + if longPress.state == .began { delegate?.didLongPressMessageView(self, contentModel) - case .changed: - print("state:changed") - case .ended: - print("state:ended") - case .cancelled: - print("state:cancelled") - case .failed: - print("state:failed") - default: - print("state:default") } } @@ -439,13 +429,17 @@ open class NEBaseChatMessageCell: NEChatBaseCell { // MARK: set data + /// 设置是否允许多选 + /// - Parameters: + /// - model: 数据模型 + /// - enableSelect: 是否处于多选状态 open func setSelect(_ model: MessageContentModel, _ enableSelect: Bool = false) { // 多选框 selectedButton.isHidden = model.isRevoked || !enableSelect selectedButton.isSelected = model.isSelected // 多选状态下,头像右移 - avatarImageLeftAnchor?.constant = enableSelect ? 42 : 16 + avatarImageLeftAnchor?.constant = enableSelect ? fun_chat_min_h : 16 // 多选状态下,消息状态视图(发送失败)位置下移,避免与多选重叠 activityViewCenterYAnchor?.constant = enableSelect ? model.contentSize.height / 2 - 12 : 0 @@ -462,9 +456,10 @@ open class NEBaseChatMessageCell: NEChatBaseCell { let avatarImage = isSend ? avatarImageRight : avatarImageLeft contentModel = model + contentModel?.cell = self tapGesture?.isEnabled = true showLeftOrRight(showRight: isSend) - updatePinStatus(model) + updatePinStatus(model, isSend) // time if let time = model.timeContent, !time.isEmpty { @@ -498,14 +493,14 @@ open class NEBaseChatMessageCell: NEChatBaseCell { avatarImage.image = nil nameLabel.isHidden = false avatarImage.backgroundColor = UIColor - .colorWithString(string: model.message?.senderId) + .colorWithString(string: ChatMessageHelper.getSenderId(model.message)) } } } else { avatarImage.image = nil nameLabel.isHidden = false avatarImage.backgroundColor = UIColor - .colorWithString(string: model.message?.senderId) + .colorWithString(string: ChatMessageHelper.getSenderId(model.message)) } if model.fullNameHeight > 0 { @@ -562,7 +557,7 @@ open class NEBaseChatMessageCell: NEChatBaseCell { SettingRepo.shared.getShowReadStatus(), NEKitChatConfig.shared.ui.messageProperties.showTeamMessageStatus == true { readView.isHidden = false - var total = ChatTeamCache.shared.getTeamInfo()?.memberCount ?? 0 + var total = NETeamUserManager.shared.getTeamInfo()?.memberCount ?? 0 if model.readCount + model.unreadCount != 0 { total = model.readCount + model.unreadCount + 1 } @@ -607,11 +602,14 @@ open class NEBaseChatMessageCell: NEChatBaseCell { readView.isHidden = !showRight } + /// 重置文本选中状态 + open func resetSelectRange() {} + + /// 选中所有文本 + open func selectAllRange() {} + /// 更新标记状态 - open func updatePinStatus(_ model: MessageContentModel) { - guard let isSend = model.message?.isSelf else { - return - } + open func updatePinStatus(_ model: MessageContentModel, _ isSend: Bool) { let pinLabel = isSend ? pinLabelRight : pinLabelLeft let pinImage = isSend ? pinImageRight : pinImageLeft let pinLabelH = isSend ? pinLabelHRight : pinLabelHLeft diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift index a47bb2b0..f92be3f3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift @@ -48,8 +48,8 @@ open class NEBaseChatTeamMemberCell: UITableViewCell { NSLayoutConstraint.activate([ headerView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 21), headerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - headerView.widthAnchor.constraint(equalToConstant: 42), - headerView.heightAnchor.constraint(equalToConstant: 42), + headerView.widthAnchor.constraint(equalToConstant: fun_chat_min_h), + headerView.heightAnchor.constraint(equalToConstant: fun_chat_min_h), ]) contentView.addSubview(nameLabel) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseLanguageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseLanguageCell.swift new file mode 100644 index 00000000..f49f6d8f --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseLanguageCell.swift @@ -0,0 +1,53 @@ +//// 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 UIKit + +@objcMembers +open class NEBaseLanguageCell: CornerCell { + /// 语言内容标签 + public lazy var languageLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 16.0) + label.translatesAutoresizingMaskIntoConstraints = false + label.backgroundColor = .clear + label.textColor = .ne_darkText + return label + }() + + /// 选中指示器 + public lazy var selectedImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = coreLoader.loadImage("language_selected") + imageView.isHidden = true + return imageView + }() + + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + setupLanguageCellUI() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// UI 初始化 + open func setupLanguageCellUI() { + dividerLine.isHidden = true + } + + /// 绑定数据 + /// - Parameter content: 语言内容 + /// - Parameter isSelect: 是否选中,选中高亮显示 + open func configureData(_ model: NElanguageCellModel) { + languageLabel.text = model.language + languageLabel.textColor = model.isSelect ? UIColor.ne_normalTheme : UIColor.ne_darkText + selectedImageView.isHidden = !model.isSelect + cornerType = model.cornerType + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift index 9b9554b6..f1fbc7ef 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift @@ -7,6 +7,16 @@ open class NEBaseUserSettingCell: CornerCell { public var model: UserSettingCellModel? + public var subCornerType: CornerType { + get { cornerType } + set { + if cornerType != newValue { + cornerType = newValue + setNeedsDisplay() + } + } + } + public lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -34,7 +44,7 @@ open class NEBaseUserSettingCell: CornerCell { open func configure(_ anyModel: Any) { if let m = anyModel as? UserSettingCellModel { model = m - cornerType = m.cornerType + subCornerType = m.cornerType titleLabel.text = m.cellName } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift index 3f7ed17f..0c03269d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift @@ -3,26 +3,38 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import UIKit -// protocol OperationCellDelegate: AnyObject { -// func didSelected(_ cell: OperationCell, _ model: OperationItem?) -// } - @objcMembers open class OperationCell: UICollectionViewCell { public var imageView = UIImageView() public var label = UILabel() -// public weak var delegate: OperationCellDelegate? public var model: OperationItem? { didSet { - imageView.image = UIImage.ne_imageNamed(name: model?.imageName) + if let imageName = model?.imageName, + !imageName.isEmpty, + let image = UIImage.ne_imageNamed(name: imageName) { + imageView.image = image + } else { + imageView.image = model?.image + } + label.text = model?.text } } override public init(frame: CGRect) { super.init(frame: frame) + commonUI() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + commonUI() + } + + public func commonUI() { contentView.accessibilityIdentifier = "id.menuCell" imageView.translatesAutoresizingMaskIntoConstraints = false @@ -48,15 +60,5 @@ open class OperationCell: UICollectionViewCell { label.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0), label.heightAnchor.constraint(equalToConstant: 18), ]) -// let tap = UITapGestureRecognizer(target: self, action: #selector(tapEvent)) -// self.contentView.addGestureRecognizer(tap) - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) } - -// @objc func tapEvent(tap: UITapGestureRecognizer) { -// self.delegate?.didSelected(self, model) -// } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift index d28a0979..632b23a3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift @@ -161,7 +161,7 @@ open class NEBasePinMessageCell: UITableViewCell { pinModel = item headerView.configHeadData(headUrl: item.chatmodel.avatar, name: item.chatmodel.shortName ?? "", - uid: item.chatmodel.message?.senderId ?? "") + uid: ChatMessageHelper.getSenderId(item.chatmodel.message) ?? "") nameLabel.text = item.chatmodel.fullName timeLabel.text = String.stringFromDate(date: Date(timeIntervalSince1970: item.message.createTime)) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift index e40ec909..a2eb3865 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift @@ -74,7 +74,7 @@ open class NEBasePinMessageVideoCell: NEBasePinMessageImageCell { if let videoObject = item.chatmodel.message?.attachment as? V2NIMMessageVideoAttachment { // 获取首帧 let videoUrl = videoObject.url ?? "" - let thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + let thumbUrl = V2NIMStorageUtil.videoCoverUrl(videoUrl, offset: 0) contentImageView.sd_setImage( with: URL(string: thumbUrl), placeholderImage: nil, diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift index 3b60f9b9..6d625cc2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift @@ -32,7 +32,6 @@ open class ChatActivityIndicatorView: UIView { failButton.isHidden = false case .successed: isHidden = true - default: print("default") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift index 0efe4ccf..244e2e19 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.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 UIKit @objc @@ -83,7 +84,6 @@ open class MessageOperationView: UIView, UICollectionViewDataSource, UICollectio open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - removeFromSuperview() delegate?.didSelectedItem(item: items[indexPath.row]) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEAITranslateView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEAITranslateView.swift new file mode 100644 index 00000000..e2232179 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEAITranslateView.swift @@ -0,0 +1,390 @@ +//// 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 Lottie +import NECommonUIKit +import UIKit + +public enum TranslateState: Int { + /// 空闲 + case Idle + /// 处理中 + case Translating + /// 使用 + case Use +} + +@objc public protocol NETranslateViewDelegate: AnyObject { + /// 切换语言 + func didSwitchLanguageClick(_ currentLanguage: String?) + /// 关闭 + func didCloseClick(_ view: NEAITranslateView) + /// 高度改变回调 + @objc optional func didChangeViewHeight(_ translateView: NEAITranslateView, _ changeHeight: CGFloat) + /// 开始翻译 + @objc optional func didStartClick() + /// 点击使用按钮 + @objc optional func didUseTranslate(_ content: String) +} + +@objcMembers +open class NEAITranslateView: UIView, NEGrowingTextViewDelegate { + /// 代理 + public weak var delegate: NETranslateViewDelegate? + + /// 输入框(外部传入,用于监听内容) + public var chatInputText: UITextView? + + /// 当前语言记录 + public var currentLanguage = "" { + didSet { + if oldValue != currentLanguage { + if currentLanguage.count > 0 { + let userDefault = UserDefaults.standard + userDefault.setValue(currentLanguage, forKey: IMKitClient.instance.account() + languageSuffix) + userDefault.synchronize() + } + if let first = currentLanguage.first { + shortLanguageLabel.text = String(first) + changeToIdleState(true) + } + } + } + } + + let languageSuffix = "_language" + + public var translateState = TranslateState.Idle + + /// 当前语言提示 + public lazy var shortLanguageLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 14) + label.textColor = .black + label.translatesAutoresizingMaskIntoConstraints = false + label.textAlignment = .center + return label + }() + + /// 语言背景 + public lazy var shortLanguageBgView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .white + view.layer.borderColor = UIColor(hexString: "#CCCCCC").cgColor + view.layer.borderWidth = 1.0 + view.layer.cornerRadius = 2.0 + return view + }() + + /// 切换语言按钮 + public lazy var switchLanguageButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + /// 关闭按钮 + public lazy var closeButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + /// 关闭图片 + public lazy var closeImageView: UIImageView = { + let imageView = UIImageView() + imageView.image = coreLoader.loadImage("top_close") + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + /// 分割线 + public lazy var separatorLine: UIView = { + let view = UIView() + view.backgroundColor = UIColor(hexString: "#DBDFE2") + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + /// 当前状态标签(内容为可使用 撤回 等待处理状态) + public lazy var statusLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 14) + label.textColor = .black + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + /// 配合statusLabel 显示loading状态 + public lazy var loadingImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + /// 翻译内容展示 + public lazy var showTranslateContentText: NEGrowingTextView = { + let textView = NEGrowingTextView() + textView.font = UIFont.systemFont(ofSize: 14) + textView.contentInset = .zero + textView.contentInset = .zero + textView.textColor = .ne_darkText + textView.clipsToBounds = true + textView.backgroundColor = .clear + textView.returnKeyType = .send + textView.isEditable = false + textView.delegate = self + textView.maxNumberOfLines = 3 + return textView + }() + + /// 使用文案富文本 + public var useAttributeString: NSMutableAttributedString = { + let attributeString = NSMutableAttributedString(string: chatLocalizable("translate_use")) + if let attachmentImage = coreLoader.loadImage("use_arrow") { + let attachment = NSTextAttachment() + attachment.image = attachmentImage + attachment.bounds = CGRectMake(0, -2, 10, 13) + attributeString.append(NSAttributedString(attachment: attachment)) + } + attributeString.addAttribute(.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(0, attributeString.length)) + attributeString.addAttribute(.font, value: UIFont.systemFont(ofSize: 14.0), range: NSMakeRange(0, attributeString.length)) + return attributeString + }() + + /// AI 处理 + public var translateString: NSMutableAttributedString = { + let attributeString = NSMutableAttributedString(string: chatLocalizable("translate_sure")) + attributeString.addAttribute(.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(0, attributeString.length)) + attributeString.addAttribute(.font, value: UIFont.systemFont(ofSize: 14.0), range: NSMakeRange(0, attributeString.length)) + return attributeString + }() + + /// 处理中文案 + public var processingString: NSMutableAttributedString = { + let attributeString = NSMutableAttributedString(string: chatLocalizable("ai_translating")) + attributeString.addAttribute(.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(0, attributeString.length)) + attributeString.addAttribute(.font, value: UIFont.systemFont(ofSize: 14.0), range: NSMakeRange(0, attributeString.length)) + return attributeString + }() + + /// 标题栏 loading 动画 + public lazy var loadingAnimationView: LottieAnimationView = { + let view = LottieAnimationView(name: "ne_loading_data", bundle: coreLoader.bundle) + view.translatesAutoresizingMaskIntoConstraints = false + view.loopMode = .loop + view.contentMode = .scaleAspectFill + view.accessibilityIdentifier = "id.loadingView" + view.isHidden = true + return view + }() + + public lazy var startButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + override public init(frame: CGRect) { + super.init(frame: frame) + setupTranslateLanguageUI() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// UI 初始化 + open func setupTranslateLanguageUI() { + backgroundColor = .ne_backgroundColor + clipsToBounds = true + + addSubview(shortLanguageBgView) + addSubview(shortLanguageLabel) + addSubview(switchLanguageButton) + addSubview(closeImageView) + addSubview(closeButton) + addSubview(separatorLine) + addSubview(statusLabel) + addSubview(loadingAnimationView) + addSubview(showTranslateContentText) + addSubview(startButton) + + NSLayoutConstraint.activate([ + shortLanguageLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 20), + shortLanguageLabel.topAnchor.constraint(equalTo: topAnchor, constant: 10), + ]) + + NSLayoutConstraint.activate([ + shortLanguageBgView.leftAnchor.constraint(equalTo: shortLanguageLabel.leftAnchor, constant: -8), + shortLanguageBgView.rightAnchor.constraint(equalTo: shortLanguageLabel.rightAnchor, constant: 8), + shortLanguageBgView.topAnchor.constraint(equalTo: shortLanguageLabel.topAnchor, constant: -2), + shortLanguageBgView.bottomAnchor.constraint(equalTo: shortLanguageLabel.bottomAnchor, constant: 2), + ]) + + NSLayoutConstraint.activate([ + switchLanguageButton.leftAnchor.constraint(equalTo: leftAnchor), + switchLanguageButton.topAnchor.constraint(equalTo: topAnchor), + switchLanguageButton.bottomAnchor.constraint(equalTo: shortLanguageLabel.bottomAnchor, constant: 15), + switchLanguageButton.rightAnchor.constraint(equalTo: shortLanguageLabel.rightAnchor, constant: 12), + ]) + + NSLayoutConstraint.activate([ + closeImageView.widthAnchor.constraint(equalToConstant: 20), + closeImageView.heightAnchor.constraint(equalToConstant: 20), + closeImageView.topAnchor.constraint(equalTo: topAnchor, constant: 7), + closeImageView.rightAnchor.constraint(equalTo: rightAnchor, constant: -12), + ]) + + NSLayoutConstraint.activate([ + closeButton.widthAnchor.constraint(equalToConstant: 40), + closeButton.heightAnchor.constraint(equalToConstant: 40), + closeButton.topAnchor.constraint(equalTo: topAnchor), + closeButton.rightAnchor.constraint(equalTo: rightAnchor), + ]) + + NSLayoutConstraint.activate([ + separatorLine.heightAnchor.constraint(equalToConstant: 1), + separatorLine.leftAnchor.constraint(equalTo: leftAnchor, constant: 12), + separatorLine.rightAnchor.constraint(equalTo: rightAnchor, constant: -12), + separatorLine.topAnchor.constraint(equalTo: topAnchor, constant: 34), + ]) + + if let width = UIApplication.shared.keyWindow?.width { + showTranslateContentText.frame = CGRectMake(12, 46, width - 112, 20) + } + + NSLayoutConstraint.activate([ + statusLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -12), + statusLabel.topAnchor.constraint(equalTo: separatorLine.bottomAnchor, constant: 11), + ]) + + loadingAnimationView.play() + NSLayoutConstraint.activate([ + loadingAnimationView.centerYAnchor.constraint(equalTo: statusLabel.centerYAnchor, constant: -1), + loadingAnimationView.rightAnchor.constraint(equalTo: statusLabel.leftAnchor, constant: -2), + loadingAnimationView.widthAnchor.constraint(equalToConstant: 12), + loadingAnimationView.heightAnchor.constraint(equalToConstant: 12), + ]) + + NSLayoutConstraint.activate([ + startButton.rightAnchor.constraint(equalTo: rightAnchor), + startButton.topAnchor.constraint(equalTo: separatorLine.bottomAnchor), + startButton.heightAnchor.constraint(equalToConstant: 40), + startButton.widthAnchor.constraint(equalToConstant: 66), + ]) + startButton.addTarget(self, action: #selector(didStart), for: .touchUpInside) + + let placehoderAttribute = NSMutableAttributedString(string: chatLocalizable("translate_default")) + placehoderAttribute.addAttribute(.foregroundColor, value: UIColor(hexString: "#AAAAAA"), range: NSMakeRange(0, placehoderAttribute.length)) + placehoderAttribute.addAttribute(.font, value: UIFont.systemFont(ofSize: 14.0), range: NSMakeRange(0, placehoderAttribute.length)) + showTranslateContentText.placeholder = placehoderAttribute + + statusLabel.attributedText = translateString + let userDefault = UserDefaults.standard + if let cacheLanguage = userDefault.value(forKey: IMKitClient.instance.account() + languageSuffix) as? String, cacheLanguage.count > 0 { + currentLanguage = cacheLanguage + shortLanguageLabel.text = cacheLanguage + } else if let firstLanguage = NETranslateLanguageManager.shared.languageDatas.first, let first = firstLanguage.first { + currentLanguage = firstLanguage + shortLanguageLabel.text = firstLanguage + } + + // 事件绑定 + closeButton.addTarget(self, action: #selector(didClickClose), for: .touchUpInside) + + switchLanguageButton.addTarget(self, action: #selector(didClickChange), for: .touchUpInside) + } + + /// 控件内容高度 + public func getCurrentHeight() -> CGFloat { + 70 + } + + /// 关闭按钮回调 + func didClickClose() { + delegate?.didCloseClick(self) + changeToIdleState(true) + } + + /// 切花语言按钮点击回调 + func didClickChange() { + delegate?.didSwitchLanguageClick(currentLanguage) + } + + // MARK: - growing text view delegate + + public func growingTextView(_ growingTextView: NEGrowingTextView, didChangeHeight height: CGFloat, difference: CGFloat) { + if frame.height < 1 { + // 首次加载 不需要回调,此时回调将导致外部显示异常 + return + } + showTranslateContentText.frame = CGRectMake(12, 47, width - 112, height) + delegate?.didChangeViewHeight?(self, 52 + height) + } + + public func didStart() { + if translateState == .Use { + if let text = showTranslateContentText.text { + delegate?.didUseTranslate?(text) + } + changeToIdleState(true) + return + } + + if translateState != .Idle { + return + } + + if let textView = chatInputText { + if textView.attributedText.length > 0 { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + UIApplication.shared.keyWindow?.endEditing(true) + UIApplication.shared.keyWindow?.ne_makeToast(commonLocalizable("network_error")) + return + } + delegate?.didStartClick?() + changeToTranslatingState() + } else { + changeToIdleState(true) + } + } + } + + public func setTranslateContent(_ content: String) { + if translateState == .Translating { + showTranslateContentText.text = content + changeToUseState() + } + } + + /// 恢复默认状态 + /// - Parameter isClearText: 是否清空输入框 + public func changeToIdleState(_ isClearText: Bool = false) { + translateState = .Idle + if isClearText { + showTranslateContentText.text = "" + } + statusLabel.attributedText = translateString + loadingAnimationView.isHidden = true + loadingAnimationView.stop() + } + + /// 进入处理中状态 + public func changeToTranslatingState() { + translateState = .Translating + statusLabel.attributedText = processingString + loadingAnimationView.isHidden = false + loadingAnimationView.play() + } + + /// 进入翻译完成状态(等待使用) + public func changeToUseState() { + translateState = .Use + statusLabel.attributedText = useAttributeString + loadingAnimationView.isHidden = true + loadingAnimationView.stop() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift index 53d2b15e..a25c5cf6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift @@ -48,6 +48,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, public var atRangeCache = [String: MessageAtCacheModel]() + public var nickAccidList = [String]() public var nickAccidDic = [String: String]() public var isMultipleLineMode = false // 是否是多行模式 @@ -66,7 +67,6 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, textView.translatesAutoresizingMaskIntoConstraints = false textView.backgroundColor = .white textView.returnKeyType = .send - textView.allowsEditingTextAttributes = true textView.typingAttributes = [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)] textView.linkTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)] textView.dataDetectorTypes = [] @@ -290,10 +290,12 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, 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)] + textView.typingAttributes = [NSAttributedString.Key.foregroundColor: NEKitChatConfig.shared.ui.messageProperties.messageTextColor, + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)] if chatInpuMode == .normal || chatInpuMode == .multipleSend, text == "\n" { guard var realText = getRealSendText(textView.attributedText) else { + delegate?.textViewDidChange() return true } if realText.trimmingCharacters(in: .whitespaces).isEmpty { @@ -315,6 +317,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, DispatchQueue.main.async { textView.selectedRange = NSMakeRange(range.location + addString.length, 0) } + delegate?.textViewDidChange() return false } @@ -323,8 +326,6 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, if let findRange = temRange { let mutableAttri = NSMutableAttributedString(attributedString: textView.attributedText) if mutableAttri.length >= findRange.location + findRange.length { - mutableAttri.removeAttribute(NSAttributedString.Key.foregroundColor, range: findRange) - mutableAttri.removeAttribute(NSAttributedString.Key.font, range: findRange) if range.length == 1 { mutableAttri.replaceCharacters(in: findRange, with: "") } @@ -337,11 +338,12 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, } return false } + delegate?.textViewDidChange() return true } else { delegate?.textChanged(text: text) } - + delegate?.textViewDidChange() return true } @@ -388,9 +390,11 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, open func selectedEmoticon(emoticonID: String, emotCatalogID: String, description: String) { if emoticonID.isEmpty { // 删除键 + delegate?.textViewDidChange() textView.deleteBackward() print("delete ward") } else { + delegate?.textViewDidChange() let range = textView.selectedRange let attribute = NEEmotionTool.getAttWithStr(str: description, font: .systemFont(ofSize: 16)) let mutaAttribute = NSMutableAttributedString(attributedString: textView.attributedText) @@ -602,6 +606,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, } open func clearAtCache() { + nickAccidList.removeAll() nickAccidDic.removeAll() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatTextView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatTextView.swift new file mode 100644 index 00000000..a0a58903 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatTextView.swift @@ -0,0 +1,19 @@ + +// 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 NECommonUIKit +import UIKit + +@objcMembers +open class NEChatTextView: UITextView { + override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + false + } + + override open func shouldChangeText(in range: UITextRange, replacementText text: String) -> Bool { + false + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift index 3b46e8ba..c903d955 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift @@ -5,6 +5,7 @@ import Foundation import NEChatKit import NECommonKit +import NECommonUIKit import NECoreIM2Kit import NECoreKit import NIMSDK @@ -99,6 +100,9 @@ public protocol ChatViewModelDelegate: NSObjectProtocol { /// 多选列表变更 /// - Parameter count: 选中的消息数量 @objc optional func selectedMessagesChanged(_ count: Int) + + /// 翻译结果回调 + @objc optional func didTranslateResult(_ content: String) } @objcMembers @@ -126,12 +130,22 @@ open class ChatViewModel: NSObject { public var topMessage: V2NIMMessage? // 置顶消息 public var isReplying = false public let messagPageNum: Int = 100 + public let aiMessagNum: Int = 30 // 从 aiMessagNum 条消息中取文本消息作为上下文内容 public var anchor: V2NIMMessage? public var isHistoryChat = false public var deletingMsgDic = Set() + /// AI 翻译 User + public var translationAIUser: V2NIMAIUser? + + /// 翻译request id 记录 + public var translationlanguageRquestId = "" + + /// 数字人请求成功code + public var aiUserRequestSuccess = 200 + override init() { conversationId = "" sessionId = "" @@ -144,8 +158,7 @@ open class ChatViewModel: NSObject { sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId) ?? "" anchor = nil super.init() - chatRepo.addChatListener(self) - contactRepo.addContactListener(self) + addListener() } init(conversationId: String, anchor: V2NIMMessage?) { @@ -157,22 +170,40 @@ open class ChatViewModel: NSObject { if anchor != nil { isHistoryChat = true } + addListener() + } + + /// 添加监听 + open func addListener() { chatRepo.addChatListener(self) - contactRepo.addContactListener(self) + + if IMKitConfigCenter.shared.enableAIUser { + AIRepo.shared.addAIListener(self) + } + } + + deinit { + chatRepo.removeChatListener(self) + + if IMKitConfigCenter.shared.enableAIUser { + AIRepo.shared.removeAIListener(self) + } } /// 根据会话id列表清空相应会话的未读数 public func clearUnreadCount() { - ConversationProvider.shared.clearUnreadCountByIds([conversationId]) { result, error in + ConversationRepo.shared.clearUnreadCountByIds([conversationId]) { result, error in NEALog.infoLog(ModuleName, desc: #function + " error" + (error?.localizedDescription ?? "")) } + ConversationRepo.shared.markConversationRead(conversationId) { result, error in + NEALog.infoLog(ModuleName, desc: #function + " makr covnersaion read error : \(error?.localizedDescription ?? "")") + } } /// 加载数据 /// - Parameter completion: 完成回调 open func loadData(_ completion: @escaping (Error?, NSInteger, NSInteger, Int) -> Void) { NEALog.infoLog(ModuleName + " " + className(), desc: #function) - weak var weakSelf = self messages.removeAll() @@ -180,20 +211,20 @@ open class ChatViewModel: NSObject { if anchor == nil { isHistoryChat = false - weakSelf?.getHistoryMessage(order: .QUERY_DIRECTION_DESC, message: nil) { error, count, models in + getHistoryMessage(order: .QUERY_DIRECTION_DESC, message: nil) { [weak self] error, count, models in NEALog.infoLog( ModuleName + " " + ChatViewModel.className(), desc: "CALLBACK getMessageList " + (error?.localizedDescription ?? "no error") ) completion(error, count, 0, 0) - weakSelf?.loadMoreWithMessage(models) + self?.loadMoreWithMessage(models) } } else { isHistoryChat = true // 有锚点消息,从两个方向拉去消息 - weakSelf?.newMsg = weakSelf?.anchor - weakSelf?.oldMsg = weakSelf?.anchor + newMsg = anchor + oldMsg = anchor let group = DispatchGroup() @@ -205,7 +236,7 @@ open class ChatViewModel: NSObject { var err: Error? group.enter() - weakSelf?.getHistoryMessage(order: .QUERY_DIRECTION_DESC, message: weakSelf?.anchor) { error, value, models in + getHistoryMessage(order: .QUERY_DIRECTION_DESC, message: anchor) { [weak self] error, value, models in moreEnd = value if error != nil { err = error @@ -215,16 +246,16 @@ open class ChatViewModel: NSObject { loadMessages.append(contentsOf: historyDatas) group.enter() - if let anchorMessage = weakSelf?.anchor { + if let anchorMessage = self?.anchor { loadMessages.append(anchorMessage) - weakSelf?.modelFromMessage(message: anchorMessage) { model in - weakSelf?.messages.append(model) + self?.modelFromMessage(message: anchorMessage) { model in + self?.messages.append(model) group.leave() } } group.enter() - weakSelf?.getHistoryMessage(order: .QUERY_DIRECTION_ASC, message: weakSelf?.anchor) { error, value, models in + self?.getHistoryMessage(order: .QUERY_DIRECTION_ASC, message: self?.anchor) { error, value, models in NEALog.infoLog( ModuleName + " " + ChatViewModel.className(), desc: "CALLBACK pullRemoteRefresh " + (error?.localizedDescription ?? "no error") @@ -241,9 +272,9 @@ open class ChatViewModel: NSObject { group.leave() } - group.notify(queue: .main) { + group.notify(queue: .main) { [weak self] in completion(err, moreEnd, newEnd, historyDatas.count) - weakSelf?.loadMoreWithMessage(loadMessages) + self?.loadMoreWithMessage(loadMessages) } } } @@ -276,50 +307,51 @@ open class ChatViewModel: NSObject { /// - Parameter messageArray: 消息列表 func loadMoreWithMessage(_ messageArray: [V2NIMMessage]) { NEALog.infoLog(ModuleName + " " + className(), desc: #function) - weak var weakSelf = self - let conversationId = weakSelf?.conversationId ?? "" let group = DispatchGroup() let sema = DispatchSemaphore(value: 0) - DispatchQueue.global().async { [self] in + DispatchQueue.global().async { [weak self] in + guard let conversationId = self?.conversationId else { return } // 群聊需要获取群昵称 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 = weakSelf?.getShowName(uid) { - let userFriend = NEFriendUserCache.shared.getFriendInfo(uid) ?? ChatUserCache.shared.getUserInfo(uid) - model.avatar = userFriend?.user?.avatar - model.fullName = fullName - model.shortName = NEFriendUserCache.getShortName(userFriend?.showName() ?? "") + let userIds = self?.messages.compactMap { $0.message?.senderId } + if let userIds = userIds { + self?.loadShowName(userIds, self?.sessionId) { [weak self] in + // 获取头像昵称 + for model in self?.messages ?? [] { + if let uid = ChatMessageHelper.getSenderId(model.message), + let fullName = self?.getShowName(uid) { + let userFriend = ChatMessageHelper.getUserFromCache(uid) + model.avatar = userFriend?.user?.avatar + model.fullName = fullName + model.shortName = NEFriendUserCache.getShortName(userFriend?.showName() ?? "") + } } + sema.signal() } - sema.signal() + sema.wait() } - sema.wait() } // 查询回复 - for model in messages { + for model in self?.messages ?? [] { group.enter() - loadReply(model) { + self?.loadReply(model) { group.leave() } } // 查找标记记录 group.enter() - chatRepo.getPinnedMessageList(conversationId: weakSelf?.conversationId ?? "") { [weak self] pinList, error in + self?.chatRepo.getPinnedMessageList(conversationId: conversationId) { [weak self] pinList, error in if let pinList = pinList { let userIds = pinList.map(\.operatorId) group.enter() - self?.loadShowName(userIds, weakSelf?.sessionId) { + self?.loadShowName(userIds, self?.sessionId) { for pin in pinList { - for model in weakSelf?.messages ?? [] { + for model in self?.messages ?? [] { if model.message?.messageClientId == pin.messageRefer?.messageClientId { model.isPined = true model.pinAccount = pin.operatorId @@ -336,7 +368,7 @@ open class ChatViewModel: NSObject { // 获取消息已读未读 group.enter() - getMessageReceipts(messages: messageArray) { reloadIndexs, error in + self?.getMessageReceipts(messages: messageArray) { reloadIndexs, error in NEALog.infoLog( ModuleName + " " + ChatViewModel.className(), desc: "CALLBACK getP2PMessageReceipt " + (error?.localizedDescription ?? "no error") @@ -344,8 +376,8 @@ open class ChatViewModel: NSObject { group.leave() } - group.notify(queue: .main) { - weakSelf?.delegate?.tableViewReload() + group.notify(queue: .main) { [weak self] in + self?.delegate?.tableViewReload() } } @@ -365,8 +397,8 @@ open class ChatViewModel: NSObject { var indexPaths = [IndexPath]() for (i, model) in messages.enumerated() { // 更新消息发送者昵称和头像 - if model.message?.senderId == accid { - let user = NEFriendUserCache.shared.getFriendInfo(accid) ?? ChatUserCache.shared.getUserInfo(accid) + if ChatMessageHelper.getSenderId(model.message) == accid { + let user = ChatMessageHelper.getUserFromCache(accid) model.fullName = showName model.shortName = NEFriendUserCache.getShortName(showName) model.avatar = user?.user?.avatar @@ -381,7 +413,7 @@ open class ChatViewModel: NSObject { } // 更新置顶消息发送者昵称 - if accid == topMessage?.senderId { + if accid == ChatMessageHelper.getSenderId(topMessage) { delegate?.updateTopName(name: showName) } @@ -441,21 +473,25 @@ open class ChatViewModel: NSObject { } } - weak var weakSelf = self - chatRepo.getMessageList(option: opt) { error, messages in + chatRepo.getMessageList(option: opt) { [weak self] messages, error in if let messageArray = messages, messageArray.count > 0 { let group = DispatchGroup() if order == .QUERY_DIRECTION_DESC { - weakSelf?.oldMsg = messageArray.last + self?.oldMsg = messageArray.last } else { - weakSelf?.newMsg = messageArray.last + self?.newMsg = messageArray.last } for msg in messageArray { + // 数字人回复的消息 + if ChatMessageHelper.isAISender(msg) { + self?.setErrorText(msg) + } + group.enter() - weakSelf?.modelFromMessage(message: msg) { model in - if weakSelf?.messages.contains(where: { $0.message?.messageClientId == model.message?.messageClientId }) == false { - weakSelf?.insertToMessages(model) + self?.modelFromMessage(message: msg) { model in + if self?.messages.contains(where: { $0.message?.messageClientId == model.message?.messageClientId }) == false { + self?.insertToMessages(model) } group.leave() } @@ -463,21 +499,31 @@ open class ChatViewModel: NSObject { group.notify(queue: .main) { // 显示时间 - weakSelf?.addTimeForHistoryMessage() + self?.addTimeForHistoryMessage() // 回调消息列表 completion(error, messageArray.count, messageArray) } // 标记已读 - weakSelf?.markRead(messages: messageArray) { error in + self?.markRead(messages: messageArray) { error in NEALog.infoLog( ModuleName + " " + ChatViewModel.className(), desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") ) } } else { - completion(error, 0, []) + if self?.messages.isEmpty == true, + let accid = self?.sessionId, + NEAIUserManager.shared.isAIUser(accid) { + if let cid = self?.conversationId, + let welcomeText = NEAIUserManager.shared.getWelcomeText(accid) { + self?.insertTextMessage(welcomeText, cid, accid) + } + completion(error, 1, []) + } else { + completion(error, 0, []) + } } } } @@ -552,24 +598,220 @@ open class ChatViewModel: NSObject { /// - completion: 回调 open func sendMessage(message: V2NIMMessage, conversationId: String? = nil, - _ completion: @escaping (V2NIMMessage?, Error?) -> Void) { + params: V2NIMSendMessageParams? = nil, + _ completion: @escaping (V2NIMMessage?, Error?, UInt) -> Void) { NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text: \(String(describing: message.text))") chatRepo.sendMessage(message: message, - conversationId: conversationId ?? self.conversationId) { result, error, pro in - completion(result?.message, error) + conversationId: conversationId ?? self.conversationId, + params: params) { result, error, pro in + completion(result?.message ?? message, error, pro) + } + } + + /// 获取请求大模型的内容 + /// - Parameters: + /// - text: 请求/响应的文本内容 + /// - type: 类型 + /// - Returns: 请求大模型的内容 + open func getAIModelCallContent(_ text: String?, + _ type: V2NIMAIModelCallContentType) -> V2NIMAIModelCallContent { + let content = V2NIMAIModelCallContent() + content.msg = text ?? "" + content.type = type + return content + } + + /// 获取上下文内容 + /// - Returns: 上下文内容 + open func getAIMessages() -> [V2NIMAIModelCallMessage]? { + guard NEAIUserManager.shared.isAIUser(sessionId) else { + return nil } + + let messageModels = messages.suffix(aiMessagNum) + let aiMessageModels = messageModels.filter { $0.type == .text || $0.type == .richText || $0.type == .reply } + + var firstUserMessage = false // 是否找到第一条用户发的消息 + var aiMessages = [V2NIMAIModelCallMessage]() + + for (i, model) in aiMessageModels.enumerated() { + var isUserMessage = false + if model.message?.aiConfig == nil || model.message?.aiConfig?.aiStatus != .MESSAGE_AI_STATUS_RESPONSE { + firstUserMessage = true + isUserMessage = true + } + + // 找到第一条用户发送的消息 + if !firstUserMessage { + continue + } + + let aiMessage = V2NIMAIModelCallMessage() + aiMessage.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT + + if isUserMessage { + aiMessage.role = .NIM_AI_MODEL_ROLE_TYPE_USER + } else { + aiMessage.role = .NIM_AI_MODEL_ROLE_TYPE_ASSISTANT + } + + if model.type == .text || model.type == .reply { + let text = model.message?.text ?? "" + aiMessage.msg = text + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "[AIChat], message text\(i + 1): \(text)") + } else if model.type == .richText, let m = model as? MessageRichTextModel { + let text = (m.titleText ?? "") + (m.message?.text ?? "") + aiMessage.msg = text + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "[AIChat], message text\(i + 1): \(text)") + } + + aiMessages.append(aiMessage) + } + + return aiMessages.isEmpty ? nil : aiMessages + } + + /// 获取消息发送参数 + /// - Parameters: + /// - aiUserAccid: 数字人 id + /// - message: 消息 + /// - Returns: 消息发送参数 + func getSendMessageParams(_ aiUserAccid: String? = nil, _ message: V2NIMMessage) -> V2NIMSendMessageParams { + var aiUserAccid = aiUserAccid + var needMessgaes = false // 是否需要上下文 + if NEAIUserManager.shared.isAIUser(sessionId) { + aiUserAccid = sessionId + needMessgaes = true // 与 AI 单聊才需要回溯上下文,@ 数字人无上下文 + } + + let params = chatRepo.getSendMessageParams() + if let aiAccid = aiUserAccid { + let aiConfig = V2NIMMessageAIConfigParams() + aiConfig.accountId = aiAccid + + // 文本消息 + if message.messageType == .MESSAGE_TYPE_TEXT, let text = message.text { + aiConfig.content = getAIModelCallContent(text, .NIM_AI_MODEL_CONTENT_TYPE_TEXT) + if needMessgaes { + aiConfig.messages = getAIMessages() + } + } + + // 换行消息 + if message.messageType == .MESSAGE_TYPE_CUSTOM, + let type = NECustomUtils.typeOfCustomMessage(message.attachment), + type == customRichTextType { + let title = NECustomUtils.titleOfRichText(message.attachment) + let body = NECustomUtils.bodyOfRichText(message.attachment) + let text = (title ?? "") + (body ?? "") + aiConfig.content = getAIModelCallContent(text, .NIM_AI_MODEL_CONTENT_TYPE_TEXT) + if needMessgaes { + aiConfig.messages = getAIMessages() + } + } + + params.aiConfig = aiConfig + } + + return params + } + + /// 获取回复消息发送参数 + /// 回复消息上下文只取被回复的消息 + /// - Parameters: + /// - aiUserAccid: 数字人 id + /// - replyMessage: 被回复的消息 + /// - message: 回复的消息 + /// - Returns: 消息发送参数 + func getReplyMessageParams(_ aiUserAccid: String? = nil, + _ replyMessage: V2NIMMessage, + _ message: V2NIMMessage) -> V2NIMSendMessageParams { + let params = chatRepo.getSendMessageParams() + if let aiAccid = aiUserAccid { + let aiConfig = V2NIMMessageAIConfigParams() + aiConfig.accountId = aiAccid + + // 文本消息 + if replyMessage.messageType == .MESSAGE_TYPE_TEXT { + let aiMessage = V2NIMAIModelCallMessage() + aiMessage.msg = replyMessage.text ?? "" + aiMessage.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT + aiMessage.role = .NIM_AI_MODEL_ROLE_TYPE_USER + + aiConfig.messages = [aiMessage] + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "[AIChat], reply message text: \(replyMessage.text ?? "")") + } + + // 换行消息 + if replyMessage.messageType == .MESSAGE_TYPE_CUSTOM, + let type = NECustomUtils.typeOfCustomMessage(replyMessage.attachment), + type == customRichTextType { + let title = NECustomUtils.titleOfRichText(replyMessage.attachment) + let body = NECustomUtils.bodyOfRichText(replyMessage.attachment) + let text = (title ?? "") + (body ?? "") + + let aiMessage = V2NIMAIModelCallMessage() + aiMessage.msg = text + aiMessage.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT + aiMessage.role = .NIM_AI_MODEL_ROLE_TYPE_USER + + aiConfig.messages = [aiMessage] + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "[AIChat], reply message text: \(replyMessage.text ?? "")") + } + + aiConfig.content = getAIModelCallContent(message.text, .NIM_AI_MODEL_CONTENT_TYPE_TEXT) + params.aiConfig = aiConfig + } + + return params + } + + /// 获取转发消息发送参数 + /// 转发消息给数字人没有上下文 + /// - Parameters: + /// - aiUserAccid: 数字人 id + /// - forwordMessage: 转发的消息 + /// - Returns: 消息发送参数 + func getForwardMessageParams(_ aiUserAccid: String? = nil, + _ forwordMessage: V2NIMMessage) -> V2NIMSendMessageParams { + let params = chatRepo.getSendMessageParams() + if let aiAccid = aiUserAccid { + let aiConfig = V2NIMMessageAIConfigParams() + aiConfig.accountId = aiAccid + + // 文本消息 + if forwordMessage.messageType == .MESSAGE_TYPE_TEXT { + aiConfig.content = getAIModelCallContent(forwordMessage.text, .NIM_AI_MODEL_CONTENT_TYPE_TEXT) + } + + // 换行消息 + if forwordMessage.messageType == .MESSAGE_TYPE_CUSTOM, + let type = NECustomUtils.typeOfCustomMessage(forwordMessage.attachment), + type == customRichTextType { + let title = NECustomUtils.titleOfRichText(forwordMessage.attachment) + let body = NECustomUtils.bodyOfRichText(forwordMessage.attachment) + let text = (title ?? "") + (body ?? "") + aiConfig.content = getAIModelCallContent(text, .NIM_AI_MODEL_CONTENT_TYPE_TEXT) + } + + params.aiConfig = aiConfig + } + + return params } /// 发送文本消息 /// - Parameters: /// - text: 文本内容 - /// - remoteExt: 扩展字段 /// - conversationId: 会话 id + /// - remoteExt: 扩展字段 + /// - aiUserAccid: 数字人 accountId /// - completion: 完成回调 open func sendTextMessage(text: String, conversationId: String? = nil, - remoteExt: [String: Any]?, + remoteExt: [String: Any]? = nil, + aiUserAccid: String? = nil, _ completion: @escaping (V2NIMMessage?, Error?) -> Void) { NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") if text.count <= 0 { @@ -578,7 +820,33 @@ open class ChatViewModel: NSObject { } let message = MessageUtils.textMessage(text: text, remoteExt: remoteExt) - sendMessage(message: message, conversationId: conversationId) { message, error in + + var aiUserAccid = aiUserAccid + if NEAIUserManager.shared.isAIUser(sessionId) { + aiUserAccid = sessionId + } + let params = getSendMessageParams(aiUserAccid, message) + sendMessage(message: message, conversationId: conversationId, params: params) { message, error, pro in + completion(message, error) + } + } + + /// 发送换行消息 + /// - Parameters: + /// - message: 换行消息 + /// - title: 标题 + /// - body: 内容 + /// - aiUserAccid: 数字人 accountId + /// - completion: 完成回调 + open func sendRichTextMessage(message: V2NIMMessage, + title: String? = nil, + body: String? = nil, + aiUserAccid: String? = nil, + _ completion: @escaping (V2NIMMessage?, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", title: \(String(describing: title)), body: \(String(describing: body))") + + let params = getSendMessageParams(aiUserAccid, message) + sendMessage(message: message, conversationId: conversationId, params: params) { message, error, pro in completion(message, error) } } @@ -597,8 +865,10 @@ open class ChatViewModel: NSObject { } 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 + let params = getSendMessageParams(nil, message) + sendMessage(message: message, conversationId: conversationId, params: params) { _, error, pro in completion(error) } } @@ -615,8 +885,10 @@ open class ChatViewModel: NSObject { 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 + let params = getSendMessageParams(nil, message) + sendMessage(message: message, conversationId: conversationId, params: params) { _, error, pro in completion(error) } } @@ -632,19 +904,19 @@ open class ChatViewModel: NSObject { height: Int32 = 0, duration: Int32 = 0, conversationId: String? = nil, - _ completion: @escaping (Error?) -> Void) { + _ completion: @escaping (V2NIMMessage?, Error?, UInt) -> Void) { NEALog.infoLog(ModuleName + " " + className(), desc: #function + ",video url.path:" + url.path) - weak var weakSelf = self - convertVideoToMP4(videoURL: url) { url, error in - if let path = url?.path, let conversationId = weakSelf?.conversationId { + convertVideoToMP4(videoURL: url) { [weak self] url, error in + if let path = url?.path { 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) + let params = self?.getSendMessageParams(nil, message) + self?.sendMessage(message: message, conversationId: conversationId, params: params) { message, error, pro in + completion(message, error, pro) } } else { NEALog.errorLog("chat veiw model", desc: "convert mov to mp4 failed") - completion(NSError(domain: "convert mov to mp4 failed", code: 414)) + completion(nil, NSError(domain: "convert mov to mp4 failed", code: 414), 0) } } } @@ -683,11 +955,14 @@ open class ChatViewModel: NSObject { open func sendLocationMessage(model: ChatLocaitonModel, conversationId: String? = nil, _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", title:\(model.title), address:\(model.address)") + 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 + let params = getSendMessageParams(nil, message) + sendMessage(message: message, conversationId: conversationId, params: params) { _, error, pro in completion(error) } } @@ -701,11 +976,13 @@ open class ChatViewModel: NSObject { open func sendFileMessage(filePath: String, displayName: String?, conversationId: String? = nil, - _ completion: @escaping (Error?) -> Void) { + _ completion: @escaping (V2NIMMessage?, Error?, UInt) -> 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) + let params = getSendMessageParams(nil, message) + sendMessage(message: message, conversationId: conversationId, params: params) { message, error, pro in + completion(message, error, pro) } } @@ -720,20 +997,54 @@ open class ChatViewModel: NSObject { 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 + let params = getSendMessageParams(nil, message) + sendMessage(message: message, conversationId: conversationId, params: params) { _, error, pro in completion(error) } } + /// 本地插入文本消息 + /// - Parameter text: 消息文本 + /// - Parameter conversationId: 会话 id + /// - Parameter senderId: 发送者 id + open func insertTextMessage(_ text: String, + _ conversationId: String, + _ senderId: String? = nil) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text:\(text)") + + let message = MessageUtils.textMessage(text: text) + chatRepo.insertMessageToLocal(message: message, conversationId: conversationId, senderId: senderId) { [weak self] _, error in + if let currentSid = self?.conversationId, currentSid == conversationId { + self?.modelFromMessage(message: message) { model in + if let index = self?.insertToMessages(model) { + self?.delegate?.sending(message, IndexPath(row: index, section: 0)) + } + } + } + } + ConversationRepo.shared.createConversation(conversationId) { [weak self] conversation, error in + NEALog.infoLog(self?.className() ?? "", desc: #function + "insertTextMessage \(error?.localizedDescription ?? "")") + } + } + /// 本地插入提示消息 + /// - Parameter text: 提示文本 /// - Parameter conversationId: 会话 id - open func insertTipMessage(_ text: String, _ conversationId: String) { + /// - Parameter senderId: 发送者 id + open func insertTipMessage(_ text: String, + _ conversationId: String? = nil, + _ senderId: String? = nil) { NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text:\(text)") + let cid = conversationId ?? self.conversationId let tip = MessageUtils.tipMessage(text: text) - chatRepo.insertMessageToLocal(message: tip, conversationId: conversationId) { [weak self] _, error in - if let currentSid = self?.conversationId, currentSid == conversationId { + chatRepo.insertMessageToLocal(message: tip, + conversationId: cid, + senderId: senderId) { [weak self] _, error in + // 当前聊天页面插入的提示消息 + if cid == self?.conversationId { self?.modelFromMessage(message: tip) { model in if let index = self?.insertToMessages(model) { self?.delegate?.sending(tip, IndexPath(row: index, section: 0)) @@ -776,13 +1087,12 @@ open class ChatViewModel: NSObject { } deletingMsgDic.insert(messageId) - weak var weakSelf = self // 本地消息 if !(message.messageServerId?.isEmpty == false) { - chatRepo.deleteMessage(message: message, onlyDeleteLocal: true) { error in + chatRepo.deleteMessage(message: message, onlyDeleteLocal: true) { [weak self] error in if error == nil { - weakSelf?.deleteMessageUpdateUI([message]) - weakSelf?.deletingMsgDic.remove(messageId) + self?.deleteMessageUpdateUI([message]) + self?.deletingMsgDic.remove(messageId) } completion(error) } @@ -790,20 +1100,20 @@ open class ChatViewModel: NSObject { } if message.messageServerId == "0" { - chatRepo.deleteMessage(message: message, onlyDeleteLocal: true) { error in + chatRepo.deleteMessage(message: message, onlyDeleteLocal: true) { [weak self] error in if error == nil { - weakSelf?.deleteMessageUpdateUI([message]) - weakSelf?.deletingMsgDic.remove(messageId) + self?.deleteMessageUpdateUI([message]) + self?.deletingMsgDic.remove(messageId) } completion(error) } return } - chatRepo.deleteMessage(message: message, onlyDeleteLocal: false) { error in + chatRepo.deleteMessage(message: message, onlyDeleteLocal: false) { [weak self] error in if error == nil { - weakSelf?.deleteMessageUpdateUI([message]) - weakSelf?.deletingMsgDic.remove(messageId) + self?.deleteMessageUpdateUI([message]) + self?.deletingMsgDic.remove(messageId) } completion(error) } @@ -834,25 +1144,24 @@ open class ChatViewModel: NSObject { } } - weak var weakSelf = self - chatRepo.deleteMessages(messages: localMsgs, onlyDeleteLocal: true) { error in + chatRepo.deleteMessages(messages: localMsgs, onlyDeleteLocal: true) { [weak self] error in if error == nil { - weakSelf?.deleteMessageUpdateUI(localMsgs) + self?.deleteMessageUpdateUI(localMsgs) for msg in localMsgs { if let msgId = msg.messageClientId { - weakSelf?.deletingMsgDic.remove(msgId) + self?.deletingMsgDic.remove(msgId) } } } completion(error) } - chatRepo.deleteMessages(messages: remoteMsgs, onlyDeleteLocal: false) { error in + chatRepo.deleteMessages(messages: remoteMsgs, onlyDeleteLocal: false) { [weak self] error in if error == nil { - weakSelf?.deleteMessageUpdateUI(remoteMsgs) + self?.deleteMessageUpdateUI(remoteMsgs) for msg in remoteMsgs { if let msgId = msg.messageClientId { - weakSelf?.deletingMsgDic.remove(msgId) + self?.deletingMsgDic.remove(msgId) } } } @@ -866,6 +1175,7 @@ open class ChatViewModel: NSObject { /// - replyMessage: 被回复的消息 open func replyMessageWithoutThread(message: V2NIMMessage, replyMessage: V2NIMMessage, + aiUserAccid: String? = nil, _ completion: @escaping (V2NIMMessage?, Error?) -> Void) { NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId:\(String(describing: message.messageClientId))") @@ -873,6 +1183,7 @@ open class ChatViewModel: NSObject { "idClient": replyMessage.messageClientId as Any, "scene": replyMessage.conversationType.rawValue, "from": replyMessage.senderId as Any, + "receiverId": replyMessage.receiverId as Any, "to": replyMessage.conversationId as Any, "idServer": replyMessage.messageServerId as Any, "time": Int(replyMessage.createTime * 1000), @@ -886,7 +1197,10 @@ open class ChatViewModel: NSObject { } message.serverExtension = NECommonUtil.getJSONStringFromDictionary(remoteExt ?? [:]) - sendMessage(message: message, conversationId: conversationId, completion) + let params = getReplyMessageParams(aiUserAccid, replyMessage, message) + sendMessage(message: message, conversationId: conversationId, params: params) { message, error, pro in + completion(message, error) + } } /// 撤回消息 @@ -929,7 +1243,7 @@ open class ChatViewModel: NSObject { open func getShowName(_ accountId: String, _ showAlias: Bool = true) -> String { NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:" + accountId) - return NEFriendUserCache.shared.getShowName(accountId, showAlias) + return NEAIUserManager.shared.getShowName(accountId) ?? NEFriendUserCache.shared.getShowName(accountId, showAlias) } /// 获取用户展示名称 @@ -1030,11 +1344,13 @@ open class ChatViewModel: NSObject { ] case .MESSAGE_TYPE_CUSTOM: if (model?.customType ?? 0) > 0 { + // 换行消息可以【复制】 if model?.customType == customRichTextType { items = [ OperationItem.copyItem(), ] } + items.append(contentsOf: [ OperationItem.replayItem(), OperationItem.forwardItem(), @@ -1049,6 +1365,7 @@ open class ChatViewModel: NSObject { items = [ OperationItem.deleteItem(), OperationItem.selectItem(), + OperationItem.collectionItem(), ] } default: @@ -1060,15 +1377,31 @@ open class ChatViewModel: NSObject { ] } + // 自己发送且非未知消息可以 【撤回】 + if model?.message?.isSelf == true { + if model?.message?.messageType == .MESSAGE_TYPE_CUSTOM, + model?.unkonwMessage == true { + return items + } + + // 【撤回】位置在【删除】后面 + for (i, item) in items.enumerated() { + if item.type == .delete { + items.insert(OperationItem.recallItem(), at: i + 1) + break + } + } + } + // 根据配置项移除 【收藏】 - if IMKitConfigCenter.shared.collectionEnable == false { + if IMKitConfigCenter.shared.enableCollectionMessage == false { items.removeAll { item in item.type == .collection } } // 根据配置项移除 【标记】 - if IMKitConfigCenter.shared.pinEnable == false { + if IMKitConfigCenter.shared.enablePinMessage == false { items.removeAll { item in item.type == .pin || item.type == .removePin } @@ -1076,28 +1409,12 @@ open class ChatViewModel: NSObject { // 根据配置项移除 【置顶】 // 单聊移除【置顶】 - if IMKitConfigCenter.shared.topEnable == false || model?.message?.conversationType == .CONVERSATION_TYPE_P2P { + if IMKitConfigCenter.shared.enableTopMessage == false || model?.message?.conversationType == .CONVERSATION_TYPE_P2P { items.removeAll { item in item.type == .top || item.type == .untop } } - // 自己发送且非未知消息可以 【撤回】 - if model?.message?.isSelf == true { - if model?.message?.messageType == .MESSAGE_TYPE_CUSTOM, - model?.unkonwMessage == true { - return items - } - - // 【撤回】位置在【删除】后面 - for (i, item) in items.enumerated() { - if item.type == .delete { - items.insert(OperationItem.recallItem(), at: i + 1) - break - } - } - } - return items } @@ -1143,9 +1460,9 @@ open class ChatViewModel: NSObject { model.isRevoked = true } - if let uid = message.senderId, + if let uid = ChatMessageHelper.getSenderId(message), let fullName = self?.getShowName(uid) { - let user = NEFriendUserCache.shared.getFriendInfo(uid) ?? ChatUserCache.shared.getUserInfo(uid) + let user = ChatMessageHelper.getUserFromCache(uid) model.avatar = user?.user?.avatar model.fullName = fullName model.shortName = NEFriendUserCache.getShortName(fullName) @@ -1174,12 +1491,16 @@ open class ChatViewModel: NSObject { model.isRevoked = true } - if let uid = message.senderId { + if let uid = ChatMessageHelper.getSenderId(message) { let fullName = getShowName(uid) - let user = NEFriendUserCache.shared.getFriendInfo(uid) ?? ChatUserCache.shared.getUserInfo(uid) + let user = ChatMessageHelper.getUserFromCache(uid) model.avatar = user?.user?.avatar model.fullName = fullName model.shortName = NEFriendUserCache.getShortName(fullName) + + if user == nil { + contactRepo.getUserWithFriend(accountIds: [uid]) { _, _ in } + } } if let replyModel = getReplyMessageWithoutThread(message: message) { @@ -1198,9 +1519,17 @@ open class ChatViewModel: NSObject { /// - completion: 完成回调 open func getReplyMessageWithoutThread(message: V2NIMMessage) -> MessageModel? { NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") - var replyId: String? = message.threadReply?.messageClientId + var replyId: String? + + // 非thread方案 let replyDic = ChatMessageHelper.getReplyDictionary(message: message) replyId = replyDic?["idClient"] as? String + + // thread 方案优先 + if let threadId = message.threadReply?.messageClientId, !threadId.isEmpty { + replyId = threadId + } + guard let replyId = replyId, !replyId.isEmpty else { return nil } @@ -1228,9 +1557,17 @@ open class ChatViewModel: NSObject { 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 + var replyId: String? + + // 非thread方案 let replyDic = ChatMessageHelper.getReplyDictionary(message: message) replyId = replyDic?["idClient"] as? String + + // thread 方案优先 + if let threadId = message.threadReply?.messageClientId, !threadId.isEmpty { + replyId = threadId + } + guard let replyId = replyId, !replyId.isEmpty else { completion(nil) return @@ -1269,12 +1606,19 @@ open class ChatViewModel: NSObject { for (i, model) in messages.enumerated() { if hasFind { - var replyId: String? = model.message?.threadReply?.messageClientId + var replyId: String? + + // 非thread方案 if let remoteExt = getDictionaryFromJSONString(model.message?.serverExtension ?? ""), let yxReplyMsg = remoteExt[keyReplyMsgKey] as? [String: Any] { replyId = yxReplyMsg["idClient"] as? String } + // thread 方案优先 + if let threadId = model.message?.threadReply?.messageClientId, !threadId.isEmpty { + replyId = threadId + } + if let id = replyId, !id.isEmpty, id == message.messageClientId { messages[i].replyText = chatLocalizable("message_not_found") replyIndex.append(i) @@ -1332,12 +1676,19 @@ open class ChatViewModel: NSObject { // 遍历查找回复该条消息的消息 for (i, model) in messages.enumerated() { if hasFind { - var replyId: String? = model.message?.threadReply?.messageClientId + var replyId: String? + + // 非thread方案 if let remoteExt = getDictionaryFromJSONString(model.message?.serverExtension ?? ""), let yxReplyMsg = remoteExt[keyReplyMsgKey] as? [String: Any] { replyId = yxReplyMsg["idClient"] as? String } + // thread 方案优先 + if let threadId = model.message?.threadReply?.messageClientId, !threadId.isEmpty { + replyId = threadId + } + if let id = replyId, !id.isEmpty, id == message.messageClientId { messages[i].replyText = chatLocalizable("message_not_found") indexs.append(IndexPath(row: i, section: 0)) @@ -1426,7 +1777,12 @@ open class ChatViewModel: NSObject { let forwardMessage = MessageUtils.forwardMessage(message: message) ChatMessageHelper.clearForwardAtMark(forwardMessage) - chatRepo.sendMessage(message: forwardMessage, conversationId: conversationId) { result, error, pro in + var params = chatRepo.getSendMessageParams() + if let sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId), + NEAIUserManager.shared.isAIUser(sessionId) { + params = getForwardMessageParams(sessionId, forwardMessage) + } + chatRepo.sendMessage(message: forwardMessage, conversationId: conversationId, params: params) { result, error, pro in } } @@ -1434,7 +1790,14 @@ open class ChatViewModel: NSObject { if let text = comment, !text.isEmpty { // 延迟 0.2s 发送,确保留言位置在最后 DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: DispatchWorkItem(block: { [weak self] in - self?.sendTextMessage(text: text, conversationId: conversationId, remoteExt: nil) { _, error in + let message = MessageUtils.textMessage(text: text, remoteExt: nil) + + var params = self?.chatRepo.getSendMessageParams() + if let sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId), + NEAIUserManager.shared.isAIUser(sessionId) { + params = self?.getForwardMessageParams(sessionId, message) + } + self?.sendMessage(message: message, conversationId: conversationId, params: params) { _, error, pro in completion(error) } })) @@ -1460,7 +1823,15 @@ open class ChatViewModel: NSObject { if forwardMessages.count <= 0 { if let text = comment, !text.isEmpty { for conversationId in conversationIds { - sendTextMessage(text: text, conversationId: conversationId, remoteExt: nil) { _, error in + let message = MessageUtils.textMessage(text: text, remoteExt: nil) + + var params = chatRepo.getSendMessageParams() + if let sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId), + NEAIUserManager.shared.isAIUser(sessionId) { + params = getForwardMessageParams(sessionId, message) + } + + sendMessage(message: message, conversationId: conversationId, params: params) { _, error, pro in completion(error) } } @@ -1515,15 +1886,31 @@ open class ChatViewModel: NSObject { // 转发到会话 for conversationId in conversationIds { - self?.sendCustomMessage(text: "[\(chatLocalizable("chat_history"))]", - rawAttachment: getJSONStringFromDictionary(jsonData), conversationId: conversationId) { error in + let message = MessageUtils.customMessage(text: "[\(chatLocalizable("chat_history"))]", + rawAttachment: getJSONStringFromDictionary(jsonData)) + + var params = self?.chatRepo.getSendMessageParams() + if let sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId), + NEAIUserManager.shared.isAIUser(sessionId) { + params = self?.getForwardMessageParams(sessionId, message) + } + self?.sendMessage(message: message, conversationId: conversationId, params: params) { _, error, pro in + completion(error) } // 发送留言 if let text = comment, !text.isEmpty { // 延迟 0.2s 发送,确保留言位置在最后 DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: DispatchWorkItem(block: { [weak self] in - self?.sendTextMessage(text: text, conversationId: conversationId, remoteExt: nil) { _, error in + let message = MessageUtils.textMessage(text: text, remoteExt: nil) + + var params = self?.chatRepo.getSendMessageParams() + if let sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId), + NEAIUserManager.shared.isAIUser(sessionId) { + params = self?.getForwardMessageParams(sessionId, message) + } + + self?.sendMessage(message: message, conversationId: conversationId, params: params) { _, error, pro in completion(error) } })) @@ -1604,11 +1991,27 @@ open class ChatViewModel: NSObject { var collectionDic = [String: Any]() if let messageString = V2NIMMessageConverter.messageSerialization(message) { - collectionDic["message"] = messageString + if let collectionMessage = V2NIMMessageConverter.messageDeserialization(messageString) { + // 移除收藏消息中的at信息 + if var remoteExt = getDictionaryFromJSONString(collectionMessage.serverExtension ?? "") as? [String: Any] { + remoteExt.removeValue(forKey: yxAtMsg) + let serverExtensionString = getJSONStringFromDictionary(remoteExt) + collectionMessage.serverExtension = serverExtensionString + } + if let saveMessageString = V2NIMMessageConverter.messageSerialization(collectionMessage) { + collectionDic["message"] = saveMessageString + } + } } collectionDic["conversationName"] = conversationName - if let senderName = model.fullName { + if let accountId = ChatMessageHelper.getSenderId(model.message) { + if let aiUser: V2NIMAIUser = NEAIUserManager.shared.getAIUserById(accountId), let senderName = aiUser.name { + collectionDic["senderName"] = senderName + } else if let senderName = model.fullName { + collectionDic["senderName"] = senderName + } + } else if let senderName = model.fullName { collectionDic["senderName"] = senderName } @@ -1806,6 +2209,16 @@ open class ChatViewModel: NSObject { messages.insert(resendModel, at: toIndex) delegate?.onResendSuccess(IndexPath(row: fromIndex, section: 0), IndexPath(row: toIndex, section: 0)) } + + /// 数字人回复的消息错误码映射 + /// - Parameter error: 错误信息 + /// - Parameter message: 消息 + func setErrorText(_ message: V2NIMMessage?) { + guard let message = message else { return } + if let content = ChatMessageHelper.getAIErrorMsage(message.messageStatus.errorCode) { + message.text = content + } + } } // MARK: - NEChatListener @@ -1863,12 +2276,18 @@ extension ChatViewModel: NEChatListener { return } + // 数字人回复的消息 + if ChatMessageHelper.isAISender(msg) { + setErrorText(msg) + } + modelFromMessage(message: msg) { [weak self] model in ChatMessageHelper.addTimeMessage(model, self?.messages.last) self?.downloadAudioFile([model]) self?.loadReply(model) { if let index = self?.insertToMessages(model) { self?.delegate?.onRecvMessages(messages, [IndexPath(row: index, section: 0)]) + self?.loadMoreWithMessage([msg]) } } } @@ -1952,7 +2371,7 @@ extension ChatViewModel: NEChatListener { let pinID = pinNotification.pin?.operatorId ?? IMKitClient.instance.account() messages[i].pinAccount = pinID - if let _ = NEFriendUserCache.shared.getFriendInfo(pinID) ?? ChatUserCache.shared.getUserInfo(pinID) { + if let _ = ChatMessageHelper.getUserFromCache(pinID) { messages[i].pinShowName = getShowName(pinID) } else { loadShowName([pinID], sessionId) { [weak self] in @@ -2059,28 +2478,49 @@ extension ChatViewModel: NEChatListener { } } -// MARK: - NEContactListener +// MARK: AI Listener -extension ChatViewModel: NEContactListener { - /// 用户信息变更回调 - /// - Parameter users: 用户列表 - public func onUserProfileChanged(_ users: [V2NIMUser]) { - for user in users { - guard let accountId = user.accountId else { - return +extension ChatViewModel: V2NIMAIListener { + public func onProxyAIModelCall(_ data: V2NIMAIModelCallResult) { + if data.code == aiUserRequestSuccess { + if data.requestId == translationlanguageRquestId { + delegate?.didTranslateResult?(data.content.msg) + NEALog.infoLog(className(), desc: #function + " ai translate result : \(data.content.msg)") } + } + } - if !NEFriendUserCache.shared.isFriend(accountId) { - ChatUserCache.shared.updateUserInfo(user) - } + /// 翻译 + /// - Parameter 需要翻译文本 + /// - Parameter 目标语言 + public func translateLanguage(_ sourceText: String, targetLanguage: String, _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(className(), desc: #function + " ai translate source : \(sourceText)") + + let request = V2NIMProxyAIModelCallParams() + translationlanguageRquestId = UUID().uuidString + request.requestId = translationlanguageRquestId + + let content = V2NIMAIModelCallContent() + content.msg = sourceText + content.type = .NIM_AI_MODEL_CONTENT_TYPE_TEXT - updateMessageInfo(accountId) + request.content = content + + let configParams = V2NIMAIModelConfigParams() + configParams.temperature = NEAIUserManager.shared.getTranslatePromptValue() + request.modelConfigParams = configParams + + if let accountId = translationAIUser?.accountId { + request.accountId = accountId } - } - /// 好友信息变更回调 - /// - Parameter friendInfo: 好友信息 - public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { - updateMessageInfo(friendInfo.accountId) + let promptKey = NEAIUserManager.shared.getTranslatePromptKey() + let promptVariables = [promptKey: targetLanguage] + let jsonString = getJSONStringFromDictionary(promptVariables) + request.promptVariables = jsonString + + AIRepo.shared.proxyAIModelCall(request) { error in + completion(error) + } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/CollectionMessageViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/CollectionMessageViewModel.swift index 48b59092..d1e5ee1f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/CollectionMessageViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/CollectionMessageViewModel.swift @@ -91,6 +91,53 @@ class CollectionMessageViewModel: NSObject { return retArray } + /// 获取请求大模型的内容 + /// - Parameters: + /// - text: 请求/响应的文本内容 + /// - type: 类型 + /// - Returns: 请求大模型的内容 + open func getAIModelCallContent(_ text: String?, + _ type: V2NIMAIModelCallContentType) -> V2NIMAIModelCallContent { + let content = V2NIMAIModelCallContent() + content.msg = text ?? "" + content.type = type + return content + } + + /// 获取消息发送参数 + /// - Parameters: + /// - aiUserAccid: 数字人 id + /// - message: 消息 + /// - Returns: 消息发送参数 + func getSendMessageParams(_ conversationId: String? = nil, _ message: V2NIMMessage) -> V2NIMSendMessageParams { + let params = chatRepo.getSendMessageParams() + guard let cid = conversationId, + let aiAccid = V2NIMConversationIdUtil.conversationTargetId(cid), + NEAIUserManager.shared.isAIUser(aiAccid) else { + return params + } + + let aiConfig = V2NIMMessageAIConfigParams() + aiConfig.accountId = aiAccid + + if message.messageType == .MESSAGE_TYPE_TEXT, let text = message.text { + aiConfig.content = getAIModelCallContent(text, .NIM_AI_MODEL_CONTENT_TYPE_TEXT) + } + + if message.messageType == .MESSAGE_TYPE_CUSTOM, + let type = NECustomUtils.typeOfCustomMessage(message.attachment), + type == customRichTextType { + let title = NECustomUtils.titleOfRichText(message.attachment) + let body = NECustomUtils.bodyOfRichText(message.attachment) + let text = (title ?? "") + (body ?? "") + aiConfig.content = getAIModelCallContent(text, .NIM_AI_MODEL_CONTENT_TYPE_TEXT) + } + + params.aiConfig = aiConfig + + return params + } + /// 发送文本消息 /// - Parameter text: 文本内容 /// - Parameter conversationId: 会话ID @@ -100,9 +147,13 @@ class CollectionMessageViewModel: NSObject { if text.count <= 0 { return } + + let message = MessageUtils.textMessage(text: text) + let params = getSendMessageParams(conversationId, message) chatRepo.sendMessage( - message: MessageUtils.textMessage(text: text), + message: message, conversationId: conversationId, + params: params, completion ) } @@ -120,16 +171,8 @@ class CollectionMessageViewModel: NSObject { for conversationId in conversationIds { let forwardMessage = MessageUtils.forwardMessage(message: message) ChatMessageHelper.clearForwardAtMark(forwardMessage) - if forwardMessage.senderId == nil { - forwardMessage.senderId = IMKitClient.instance.account() - } - - if forwardMessage.conversationId == nil { - forwardMessage.conversationId = conversationId - } - - ChatProvider.shared.sendMessage(message: forwardMessage, conversationId: conversationId, params: nil) { resut, error, progress in - } + let params = getSendMessageParams(conversationId, message) + chatRepo.sendMessage(message: forwardMessage, conversationId: conversationId, params: params, completion) if let text = comment, !text.isEmpty { sendTextMessage(text, conversationId, completion) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift index fdc9b65b..f595848c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift @@ -82,7 +82,7 @@ open class MultiForwardViewModel: NSObject { model.shortName = NEFriendUserCache.getShortName(model.fullName ?? "") model.avatar = remoteExt[mergedMessageAvatarKey] as? String } else { - model.fullName = message.senderId + model.fullName = ChatMessageHelper.getSenderId(message) model.shortName = NEFriendUserCache.getShortName(model.fullName ?? "") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/P2PChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/P2PChatViewModel.swift index 3bab0d4b..e853e3ab 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/P2PChatViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/P2PChatViewModel.swift @@ -13,15 +13,23 @@ 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) + } + + /// 添加子类监听 + override open func addListener() { + super.addListener() chatRepo.addNotiListener(self) } + deinit { + chatRepo.removeNotiListener(self) + } + /// 重写 获取用户展示名称 /// - Parameters: /// - accountId: 用户 accountId @@ -33,7 +41,7 @@ open class P2PChatViewModel: ChatViewModel { if NEFriendUserCache.shared.isFriend(accountId) { return NEFriendUserCache.shared.getShowName(accountId, showAlias) } else { - return ChatUserCache.shared.getShowName(accountId, showAlias) + return NEP2PChatUserCache.shared.getShowName(accountId, showAlias) } } @@ -50,7 +58,7 @@ open class P2PChatViewModel: ChatViewModel { for user in users ?? [] { // 非好友,单独缓存 if let uid = user.user?.accountId, !NEFriendUserCache.shared.isFriend(uid) { - ChatUserCache.shared.updateUserInfo(user) + NEP2PChatUserCache.shared.updateUserInfo(user) } } completion() diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift index ed18cd38..20eb89df 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift @@ -73,18 +73,17 @@ open class PinMessageViewModel: NSObject, NEChatListener { } 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 senderId = item.chatmodel.message?.senderId ?? "" - let name = ChatTeamCache.shared.getShowName(senderId) - let user = NEFriendUserCache.shared.getFriendInfo(senderId) ?? ChatUserCache.shared.getUserInfo(senderId) + NETeamUserManager.shared.getTeamMembers(accountIds: userIds) { + for item in items { + if let senderId = ChatMessageHelper.getSenderId(item.chatmodel.message) { + let name = NETeamUserManager.shared.getShowName(senderId) + let user = ChatMessageHelper.getUserFromCache(senderId) item.chatmodel.avatar = user?.user?.avatar item.chatmodel.fullName = name item.chatmodel.shortName = NEFriendUserCache.getShortName(user?.showName() ?? "") } - completion() } + completion() } } @@ -97,14 +96,65 @@ open class PinMessageViewModel: NSObject, NEChatListener { } } + /// 获取请求大模型的内容 + /// - Parameters: + /// - text: 请求/响应的文本内容 + /// - type: 类型 + /// - Returns: 请求大模型的内容 + open func getAIModelCallContent(_ text: String?, + _ type: V2NIMAIModelCallContentType) -> V2NIMAIModelCallContent { + let content = V2NIMAIModelCallContent() + content.msg = text ?? "" + content.type = type + return content + } + + /// 获取消息发送参数 + /// - Parameters: + /// - aiUserAccid: 数字人 id + /// - message: 消息 + /// - Returns: 消息发送参数 + func getSendMessageParams(_ aiUserAccid: String? = nil, _ message: V2NIMMessage) -> V2NIMSendMessageParams { + let params = chatRepo.getSendMessageParams() + guard let cid = conversationId, + let aiAccid = V2NIMConversationIdUtil.conversationTargetId(cid), + NEAIUserManager.shared.isAIUser(aiAccid) else { + return params + } + + let aiConfig = V2NIMMessageAIConfigParams() + aiConfig.accountId = aiAccid + + if message.messageType == .MESSAGE_TYPE_TEXT, let text = message.text { + aiConfig.content = getAIModelCallContent(text, .NIM_AI_MODEL_CONTENT_TYPE_TEXT) + } + + if message.messageType == .MESSAGE_TYPE_CUSTOM, + let type = NECustomUtils.typeOfCustomMessage(message.attachment), + type == customRichTextType { + let title = NECustomUtils.titleOfRichText(message.attachment) + let body = NECustomUtils.bodyOfRichText(message.attachment) + let text = (title ?? "") + (body ?? "") + aiConfig.content = getAIModelCallContent(text, .NIM_AI_MODEL_CONTENT_TYPE_TEXT) + } + + params.aiConfig = aiConfig + + return params + } + 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 } + + let message = MessageUtils.textMessage(text: text) + let params = getSendMessageParams(conversationId, message) chatRepo.sendMessage( - message: MessageUtils.textMessage(text: text), + message: message, conversationId: conversationId, + params: params, completion ) } @@ -122,7 +172,8 @@ open class PinMessageViewModel: NSObject, NEChatListener { for conversationId in conversationIds { let forwardMessage = MessageUtils.forwardMessage(message: message) ChatMessageHelper.clearForwardAtMark(forwardMessage) - chatRepo.sendMessage(message: forwardMessage, conversationId: conversationId, completion) + let params = getSendMessageParams(conversationId, message) + chatRepo.sendMessage(message: forwardMessage, conversationId: conversationId, params: params, completion) if let text = comment, !text.isEmpty { sendTextMessage(text: text, conversationId: conversationId, completion) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ReadViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ReadViewModel.swift index b5094622..4d0af9fb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ReadViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ReadViewModel.swift @@ -24,9 +24,13 @@ open class ReadViewModel: NSObject { /// - completion: 完成回调 public func getTeamMessageReceiptDetail(_ message: V2NIMMessage, _ teamId: String, _ completion: @escaping (Error?) -> Void) { chatRepo.getTeamMessageReceiptDetail(message: message, memberAccountIds: []) { [weak self] readReceiptDetail, error in - guard let readReceiptDetail = readReceiptDetail else { return } + guard let readReceiptDetail = readReceiptDetail else { + completion(error) + return + } + let group = DispatchGroup() - if let error = error as? NSError { + if let error = error { completion(error) return } @@ -34,20 +38,20 @@ open class ReadViewModel: NSObject { // 加载用户信息 let loadUserIds = readReceiptDetail.readAccountList + readReceiptDetail.unreadAccountList group.enter() - ChatTeamCache.shared.loadShowName(userIds: loadUserIds, teamId: teamId) { + NETeamUserManager.shared.getTeamMembers(accountIds: loadUserIds) { // 已读用户 for userId in readReceiptDetail.readAccountList { let memberInfo = NETeamMemberInfoModel() - memberInfo.teamMember = ChatTeamCache.shared.getTeamMemberInfo(accountId: userId) - memberInfo.nimUser = NEFriendUserCache.shared.getFriendInfo(userId) ?? ChatUserCache.shared.getUserInfo(userId) + memberInfo.teamMember = NETeamUserManager.shared.getTeamMemberInfo(userId) + memberInfo.nimUser = ChatMessageHelper.getUserFromCache(userId) self?.readUsers.append(memberInfo) } // 未读用户 for userId in readReceiptDetail.unreadAccountList { let memberInfo = NETeamMemberInfoModel() - memberInfo.teamMember = ChatTeamCache.shared.getTeamMemberInfo(accountId: userId) - memberInfo.nimUser = NEFriendUserCache.shared.getFriendInfo(userId) ?? ChatUserCache.shared.getUserInfo(userId) + memberInfo.teamMember = NETeamUserManager.shared.getTeamMemberInfo(userId) + memberInfo.nimUser = ChatMessageHelper.getUserFromCache(userId) self?.unReadUsers.append(memberInfo) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/SelectLanguageViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/SelectLanguageViewModel.swift new file mode 100644 index 00000000..6a8dcd64 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/SelectLanguageViewModel.swift @@ -0,0 +1,40 @@ +//// 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 + +@objcMembers +open class NElanguageCellModel: NSObject { + public var language: String = "" + public var isSelect: Bool = false + var cornerType: CornerType = .none +} + +@objcMembers +open class SelectLanguageViewModel: NSObject { + public var datas = [NElanguageCellModel]() + + func setupData(_ isFun: Bool) { + let languageDatas = NETranslateLanguageManager.shared.languageDatas + + if isFun { + for index in 0 ..< languageDatas.count { + let model = NElanguageCellModel() + model.language = languageDatas[index] + datas.append(model) + if index == 0 { + model.cornerType = .topLeft.union(.topRight) + } else if index == languageDatas.count - 1 { + model.cornerType = .bottomLeft.union(.bottomRight) + } + } + } else { + for index in 0 ..< languageDatas.count { + let model = NElanguageCellModel() + model.language = languageDatas[index] + datas.append(model) + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift index f97d5220..226150db 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift @@ -24,16 +24,23 @@ open class TeamChatViewModel: ChatViewModel, NETeamListener { override init(conversationId: String) { super.init(conversationId: conversationId) - getTeamMember {} - teamRepo.addTeamListener(self) - IMKitClient.instance.addLoginListener(self) } override init(conversationId: String, anchor: V2NIMMessage?) { super.init(conversationId: conversationId, anchor: anchor) - getTeamMember {} + } + + /// 添加子类监听 + override open func addListener() { + super.addListener() teamRepo.addTeamListener(self) - IMKitClient.instance.addLoginListener(self) + NETeamUserManager.shared.addListener(self) + NETeamUserManager.shared.loadData(sessionId) + } + + deinit { + teamRepo.removeTeamListener(self) + NETeamUserManager.shared.removeListener(self) } /// 重写 获取用户展示名称 @@ -43,7 +50,7 @@ open class TeamChatViewModel: ChatViewModel, NETeamListener { /// - Returns: 名称和好友信息 override open func getShowName(_ accountId: String, _ showAlias: Bool = true) -> String { NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:" + accountId) - return ChatTeamCache.shared.getShowName(accountId, showAlias) + return NETeamUserManager.shared.getShowName(accountId, showAlias) } /// 重写 获取用户展示名称 @@ -55,17 +62,13 @@ open class TeamChatViewModel: ChatViewModel, NETeamListener { _ 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) + NETeamUserManager.shared.getTeamMembers(accountIds: accountIds, completion) } /// 加载置顶消息 override open func loadTopMessage() { // 校验配置项 - if !IMKitConfigCenter.shared.topEnable { + if !IMKitConfigCenter.shared.enableTopMessage { return } @@ -75,7 +78,8 @@ open class TeamChatViewModel: ChatViewModel, NETeamListener { let refer = ChatMessageHelper.createMessageRefer(topInfo) chatRepo.getMessageListByRefers([refer]) { [weak self] messages, error in // 这里查询只是为了校验消息是否还存在(未被删除或撤回) - if let topMessage = messages?.first, let senderId = topMessage.senderId { + if let topMessage = messages?.first, + let senderId = ChatMessageHelper.getSenderId(topMessage) { var senderName = self?.getShowName(senderId) ?? "" let group = DispatchGroup() @@ -95,12 +99,12 @@ open class TeamChatViewModel: ChatViewModel, NETeamListener { // 获取图片缩略图 if let attach = topMessage.attachment as? V2NIMMessageImageAttachment, let imageUrl = attach.url { - thumbUrl = ResourceRepo.shared.imageThumbnailURL(imageUrl) + thumbUrl = V2NIMStorageUtil.imageThumbUrl(imageUrl, thumbSize: 350) } // 获取视频首帧 if let attach = topMessage.attachment as? V2NIMMessageVideoAttachment, let videoUrl = attach.url { - thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + thumbUrl = V2NIMStorageUtil.videoCoverUrl(videoUrl, offset: 0) isVideo = true } @@ -116,6 +120,10 @@ open class TeamChatViewModel: ChatViewModel, NETeamListener { hideClose: hideClose) self?.topMessage = topMessage } + } else { + // 置顶消息已被删除 + self?.topMessage = nil + self?.delegate?.setTopValue(name: nil, content: nil, url: nil, isVideo: false, hideClose: false) } } } else { @@ -164,6 +172,7 @@ open class TeamChatViewModel: ChatViewModel, NETeamListener { "idClient": message.messageClientId as Any, "scene": message.conversationType.rawValue, "from": message.senderId as Any, + "receiverId": message.receiverId as Any, "to": message.conversationId as Any, "idServer": message.messageServerId as Any, "time": Int(message.createTime * 1000), @@ -254,19 +263,30 @@ open class TeamChatViewModel: ChatViewModel, NETeamListener { 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 + if let team = NETeamUserManager.shared.getTeamInfo() { + self.team = team + teamMember = NETeamUserManager.shared.getTeamMemberInfo(IMKitClient.instance.account()) + completion(nil, team) + } else { + teamRepo.getTeamInfo(teamId) { [weak self] team, error in + if error == nil { + self?.team = team + } + completion(error, team) } - completion(error, team) } } /// 获取自己的群成员信息 public func getTeamMember(_ completion: @escaping () -> Void) { - teamRepo.getTeamMember(sessionId, .TEAM_TYPE_NORMAL, IMKitClient.instance.account()) { [weak self] member, error in - self?.teamMember = member + if let teamMember = NETeamUserManager.shared.getTeamMemberInfo(IMKitClient.instance.account()) { + self.teamMember = teamMember completion() + } else { + teamRepo.getTeamMember(sessionId, .TEAM_TYPE_NORMAL, IMKitClient.instance.account()) { [weak self] member, error in + self?.teamMember = member + completion() + } } } @@ -357,45 +377,38 @@ open class TeamChatViewModel: ChatViewModel, NETeamListener { } } } +} - 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) - } - } +// MARK: - NETeamChatUserCacheListener - loadTopMessage() - } +extension TeamChatViewModel: NETeamChatUserCacheListener { + /// 群信息更新 + /// - Parameter teamId: 群 id + public func onTeamInfoUpdate(_ teamId: String) { + guard let team = NETeamUserManager.shared.getTeamInfo(), team.teamId == sessionId else { return } - /// 群成员加入回调 - /// - Parameter teamMembers: 群成员列表 - public func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { - for teamMember in teamMembers { - guard teamMember.teamId == team?.teamId else { break } + self.team = team + loadTopMessage() - ChatTeamCache.shared.updateTeamMemberInfo(teamMember) - updateMessageInfo(teamMember.accountId) + if let delegate = delegate as? TeamChatViewModelDelegate { + delegate.onTeamUpdate?(team: team) } } - public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { - for teamMember in teamMembers { - guard teamMember.teamId == team?.teamId else { break } + /// 群成员更新 + /// - Parameter accountId: 用户 id + public func onTeamMemberUpdate(_ accountId: String) { + guard let teamMember = NETeamUserManager.shared.getTeamMemberInfo(accountId) else { return } - if teamMember.accountId == self.teamMember?.accountId { - self.teamMember = teamMember - loadTopMessage() - } - - ChatTeamCache.shared.updateTeamMemberInfo(teamMember) - updateMessageInfo(teamMember.accountId) + if self.teamMember == nil || accountId == self.teamMember?.accountId { + self.teamMember = teamMember + loadTopMessage() } + updateMessageInfo(accountId) + if let delegate = delegate as? TeamChatViewModelDelegate { - delegate.onTeamMemberUpdate?(teamMembers) + delegate.onTeamMemberUpdate?([teamMember]) } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift index b0e887d2..22c07d7a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift @@ -14,127 +14,25 @@ open class TeamMemberSelectVM: NSObject { let teamProvider = TeamProvider.shared - open func fetchTeamMembers(_ teamId: String, - _ completion: @escaping (Error?, NETeamInfoModel?) -> Void) { + open func getTeamMembers(_ teamId: String, + _ completion: @escaping (Error?, NETeamInfoModel?) -> Void) { NEALog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + teamId) - getTeamWithMembers(teamId, .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL, completion) - } - - /// 分页查询群成员信息 - /// - Parameter members: 要查询的群成员列表 - /// - Parameter model : 群信息 - /// - Parameter maxSizeByPage: 单页最大查询数量 - /// - Parameter completion: 完成后的回调 - public func splitTeamMember(_ 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)) - fetchAtListUserInfo(&remaind, model, completion) - } - - /// 获取群信息 - /// - Parameter teamId: 群id - /// - Parameter queryType: 查询类型 - /// - Parameter completion: 完成后的回调 - public func getTeamWithMembers(_ 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 { - var memberLists = [V2NIMTeamMember]() - - weakSelf?.getAllTeamMemberList(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 { - let model = NETeamInfoModel() - model.team = team - if let members = ms { - weakSelf?.splitTeamMember(members, model, 150, completion) - } else { - completion(error, model) - } - } - } - } - } - } - - /// 从云信服务器批量获取用户资料 - /// - Parameter remainUserIds: 用户集合 - /// - Parameter completion: 成功回调 - private func fetchAtListUserInfo(_ 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 { + if let team = NETeamUserManager.shared.getTeamInfo(), + let teamMembers = NETeamUserManager.shared.getAllTeamMemberModels() { + let model = NETeamInfoModel() + model.team = team + model.users = teamMembers completion(nil, model) - return - } - - let accids = members.map(\.accountId) - var temArray = remainUserIds - weak var weakSelf = self - - ContactRepo.shared.getUserWithFriend(accountIds: accids) { users, v2Error in - if let err = v2Error { - completion(err as NSError, model) - } else { - if let us = users { - for index in 0 ..< members.count { - let memberInfoModel = NETeamMemberInfoModel() - memberInfoModel.teamMember = members[index] - if us.count > index { - let user = us[index] - memberInfoModel.nimUser = user - } - model.users.append(memberInfoModel) - } - } - temArray.removeFirst() - weakSelf?.fetchAtListUserInfo(&temArray, model, completion) - } - } - } - - /// 获取群成员 - /// - Parameter teamId: 群ID - /// - Parameter completion: 完成回调 - public func getAllTeamMemberList(_ teamId: String, _ nextToken: String? = nil, _ memberList: inout [V2NIMTeamMember], _ queryType: V2NIMTeamMemberRoleQueryType, _ completion: @escaping ([V2NIMTeamMember]?, NSError?) -> Void) { - let option = V2NIMTeamMemberQueryOption() - if let token = nextToken { - option.nextToken = token } else { - option.nextToken = "" - } - option.limit = 1000 - option.direction = .QUERY_DIRECTION_ASC - option.onlyChatBanned = false - option.roleQueryType = queryType - var temMemberLists = memberList - teamProvider.getTeamMemberList(teamId: teamId, teamType: .TEAM_TYPE_NORMAL, queryOption: option) { 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.getAllTeamMemberList(teamId, result?.nextToken, &temMemberLists, queryType, completion) - } + NETeamUserManager.shared.getAllTeamMembers(teamId, .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL) { _ in + let team = NETeamUserManager.shared.getTeamInfo() + if let teamMembers = NETeamUserManager.shared.getAllTeamMemberModels() { + let model = NETeamInfoModel() + model.team = team + model.users = teamMembers + completion(nil, model) + } else { + completion(nil, nil) } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift index 3dee4b48..027510d2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift @@ -4,16 +4,18 @@ import Foundation import NEChatKit +import NECommonUIKit import NECoreIM2Kit import NIMSDK protocol UserSettingViewModelDelegate: NSObjectProtocol { func didNeedRefreshUI() func didError(_ error: Error) + func didShowErrorMsg(_ msg: String) } @objcMembers -open class UserSettingViewModel: NSObject, NEConversationListener { +open class UserSettingViewModel: NSObject, NEConversationListener, AIUserPinListener { var chatRepo = ChatRepo.shared var contactRepo = ContactRepo.shared var conversationRepo = ConversationRepo.shared @@ -29,11 +31,17 @@ open class UserSettingViewModel: NSObject, NEConversationListener { override public init() { super.init() - conversationRepo.addListener(self) + conversationRepo.addConversationListener(self) + if IMKitConfigCenter.shared.enableAIUser { + NEAIUserPinManager.shared.addPinManagerListener(self) + } } deinit { - conversationRepo.removeListener(self) + conversationRepo.removeConversationListener(self) + if IMKitConfigCenter.shared.enableAIUser { + NEAIUserPinManager.shared.removePinManagerListener(self) + } } private let className = "UserSettingViewModel" @@ -86,6 +94,12 @@ open class UserSettingViewModel: NSObject, NEConversationListener { weak var weakSelf = self remind.swichChange = { isOpen in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.delegate?.didShowErrorMsg(commonLocalizable("network_error")) + remind.switchOpen = !isOpen + weakSelf?.delegate?.didNeedRefreshUI() + return + } 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 @@ -101,13 +115,18 @@ open class UserSettingViewModel: NSObject, NEConversationListener { let setTop = UserSettingCellModel() setTop.cellName = chatLocalizable("session_set_top") -// setTop.cornerType = .bottomRight.union(.bottomLeft) if let currentConversation = conversation { setTop.switchOpen = currentConversation.stickTop } setTop.swichChange = { isOpen in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.delegate?.didShowErrorMsg(commonLocalizable("network_error")) + setTop.switchOpen = !isOpen + weakSelf?.delegate?.didNeedRefreshUI() + return + } if let uid = weakSelf?.userInfo?.user?.accountId, let cid = V2NIMConversationIdUtil.p2pConversationId(uid) { if isOpen { weakSelf?.conversationRepo.setStickTop(cid, true) { error in @@ -133,12 +152,42 @@ open class UserSettingViewModel: NSObject, NEConversationListener { } } } - if IMKitConfigCenter.shared.pinEnable { + if IMKitConfigCenter.shared.enablePinMessage { cellDatas.append(mark) } cellDatas.append(remind) cellDatas.append(setTop) + if let user = userInfo?.user, let account = user.accountId, let serverExtensions = user.serverExtension, let jsonObject = NECommonUtil.getDictionaryFromJSONString(serverExtensions) { + if jsonObject[aiUserPinKey] != nil { + let changePin = UserSettingCellModel() + changePin.cellName = chatLocalizable("ai_user_pin_top") + if NEAIUserPinManager.shared.checkoutUnPinAIUser(user) == true { + changePin.switchOpen = true + } else { + changePin.switchOpen = false + } + changePin.swichChange = { isOpen in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.delegate?.didShowErrorMsg(commonLocalizable("network_error")) + changePin.switchOpen = !isOpen + weakSelf?.delegate?.didNeedRefreshUI() + return + } + if isOpen { + NEAIUserPinManager.shared.pinAIUser(account) { error, finish in + NEALog.infoLog(ModuleName, desc: #function + " pinAIUser error: \(String(describing: error)), finish: \(finish)") + } + } else { + NEAIUserPinManager.shared.unpinAIUser(account) { error, finish in + NEALog.infoLog(ModuleName, desc: #function + " unpinAIUser error: \(String(describing: error)), finish: \(finish)") + } + } + } + cellDatas.append(changePin) + } + } + setAudoType() } @@ -176,4 +225,9 @@ open class UserSettingViewModel: NSObject, NEConversationListener { } } } + + public func userInfoDidChange() { + getSectionDatas() + delegate?.didNeedRefreshUI() + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift b/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift index ebf8f7be..1c72ce9c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.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 @@ -54,7 +55,7 @@ public class MessageProperties: NSObject { public var avatarCornerRadius: CGFloat = 0 // 头像类型 - public var avatarType: NEChatAvatarType? + public var avatarType: NEChatAvatarType = .rectangle // 设置聊天消息标记的背景色 public var signalBgColor = UIColor.ne_yellowBackgroundColor diff --git a/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift b/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift index 2308d614..e7dbed1d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift @@ -6,22 +6,9 @@ import Foundation import NEChatKit import NECommonKit -import NIMSDK -import SDWebImage -import SDWebImageSVGKitPlugin -import SDWebImageWebPCoder @objcMembers open class ChatRouter: NSObject { - public static func setupInit() { - NIMKitFileLocationHelper.setStaticAppkey(NIMSDK.shared().appKey()) - NIMKitFileLocationHelper.setStaticUserId(IMKitClient.instance.account()) - let webpCoder = SDImageWebPCoder() - SDImageCodersManager.shared.addCoder(webpCoder) - let svgCoder = SDImageSVGKCoder.shared - SDImageCodersManager.shared.addCoder(svgCoder) - } - public static func registerCommon() { // sendMessage Router.shared.register(ChatAddFriendRouter) { param in diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift index c79789dd..40057f0b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift @@ -24,6 +24,9 @@ public let chat_file_size = CGSize(width: 254, height: 56) // 单行气泡高度 public let chat_min_h: CGFloat = 40.0 +// 单行气泡高度(通用版) +public let fun_chat_min_h: CGFloat = 42.0 + // 回复消息replyLabel高度 public let chat_reply_height: CGFloat = 16.0 diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift index bb7f5fa7..61fdf866 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.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 NIMSDK import UIKit @@ -14,6 +15,12 @@ open class NEChatUIKitClient: NSObject { public var moreAction = [NEMoreItemModel]() override init() { + let photo = NEMoreItemModel() + photo.image = UIImage.ne_imageNamed(name: "fun_chat_photo") + photo.title = chatLocalizable("chat_photo") + photo.type = .photo + moreAction.append(photo) + let picture = NEMoreItemModel() picture.image = UIImage.ne_imageNamed(name: "chat_takePicture") picture.title = chatLocalizable("chat_takePicture") @@ -39,6 +46,14 @@ open class NEChatUIKitClient: NSObject { rtc.type = .rtc moreAction.append(rtc) } + + if IMKitConfigCenter.shared.enableAIUser == true { + let translate = NEMoreItemModel() + translate.image = UIImage.ne_imageNamed(name: "chat_translation") + translate.title = chatLocalizable("chat_translate") + translate.type = .translate + moreAction.append(translate) + } } /// 获取更多面板数据 diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/NETranslateLanguageManager.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/NETranslateLanguageManager.swift new file mode 100644 index 00000000..f9fa88a3 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/NETranslateLanguageManager.swift @@ -0,0 +1,40 @@ +//// 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 NIMSDK +import UIKit + +@objcMembers +open class NETranslateLanguageManager: NSObject { + public static var shared = NETranslateLanguageManager() + + override private init() {} + + public var languageDatas = ["英语", "日语", "韩语", "俄语", "法语", "德语"] + +// public var translateAIUserAccountId: String { +// NEAIUserManager.shared.getAITranslateUser()?.accountId ?? "" +// } + +// var translationAIUser: V2NIMAIUser? + +// public func getTranslationAIUser(_ completion: @escaping (V2NIMAIUser?) -> Void) { +// if translateAIUserAccountId.count <= 0 { +// completion(nil) +// } +// if translationAIUser == nil { +// AIRepo.shared.getAIUserList { [weak self] users, error in +// users?.forEach { aiUser in +// if self?.translateAIUserAccountId == aiUser.accountId { +// self?.translationAIUser = aiUser +// completion(aiUser) +// } +// } +// } +// } else { +// completion(translationAIUser) +// } +// } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/NSBundleExtension.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/NSBundleExtension.swift index ae66a13b..26e76745 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/NSBundleExtension.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/NSBundleExtension.swift @@ -20,7 +20,7 @@ public extension Bundle { class func nim_EmojiPlistFile() -> String? { let bundle = Bundle.nim_defaultEmojiBundle() - let resource = (CommonTool.getCurrentLanguage() == "cn") ?"emoji_ios_cn" : "emoji_ios_en" + let resource = "emoji_ios_cn" // (CommonTool.getCurrentLanguage() == "cn") ?"emoji_ios_cn" : "emoji_ios_en" let filepath = bundle?.path( forResource: resource, ofType: "plist", diff --git a/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift b/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift index b7f528c5..e7dac33a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift @@ -4,13 +4,14 @@ // found in the LICENSE file. import Foundation +import NECommonUIKit extension UIAlertController { class func reconfimAlertView(title: String?, message: String?, confirm: @escaping () -> Void) -> UIAlertController { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: chatLocalizable("cancel"), style: .cancel, handler: nil)) - alert.addAction(UIAlertAction(title: chatLocalizable("ok"), style: .default) { action in + alert.addAction(UIAlertAction(title: commonLocalizable("ok"), style: .default) { action in confirm() }) return alert @@ -19,7 +20,7 @@ extension UIAlertController { class func singleAlertView(title: String?, message: String?, confirm: @escaping () -> Void) -> UIAlertController { let alert = UIAlertController(title: title, message: message, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: chatLocalizable("ok"), style: .default) { action in + alert.addAction(UIAlertAction(title: commonLocalizable("ok"), style: .default) { action in confirm() }) return alert diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift index 13d784dd..95d6004a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift @@ -27,7 +27,7 @@ open class FunChatMessageBaseCell: NEBaseChatMessageCell { override open func baseCommonUI() { super.baseCommonUI() - setAvatarImgSize(size: 42) + setAvatarImgSize(size: fun_chat_min_h) contentView.updateLayoutConstraint(firstItem: fullNameLabel, seconedItem: avatarImageLeft, attribute: .left, constant: 8 + funMargin) contentView.updateLayoutConstraint(firstItem: fullNameLabel, seconedItem: avatarImageLeft, attribute: .top, constant: -4) @@ -37,13 +37,12 @@ open class FunChatMessageBaseCell: NEBaseChatMessageCell { } override open func initSubviewsLayout() { - if NEKitChatConfig.shared.ui.messageProperties.avatarType == .rectangle, - NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius > 0 { - avatarImageRight.layer.cornerRadius = NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius - avatarImageLeft.layer.cornerRadius = NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius - } else if NEKitChatConfig.shared.ui.messageProperties.avatarType == .cycle { + if NEKitChatConfig.shared.ui.messageProperties.avatarType == .cycle { avatarImageRight.layer.cornerRadius = 21.0 avatarImageLeft.layer.cornerRadius = 21.0 + } else if NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius > 0 { + avatarImageRight.layer.cornerRadius = NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius + avatarImageLeft.layer.cornerRadius = NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius } else { avatarImageRight.layer.cornerRadius = 4 avatarImageLeft.layer.cornerRadius = 4 diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift index 666772db..6c5024c6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift @@ -293,8 +293,8 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { } } - override open func uploadProgress(byRight: Bool, _ progress: UInt) { - let stateView = byRight ? stateViewRight : stateViewLeft + override open func uploadProgress(_ progress: UInt) { + let stateView = stateViewLeft.isHidden ? stateViewRight : stateViewLeft stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift index ce027698..cbacf99f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift @@ -214,7 +214,7 @@ open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { } var contentText = "" - if var senderNick = abstracts[i]["senderNick"] as? String { + if let senderNick = abstracts[i]["senderNick"] as? String { contentText = NEFriendUserCache.getCutName(senderNick) if let content = abstracts[i]["content"] as? String { contentText += ":" + content diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift index 7d5a0d5d..228e26f0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift @@ -122,8 +122,14 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { super.setModel(model, isSend) - let replyLabel = isSend ? replyLabelRight : replyLabelLeft + if model.contentSize.height == fun_chat_min_h { + // 单行消息单独设置文本内边距 + let contentLabel = isSend ? contentLabelRight : contentLabelLeft + contentLabel.textContainerInset = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0) + } + + let replyLabel = isSend ? replyLabelRight : replyLabelLeft if let text = model.replyText, let font = replyLabel.font { replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift index 32815a98..8a3c542e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift @@ -6,32 +6,50 @@ import UIKit @objcMembers open class FunChatMessageRichTextCell: FunChatMessageReplyCell { - public lazy var titleLabelLeft: UILabel = { - let label = UILabel() + public lazy var titleLabelLeft: NEChatTextView = { + let label = NEChatTextView() label.translatesAutoresizingMaskIntoConstraints = false - label.isEnabled = false - label.numberOfLines = 0 + label.isEditable = false + label.isSelectable = true + label.isScrollEnabled = false + label.delegate = self + label.textContainerInset = .zero + label.contentInset = .zero + label.textContainer.lineFragmentPadding = 0.0 label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageTitle" + + let tap = UITapGestureRecognizer(target: nil, action: nil) + label.addGestureRecognizer(tap) return label }() - public lazy var titleLabelRight: UILabel = { - let label = UILabel() + public lazy var titleLabelRight: NEChatTextView = { + let label = NEChatTextView() label.translatesAutoresizingMaskIntoConstraints = false - label.isEnabled = false - label.numberOfLines = 0 + label.isEditable = false + label.isSelectable = true + label.isScrollEnabled = false + label.delegate = self + label.textContainerInset = .zero + label.contentInset = .zero + label.textContainer.lineFragmentPadding = 0.0 label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageTitle" + + let tap = UITapGestureRecognizer(target: nil, action: nil) + label.addGestureRecognizer(tap) return label }() public var titleLabelLeftHeightAnchor: NSLayoutConstraint? public var titleLabelRightHeightAnchor: NSLayoutConstraint? + public var titleLabelLeftBottomAnchor: NSLayoutConstraint? + public var titleLabelRightBottomAnchor: NSLayoutConstraint? public var contentLabelLeftHeightAnchor: NSLayoutConstraint? public var contentLabelRightHeightAnchor: NSLayoutConstraint? @@ -41,6 +59,7 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { titleLabelLeftHeightAnchor = titleLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) titleLabelLeftHeightAnchor?.priority = .fittingSizeLevel titleLabelLeftHeightAnchor?.isActive = true + titleLabelLeftBottomAnchor = titleLabelLeft.bottomAnchor.constraint(equalTo: bubbleImageLeft.bottomAnchor, constant: -chat_content_margin) NSLayoutConstraint.activate([ titleLabelLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor, constant: -chat_content_margin), titleLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: chat_content_margin + funMargin), @@ -64,6 +83,7 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { titleLabelRightHeightAnchor = titleLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) titleLabelRightHeightAnchor?.priority = .fittingSizeLevel titleLabelRightHeightAnchor?.isActive = true + titleLabelRightBottomAnchor = titleLabelRight.bottomAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: -chat_content_margin) NSLayoutConstraint.activate([ titleLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -chat_content_margin - funMargin), titleLabelRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: chat_content_margin), @@ -89,11 +109,60 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { titleLabelRight.isHidden = !showRight } + /// 重设文本选中范围 + override open func resetSelectRange() { + super.resetSelectRange() + titleLabelLeft.selectedRange = .init() + titleLabelRight.selectedRange = .init() + } + + /// 选中所有文本 + override open func selectAllRange() { + super.selectAllRange() + if let model = contentModel as? MessageTextModel, model.attributeStr == nil { + titleLabelLeft.selectAll(nil) + titleLabelRight.selectAll(nil) + } + } + + /// 设置是否允许多选 + /// 多选状态下文本不可选中 + /// - Parameters: + /// - model: 数据模型 + /// - enableSelect: 是否处于多选状态 + override open func setSelect(_ model: MessageContentModel, _ enableSelect: Bool = false) { + super.setSelect(model, enableSelect) + let titleLabel = titleLabelLeft.isHidden ? titleLabelRight : titleLabelLeft + titleLabel.isUserInteractionEnabled = !enableSelect + } + + /// 重写 bubbleImage 长按事件 + /// 文本类消息长按默认选中所有文本 + /// - Parameter longPress: 长按手势 + override open func longPress(longPress: UILongPressGestureRecognizer) { + if let model = contentModel as? MessageTextModel, model.attributeStr == nil { + let titleLabel = titleLabelLeft.isHidden ? titleLabelRight : titleLabelLeft + + // 选中所有 + let length = titleLabel.text.utf16.count + let range = NSRange(location: 0, length: length) + titleLabel.selectedRange = range + contentModel?.selectRange = range + + delegate?.didLongPressMessageView(self, contentModel) + titleLabel.becomeFirstResponder() + } else { + super.longPress(longPress: longPress) + } + } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { super.setModel(model, isSend) let replyView = isSend ? replyTextViewRight : replyTextViewLeft let titleLabel = isSend ? titleLabelRight : titleLabelLeft + let contentLabel = isSend ? contentLabelRight : contentLabelLeft let titleLabelHeightAnchor = isSend ? titleLabelRightHeightAnchor : titleLabelLeftHeightAnchor + let titleLabelBottomAnchor = isSend ? titleLabelRightBottomAnchor : titleLabelLeftBottomAnchor let contentLabelHeightAnchor = isSend ? contentLabelRightHeightAnchor : contentLabelLeftHeightAnchor if model.replyText == nil || model.replyText!.isEmpty { @@ -108,7 +177,55 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { if let m = model as? MessageRichTextModel { titleLabel.attributedText = m.titleAttributeStr - titleLabelHeightAnchor?.constant = m.titleTextHeight + if replyView.isHidden { + titleLabel.textContainerInset = UIEdgeInsets(top: model.offset / 2, left: 0, bottom: 0, right: 0) + } + + if m.attributeStr == nil { + // contentLabel 为空(只有 title) + titleLabel.isUserInteractionEnabled = true + titleLabelHeightAnchor?.isActive = false + titleLabelBottomAnchor?.isActive = true + } else { + titleLabel.isUserInteractionEnabled = false + titleLabelBottomAnchor?.isActive = false + titleLabelHeightAnchor?.isActive = true + titleLabelHeightAnchor?.constant = m.titleTextHeight + } + } + } + + override open func selectText() { + if let model = contentModel as? MessageTextModel, model.attributeStr != nil { + super.selectText() + return + } + + let titleLabel = titleLabelLeft.isHidden ? titleLabelRight : titleLabelLeft + let range = titleLabel.selectedRange + contentModel?.selectRange = range + + if range.location == lastRange?.location || range.location + range.length == (lastRange?.location ?? 0) + (lastRange?.length ?? 0) { + lastRange = range + } else { + contentModel?.selectRange = nil + } + + // 首次全选 + if contentModel?.selectRange == nil || range.length == 0 { + let length = titleLabel.text.utf16.count + let range = NSRange(location: 0, length: length) + titleLabel.selectedRange = range + contentModel?.selectRange = range + lastRange = range + } + + delegate?.didLongPressMessageView(self, contentModel) + + if (contentModel?.selectRange?.length ?? 0) > 0 { + titleLabel.becomeFirstResponder() + } else { + titleLabel.resignFirstResponder() } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift index 5daad0d6..fb9c54bc 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift @@ -6,27 +6,45 @@ import UIKit @objcMembers open class FunChatMessageTextCell: FunChatMessageBaseCell { - public lazy var contentLabelLeft: UILabel = { - let label = UILabel() + var lastRange: NSRange? // 上一次的文本选中范围 + + public lazy var contentLabelLeft: NEChatTextView = { + let label = NEChatTextView() label.translatesAutoresizingMaskIntoConstraints = false - label.isEnabled = false - label.numberOfLines = 0 - label.isUserInteractionEnabled = false + label.isEditable = false + label.isSelectable = true + label.isScrollEnabled = false + label.delegate = self + label.textContainerInset = .zero + label.contentInset = .zero + label.textContainer.lineFragmentPadding = 0.0 + label.isUserInteractionEnabled = true label.font = messageTextFont label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageText" + + let tap = UITapGestureRecognizer(target: nil, action: nil) + label.addGestureRecognizer(tap) return label }() - public lazy var contentLabelRight: UILabel = { - let label = UILabel() + public lazy var contentLabelRight: NEChatTextView = { + let label = NEChatTextView() label.translatesAutoresizingMaskIntoConstraints = false - label.isEnabled = false - label.numberOfLines = 0 - label.isUserInteractionEnabled = false + label.isEditable = false + label.isSelectable = true + label.isScrollEnabled = false + label.delegate = self + label.textContainerInset = .zero + label.contentInset = .zero + label.textContainer.lineFragmentPadding = 0.0 + label.isUserInteractionEnabled = true label.font = messageTextFont label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageText" + + let tap = UITapGestureRecognizer(target: nil, action: nil) + label.addGestureRecognizer(tap) return label }() @@ -63,15 +81,131 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { contentLabelRight.isHidden = !showRight } + /// 重设文本选中范围 + override open func resetSelectRange() { + contentLabelLeft.selectedRange = .init() + contentLabelRight.selectedRange = .init() + } + + /// 选中所有文本 + override open func selectAllRange() { + contentLabelLeft.selectAll(nil) + contentLabelRight.selectAll(nil) + } + + /// 设置是否允许多选 + /// 多选状态下文本不可选中 + /// - Parameters: + /// - model: 数据模型 + /// - enableSelect: 是否处于多选状态 + override open func setSelect(_ model: MessageContentModel, _ enableSelect: Bool = false) { + super.setSelect(model, enableSelect) + contentLabelLeft.isUserInteractionEnabled = !enableSelect + contentLabelRight.isUserInteractionEnabled = !enableSelect + + bubbleImageLeft.isUserInteractionEnabled = !enableSelect + bubbleImageRight.isUserInteractionEnabled = !enableSelect + } + + /// 重写 bubbleImage 长按事件 + /// 文本类消息长按默认选中所有文本 + /// - Parameter longPress: 长按手势 + override open func longPress(longPress: UILongPressGestureRecognizer) { + let contentLabel = contentLabelLeft.isHidden ? contentLabelRight : contentLabelLeft + + // 选中所有 + let length = contentLabel.text.utf16.count + let range = NSRange(location: 0, length: length) + contentLabel.selectedRange = range + contentModel?.selectRange = range + + delegate?.didLongPressMessageView(self, contentModel) + contentLabel.becomeFirstResponder() + } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { super.setModel(model, isSend) + let contentLabel = isSend ? contentLabelRight : contentLabelLeft let bubbleW = isSend ? bubbleWRight : bubbleWLeft if let m = model as? MessageTextModel { contentLabel.attributedText = m.attributeStr contentLabel.accessibilityValue = m.message?.text + contentSizeToFit(contentLabel, m) } bubbleW?.constant += funMargin } } + +// MARK: - UITextViewDelegate + +/// 划词(文本选中)实现 +extension FunChatMessageTextCell: UITextViewDelegate { + /// 选中文本 + open func selectText() { + let contentLabel = contentLabelLeft.isHidden ? contentLabelRight : contentLabelLeft + let range = contentLabel.selectedRange + contentModel?.selectRange = range + + if range.location == lastRange?.location || range.location + range.length == (lastRange?.location ?? 0) + (lastRange?.length ?? 0) { + lastRange = range + } else { + contentModel?.selectRange = nil + } + + // 首次全选 + if contentModel?.selectRange == nil || range.length == 0 { + let length = contentLabel.text.utf16.count + let range = NSRange(location: 0, length: length) + contentLabel.selectedRange = range + contentModel?.selectRange = range + lastRange = range + } + + delegate?.didLongPressMessageView(self, contentModel) + + if (contentModel?.selectRange?.length ?? 0) > 0 { + contentLabel.becomeFirstResponder() + } else { + contentLabel.resignFirstResponder() + } + } + + /// 拦截系统菜单 + override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + false + } + + /// 选中范围变更 + /// - Parameter textView: textview + public func textViewDidChangeSelection(_ textView: UITextView) { + if textView.selectedRange.length == 0 { + delegate?.didTextViewLoseFocus?(self, contentModel) + } else { + selectText() + } + } + + // textView 垂直居中 + func contentSizeToFit(_ contentLabel: UITextView, _ model: MessageTextModel) { + let messageTextFont = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + let messageMaxSize = CGSize(width: chat_content_maxW, height: CGFloat.greatestFiniteMagnitude) + let titleSize = NSAttributedString.getRealSize(contentLabel.attributedText, messageTextFont, messageMaxSize) + + if model.contentSize.height == fun_chat_min_h { + // 单行消息单独设置文本内边距 + contentLabel.textContainerInset = UIEdgeInsets(top: model.offset / 2, left: 0, bottom: 0, right: 0) + return + } + + let textHeight = titleSize.height + let textViewHeight = model.textHeight + if textHeight <= textViewHeight { + let offsetY = (textViewHeight - textHeight) / 2 + contentLabel.textContainerInset = UIEdgeInsets(top: offsetY, left: 0, bottom: 0, right: 0) + } else { + contentLabel.textContainerInset = .zero + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift index 74dc0b7e..0807f4a0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift @@ -121,6 +121,18 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { ]) } + /// 根据消息发送方向决定元素的显隐 + /// @param showRight 是否右侧显示(是否是发送的消息) + override open func showLeftOrRight(showRight: Bool) { + super.showLeftOrRight(showRight: showRight) + contentImageViewLeft.isHidden = showRight + timeViewLeft.isHidden = showRight + stateViewLeft.isHidden = showRight + contentImageViewRight.isHidden = !showRight + timeViewRight.isHidden = !showRight + stateViewRight.isHidden = !showRight + } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { super.setModel(model, isSend) let contentImageView = isSend ? contentImageViewRight : contentImageViewLeft @@ -131,7 +143,7 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { if let videoObject = model.message?.attachment as? V2NIMMessageVideoAttachment { // 获取首帧 let videoUrl = videoObject.url ?? "" - let thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + let thumbUrl = V2NIMStorageUtil.videoCoverUrl(videoUrl, offset: 0) contentImageView.sd_setImage( with: URL(string: thumbUrl), placeholderImage: nil, @@ -164,8 +176,8 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { } } - override open func uploadProgress(byRight: Bool, _ progress: UInt) { - let stateView = byRight ? stateViewRight : stateViewLeft + override open func uploadProgress(_ progress: UInt) { + let stateView = stateViewLeft.isHidden ? stateViewRight : stateViewLeft stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunLanguageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunLanguageCell.swift new file mode 100644 index 00000000..c0cb6fdf --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunLanguageCell.swift @@ -0,0 +1,49 @@ +//// 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 + +@objcMembers +open class FunLanguageCell: NEBaseLanguageCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + override open func setupLanguageCellUI() { + super.setupLanguageCellUI() + contentView.addSubview(languageLabel) + NSLayoutConstraint.activate([ + languageLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 40), + languageLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + languageLabel.topAnchor.constraint(equalTo: contentView.topAnchor), + languageLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + + contentView.addSubview(selectedImageView) + NSLayoutConstraint.activate([ + selectedImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -40), + selectedImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + selectedImageView.widthAnchor.constraint(equalToConstant: 16), + selectedImageView.heightAnchor.constraint(equalToConstant: 16), + ]) + + contentView.backgroundColor = .clear + } + + override open func configureData(_ model: NElanguageCellModel) { + if model.isSelect == true { + fillColor = UIColor(hexString: "#F5F8FF") + } else { + fillColor = UIColor.white + } + super.configureData(model) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift index 050ff5a3..3b6ed05a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift @@ -14,12 +14,6 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, override public init(conversationId: String) { super.init(conversationId: conversationId) cellRegisterDic = ChatMessageHelper.getChatCellRegisterDic(isFun: true) - - normalInputHeight = 90 - brokenNetworkViewHeight = 48 - navigationView.titleBarBottomLine.backgroundColor = .funChatNavigationBottomLineColor - - topMessageView.topImageView.image = UIImage.ne_imageNamed(name: "top_message_image") } public required init?(coder: NSCoder) { @@ -29,10 +23,18 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funChatBackgroundColor // 换肤颜色提取 - view.bringSubviewToFront(chatInputView) + + normalInputHeight = 90 + brokenNetworkViewHeight = 48 + navigationView.titleBarBottomLine.backgroundColor = .funChatNavigationBottomLineColor + + topMessageView.topImageView.image = UIImage.ne_imageNamed(name: "top_message_image") + brokenNetworkView.errorIconView.isHidden = false brokenNetworkView.backgroundColor = .funChatNetworkBrokenBackgroundColor brokenNetworkView.contentLabel.textColor = .funChatNetworkBrokenTitleColor + + view.bringSubviewToFront(chatInputView) getFunInputView()?.funDelegate = self } @@ -57,8 +59,8 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } /// 获取@列表视图控制器 - 通用版 - override func getUserSelectVC() -> NEBaseSelectUserViewController { - FunSelectUserViewController(sessionId: viewModel.sessionId, showSelf: false) + override func getUserSelectVC(showTeamMembers: Bool) -> NEBaseSelectUserViewController { + FunSelectUserViewController(conversationId: viewModel.conversationId, showSelf: false, showTeamMembers: showTeamMembers) } /// 获取文本详情页视图控制器 - 通用版 @@ -69,6 +71,8 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } open func recordModeChangeDidClick() { + translateLanguageView.changeToIdleState(true) + translateLanguageViewHeightAnchor?.constant = 0 normalOffset = 0 if chatInputView.chatInpuMode == .multipleSend { normalInputHeight = 90 @@ -108,27 +112,14 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, chatInputView.textView.becomeFirstResponder() } - override open func expandMoreAction() { - 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") - photo.type = .photo - photo.customDelegate = self - photo.action = #selector(openPhoto) - items.insert(photo, at: 0) + @discardableResult + override open func expandMoreAction() -> [NEMoreItemModel] { + let items = super.expandMoreAction() chatInputView.chatAddMoreView.configData(data: items) + return items } - func openPhoto() { + override open func openPhoto() { NEALog.infoLog(className(), desc: "open photo") willSelectItem(button: chatInputView.currentButton, index: showPhotoTag) } @@ -301,14 +292,14 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } } else { var text = chatLocalizable("msg_reply") - if let uid = message.senderId { - var showName = ChatTeamCache.shared.getShowName(uid, false) + if let uid = ChatMessageHelper.getSenderId(message) { + var showName = NETeamUserManager.shared.getShowName(uid, false) if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) != .CONVERSATION_TYPE_P2P, !IMKitClient.instance.isMe(uid) { addToAtUsers(addText: "@" + showName + "", isReply: true, accid: uid) } - showName = ChatTeamCache.shared.getShowName(uid) + showName = NETeamUserManager.shared.getShowName(uid) text += " " + showName text += ": \(ChatMessageHelper.contentOfMessage(message))" getFunInputView()?.replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, @@ -352,9 +343,9 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, let contentWidth = model.contentSize.width let contentHeight = model.contentSize.height - if contentHeight < 42 { - let subHeight = 42 - contentHeight - model.contentSize = CGSize(width: contentWidth, height: 42) + if contentHeight < fun_chat_min_h { + let subHeight = fun_chat_min_h - contentHeight + model.contentSize = CGSize(width: contentWidth, height: fun_chat_min_h) model.offset = CGFloat(subHeight) } @@ -424,4 +415,12 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } layoutInputViewWithAnimation(offset: currentKeyboardHeight) } + + override open func didSwitchLanguageClick(_ currentLanguage: String?) { + let funLanguageSelectController = FunSelectLanguageViewController() + if let current = currentLanguage { + funLanguageSelectController.currentContent = current + } + showLanguageContentController(funLanguageSelectController) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunCollectionMessageController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunCollectionMessageController.swift index bc78d670..79f7aa92 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunCollectionMessageController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunCollectionMessageController.swift @@ -45,7 +45,7 @@ open class FunCollectionMessageController: NEBaseCollectionMessageController { if let message = model.message, message.messageType != .MESSAGE_TYPE_AUDIO { let forwardAction = NECustomAlertAction(title: chatLocalizable("operation_forward")) { - weakSelf?.forwardCollectionMessage(message, model.senderName ?? "") + weakSelf?.forwardCollectionMessage(message, model.conversationName ?? "") } actions.append(forwardAction) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift index a78acf2d..3682eecd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.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 @@ -51,9 +52,9 @@ open class FunMultiForwardViewController: MultiForwardViewController { let contentWidth = model.contentSize.width let contentHeight = model.contentSize.height - if contentHeight < 42 { - let subHeight = 42 - contentHeight - model.contentSize = CGSize(width: contentWidth, height: 42) + if contentHeight < fun_chat_min_h { + let subHeight = fun_chat_min_h - contentHeight + model.contentSize = CGSize(width: contentWidth, height: fun_chat_min_h) model.offset = CGFloat(subHeight) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift index 43a7122d..da376001 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift @@ -27,6 +27,20 @@ open class FunP2PChatViewController: FunChatViewController { super.init(coder: coder) } + /// 添加子类监听 + override open func addListener() { + super.addListener() + ContactRepo.shared.addContactListener(self) + NEP2PChatUserCache.shared.addListener(self) + } + + /// 移除子类监听 + override open func removeListener() { + super.removeListener() + ContactRepo.shared.removeContactListener(self) + NEP2PChatUserCache.shared.removeListener(self) + } + override open var title: String? { didSet { super.title = title @@ -93,3 +107,37 @@ open class FunP2PChatViewController: FunChatViewController { } } } + +// MARK: - NEContactListener + +extension FunP2PChatViewController: NEContactListener { + /// 好友信息缓存更新 + /// - Parameter accountId: 用户 id + public func onContactChange(_ changeType: NEContactChangeType, _ contacts: [NEUserWithFriend]) { + for contact in contacts { + if let accid = contact.user?.accountId, contact.user?.accountId == viewModel.sessionId { + // 好友添加,则从 NEP2PChatUserCache 中移除信息缓存 + if changeType == .addFriend { + NEP2PChatUserCache.shared.removeUserInfo(viewModel.sessionId) + } + + // 好友被删除,则信息缓存移至 NEP2PChatUserCache + if changeType == .deleteFriend { + contact.friend = nil + NEP2PChatUserCache.shared.updateUserInfo(contact) + } + onUserOrFriendInfoChanged(accid) + } + } + } +} + +// MARK: - NEP2PChatUserCacheListener + +extension FunP2PChatViewController: NEP2PChatUserCacheListener { + /// 非好友单聊信息缓存更新 + /// - Parameter accountId: 用户 id + public func onUserInfoUpdate(_ accountId: String) { + onUserOrFriendInfoChanged(accountId) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectLanguageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectLanguageViewController.swift new file mode 100644 index 00000000..ea63cf33 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectLanguageViewController.swift @@ -0,0 +1,79 @@ +//// 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 + +@objcMembers +open class FunSelectLanguageViewController: NEBaseSelectLanguageViewController { + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + override open func setupLanguageUI() { + super.setupLanguageUI() + + view.backgroundColor = .funChatBackgroundColor + + viewModel.setupData(true) + let funBackButton = UIButton(type: .custom) + funBackButton.translatesAutoresizingMaskIntoConstraints = false + funBackButton.accessibilityIdentifier = "id.cancel" + funBackButton.setImage(UIImage.ne_imageNamed(name: "arrowDown"), for: .normal) + funBackButton.addTarget(self, action: #selector(cancelClick), for: .touchUpInside) + view.addSubview(funBackButton) + + if #available(iOS 11.0, *) { + NSLayoutConstraint.activate([ + funBackButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 16), + funBackButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), + funBackButton.widthAnchor.constraint(equalToConstant: 50), + funBackButton.heightAnchor.constraint(equalToConstant: 50), + ]) + } else { + // Fallback on earlier versions + NSLayoutConstraint.activate([ + funBackButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + funBackButton.topAnchor.constraint(equalTo: view.topAnchor), + funBackButton.widthAnchor.constraint(equalToConstant: 50), + funBackButton.heightAnchor.constraint(equalToConstant: 50), + ]) + } + + let funNavTitleLabel = UILabel() + funNavTitleLabel.translatesAutoresizingMaskIntoConstraints = false + funNavTitleLabel.text = chatLocalizable("language_title") + funNavTitleLabel.font = UIFont.systemFont(ofSize: 16, weight: .semibold) + funNavTitleLabel.textAlignment = .center + funNavTitleLabel.textColor = .ne_darkText + view.addSubview(funNavTitleLabel) + NSLayoutConstraint.activate([ + funNavTitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), + funNavTitleLabel.topAnchor.constraint(equalTo: view.topAnchor), + funNavTitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), + funNavTitleLabel.heightAnchor.constraint(equalToConstant: 50), + ]) + + view.addSubview(languageTableView) + NSLayoutConstraint.activate([ + languageTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + languageTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + languageTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 58), + languageTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + languageTableView.register(FunLanguageCell.self, forCellReuseIdentifier: "\(NEBaseLanguageCell.self)") + } + + /* + // 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/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift index d819c0fb..e3e5a306 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift @@ -9,8 +9,8 @@ import UIKit @objcMembers open class FunSelectUserViewController: NEBaseSelectUserViewController { - override init(sessionId: String, showSelf: Bool = true) { - super.init(sessionId: sessionId, showSelf: showSelf) + override init(conversationId: String, showSelf: Bool = true, showTeamMembers: Bool = false) { + super.init(conversationId: conversationId, showSelf: showSelf, showTeamMembers: showTeamMembers) className = "FunSelectUserViewController" } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunTeamChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunTeamChatViewController.swift index 9aea8a7b..1247f09b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunTeamChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunTeamChatViewController.swift @@ -33,7 +33,7 @@ open class FunTeamChatViewController: FunChatViewController, TeamChatViewModelDe deinit { NotificationCenter.default.removeObserver(self) - ChatTeamCache.shared.removeAllTeamInfo() + NETeamUserManager.shared.removeAllTeamInfo() } override open func viewWillAppear(_ animated: Bool) { @@ -74,7 +74,7 @@ open class FunTeamChatViewController: FunChatViewController, TeamChatViewModelDe if let vm = self?.viewModel as? TeamChatViewModel { vm.getTeamInfo(teamId: sessionId) { error, team in if let team = team { - if IMKitConfigCenter.shared.dismissTeamDeleteConversation == true, team.isValidTeam == false { + if IMKitConfigCenter.shared.enabledismissTeamDeleteConversation == true, team.isValidTeam == false { self?.showSingleAlert(message: coreLoader.localizable("team_not_exist")) { NotificationCenter.default.post(name: NENotificationName.deleteConversationNotificationName, object: V2NIMConversationIdUtil.teamConversationId(team.teamId)) self?.popGroupChatVC() @@ -122,7 +122,7 @@ open class FunTeamChatViewController: FunChatViewController, TeamChatViewModelDe open func updateTeamTitle(_ noti: Notification) { if let tid = noti.userInfo?["teamId"] as? String, tid == viewModel.sessionId, - let team = ChatTeamCache.shared.getTeamInfo() { + let team = NETeamUserManager.shared.getTeamInfo() { updateTeamInfo(team: team) } } @@ -131,7 +131,6 @@ open class FunTeamChatViewController: FunChatViewController, TeamChatViewModelDe /// - Parameter team: 群聊信息 open func updateTeamInfo(team: V2NIMTeam) { title = team.name - ChatTeamCache.shared.updateTeamInfo(team) setMute(team: team) } @@ -226,7 +225,7 @@ open class FunTeamChatViewController: FunChatViewController, TeamChatViewModelDe /// 群成员更新回调 /// - Parameter teamMembers: 群成员列表 public func onTeamMemberUpdate(_ teamMembers: [V2NIMTeamMember]) { - if let team = ChatTeamCache.shared.getTeamInfo() { + if let team = NETeamUserManager.shared.getTeamInfo() { setMute(team: team) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift index 36ef1163..8dce0eb9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift @@ -73,7 +73,7 @@ open class FunUserSettingViewController: NEBaseUserSettingViewController { nameLabel.text = viewModel.userInfo?.showName() cornerBackView.addSubview(nameLabel) - if IMKitConfigCenter.shared.teamEnable { + if IMKitConfigCenter.shared.enableTeam { NSLayoutConstraint.activate([ userHeaderView.leftAnchor.constraint(equalTo: cornerBackView.leftAnchor, constant: 22), userHeaderView.topAnchor.constraint(equalTo: cornerBackView.topAnchor, constant: 22), diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift index 74a253de..89745536 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift @@ -161,7 +161,7 @@ open class FunChatInputView: NEBaseChatInputView { replyLabel.leftAnchor.constraint(equalTo: replyBackView.leftAnchor, constant: 5), replyLabel.topAnchor.constraint(equalTo: replyBackView.topAnchor), replyLabel.bottomAnchor.constraint(equalTo: replyBackView.bottomAnchor), - replyLabel.rightAnchor.constraint(equalTo: replyBackView.rightAnchor, constant: -42), + replyLabel.rightAnchor.constraint(equalTo: replyBackView.rightAnchor, constant: -fun_chat_min_h), ]) replyBackView.addSubview(clearBtn) diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift index f196ed48..6896c31d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift @@ -294,8 +294,8 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { } } - override open func uploadProgress(byRight: Bool, _ progress: UInt) { - let stateView = byRight ? stateViewRight : stateViewLeft + override open func uploadProgress(_ progress: UInt) { + let stateView = stateViewLeft.isHidden ? stateViewRight : stateViewLeft stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift index dc08557c..1ee7209d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift @@ -210,7 +210,7 @@ open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { } var contentText = "" - if var senderNick = abstracts[i]["senderNick"] as? String { + if let senderNick = abstracts[i]["senderNick"] as? String { contentText = NEFriendUserCache.getCutName(senderNick) if let content = abstracts[i]["content"] as? String { contentText += ":" + content diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift index f42d32f2..78685e14 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift @@ -96,9 +96,9 @@ open class ChatMessageReplyCell: ChatMessageTextCell { color: replyLabel.textColor) replyLabel.accessibilityValue = text - if let attriText = replyLabel.attributedText, let model = model as? MessageTextModel { + if let attriText = replyLabel.attributedText { 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 + model.contentSize.width = max(textSize.width, model.textWidth) + chat_content_margin * 2 } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift index 2c9b514f..c6a36a6e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift @@ -7,27 +7,43 @@ import UIKit @objcMembers open class ChatMessageRichTextCell: ChatMessageReplyCell { - public lazy var titleLabelLeft: UILabel = { - let label = UILabel() + public lazy var titleLabelLeft: NEChatTextView = { + let label = NEChatTextView() label.translatesAutoresizingMaskIntoConstraints = false - label.isEnabled = false - label.numberOfLines = 0 + label.isEditable = false + label.isSelectable = true + label.isScrollEnabled = false + label.delegate = self + label.textContainerInset = .zero + label.contentInset = .zero + label.textContainer.lineFragmentPadding = 0.0 label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageTitle" + + let tap = UITapGestureRecognizer(target: nil, action: nil) + label.addGestureRecognizer(tap) return label }() - public lazy var titleLabelRight: UILabel = { - let label = UILabel() + public lazy var titleLabelRight: NEChatTextView = { + let label = NEChatTextView() label.translatesAutoresizingMaskIntoConstraints = false - label.isEnabled = false - label.numberOfLines = 0 + label.isEditable = false + label.isSelectable = true + label.isScrollEnabled = false + label.delegate = self + label.textContainerInset = .zero + label.contentInset = .zero + label.textContainer.lineFragmentPadding = 0.0 label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageTitle" + + let tap = UITapGestureRecognizer(target: nil, action: nil) + label.addGestureRecognizer(tap) return label }() @@ -106,6 +122,53 @@ open class ChatMessageRichTextCell: ChatMessageReplyCell { titleLabelRight.isHidden = !showRight } + /// 重设文本选中范围 + override open func resetSelectRange() { + super.resetSelectRange() + titleLabelLeft.selectedRange = .init() + titleLabelRight.selectedRange = .init() + } + + /// 选中所有文本 + override open func selectAllRange() { + super.selectAllRange() + if let model = contentModel as? MessageTextModel, model.attributeStr == nil { + titleLabelLeft.selectAll(nil) + titleLabelRight.selectAll(nil) + } + } + + /// 设置是否允许多选 + /// 多选状态下文本不可选中 + /// - Parameters: + /// - model: 数据模型 + /// - enableSelect: 是否处于多选状态 + override open func setSelect(_ model: MessageContentModel, _ enableSelect: Bool = false) { + super.setSelect(model, enableSelect) + let titleLabel = titleLabelLeft.isHidden ? titleLabelRight : titleLabelLeft + titleLabel.isUserInteractionEnabled = !enableSelect + } + + /// 重写 bubbleImage 长按事件 + /// 文本类消息长按默认选中所有文本 + /// - Parameter longPress: 长按手势 + override open func longPress(longPress: UILongPressGestureRecognizer) { + if let model = contentModel as? MessageTextModel, model.attributeStr == nil { + let titleLabel = titleLabelLeft.isHidden ? titleLabelRight : titleLabelLeft + + // 选中所有 + let length = titleLabel.text.utf16.count + let range = NSRange(location: 0, length: length) + titleLabel.selectedRange = range + contentModel?.selectRange = range + + delegate?.didLongPressMessageView(self, contentModel) + titleLabel.becomeFirstResponder() + } else { + super.longPress(longPress: longPress) + } + } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { super.setModel(model, isSend) let replyLabelHeightAnchor = isSend ? replyLabelRightHeightAnchor : replyLabelLeftHeightAnchor @@ -129,6 +192,47 @@ open class ChatMessageRichTextCell: ChatMessageReplyCell { if let m = model as? MessageRichTextModel { titleLabel.attributedText = m.titleAttributeStr titleLabelHeightAnchor?.constant = m.titleTextHeight + + if m.attributeStr == nil { + // contentLabel 为空(只有 title) + titleLabel.isUserInteractionEnabled = true + } else { + titleLabel.isUserInteractionEnabled = false + } + } + } + + override open func selectText() { + if let model = contentModel as? MessageTextModel, model.attributeStr != nil { + super.selectText() + return + } + + let titleLabel = titleLabelLeft.isHidden ? titleLabelRight : titleLabelLeft + let range = titleLabel.selectedRange + contentModel?.selectRange = range + + if range.location == lastRange?.location || range.location + range.length == (lastRange?.location ?? 0) + (lastRange?.length ?? 0) { + lastRange = range + } else { + contentModel?.selectRange = nil + } + + // 首次全选 + if contentModel?.selectRange == nil || range.length == 0 { + let length = titleLabel.text.utf16.count + let range = NSRange(location: 0, length: length) + titleLabel.selectedRange = range + contentModel?.selectRange = range + lastRange = range + } + + delegate?.didLongPressMessageView(self, contentModel) + + if (contentModel?.selectRange?.length ?? 0) > 0 { + titleLabel.becomeFirstResponder() + } else { + titleLabel.resignFirstResponder() } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift index a21ad783..9935f333 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift @@ -3,31 +3,50 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NECoreKit import UIKit @objcMembers open class ChatMessageTextCell: NormalChatMessageBaseCell { - public lazy var contentLabelLeft: UILabel = { - let label = UILabel() + var lastRange: NSRange? // 上一次的文本选中范围 + + public lazy var contentLabelLeft: NEChatTextView = { + let label = NEChatTextView() label.translatesAutoresizingMaskIntoConstraints = false - label.isEnabled = false - label.numberOfLines = 0 - label.isUserInteractionEnabled = false + label.isEditable = false + label.isSelectable = true + label.isScrollEnabled = false + label.delegate = self + label.textContainerInset = .zero + label.contentInset = .zero + label.textContainer.lineFragmentPadding = 0.0 + label.isUserInteractionEnabled = true label.font = messageTextFont label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageText" + + let tap = UITapGestureRecognizer(target: nil, action: nil) + label.addGestureRecognizer(tap) return label }() - public lazy var contentLabelRight: UILabel = { - let label = UILabel() + public lazy var contentLabelRight: NEChatTextView = { + let label = NEChatTextView() label.translatesAutoresizingMaskIntoConstraints = false - label.isEnabled = false - label.numberOfLines = 0 - label.isUserInteractionEnabled = false + label.isEditable = false + label.isSelectable = true + label.isScrollEnabled = false + label.delegate = self + label.textContainerInset = .zero + label.contentInset = .zero + label.textContainer.lineFragmentPadding = 0.0 + label.isUserInteractionEnabled = true label.font = messageTextFont label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageText" + + let tap = UITapGestureRecognizer(target: nil, action: nil) + label.addGestureRecognizer(tap) return label }() @@ -64,12 +83,126 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { contentLabelRight.isHidden = !showRight } + /// 重设文本选中范围 + override open func resetSelectRange() { + contentLabelLeft.selectedRange = .init() + contentLabelRight.selectedRange = .init() + } + + /// 选中所有文本 + override open func selectAllRange() { + contentLabelLeft.selectAll(nil) + contentLabelRight.selectAll(nil) + } + + /// 设置是否允许多选 + /// 多选状态下文本不可选中 + /// - Parameters: + /// - model: 数据模型 + /// - enableSelect: 是否处于多选状态 + override open func setSelect(_ model: MessageContentModel, _ enableSelect: Bool = false) { + super.setSelect(model, enableSelect) + contentLabelLeft.isUserInteractionEnabled = !enableSelect + contentLabelRight.isUserInteractionEnabled = !enableSelect + + bubbleImageLeft.isUserInteractionEnabled = !enableSelect + bubbleImageRight.isUserInteractionEnabled = !enableSelect + } + + /// 重写 bubbleImage 长按事件 + /// 文本类消息长按默认选中所有文本 + /// - Parameter longPress: 长按手势 + override open func longPress(longPress: UILongPressGestureRecognizer) { + let contentLabel = contentLabelLeft.isHidden ? contentLabelRight : contentLabelLeft + + // 选中所有 + let length = contentLabel.text.utf16.count + let range = NSRange(location: 0, length: length) + contentLabel.selectedRange = range + contentModel?.selectRange = range + + delegate?.didLongPressMessageView(self, contentModel) + contentLabel.becomeFirstResponder() + } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { super.setModel(model, isSend) + contentModel?.cell = self + let contentLabel = isSend ? contentLabelRight : contentLabelLeft if let m = model as? MessageTextModel { contentLabel.attributedText = m.attributeStr contentLabel.accessibilityValue = m.message?.text + contentSizeToFit(contentLabel, m) + } else { + contentLabel.text = model.message?.text + contentLabel.accessibilityValue = model.message?.text + } + } +} + +// MARK: - UITextViewDelegate + +/// 划词(文本选中)实现 +extension ChatMessageTextCell: UITextViewDelegate { + /// 选中文本 + open func selectText() { + let contentLabel = contentLabelLeft.isHidden ? contentLabelRight : contentLabelLeft + let range = contentLabel.selectedRange + contentModel?.selectRange = range + + if range.location == lastRange?.location || range.location + range.length == (lastRange?.location ?? 0) + (lastRange?.length ?? 0) { + lastRange = range + } else { + contentModel?.selectRange = nil + } + + // 首次全选 + if contentModel?.selectRange == nil || range.length == 0 { + let length = contentLabel.text.utf16.count + let range = NSRange(location: 0, length: length) + contentLabel.selectedRange = range + contentModel?.selectRange = range + lastRange = range + } + + delegate?.didLongPressMessageView(self, contentModel) + + if (contentModel?.selectRange?.length ?? 0) > 0 { + contentLabel.becomeFirstResponder() + } else { + contentLabel.resignFirstResponder() + } + } + + /// 拦截系统菜单 + override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + false + } + + /// 选中范围变更 + /// - Parameter textView: textview + public func textViewDidChangeSelection(_ textView: UITextView) { + if textView.selectedRange.length == 0 { + delegate?.didTextViewLoseFocus?(self, contentModel) + } else { + selectText() + } + } + + // textView 垂直居中 + func contentSizeToFit(_ contentLabel: UITextView, _ model: MessageTextModel) { + let messageTextFont = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + let messageMaxSize = CGSize(width: chat_content_maxW, height: CGFloat.greatestFiniteMagnitude) + let titleSize = NSAttributedString.getRealSize(contentLabel.attributedText, messageTextFont, messageMaxSize) + + let textHeight = titleSize.height + let textViewHeight = model.textHeight + if textHeight <= textViewHeight { + let offsetY = (textViewHeight - textHeight) / 2 + contentLabel.textContainerInset = UIEdgeInsets(top: offsetY, left: 0, bottom: 0, right: 0) + } else { + contentLabel.textContainerInset = .zero } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift index dae2602c..b42dcccf 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift @@ -124,6 +124,18 @@ open class ChatMessageVideoCell: ChatMessageImageCell { ]) } + /// 根据消息发送方向决定元素的显隐 + /// @param showRight 是否右侧显示(是否是发送的消息) + override open func showLeftOrRight(showRight: Bool) { + super.showLeftOrRight(showRight: showRight) + contentImageViewLeft.isHidden = showRight + timeViewLeft.isHidden = showRight + stateViewLeft.isHidden = showRight + contentImageViewRight.isHidden = !showRight + timeViewRight.isHidden = !showRight + stateViewRight.isHidden = !showRight + } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { super.setModel(model, isSend) let contentImageView = isSend ? contentImageViewRight : contentImageViewLeft @@ -134,7 +146,7 @@ open class ChatMessageVideoCell: ChatMessageImageCell { if let videoObject = model.message?.attachment as? V2NIMMessageVideoAttachment { // 获取首帧 let videoUrl = videoObject.url ?? "" - let thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + let thumbUrl = V2NIMStorageUtil.videoCoverUrl(videoUrl, offset: 0) contentImageView.sd_setImage( with: URL(string: thumbUrl), placeholderImage: nil, @@ -167,8 +179,8 @@ open class ChatMessageVideoCell: ChatMessageImageCell { } } - override open func uploadProgress(byRight: Bool, _ progress: UInt) { - let stateView = byRight ? stateViewRight : stateViewLeft + override open func uploadProgress(_ progress: UInt) { + let stateView = stateViewLeft.isHidden ? stateViewRight : stateViewLeft stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/LanguageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/LanguageCell.swift new file mode 100644 index 00000000..126f08df --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/LanguageCell.swift @@ -0,0 +1,38 @@ +//// 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 + +@objcMembers +open class LanguageCell: NEBaseLanguageCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + override open func setupLanguageCellUI() { + super.setupLanguageCellUI() + contentView.addSubview(languageLabel) + NSLayoutConstraint.activate([ + languageLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + languageLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + languageLabel.topAnchor.constraint(equalTo: contentView.topAnchor), + languageLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + + contentView.addSubview(selectedImageView) + NSLayoutConstraint.activate([ + selectedImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + selectedImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + selectedImageView.widthAnchor.constraint(equalToConstant: 16), + selectedImageView.heightAnchor.constraint(equalToConstant: 16), + ]) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift index 7197e8e3..281ca55e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.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 NIMSDK import UIKit @@ -9,13 +10,7 @@ import UIKit open class NormalChatViewController: ChatViewController { override public init(conversationId: String) { super.init(conversationId: conversationId) - navigationView.backgroundColor = .white - navigationController?.navigationBar.backgroundColor = .white cellRegisterDic = ChatMessageHelper.getChatCellRegisterDic(isFun: false) - - topMessageView.topImageView.image = UIImage.ne_imageNamed(name: "top_message_image") - topMessageView.layer.borderColor = UIColor(hexString: "#E8EAED").cgColor - topMessageView.layer.borderWidth = 1 } public required init?(coder: NSCoder) { @@ -24,6 +19,12 @@ open class NormalChatViewController: ChatViewController { override open func viewDidLoad() { super.viewDidLoad() + navigationController?.navigationBar.backgroundColor = .white + navigationView.backgroundColor = .white + + topMessageView.topImageView.image = UIImage.ne_imageNamed(name: "top_message_image") + topMessageView.layer.borderColor = UIColor(hexString: "#E8EAED").cgColor + topMessageView.layer.borderWidth = 1 } override open func getMenuView() -> NEBaseChatInputView { @@ -45,8 +46,8 @@ open class NormalChatViewController: ChatViewController { } /// 获取@列表视图控制器 - 协同版 - override func getUserSelectVC() -> NEBaseSelectUserViewController { - SelectUserViewController(sessionId: viewModel.sessionId, showSelf: false) + override func getUserSelectVC(showTeamMembers: Bool) -> NEBaseSelectUserViewController { + SelectUserViewController(conversationId: viewModel.conversationId, showSelf: false, showTeamMembers: showTeamMembers) } open func getMessageModel(model: MessageModel) { @@ -60,6 +61,21 @@ open class NormalChatViewController: ChatViewController { } } + @discardableResult + override open func expandMoreAction() -> [NEMoreItemModel] { + var items = super.expandMoreAction() + + items.removeAll { item in + if item.type == .photo { + return true + } + return false + } + + chatInputView.chatAddMoreView.configData(data: items) + return items + } + override open func expandButtonDidClick() { print("expandButtonDidClick ") super.expandButtonDidClick() @@ -97,14 +113,21 @@ open class NormalChatViewController: ChatViewController { func checkAndRestoreReplyView() { if viewModel.isReplying == true, replyView.superview == nil { view.addSubview(replyView) - replyView.closeButton.addTarget(self, action: #selector(closeReply), for: .touchUpInside) - replyView.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - replyView.leadingAnchor.constraint(equalTo: chatInputView.leadingAnchor), - replyView.trailingAnchor.constraint(equalTo: chatInputView.trailingAnchor), - replyView.bottomAnchor.constraint(equalTo: chatInputView.topAnchor), - replyView.heightAnchor.constraint(equalToConstant: 36), - ]) + if IMKitConfigCenter.shared.enableAIUser { + NSLayoutConstraint.activate([ + replyView.leadingAnchor.constraint(equalTo: translateLanguageView.leadingAnchor), + replyView.trailingAnchor.constraint(equalTo: translateLanguageView.trailingAnchor), + replyView.bottomAnchor.constraint(equalTo: translateLanguageView.topAnchor), + replyView.heightAnchor.constraint(equalToConstant: 36), + ]) + } else { + NSLayoutConstraint.activate([ + replyView.leadingAnchor.constraint(equalTo: chatInputView.leadingAnchor), + replyView.trailingAnchor.constraint(equalTo: chatInputView.trailingAnchor), + replyView.bottomAnchor.constraint(equalTo: chatInputView.topAnchor), + replyView.heightAnchor.constraint(equalToConstant: 36), + ]) + } } } @@ -143,4 +166,12 @@ open class NormalChatViewController: ChatViewController { } bottomViewTopAnchor?.constant = -(normalInputHeight + currentKeyboardHeight) } + + override open func didSwitchLanguageClick(_ currentLanguage: String?) { + let languageSelectController = SelectLanguageViewController() + if let current = currentLanguage { + languageSelectController.currentContent = current + } + showLanguageContentController(languageSelectController) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift index 288909d5..52a0423e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift @@ -11,8 +11,6 @@ open class NormalMultiForwardViewController: MultiForwardViewController { _ attachmentFilePath: String, _ attachmentMD5: String?) { super.init(attachmentUrl, attachmentFilePath, attachmentMD5) - navigationView.backgroundColor = .white - navigationController?.navigationBar.backgroundColor = .white cellRegisterDic = ChatMessageHelper.getChatCellRegisterDic(isFun: false) } @@ -20,6 +18,12 @@ open class NormalMultiForwardViewController: MultiForwardViewController { super.init(coder: coder) } + override open func viewDidLoad() { + super.viewDidLoad() + navigationView.backgroundColor = .white + navigationController?.navigationBar.backgroundColor = .white + } + override open func getMultiForwardViewController(_ messageAttachmentUrl: String?, _ messageAttachmentFilePath: String, _ messageAttachmentMD5: String?) -> MultiForwardViewController { diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift index 3ad97b99..231ee075 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift @@ -28,6 +28,20 @@ open class P2PChatViewController: NormalChatViewController { super.init(coder: coder) } + /// 添加子类监听 + override open func addListener() { + super.addListener() + ContactRepo.shared.addContactListener(self) + NEP2PChatUserCache.shared.addListener(self) + } + + /// 移除子类监听 + override open func removeListener() { + super.removeListener() + ContactRepo.shared.removeContactListener(self) + NEP2PChatUserCache.shared.removeListener(self) + } + override open var title: String? { didSet { super.title = title @@ -98,3 +112,37 @@ open class P2PChatViewController: NormalChatViewController { } } } + +// MARK: - NEContactListener + +extension P2PChatViewController: NEContactListener { + /// 好友信息缓存更新 + /// - Parameter accountId: 用户 id + public func onContactChange(_ changeType: NEContactChangeType, _ contacts: [NEUserWithFriend]) { + for contact in contacts { + if let accid = contact.user?.accountId, contact.user?.accountId == viewModel.sessionId { + // 好友添加,则从 NEP2PChatUserCache 中移除信息缓存 + if changeType == .addFriend { + NEP2PChatUserCache.shared.removeUserInfo(viewModel.sessionId) + } + + // 好友被删除,则信息缓存移至 NEP2PChatUserCache + if changeType == .deleteFriend { + contact.friend = nil + NEP2PChatUserCache.shared.updateUserInfo(contact) + } + onUserOrFriendInfoChanged(accid) + } + } + } +} + +// MARK: - NEP2PChatUserCacheListener + +extension P2PChatViewController: NEP2PChatUserCacheListener { + /// 非好友单聊信息缓存更新 + /// - Parameter accountId: 用户 id + public func onUserInfoUpdate(_ accountId: String) { + onUserOrFriendInfoChanged(accountId) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift index 0c692751..971b1c90 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift @@ -12,14 +12,18 @@ import UIKit open class ReadViewController: NEBaseReadViewController { override init(message: V2NIMMessage, teamId: String) { super.init(message: message, teamId: teamId) - navigationView.backgroundColor = .white - navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { super.init(coder: coder) } + override open func viewDidLoad() { + super.viewDidLoad() + navigationView.backgroundColor = .white + navigationController?.navigationBar.backgroundColor = .white + } + override open func commonUI() { super.commonUI() navigationView.titleBarBottomLine.isHidden = false diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectLanguageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectLanguageViewController.swift new file mode 100644 index 00000000..216e59f6 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectLanguageViewController.swift @@ -0,0 +1,80 @@ +//// 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 + +@objcMembers +open class SelectLanguageViewController: NEBaseSelectLanguageViewController { + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + override open func setupLanguageUI() { + super.setupLanguageUI() + viewModel.setupData(false) + + // 隐藏导航栏后自定义顶部样式 + let backButton = UIButton(type: .custom) + backButton.translatesAutoresizingMaskIntoConstraints = false + backButton.accessibilityIdentifier = "id.cancel" + backButton.addTarget(self, action: #selector(cancelClick), for: .touchUpInside) + backButton.setImage(UIImage.ne_imageNamed(name: "arrowDown"), for: .normal) + backButton.titleLabel?.textColor = .ne_greyText + backButton.titleLabel?.font = UIFont.systemFont(ofSize: 18.0) + view.addSubview(backButton) + + if #available(iOS 11.0, *) { + NSLayoutConstraint.activate([ + backButton.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 16), + backButton.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), + backButton.widthAnchor.constraint(equalToConstant: 50), + backButton.heightAnchor.constraint(equalToConstant: 50), + ]) + } else { + NSLayoutConstraint.activate([ + backButton.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + backButton.topAnchor.constraint(equalTo: view.topAnchor), + backButton.widthAnchor.constraint(equalToConstant: 50), + backButton.heightAnchor.constraint(equalToConstant: 50), + ]) + } + + let navTitleLabel = UILabel() + navTitleLabel.translatesAutoresizingMaskIntoConstraints = false + navTitleLabel.text = chatLocalizable("language_title") + navTitleLabel.font = UIFont.systemFont(ofSize: 16, weight: .semibold) + navTitleLabel.textAlignment = .center + navTitleLabel.textColor = .ne_darkText + view.addSubview(navTitleLabel) + NSLayoutConstraint.activate([ + navTitleLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0), + navTitleLabel.topAnchor.constraint(equalTo: view.topAnchor), + navTitleLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0), + navTitleLabel.heightAnchor.constraint(equalToConstant: 50), + ]) + + view.addSubview(languageTableView) + NSLayoutConstraint.activate([ + languageTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + languageTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + languageTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + languageTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + languageTableView.backgroundColor = .clear + + languageTableView.register(LanguageCell.self, forCellReuseIdentifier: "\(NEBaseLanguageCell.self)") + } + + /* + // 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/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift index 25a81491..c8201851 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift @@ -9,8 +9,8 @@ import UIKit @objcMembers open class SelectUserViewController: NEBaseSelectUserViewController { - override init(sessionId: String, showSelf: Bool = true) { - super.init(sessionId: sessionId, showSelf: showSelf) + override init(conversationId: String, showSelf: Bool = true, showTeamMembers: Bool = false) { + super.init(conversationId: conversationId, showSelf: showSelf, showTeamMembers: showTeamMembers) className = "SelectUserViewController" } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/TeamChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/TeamChatViewController.swift index 5716ccca..24fc900a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/TeamChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/TeamChatViewController.swift @@ -35,7 +35,7 @@ open class TeamChatViewController: NormalChatViewController, TeamChatViewModelDe deinit { NotificationCenter.default.removeObserver(self) - ChatTeamCache.shared.removeAllTeamInfo() + NETeamUserManager.shared.removeAllTeamInfo() } override open func viewWillAppear(_ animated: Bool) { @@ -76,7 +76,7 @@ open class TeamChatViewController: NormalChatViewController, TeamChatViewModelDe if let vm = self?.viewModel as? TeamChatViewModel { vm.getTeamInfo(teamId: sessionId) { error, team in if let team = team { - if IMKitConfigCenter.shared.dismissTeamDeleteConversation == true, team.isValidTeam == false { + if IMKitConfigCenter.shared.enabledismissTeamDeleteConversation == true, team.isValidTeam == false { self?.showSingleAlert(message: coreLoader.localizable("team_not_exist")) { NotificationCenter.default.post(name: NENotificationName.deleteConversationNotificationName, object: V2NIMConversationIdUtil.teamConversationId(team.teamId)) self?.popGroupChatVC() @@ -124,7 +124,7 @@ open class TeamChatViewController: NormalChatViewController, TeamChatViewModelDe open func updateTeamTitle(_ noti: Notification) { if let tid = noti.userInfo?["teamId"] as? String, tid == viewModel.sessionId, - let team = ChatTeamCache.shared.getTeamInfo() { + let team = NETeamUserManager.shared.getTeamInfo() { updateTeamInfo(team: team) } } @@ -133,7 +133,6 @@ open class TeamChatViewController: NormalChatViewController, TeamChatViewModelDe /// - Parameter team: 群聊信息 open func updateTeamInfo(team: V2NIMTeam) { title = team.name - ChatTeamCache.shared.updateTeamInfo(team) setMute(team: team) } @@ -228,7 +227,7 @@ open class TeamChatViewController: NormalChatViewController, TeamChatViewModelDe /// 群成员更新回调 /// - Parameter teamMembers: 群成员列表 public func onTeamMemberUpdate(_ teamMembers: [V2NIMTeamMember]) { - if let team = ChatTeamCache.shared.getTeamInfo() { + if let team = NETeamUserManager.shared.getTeamInfo() { setMute(team: team) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift index 77590cbd..687426e0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift @@ -12,8 +12,7 @@ import UIKit open class UserSettingViewController: NEBaseUserSettingViewController { override public init(userId: String) { super.init(userId: userId) - navigationView.backgroundColor = .white - navigationController?.navigationBar.backgroundColor = .white + cellClassDic = [ UserSettingType.SwitchType.rawValue: UserSettingSwitchCell.self, UserSettingType.SelectType.rawValue: UserSettingSelectCell.self, @@ -26,7 +25,9 @@ open class UserSettingViewController: NEBaseUserSettingViewController { override func setupUI() { super.setupUI() - userHeaderView.layer.cornerRadius = IMKitConfigCenter.shared.teamEnable ? 21.0 : 30.0 + navigationView.backgroundColor = .white + navigationController?.navigationBar.backgroundColor = .white + userHeaderView.layer.cornerRadius = IMKitConfigCenter.shared.enableTeam ? 21.0 : 30.0 } override func getPinMessageViewController(conversationId: String) -> NEBasePinMessageViewController { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Protocol/ChatInputViewDelegate.swift b/NEChatUIKit/NEChatUIKit/Classes/Protocol/ChatInputViewDelegate.swift index c4a72527..1c88fa05 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Protocol/ChatInputViewDelegate.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Protocol/ChatInputViewDelegate.swift @@ -21,4 +21,5 @@ public protocol ChatInputViewDelegate: NSObjectProtocol { func textFieldDidEndEditing(_ text: String?) func textFieldDidBeginEditing(_ text: String?) func titleTextDidClearEmpty() + func textViewDidChange() } diff --git a/NEContactUIKit/NEContactUIKit.podspec b/NEContactUIKit/NEContactUIKit.podspec index b6450f25..dbb4b5eb 100644 --- a/NEContactUIKit/NEContactUIKit.podspec +++ b/NEContactUIKit/NEContactUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEContactUIKit' - s.version = '10.2.1' + s.version = '10.3.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. @@ -27,7 +27,7 @@ Pod::Spec.new do |s| 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64', 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' } - s.ios.deployment_target = '11.0' + s.ios.deployment_target = '12.0' s.swift_version = '5.0' s.source_files = 'NEContactUIKit/Classes/**/*' diff --git a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funAIUser.imageset/Contents.json b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funAIUser.imageset/Contents.json new file mode 100644 index 00000000..a702b8f6 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funAIUser.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "ai_robot@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ai_robot@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funAIUser.imageset/ai_robot@2x.png b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funAIUser.imageset/ai_robot@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..093e6a6396387b5a5bd3694e2db454fc33d0a5f4 GIT binary patch literal 1071 zcmV+~1kn45P)Px#1am@3R0s$N2z&@+hyVZs*-1n}RCt{2ojpifQ547j_a$mYiIuvTMI62rs zoeC|)G3wBv;F!H1bm|9~LUpXxJ!7F`Y`Q75;G`Yep_5B&phE&}UkJfyOl}8_1Y3xY zbMCvjcz;uZmy>gU9`8Hv=JFIcM|Po@7?80gKu!W80l+&y=8_QrG7wVakDS)qY(BZ) z{X^0HW@WClDg_=#nx0_@iM8h+Zaq1B-+8ezU-~40yIQg`3;}$8zH#dw064&Dy%$fkdo8B32-gxT?UeAH$soE^Z_T;QDb~ z8Hx6H)u^}8sJF4VbAT^}8mg6+lFt##^Cih0G?no%ogU>5OJcWB!&WX}kIP;bi?98@ zlLr9P=}|1c_SxgI3QT1@j7y%$ zfkdo8BH9IZ@68Qe;2t&Xc-3gnU^a6VLn97cKZf+fYwY7y6T0=TR*ug)_Mmc1PGEdy z7*F5bR5rdB)cJ(2z|CBMM!k({!cl&I)Sf#qV8E0l6LchT7dZojW`g;LI!LX9mJrBV`vMINhF7eRfBA1*SB} z0}$ES9pwT=?;*;FqPGH#m3leTZz395^EiKz<+k!vkS$v9;Esh@zB186{D~{C$}Z^J(Ut-+TA|&bjxUd*6NMM>N6d!J&ds5C{av-qbb) zZ1Bm3`~;k;PD&$yVI!F8X@P2ngjWG_ilBuxhX5-K;*F%D(yE=5^ypJepHf!n`!(y*@AT&Sq{I{Wjly-PV5}78>yQA-5rC|iV z%qbc)N>_NY$kxZ+EhJS>f10<^@MrMzgE|3oOSuWjbm?ta`}GHP zjie8&drtgcp>q75uU_bnwPT(^ZSOLT`)UZ6+2(UZw_!5RcwcSmptLCr79K4o88+P+ z*NlNpDa)>P^#L5@)Vup;R9pxRO-5qKN+s zijL?=k*P2+VziiK6&&I9_*AhwaoAs-b!>(> zWf_X*7#EoIIR;+Bqmn8#?x~}qMRM{(?g07Whb;cN3w8qi9hUuk z`sWXs{S>D!;C46bg)+MxsSH$)pTbD98R5QEMuVi5P$6)khxfouuZ<`(cjDV|-&u#` zmsE6$*Ns92tWatRPpW{~P1*O)jJ+qh?2WuZX6Tj)R{iHr*x zSI!exGlqO_qh4-lc&b-N?chvZRj$&OcX1*f=MJyi-m}pO5~{NwOye8)8qa}1j@cb* zU5TEGy>>RD_>E*m=8i%o(ja4lFMD3=3cb2ZmTe=0#a(7a9y|yS6 zv)P2QIq^Aup5D1OaHG6#tv-2@%f&m60|C_XFCh1#%-92O!lXwnQt{m^A$6M&rjB#- zNs7GEcRu#b7U#_Hd z`cL@o{M?V#AcIt&VVF;EXhUm4`6t;i`n<=N+>$ezK+^}6Zp$u~3cig{Z=T6ZzIOf; zHm(Vndk5Tz@m9&KmQ3y$m1eqqiw54;uVQ~draH?7bmedPlzd$+Fy&T-ANF9j7Lc% zKm>_+FAUXM_1$x_(*CGR5M}5$8i$OEJUa6!@MV^8?FSDm$|H4Wm9yi0J20m1L||@` zrDnZqG|PPETzQaBBZIB%o$R7xsHGT*7#Hl%u7z>6iTrg} z8!lVSzU-N$zr;@1EH~!s5`}8NNP9eSny(Z=2C z@}R0m=J6{pV2o%q8KXiO_9bXe55rU4f^t zyVa|UuAIk-fa9m8o;rV2W!viXku&Ut#0~<�#44wBxihGV44LKME)ay?8G$FI|Iq z)rH{Ld6}D~KNX-}@Sy1aV8Dzuxv7YA+#xpf)s{q2a1)KN+#7nu`RKz`PW_cxRH zA>k!4MGON-=l literal 0 HcmV?d00001 diff --git a/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/search_icon.imageset/Contents.json b/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/search_icon.imageset/Contents.json new file mode 100644 index 00000000..869676f0 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/search_icon.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "search_icon@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "search_icon@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/search_icon.imageset/search_icon@2x.png b/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/search_icon.imageset/search_icon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e32373148a33f8831561cbfdf44e8a53cf92db39 GIT binary patch literal 791 zcmV+y1L*vTP)~yBcalt$qgEA;3EV9 z5_aPqW;YH>$%og8O8cWvvb?rt*KeL@XCJ^lZW)*-*!P2yQ?x21t&)0{D8up3@vzzS zMzAJ~2%MgsHxO|j2r5ff84yvY{<3xgIkA$!>8JBD3h)83tey#gGsPZ~{63kDPxxJh z7txAvz%aUsyymVK!lne&$HBtBUYuZ%P||+$>cwDTtWpTD#Y2~`Fp8t9DT##y{O*No z+jKekI34fp)D9s7e!%sSI7htVejXBH(M!iBojC!uO+K#B^t{pD&hrKjdwjW$K)`nB%-FS6)xBhpTAVH;^zn;!GRtSyoEKOlmJP`YHV3nD=YpQ0rl_G ziVs=N9;}JD5FhCQL0R<Tm+5+tp18 zPSI0`k$q)vvx>fojef+{RV2(U8L=h9c}u2$6+59>!=k-`bS^cy3=+9aslx0LD+*#% z_ZIJSt)8TQ7Kz*h^1y_|jS?^+A>f9w>pcrmU=`HX6)XN}7-T^EUAIL4O4OSToL?7!r@q9W zjQInU!r6LyZvZ)AEa3I~PvuKoJoEvo)Jbv1>kb3fKl9FZYtJeUWQewa*5Qd=C}0Z^ z%3uHqIml9omVkEWOC6+aURZI2KK=QQ5+YMU!HOIMv${o1d#o!5aJaMiM(`YaL8l1D{v&x-aSb(_%j89&zI1?^e zfu0#0c1~=R*n_`!WM}j7-90d5rT}F&Ov-2g60*DX_+AqXX>~fqf-JP?@lp7hFa6uk zH}3brfT;q418d=ru)XNylf3=#xksay&zp|>%e`5vO*h3f__QzAEodZ~$5~BY;R?3` z>M)P6t!vgc7%)*li<}d!cr|P@?(RT_sDn?YQ;YfsR&TXBv;4^gCJI;=yq@U%K=;7T zLtyv86&z43Tq#sRlZgT(;G_7hvxTm@Y4|YQj~W6Vf+nc~RG1JsD}+HFs~<8Hen0Rj zqp`}mW|S&`aZ!p}_!V+Qodt&Igl+4xWrGIk0wl((fXsp1Vc!#t5@?VvfH6CKMnvtq zSr@wPjK|>ckCQ3@6E;b^p?x>&LStNyo;lOnjr1MB$U~|QZ$RE4+To**{^9~WNW}Gk zWylc~Z_yS+2UjWmeWg+4`QR*d%1J34#Sz0+&fQnNj4x^Y|-4=m}!;TGxgp-GJ(@_gV zxyEfUWTq#6PNfe@#N%*UiLeU2_EEPAnkbvMEc^zEq@E$v7KwJ}E~~WPcbnwY8e_li ztDD37}y!)FCtRQujUri3NR$)usMCvfZ#X{BS4o5hp zEp>btg&%C>1TVgeIAFr7sFiPj=-!N*uYZ zYNA2MKE?Q#VBmr0%ql%9OhsfOAS+BnER_I-u85@*pwJPqlmZmvA}BAZrTK}a&56}j xn~VS$rdKPLI;jst(8?Px#1am@3R0s$N2z&@+hyVZv?MXyIRCt{2oK0*KM-<2ZGq#&R1a<^<)JyHXz$GHi zA>}52oG1X2oU+w$OiDJ=UsCT2L_)IPmBlcpWQ>3W@LmL~r~prZlSGEHxkyY> z?eMb_brUEWl*ctuhU;|=03aX<&a!jixb&bt_H9Iu7Et;<0Kc8K4WKg4?q(_O*cnx} z2^0;=wIS7N;sAgLoGwN~^0=9k3=1?9lqbRIeba6=7ankWeu*>0C?a5_Lfm%4{)P}JBS~;nzK-%4^RdbFJ)C^($)$VRpbC4@H0c=-s0KlV7 zS1M9bAZ>ybu-&ER!+&E`j#T=y>=3&Vl80#xW!sJl`KW9JQBH-X=PgMtoqWIt0%>!E zgD|v8@kfInP9lqjSR^EST%5{i@5EdUB}3Rq;a$yH_^*Ru5=2ZKx7UfGi*EEtF1Wo; zban|X7}R8En_*|0;l=Vhti-pFPVI@9%Psf;5A+|3A}D%peG>xFb9SN|r-MhVo^A_I z<6C$3KAW~cr}I<*I1Zzs-9bX&H`DamU=fn0Lt;KNpoxxLQ(uzGmwm1qtS zVnfd0CoOVL2amw*b=EkeJOpGo5|aIZCXkC!%h5BSiL*4H#)h1+YQ{FwsXaVWRTa_+p|P z9bFU;Z@t3HwOw0}uY@JPvj|X_eC|3qco-)K51Toqh%c`k%g?ZbSB}|!d?l>-O$hCy zxmQpcM`73-$q{Ccw-ZjdpK9;te55SIPjPUc+>PM*C{T4i3RDA6eHUxm0E*npk`1Hq zVRpyBueTMU3dn90S3lLeAKUzBc}1`JJe#B!vS)ul6s^rvU(`&U7{WS*aX7k}JQGxDwwI zvc)M|rCL4O@~u9d+QU=Lk*{aGLaTV9xV=t%b@fvKx$=+T`nVd7Nl7OFaPXu^%fRE= zP562{&?9|cR3;<66T4j|mJ_jn+ zQ!zXBPrh(CbkU6=SYI#~!G^+N4o(t)CQ!zKgo}&Yd_22}mG~A0f=6&-@DMt?Xx&TC z0I(tdhYdM{rTMgNWv2{fb3j35b>^J1Sntl9R)K&dzFYWw1SqVmGCsF|E;SdNW%F8; zzPuo#b=nK+g&JP)T#j!hY_AHzZHIX zEgx!=nQ z6pl$r#t+U=BOG@swQg3e${&!SR(5FVRDBM}5H^AaFX}fKHB%#|Lq}vcM+4bBOt(gW}G4qF; zwco-mn)PoB2XZ+nLs&l`w%Ffbl#GG?)N%;z-Enht{Wd2s++w{SFwUm0|0YGOo-e$) ziPoLijP08W2s-B8+HlG44VF9(hDl23w`jaj3K*Nt zIFPWiiAg2uxPx#1am@3R0s$N2z&@+hyVZzj7da6RCt{2oiS_^$r{K1-#D?8vrv5E&Y)KiyGfOU zyaG}o1@vA=E{jA#aYYa(nkPY0LO8Ke$X!W-MDuir6ciLE;Q-|mSfUD7V7OFic1?l6 z@1eIA@{z>x%oSccYsZe)_Rg;D>`#j1bvEfSk`R>SjK48OG#dYzH*}m}s-i{u0R@7l08%`tflncn6>Zu> zk?LU(C{>`sasfzof_R<^rYc&jADDn(w*bVvtcwi{F8FeRVaZG3JP}M)v{-*pBn8g* zNwV=&I9CNz6)hIz?3y6VxM^1wB(Sv2_`I9Kxh7b#kLn87*DQu0K7TV2Uvk5kbio!C zZ481vM(+t#0fCYQD$M_sh$m&*VKNI}P&5^Sy+)6V1poqKf(iCwLD5v1cBHg0GpKkB zU`$5&oeqJL+f=wDV-&^l7PhEp)BS?IzyTEtD7LnK!Co&Yn&Z!ujtXYFzy~-0F3<)M zCfH-AU8G|!aY4~kR*V9Dsvv=-8&8I39Vx5b0#rqd^ozh9KukMOpB{!*@>G~@_nECN z!HQxX?Eb1rQhDNM+7Yc~!F;h=rY+H04myf%&=;!(#>{)y5-kb#^Y>}L&uR-X-D>pM zv|tO0ruNFQ+?QiqA9F6g5l#7#4pVy;w!dKw|k;F-1HjV_x`HW(aj)4 zw{x;>mlza*2k^y`L22C87PbUiP&8FQOtf4jpGJ(??avyz#05p0V8!PxAIap2#~ZoE zhAk{00{-}*OZ3}l82bu#ThV5GSLiCSMMaz5?}WN8(Hwfx;0GFRvv1T`w}k~cy9Hk? zFNg&>yH@X)HCve3y8bV(PCkvO7l8EDIyt~G<3%Q7LAS5H6V z<)f@@;fy@-zgM@iJ^#)Dju}W4Yh-*7m+u_yTCo7&)Z`)j>)(IFvC)uBGm(B^qWa5< z7ACuOoxV7P(-(&-zS~&)6B}z^@KM`_ktw+1Zkiz!W*7()M#jIw$oL?JM+Yi?|MF25 zFCS%Pnb+r5hvU{SDg=``8b2~Vhzr-hEgxqyJ9zmhi?`2nGR#Ti*k}loR}W(-y4(H# z_RD{;M1;!fcPL zcMeiiMJsnvWx;}+O+iB66$&$)nmklKetP#)SBq6*E5|GPuH)vTi46LS^XLFrT_Vx6 z3}e=4FulHsck5d+%y`1v=Q(Vw{aHR9mK8Nx+#&qYcJuu+Zmnq&$lNGL!#=%9&q3ml5};pF5YscxZUl0~;z696JcN5!-nU)BYL3MFapC&6<%>UkaR~QkKa`(^+%Crs z4&Rf=qN0tlfJ6E5N6g?tRtx*b<*zHs9tbmBxc-f2<=DaLd%}STW1I~;QkrC{2VH^P z8{$PdUcfjz!N{N9+BFamV}eA?>x%f;Ly(9ufMOok!^b89h%oq>QC$OpVx5)o;?oen zg>@IcOYAZtzDw*f76yC6)$HYuykh`0bbfVbk&| zBb(X5f9^M*{_j*dc93yT`UHe-*;S!s9^TC0_}Q?u)U|A82luYLFXxDCtbJ)|gx#rf z>>%Nu7=7fBODrXZ%493Y@%z)y9aBytoWCbRXCz2>u*5xB-H*(dgusiU8`F z3o~*`8kJ+x#i_LPJnC9gX4C7N*jW35jkQ0qnG+qC9UKZXM4|y4i3c$;2lu8=39(Z8SZ-lntbP*;GaNq~#_(te(Ri>U7ghlPBeR2S zW(R*f&mq0OC2g>{p81kH1gcgMaLX z_QxmyN$VGTI}GCUZq>$Tf4Jbw)mwh-5iDO+v@7yFdK{y`uxBIGJ%X95he;i(@@Y)1 z)&9sPcH3H%_n`Rn;3!@k{snxoGI!nl^iMURp_8-XLq-CX?PGKO- zJgXOp`n#~#eM;SCd>Jl9jK4qqEdBL*E4Y!UQv+k0)erpkT`1Pv-TZ;`b@Hi)AU?a_ zuImj~_FbGxYw)Mlx?O%sI?{tEpkYmNx8IP&Ablx0>*25fWIs@nm?m>A!2$;NsM~2mW5p ztwJkvtH*5jTUJ_5pvY5U7J#&j;T|Ss-n*7)YY!tcapwj6aNjN|b2GP=-#50EqNFC` zMxOW?fL7Wm=U_l9?TQ7o*}|;+{C)b4zvX>x%xF+`qAbUf0B<}Qp7lfiy*Mcr;9v_Y zVeXLR@2U>yAU<#1naByGY@DbnS}Z664-W8_!l^v*^UaK4*hT72mk03y+i4eAjr6Ta z7ZhzGAOg1zKx@-M{Jc2@SsE_vC0pdSqRo)#$-SYa^i}n6O)#MNpdIAwR Void) { + AIRepo.shared.getAIUserList { [weak self] users, error in + users?.forEach { aiUser in + let model = NEAIUserModel() + model.aiUser = aiUser + self?.datas.append(model) + } + completion(error) + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/AIRobot/NEBaseAIUserController.swift b/NEContactUIKit/NEContactUIKit/Classes/AIRobot/NEBaseAIUserController.swift new file mode 100644 index 00000000..6140ac91 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/AIRobot/NEBaseAIUserController.swift @@ -0,0 +1,226 @@ +//// 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 NECommonUIKit +import NECoreKit +import UIKit + +@objcMembers +open class NEBaseAIUserController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource { + let viewModel = AIUserViewModel() + + /// 输入框 + public lazy var searchAIUserTextField: UITextField = { + let field = UITextField() + field.translatesAutoresizingMaskIntoConstraints = false + field.placeholder = commonLocalizable("search") + field.clearButtonMode = .always + field.textColor = .ne_greyText + field.font = UIFont.systemFont(ofSize: 14.0) + field.backgroundColor = UIColor.ne_backcolor + if let clearButton = field.value(forKey: "_clearButton") as? UIButton { + clearButton.accessibilityIdentifier = "id.clear" + } + field.accessibilityIdentifier = "id.search" + return field + }() + + /// AI 机器人列表 + public lazy var aiUserTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + tableView.dataSource = self + tableView.delegate = self + tableView + .tableFooterView = + UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + + /// 无数字人空占位图 + public lazy var aiUserEmptyView: NEEmptyDataView = { + let view = NEEmptyDataView(imageName: "user_empty", content: localizable("no_ai_user"), frame: .zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + return view + }() + + public var backViewTopAnchor: NSLayoutConstraint? + lazy var backView: UIView = { + let view = UIView() + view.backgroundColor = .clear + view.translatesAutoresizingMaskIntoConstraints = false + view.clipsToBounds = true + view.layer.cornerRadius = 4.0 + return view + }() + + /// 搜索背景图 + public lazy var searchIconImageView: UIImageView = { + let searchIconImageView = UIImageView() + searchIconImageView.image = coreLoader.loadImage("search_icon") + searchIconImageView.translatesAutoresizingMaskIntoConstraints = false + return searchIconImageView + }() + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + backViewTopAnchor?.constant = 8.0 + topConstant + } + + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + setupAIUserListUI() + addTextFiledObserver() + navigationView.moreButton.isHidden = true + + viewModel.getAIUsers { [weak self] error in + self?.refreshTableView() + } + } + + /// UI 初始化 + open func setupAIUserListUI() { + title = localizable("my_ai_user") + + view.addSubview(backView) + backViewTopAnchor = backView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0 + topConstant) + backViewTopAnchor?.isActive = true + + NSLayoutConstraint.activate([ + backView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + backView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + backView.heightAnchor.constraint(equalToConstant: 0), + ]) + + backView.addSubview(searchIconImageView) + NSLayoutConstraint.activate([ + searchIconImageView.centerYAnchor.constraint(equalTo: backView.centerYAnchor), + searchIconImageView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16.0), + ]) + + backView.addSubview(searchAIUserTextField) + NSLayoutConstraint.activate([ + searchAIUserTextField.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 36.0), + searchAIUserTextField.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -16.0), + searchAIUserTextField.topAnchor.constraint(equalTo: backView.topAnchor), + searchAIUserTextField.bottomAnchor.constraint(equalTo: backView.bottomAnchor), + ]) + + view.addSubview(aiUserTableView) + NSLayoutConstraint.activate([ + aiUserTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + aiUserTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + aiUserTableView.topAnchor.constraint(equalTo: backView.bottomAnchor, constant: 0), + aiUserTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + aiUserTableView.register(NEBaseAIUserListCell.self, forCellReuseIdentifier: "\(NEBaseAIUserListCell.self)") + + view.addSubview(aiUserEmptyView) + NSLayoutConstraint.activate([ + aiUserEmptyView.leftAnchor.constraint(equalTo: aiUserTableView.leftAnchor), + aiUserEmptyView.rightAnchor.constraint(equalTo: aiUserTableView.rightAnchor), + aiUserEmptyView.topAnchor.constraint(equalTo: aiUserTableView.topAnchor, constant: 50), + aiUserEmptyView.bottomAnchor.constraint(equalTo: aiUserTableView.bottomAnchor), + ]) + } + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if let text = searchAIUserTextField.text, text.count > 0 { + return viewModel.searchDatas.count + } + return viewModel.datas.count + } + + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + UITableViewCell() + } + + open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 0 + } + + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let info = viewModel.datas[indexPath.row] + Router.shared.use( + ContactUserInfoPageRouter, + parameters: ["nav": navigationController as Any, "nim_user": info.aiUser as Any], + closure: nil + ) + } + + func isLastAIUser(_ index: Int) -> Bool { + if let text = searchAIUserTextField.text, text.count > 0 { + if viewModel.searchDatas.count - 1 == index { + return true + } + } + if viewModel.datas.count - 1 == index { + return true + } + return false + } + + /// 判断该当前是搜索列表还是内容列表 + /// - Parameter index: 列表索引 + func getRealAIUserModel(_ index: Int) -> NEAIUserModel? { + if let text = searchAIUserTextField.text, text.count > 0 { + return viewModel.searchDatas[index] + } + return viewModel.datas[index] + } + + /// 添加输入框变更监听 + func addTextFiledObserver() { + NotificationCenter.default.addObserver( + self, + selector: #selector(textChange), + name: UITextField.textDidChangeNotification, + object: nil + ) + } + + /// 输入变更 + func textChange() { + viewModel.searchDatas.removeAll() + if let text = searchAIUserTextField.text, text.count > 0 { + for model in viewModel.datas { + if let uid = model.aiUser?.accountId, uid.contains(text) { + viewModel.searchDatas.append(model) + } else if let nick = model.aiUser?.name, nick.contains(text) { + viewModel.searchDatas.append(model) + } + } + } else { + aiUserEmptyView.isHidden = true + } + refreshTableView() + } + + /// 刷新数据列表 + open func refreshTableView() { + aiUserTableView.reloadData() + if viewModel.datas.count <= 0 { + aiUserEmptyView.isHidden = false + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/AIRobot/NEBaseAIUserListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/AIRobot/NEBaseAIUserListCell.swift new file mode 100644 index 00000000..e100cbf3 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/AIRobot/NEBaseAIUserListCell.swift @@ -0,0 +1,79 @@ +//// 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 NECommonUIKit +import UIKit + +@objcMembers +open class NEBaseAIUserListCell: UITableViewCell { + var currentModel: NEAIUserModel? + + /// 数字人头像 + public lazy var aiUserHeaderView: NEUserHeaderView = { + let header = NEUserHeaderView(frame: .zero) + header.titleLabel.font = NEConstant.defaultTextFont(14) + header.titleLabel.textColor = UIColor.white + header.layer.cornerRadius = 21 + header.clipsToBounds = true + header.translatesAutoresizingMaskIntoConstraints = false + return header + }() + + /// 数字人名称 + public lazy var aiUserNameLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = NEConstant.defaultTextFont(16.0) + label.textColor = .ne_darkText + label.accessibilityIdentifier = "id.userName" + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + setupAIUserListCellUI() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// UI初始化 + open func setupAIUserListCellUI() { + contentView.addSubview(aiUserHeaderView) + NSLayoutConstraint.activate([ + aiUserHeaderView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 21), + aiUserHeaderView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + aiUserHeaderView.widthAnchor.constraint(equalToConstant: 42), + aiUserHeaderView.heightAnchor.constraint(equalToConstant: 42), + ]) + + contentView.addSubview(aiUserNameLabel) + NSLayoutConstraint.activate([ + aiUserNameLabel.leftAnchor.constraint(equalTo: aiUserHeaderView.rightAnchor, constant: 14.0), + aiUserNameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + aiUserNameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -116), + ]) + } + + /// 数据源与UI绑定 + func configure(_ model: NEAIUserModel) { + currentModel = model + if let url = model.aiUser?.avatar, !url.isEmpty { + aiUserHeaderView.sd_setImage(with: URL(string: url), completed: nil) + aiUserHeaderView.setTitle("") + } else { + aiUserHeaderView.image = nil + aiUserHeaderView.setTitle(model.aiUser?.name ?? "") + aiUserHeaderView.backgroundColor = UIColor.colorWithString(string: model.aiUser?.accountId) + } + if let name = model.aiUser?.name { + aiUserNameLabel.text = name + } else { + aiUserNameLabel.text = model.aiUser?.accountId + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Base/NEKitContactUI.h b/NEContactUIKit/NEContactUIKit/Classes/Base/NEContactLoader.h similarity index 81% rename from NEContactUIKit/NEContactUIKit/Classes/Base/NEKitContactUI.h rename to NEContactUIKit/NEContactUIKit/Classes/Base/NEContactLoader.h index 7a1b45c2..abbc4065 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Base/NEKitContactUI.h +++ b/NEContactUIKit/NEContactUIKit/Classes/Base/NEContactLoader.h @@ -5,6 +5,9 @@ #import +#ifndef NEContactLoader_h +#define NEContactLoader_h + //! Project version number for contactkit_ui. FOUNDATION_EXPORT double contactkit_uiVersionNumber; @@ -13,3 +16,9 @@ FOUNDATION_EXPORT const unsigned char contactkit_uiVersionString[]; // In this header, you should import all the public headers of your framework // using statements like #import + +@interface NEContactLoader : NSObject + +@end + +#endif /* NEContactLoader */ diff --git a/NEContactUIKit/NEContactUIKit/Classes/Base/NEContactLoader.m b/NEContactUIKit/NEContactUIKit/Classes/Base/NEContactLoader.m new file mode 100644 index 00000000..213580bb --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Base/NEContactLoader.m @@ -0,0 +1,31 @@ +//// 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 "NEContactLoader.h" +#import + +#if __has_include() +#import +#else +#import "NEContactUIKit-Swift.h" +#endif + +@implementation NEContactLoader + +static id gShareInstance = nil; + ++ (instancetype)shareInstance { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + gShareInstance = [[self alloc] init]; + }); + return gShareInstance; +} + ++ (void)load { + NSLog(@"NEContactUIKit load"); + [NEContactLoaderService.shared setupInit]; +} + +@end diff --git a/NEContactUIKit/NEContactUIKit/Classes/Base/NEContactLoaderService.swift b/NEContactUIKit/NEContactUIKit/Classes/Base/NEContactLoaderService.swift new file mode 100644 index 00000000..0595064d --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Base/NEContactLoaderService.swift @@ -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 Foundation +import NEChatKit + +@objcMembers +public class NEContactLoaderService: NSObject { + public static let shared = NEContactLoaderService() + + override private init() { + super.init() + } + + /// 初始化方法 + /// 此方法会在模块被加载时调用 + public func setupInit() { + ChatKitClient.shared.registerInit(NEContactService.shared) + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Base/NEContactService.swift b/NEContactUIKit/NEContactUIKit/Classes/Base/NEContactService.swift new file mode 100644 index 00000000..9d12f0e8 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Base/NEContactService.swift @@ -0,0 +1,31 @@ +//// 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 + +@objcMembers +public class NEContactService: NSObject, ChatServiceDelegate { + public static let shared = NEContactService() + + override private init() { + super.init() + } + + /// 注册 NEContactUIKit 初始化协议 + /// - Parameter params: 初始化参数 + public func setupInit(_ params: [String: Any]?) { + registerRouter(params) + } + + /// 注册路由 + /// - Parameter param: 参数 + public func registerRouter(_ param: [String: Any]?) { + if let isFun = param?["isFun"] as? Bool, isFun { + ContactRouter.registerFun() + } else { + ContactRouter.register() + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift index 70c3307f..45db5b4a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift @@ -9,15 +9,15 @@ import NECoreKit import UIKit @objcMembers -open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, - BlackListCellDelegate, UIGestureRecognizerDelegate, BlackListViewModelDelegate { - public let navigationView = NENavigationView() - var tableView = UITableView(frame: .zero, style: .plain) +open class NEBaseBlackListViewController: NEContactBaseViewController, UITableViewDelegate, UITableViewDataSource, + BlackListCellDelegate, BlackListViewModelDelegate { var viewModel = BlackListViewModel() + public var tableViewTopAnchor: NSLayoutConstraint? public lazy var headView: UIView = { let headView = UIView(frame: CGRect(x: 0, y: 0, width: Int(NEConstant.screenWidth), height: 40)) + headView.addSubview(contentLabel) return headView }() @@ -31,10 +31,34 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, return contentLabel }() + 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 = headView + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableViewTopAnchor?.constant = topConstant + } + override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white - navigationController?.interactivePopGestureRecognizer?.delegate = self if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { navigationController?.isNavigationBarHidden = false } else { @@ -46,10 +70,7 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, loadData() } - /// UI 初始化 - func commonUI() { - title = localizable("blacklist") - navigationView.navTitle.text = title + func initNav() { let image = UIImage.ne_imageNamed(name: "backArrow")?.withRenderingMode(.alwaysOriginal) let backItem = UIBarButtonItem( image: image, @@ -65,37 +86,27 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, image: addImage, style: .plain, target: self, - action: #selector(addBlack) + action: #selector(toSetting) ) addItem.accessibilityIdentifier = "id.threePoint" navigationItem.rightBarButtonItem = addItem - navigationView.translatesAutoresizingMaskIntoConstraints = false - navigationView.addBackButtonTarget(target: self, selector: #selector(backEvent)) navigationView.setMoreButtonImage(UIImage.ne_imageNamed(name: "add")) - navigationView.addMoreButtonTarget(target: self, selector: #selector(addBlack)) - view.addSubview(navigationView) - NSLayoutConstraint.activate([ - navigationView.leftAnchor.constraint(equalTo: view.leftAnchor), - navigationView.rightAnchor.constraint(equalTo: view.rightAnchor), - navigationView.topAnchor.constraint(equalTo: view.topAnchor), - navigationView.heightAnchor.constraint(equalToConstant: NEConstant.navigationAndStatusHeight), - ]) + } + + /// UI 初始化 + func commonUI() { + title = localizable("blacklist") + initNav() - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) + tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight) + tableViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight), tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - - headView.addSubview(contentLabel) - tableView.tableHeaderView = headView } func loadData() { @@ -112,15 +123,11 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, UITableViewCell() } - func backEvent() { - navigationController?.popViewController(animated: true) - } - open func getContactSelectVC() -> NEBaseContactSelectedViewController { NEBaseContactSelectedViewController() } - func addBlack() { + override open func toSetting() { let contactSelectVC = getContactSelectVC() navigationController?.pushViewController(contactSelectVC, animated: true) contactSelectVC.callBack = { [weak self] selectMemberarray in diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift index 7304d596..056fca8a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift @@ -15,7 +15,7 @@ public protocol BlackListViewModelDelegate: NSObjectProtocol { } @objcMembers -open class BlackListViewModel: NSObject, NEContactListener { +open class BlackListViewModel: NSObject { var contactRepo = ContactRepo.shared public var blockList = [NEUserWithFriend]() public weak var delegate: BlackListViewModelDelegate? @@ -26,10 +26,21 @@ open class BlackListViewModel: NSObject, NEContactListener { contactRepo.addContactListener(self) } + deinit { + contactRepo.removeContactListener(self) + } + /// 获取黑名单列表 func getBlackList() { NEALog.infoLog(ModuleName + " " + className(), desc: #function) - blockList = NEFriendUserCache.shared.getBlocklist().map(\.value) + if let blockList = NEFriendUserCache.shared.getBlocklist() { + NEFriendUserCache.shared.loadShowName(blockList) { users in + if let users = users { + self.blockList = users + } + self.delegate?.tableViewReload() + } + } } /// 移除黑名单 @@ -40,7 +51,7 @@ open class BlackListViewModel: NSObject, NEContactListener { func removeFromBlackList(account: String, _ completion: @escaping (NSError?) -> Void) { NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", account:\(account)") contactRepo.removeBlockList(accountId: account) { error in - if let err = error as? NSError { + if let err = error { NEALog.errorLog(ModuleName + " " + BlackListViewModel.className(), desc: #function + ", error:\(err)") completion(err) } else { @@ -57,9 +68,9 @@ open class BlackListViewModel: NSObject, NEContactListener { func addBlackList(users: [NEUserWithFriend], _ completion: @escaping (NSError?) -> Void) { NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", users.count:\(users.count)") - for (i, user) in users.enumerated() { + for user in users { contactRepo.addBlockList(accountId: user.user?.accountId ?? "") { error in - if let err = error as? NSError { + if let err = error { NEALog.errorLog(ModuleName + " " + BlackListViewModel.className(), desc: #function + ", error:\(err)") completion(err) } else { @@ -68,70 +79,56 @@ open class BlackListViewModel: NSObject, NEContactListener { } } } +} - // MARK: - NEContactListener - - /// 用户信息变更回调 - /// - 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 - } - } - } - } - - /// 黑名单添加回调 - /// - 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]) - } +// MARK: - NEContactListener - /// 黑名单移除回调 - /// - Parameter accountId: 好友 Id +extension BlackListViewModel: NEContactListener { + /// 黑名单移除回调 (非好友) + /// - 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 } } } - /// 删除好友通知 - /// 本端删除好友,多端同步 - /// - Parameters: - /// - accountId: 删除的好友账号ID - /// - deletionType: 好友删除的类型 - public func onFriendDeleted(_ accountId: String, deletionType: V2NIMFriendDeletionType) { - if NEFriendUserCache.shared.isBlockAccount(accountId) { - onBlockListRemoved(accountId) - } - } + /// 好友信息缓存更新 + /// - Parameter accountId: 用户 id + public func onContactChange(_ changeType: NEContactChangeType, _ contacts: [NEUserWithFriend]) { + for contact in contacts { + // 添加黑名单 + if changeType == .addBlock, + !blockList.contains(where: { $0.user?.accountId == contact.user?.accountId }) { + let index = IndexPath(row: blockList.count, section: 0) + blockList.append(contact) + delegate?.tableViewInsert([index]) + } - /// 好友信息变更回调 - /// - 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 + for (index, friendUser) in blockList.enumerated() { + // 用户信息更新 + if changeType == .update, + friendUser.user?.accountId == contact.user?.accountId { + blockList[index] = contact + delegate?.tableViewReload([IndexPath(row: index, section: 0)]) + } + + // 移除黑名单 + if changeType == .removeBlock, + friendUser.user?.accountId == contact.user?.accountId { + blockList.remove(at: index) + delegate?.tableViewDelete([IndexPath(row: index, section: 0)]) + } + + // 删除好友 + if changeType == .deleteFriend, + friendUser.user?.accountId == contact.user?.accountId { + blockList.remove(at: index) + delegate?.tableViewDelete([IndexPath(row: index, section: 0)]) + } } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift b/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift index f5400a94..2204d81f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift @@ -63,10 +63,10 @@ public class ContactUIConfig: NSObject { @objcMembers public class ContactProperties: NSObject { /// 头像圆角大小 - public var avatarCornerRadius = 4.0 + public var avatarCornerRadius = 0.0 /// 头像类型 - public var avatarType: NEContactAvatarType? + public var avatarType: NEContactAvatarType = .rectangle // 通讯录好友标题大小 public var itemTitleSize: CGFloat = 0 diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunAIUserListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunAIUserListCell.swift new file mode 100644 index 00000000..a32501e0 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunAIUserListCell.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 UIKit + +@objcMembers +open class FunAIUserListCell: NEBaseAIUserListCell { + /// 列表分隔线 + public var dividerLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.funContactDividerLineColor + return view + }() + + /// 通用版UI初始化 + override open func setupAIUserListCellUI() { + contentView.addSubview(aiUserHeaderView) + NSLayoutConstraint.activate([ + aiUserHeaderView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 21), + aiUserHeaderView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + aiUserHeaderView.widthAnchor.constraint(equalToConstant: 40), + aiUserHeaderView.heightAnchor.constraint(equalToConstant: 40), + ]) + aiUserHeaderView.layer.cornerRadius = 4.0 + + contentView.addSubview(aiUserNameLabel) + NSLayoutConstraint.activate([ + aiUserNameLabel.leftAnchor.constraint(equalTo: aiUserHeaderView.rightAnchor, constant: 14.0), + aiUserNameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + aiUserNameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -21), + ]) + + contentView.addSubview(dividerLine) + NSLayoutConstraint.activate([ + dividerLine.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + dividerLine.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: 0), + dividerLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + dividerLine.heightAnchor.constraint(equalToConstant: 1), + ]) + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift index 0ecc0589..9cad7bc0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift @@ -28,10 +28,10 @@ open class FunContactSelectedCell: NEBaseContactSelectedCell { } override open func initSubviewsLayout() { - if NEKitContactConfig.shared.ui.contactProperties.avatarType == .rectangle { - avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius - } else if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { + if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { avatarImageView.layer.cornerRadius = 20.0 + } else if NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius > 0 { + avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius } else { avatarImageView.layer.cornerRadius = 4.0 // Fun UI } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift index f4fc3abe..24c58d33 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift @@ -27,10 +27,10 @@ open class FunContactTableViewCell: NEBaseContactTableViewCell { } override open func initSubviewsLayout() { - if NEKitContactConfig.shared.ui.contactProperties.avatarType == .rectangle { - avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius - } else if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { + if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { avatarImageView.layer.cornerRadius = 20.0 + } else if NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius > 0 { + avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius } else { avatarImageView.layer.cornerRadius = 4.0 // Fun UI } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunFusionContactSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunFusionContactSelectedCell.swift new file mode 100644 index 00000000..e5eeb62a --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunFusionContactSelectedCell.swift @@ -0,0 +1,53 @@ +//// 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 + +@objcMembers +open class FunFusionContactSelectedCell: NEBaseFusionContactSelectedCell { + /// UI 初始化 + override open func setupFusionSelectedCellUI() { + super.setupFusionSelectedCellUI() + + contentView.addSubview(selectedStateImage) + selectedStateImage.highlightedImage = UIImage.ne_imageNamed(name: "fun_select") + NSLayoutConstraint.activate([ + selectedStateImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + selectedStateImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + ]) + + contentView.addSubview(avatarImageView) + NSLayoutConstraint.activate([ + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 50), + ]) + avatarImageView.layer.cornerRadius = 4 + + contentView.addSubview(nameLabel) + NSLayoutConstraint.activate([ + nameLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 12), + nameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35), + nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor), + nameLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + + avatarImageView.addSubview(avatarNameLabel) + NSLayoutConstraint.activate([ + avatarNameLabel.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor, constant: 1), + avatarNameLabel.rightAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: -1), + avatarNameLabel.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor), + avatarNameLabel.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor), + ]) + + contentView.addSubview(bottomLine) + NSLayoutConstraint.activate([ + 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/FunFusionContactUnCheckCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunFusionContactUnCheckCell.swift new file mode 100644 index 00000000..f57a1269 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunFusionContactUnCheckCell.swift @@ -0,0 +1,26 @@ +//// 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 + +@objcMembers +open class FunFusionContactUnCheckCell: FunContactUnCheckCell { + override func configure(_ model: Any) { + if let cellModel = model as? NEFusionContactCellModel { + if cellModel.user != nil { + avatarImageView.configHeadData( + headUrl: cellModel.user?.user?.avatar, + name: cellModel.getShowName(), + uid: cellModel.getAccountId() + ) + } else if cellModel.aiUser != nil { + avatarImageView.configHeadData( + headUrl: cellModel.aiUser?.avatar, + name: cellModel.getShowName(), + uid: cellModel.getAccountId() + ) + } + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift index 2efbb344..318eacd0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift @@ -23,6 +23,20 @@ public extension ContactRouter { nav?.pushViewController(contactSelectVC, animated: true) } + // 携带数字人的成员选择页面 + Router.shared.register(ContactFusionSelectRouter) { param in + let nav = param["nav"] as? UINavigationController + let userFilters = param["filters"] as? Set + let contactSelectedPageController = FunContactSelectedPageController(filterUsers: userFilters) + if let limit = param["limit"] as? Int, limit > 0 { + contactSelectedPageController.limit = limit + } + if let uid = param["uid"] as? String { + contactSelectedPageController.userId = uid + } + nav?.pushViewController(contactSelectedPageController, animated: true) + } + // 转发选择页面 Router.shared.register(ForwardMultiSelectRouter) { param in let nav = param["nav"] as? UINavigationController @@ -79,6 +93,12 @@ public extension ContactRouter { nav.pushViewController(blackVC, animated: true) } } + Router.shared.register(ContactAIUserListRouter) { param in + if let nav = param["nav"] as? UINavigationController { + let blackVC = FunAIUserController() + nav.pushViewController(blackVC, animated: true) + } + } Router.shared.register(ContactTeamListRouter) { param in if let nav = param["nav"] as? UINavigationController { diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactUIColor.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactUIColor.swift index bbe4af35..2d492867 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactUIColor.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactUIColor.swift @@ -11,4 +11,9 @@ public extension UIColor { static let funContactBackgroundColor = UIColor(hexString: "#EDEDED") static let funContactLineBorderColor = UIColor(hexString: "#E5E5E5") static let funContactUserViewChatTitleTextColor = UIColor(hexString: "#525C8C") + static let funContactDividerLineColor = UIColor(hexString: "#E4E9F2") + + static let funContactGreenDividerLineColor = UIColor(hexString: "#58BE6B") + + static let funContactNormalTextColor = UIColor(hexString: "#333333") } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunAIUserController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunAIUserController.swift new file mode 100644 index 00000000..5e9d4fd0 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunAIUserController.swift @@ -0,0 +1,75 @@ +//// 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 + +@objcMembers +open class FunAIUserController: NEBaseAIUserController { + public var searchGrayBackViewTopAnchor: NSLayoutConstraint? + + let searchGrayBackView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.funContactBackgroundColor + return view + }() + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + searchGrayBackViewTopAnchor?.constant = topConstant + } + + override open func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .funContactBackgroundColor + aiUserTableView.register(FunAIUserListCell.self, forCellReuseIdentifier: "\(FunAIUserListCell.self)") + view.insertSubview(searchGrayBackView, belowSubview: backView) + searchGrayBackViewTopAnchor = searchGrayBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + searchGrayBackViewTopAnchor?.isActive = true + 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: aiUserTableView.topAnchor), + ]) + backView.backgroundColor = UIColor.white + searchAIUserTextField.backgroundColor = UIColor.white + + aiUserEmptyView.setEmptyImage(name: "fun_user_empty") + } + + override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if let cell = tableView.dequeueReusableCell( + withIdentifier: "\(FunAIUserListCell.self)", + for: indexPath + ) as? FunAIUserListCell { + if let model = getRealAIUserModel(indexPath.row) { + cell.configure(model) + + if isLastAIUser(indexPath.row) { + cell.dividerLine.isHidden = true + } else { + cell.dividerLine.isHidden = false + } + + return cell + } + } + return UITableViewCell() + } + + override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 64.0 + } + + /* + // 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/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactRemakNameViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactAliasViewController.swift similarity index 79% rename from NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactRemakNameViewController.swift rename to NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactAliasViewController.swift index b6f7f1eb..c66801fa 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactRemakNameViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactAliasViewController.swift @@ -7,7 +7,7 @@ import NECoreKit import UIKit @objcMembers -open class FunContactRemakNameViewController: NEBaseContactRemakNameViewController { +open class FunContactAliasViewController: NEBaseContactAliasViewController { override func setupUI() { super.setupUI() let clearItem = UIBarButtonItem( @@ -21,10 +21,11 @@ open class FunContactRemakNameViewController: NEBaseContactRemakNameViewControll navigationView.moreButton.setTitleColor(.funContactThemeColor, for: .normal) + aliasInputTopAnchor = aliasInput.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + aliasInputTopAnchor?.isActive = true NSLayoutConstraint.activate([ aliasInput.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), aliasInput.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), - aliasInput.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), aliasInput.heightAnchor.constraint(equalToConstant: 60), ]) } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactSelectedPageController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactSelectedPageController.swift new file mode 100644 index 00000000..c44a5d4f --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactSelectedPageController.swift @@ -0,0 +1,78 @@ +//// 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 + +@objcMembers +open class FunContactSelectedPageController: NEBaseContactSelectedPageController { + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + pagingViewControllerTopAnchor?.constant = topConstant + } + + override open func getContentControllers(_ filterUsers: Set? = nil) -> [NEBaseFusionContactSelectedController]? { + if childrenControllers.count == 0 { + let userSelectController = FunFusionContactSelectedController(filterIds: filterUsers, type: .FusionContactTypeUser) + userSelectController.delegate = self + userSelectController.limit = limit + let aiUserSelectController = FunFusionContactSelectedController(filterIds: filterUsers, type: .FusionContactTypeAIUser) + aiUserSelectController.delegate = self + aiUserSelectController.limit = limit + childrenControllers.append(userSelectController) + childrenControllers.append(aiUserSelectController) + } + return childrenControllers + } + + override open func setupPageContent() { + collectionBackViewTopMargin = 12 + super.setupPageContent() + let pagingViewController = NEPagingViewController(viewControllers: contentControllers) + addChild(pagingViewController) + view.addSubview(pagingViewController.view) + pagingViewController.view.backgroundColor = .white + pagingViewControllerTopAnchor = pagingViewController.view.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + pagingViewControllerTopAnchor?.isActive = true + pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + pagingViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor), + pagingViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor), + pagingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + pagingViewController.selectedTextColor = .funContactGreenDividerLineColor + pagingViewController.textColor = .funContactNormalTextColor + pagingViewController.indicatorColor = .funContactGreenDividerLineColor + pagingViewController.indicatorOptions = NEPagingIndicatorOptions.visible( + height: 2, + zIndex: Int.max, + spacing: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0), + insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + ) + pagingViewController.borderOptions = NEPagingBorderOptions.visible(height: 1, zIndex: Int.max, insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)) + pagingViewController.didMove(toParent: self) + selectCollectionView.register(FunFusionContactUnCheckCell.self, forCellWithReuseIdentifier: "\(NSStringFromClass(FunFusionContactUnCheckCell.self))") + } + + override open func setupNavSureItem() { + super.setupNavSureItem() + navigationView.setBackButtonTitle(localizable("close")) + navigationView.backButton.setTitleColor(.ne_darkText, for: .normal) + navigationView.moreButton.backgroundColor = .funContactThemeColor + selectedSureButton.backgroundColor = .funContactThemeColor + view.backgroundColor = .ne_backcolor + navigationView.backgroundColor = .ne_backcolor + selectCollectionBackView.backgroundColor = .white + } + + override open func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let contactInfo = selectArray[indexPath.row] + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: "\(NSStringFromClass(FunFusionContactUnCheckCell.self))", + for: indexPath + ) as? FunFusionContactUnCheckCell + cell?.configure(contactInfo) + return cell ?? UICollectionViewCell() + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift index edecfe44..3056280f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift @@ -61,8 +61,8 @@ open class FunContactUserViewController: NEBaseContactUserViewController { return 46 } - override open func getContactRemakNameViewController() -> NEBaseContactRemakNameViewController { - FunContactRemakNameViewController() + override open func getContactAliasViewController() -> NEBaseContactAliasViewController { + FunContactAliasViewController() } override open func deleteFriend(user: NEUserWithFriend?) { diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactViewController.swift index dabbf3bb..2330fefc 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactViewController.swift @@ -36,7 +36,7 @@ open class FunContactViewController: NEBaseContactViewController { ), ] - if IMKitConfigCenter.shared.teamEnable { + if IMKitConfigCenter.shared.enableTeam { contactHeaders.append(ContactHeadItem( name: localizable("my_teams"), imageName: "funGroup", @@ -45,6 +45,15 @@ open class FunContactViewController: NEBaseContactViewController { )) } + if IMKitConfigCenter.shared.enableAIUser { + contactHeaders.append(ContactHeadItem( + name: localizable("my_ai_user"), + imageName: "funAIUser", + router: ContactAIUserListRouter, + color: UIColor(hexString: "#BE65D9") + )) + } + if let headerDataCallback = NEKitContactConfig.shared.ui.headerData { headerDataCallback(contactHeaders) } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift index 4e32dc84..f04ebade 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift @@ -8,12 +8,6 @@ import UIKit @objc open class FunFindFriendViewController: NEBaseFindFriendViewController { - override open func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - } - override open func setupUI() { view.backgroundColor = UIColor(hexString: "0xEDEDED") @@ -23,10 +17,11 @@ open class FunFindFriendViewController: NEBaseFindFriendViewController { searchBackView.translatesAutoresizingMaskIntoConstraints = false searchBackView.clipsToBounds = true searchBackView.layer.cornerRadius = 4.0 + searchBackViewTopAnchor = searchBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10 + topConstant) + searchBackViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ 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), ]) diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFusionContactSelectedController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFusionContactSelectedController.swift new file mode 100644 index 00000000..10aed25a --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFusionContactSelectedController.swift @@ -0,0 +1,28 @@ +//// 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 + +@objcMembers +open class FunFusionContactSelectedController: NEBaseFusionContactSelectedController { + override open func viewDidLoad() { + fusionRegisterCellDic = [0: FunFusionContactSelectedCell.self] + super.viewDidLoad() + fusionEmptyView.setEmptyImage(name: "fun_user_empty") + } + + override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 64 + } + + /* + // 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/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunMultiSelectViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunMultiSelectViewController.swift index 6008153d..3c4ecb52 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunMultiSelectViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunMultiSelectViewController.swift @@ -3,6 +3,7 @@ // found in the LICENSE file. import NEChatKit +import NECommonUIKit import NECoreIM2Kit import NECoreKit import NIMSDK @@ -15,7 +16,7 @@ open class FunMultiSelectViewController: NEBaseMultiSelectViewController { super.init(filterUsers: filterUsers) themeColor = .funContactThemeColor titleText = localizable("select") - sureButtonText = localizable("complete") + sureButtonText = commonLocalizable("complete") } public required init?(coder: NSCoder) { diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift index 088e8370..e8b12e81 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift @@ -17,13 +17,13 @@ open class FunValidationMessageViewController: NEBaseValidationMessageViewContro super.init(coder: coder) } - override open func setupUI() { - super.setupUI() + override func initNav() { + super.initNav() let clearItem = UIBarButtonItem( title: localizable("clear"), style: .done, target: self, - action: #selector(clearMessage) + action: #selector(toSetting) ) clearItem.tintColor = .ne_darkText let textAttributes: [NSAttributedString.Key: Any] = [.font: UIFont.systemFont(ofSize: 16, weight: .regular)] @@ -32,6 +32,10 @@ open class FunValidationMessageViewController: NEBaseValidationMessageViewContro navigationItem.rightBarButtonItem = clearItem navigationView.moreButton.titleLabel?.font = .systemFont(ofSize: 16) + } + + override open func setupUI() { + super.setupUI() tableView.register( FunSystemNotificationCell.self, diff --git a/NEContactUIKit/NEContactUIKit/Classes/Model/NEFusionContactCellModel.swift b/NEContactUIKit/NEContactUIKit/Classes/Model/NEFusionContactCellModel.swift new file mode 100644 index 00000000..c7724e04 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Model/NEFusionContactCellModel.swift @@ -0,0 +1,44 @@ +//// 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 UIKit + +@objcMembers +open class NEFusionContactCellModel: NSObject { + /// 用户信息 + public var user: NEUserWithFriend? + /// cell 类型 + public var type = 0 + /// 是否选中 + public var selected = false + /// 机器人数据 + public var aiUser: V2NIMAIUser? + + /// 获取accid + public func getAccountId() -> String { + if let aiAccountId = aiUser?.accountId { + return aiAccountId + } else if let uid = user?.user?.accountId { + return uid + } + return "" + } + + /// 获取显示名称 + public func getShowName() -> String { + if let name = user?.showName() { + if name.count > 0 { + return name + } + return user?.user?.accountId ?? "" + } else if let name = aiUser?.name { + if name.count > 0 { + return name + } + return aiUser?.accountId ?? "" + } + return "" + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectedListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectedListCell.swift index e7504091..c42d8a95 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectedListCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectedListCell.swift @@ -28,7 +28,7 @@ open class NEBaseSelectedListCell: NEBaseSelectCell { return button }() - /// 分割线 + /// 分隔线 public lazy var bottomLine: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -36,7 +36,7 @@ open class NEBaseSelectedListCell: NEBaseSelectCell { return view }() - public var bottomLineLeftConstraint: NSLayoutConstraint? // 分割线左边约束 + public var bottomLineLeftConstraint: NSLayoutConstraint? // 分隔线左边约束 override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) diff --git a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectViewController.swift index 62883e5a..e1b3e01f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectViewController.swift @@ -241,7 +241,7 @@ open class NEBaseMultiSelectViewController: NEContactBaseViewController, UIColle arrowRight.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -5), ]) - // 分割线 + // 分隔线 let dividerLine = UIView() dividerLine.translatesAutoresizingMaskIntoConstraints = false dividerLine.backgroundColor = UIColor.ne_backcolor @@ -298,7 +298,7 @@ open class NEBaseMultiSelectViewController: NEContactBaseViewController, UIColle recentCollectionView.heightAnchor.constraint(equalToConstant: 84), ]) - // 分割线 + // 分隔线 let dividerLine = UIView() dividerLine.translatesAutoresizingMaskIntoConstraints = false dividerLine.backgroundColor = UIColor.ne_backcolor @@ -432,7 +432,7 @@ open class NEBaseMultiSelectViewController: NEContactBaseViewController, UIColle view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .clear - let tabButtonWidth = IMKitConfigCenter.shared.teamEnable ? NEConstant.screenWidth / 3.0 : NEConstant.screenWidth / 2.0 + let tabButtonWidth = IMKitConfigCenter.shared.enableTeam ? NEConstant.screenWidth / 3.0 : NEConstant.screenWidth / 2.0 view.addSubview(recentButton) NSLayoutConstraint.activate([ @@ -450,7 +450,7 @@ open class NEBaseMultiSelectViewController: NEContactBaseViewController, UIColle friendButton.heightAnchor.constraint(equalTo: recentButton.heightAnchor), ]) - if IMKitConfigCenter.shared.teamEnable { + if IMKitConfigCenter.shared.enableTeam { view.addSubview(teamButton) NSLayoutConstraint.activate([ teamButton.topAnchor.constraint(equalTo: recentButton.topAnchor), @@ -497,8 +497,15 @@ open class NEBaseMultiSelectViewController: NEContactBaseViewController, UIColle tableView.delegate = self tableView.dataSource = self tableView.separatorStyle = .none + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { - tableView.sectionHeaderTopPadding = 0 + tableView.sectionHeaderTopPadding = 0.0 } return tableView }() @@ -512,10 +519,17 @@ open class NEBaseMultiSelectViewController: NEContactBaseViewController, UIColle tableView.delegate = self tableView.dataSource = self tableView.separatorStyle = .none + tableView.isHidden = true + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { - tableView.sectionHeaderTopPadding = 0 + tableView.sectionHeaderTopPadding = 0.0 } - tableView.isHidden = true return tableView }() @@ -528,10 +542,17 @@ open class NEBaseMultiSelectViewController: NEContactBaseViewController, UIColle tableView.delegate = self tableView.dataSource = self tableView.separatorStyle = .none + tableView.isHidden = true + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { - tableView.sectionHeaderTopPadding = 0 + tableView.sectionHeaderTopPadding = 0.0 } - tableView.isHidden = true return tableView }() diff --git a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectedViewController.swift index 29f88b9f..531768b2 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectedViewController.swift @@ -107,6 +107,16 @@ open class NEBaseMultiSelectedViewController: NEContactBaseViewController, UITab tableView.separatorStyle = .none tableView.delegate = self tableView.dataSource = self + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/AIUserListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/AIUserListCell.swift new file mode 100644 index 00000000..6b5e9821 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/AIUserListCell.swift @@ -0,0 +1,8 @@ +//// 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 + +@objcMembers +open class AIUserListCell: NEBaseAIUserListCell {} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/FusionContactSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/FusionContactSelectedCell.swift new file mode 100644 index 00000000..1e19dbca --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/FusionContactSelectedCell.swift @@ -0,0 +1,53 @@ +//// 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 + +@objcMembers +open class FusionContactSelectedCell: NEBaseFusionContactSelectedCell { + /// UI 初始化 + override open func setupFusionSelectedCellUI() { + super.setupFusionSelectedCellUI() + + contentView.addSubview(selectedStateImage) + selectedStateImage.highlightedImage = UIImage.ne_imageNamed(name: "select") + NSLayoutConstraint.activate([ + selectedStateImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + selectedStateImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + ]) + + contentView.addSubview(avatarImageView) + NSLayoutConstraint.activate([ + avatarImageView.widthAnchor.constraint(equalToConstant: 36), + avatarImageView.heightAnchor.constraint(equalToConstant: 36), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 50), + ]) + avatarImageView.layer.cornerRadius = 18 + + contentView.addSubview(nameLabel) + NSLayoutConstraint.activate([ + nameLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 12), + nameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35), + nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor), + nameLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + + avatarImageView.addSubview(avatarNameLabel) + NSLayoutConstraint.activate([ + avatarNameLabel.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor, constant: 1), + avatarNameLabel.rightAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: -1), + avatarNameLabel.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor), + avatarNameLabel.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor), + ]) + + contentView.addSubview(bottomLine) + NSLayoutConstraint.activate([ + 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/NormalUI/Cell/FusionContactUnCheckCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/FusionContactUnCheckCell.swift new file mode 100644 index 00000000..61c2b4e0 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/FusionContactUnCheckCell.swift @@ -0,0 +1,26 @@ +//// 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 + +@objcMembers +open class FusionContactUnCheckCell: ContactUnCheckCell { + override func configure(_ model: Any) { + if let cellModel = model as? NEFusionContactCellModel { + if cellModel.user != nil { + avatarImageView.configHeadData( + headUrl: cellModel.user?.user?.avatar, + name: cellModel.getShowName(), + uid: cellModel.getAccountId() + ) + } else if cellModel.aiUser != nil { + avatarImageView.configHeadData( + headUrl: cellModel.aiUser?.avatar, + name: cellModel.getShowName(), + uid: cellModel.getAccountId() + ) + } + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift index 57de5c31..a04e2c56 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift @@ -24,6 +24,20 @@ public extension ContactRouter { nav?.pushViewController(contactSelectVC, animated: true) } + // 携带机器人的成员选择页面 + Router.shared.register(ContactFusionSelectRouter) { param in + let nav = param["nav"] as? UINavigationController + let userFilters = param["filters"] as? Set + let contactSelectedPageController = ContactSelectedPageController(filterUsers: userFilters) + if let limit = param["limit"] as? Int, limit > 0 { + contactSelectedPageController.limit = limit + } + if let uid = param["uid"] as? String { + contactSelectedPageController.userId = uid + } + nav?.pushViewController(contactSelectedPageController, animated: true) + } + // 转发选择页面 Router.shared.register(ForwardMultiSelectRouter) { param in let nav = param["nav"] as? UINavigationController @@ -80,6 +94,13 @@ public extension ContactRouter { } } + Router.shared.register(ContactAIUserListRouter) { param in + if let nav = param["nav"] as? UINavigationController { + let blackVC = AIUserController() + nav.pushViewController(blackVC, animated: true) + } + } + Router.shared.register(ContactTeamListRouter) { param in if let nav = param["nav"] as? UINavigationController { let team = TeamListViewController() diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactUIColor.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactUIColor.swift index 33bad259..114e064f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactUIColor.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactUIColor.swift @@ -9,4 +9,10 @@ public extension UIColor { static let disableButtonTitleColor = UIColor(hexString: "#DDDDDD") /// #F2F4F5, 搜索区域背景颜色 static let searchTextFeildBackColor = UIColor(hexString: "#F2F4F5") + + static let contactBlueDividerLineColor = UIColor(hexString: "#337EFF") + + static let contactNormalTextColor = UIColor(hexString: "#333333") + + static let contactFusionSelectButtonBGColor = UIColor(hexString: "#337EFF") } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/AIUserController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/AIUserController.swift new file mode 100644 index 00000000..1421b87e --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/AIUserController.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 UIKit + +@objcMembers +open class AIUserController: NEBaseAIUserController { + override open func viewDidLoad() { + super.viewDidLoad() + navigationView.backgroundColor = .white + navigationController?.navigationBar.backgroundColor = .white + backView.backgroundColor = .ne_backcolor + aiUserTableView.register(AIUserListCell.self, forCellReuseIdentifier: "\(AIUserListCell.self)") + } + + override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if let cell = tableView.dequeueReusableCell( + withIdentifier: "\(AIUserListCell.self)", + for: indexPath + ) as? AIUserListCell { + if let model = getRealAIUserModel(indexPath.row) { + cell.configure(model) + + return cell + } + } + return UITableViewCell() + } + + override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 64.0 + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift index 960a6991..dda0c3f8 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift @@ -12,8 +12,6 @@ import UIKit open class BlackListViewController: NEBaseBlackListViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - navigationView.backgroundColor = .white - navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { @@ -22,6 +20,9 @@ open class BlackListViewController: NEBaseBlackListViewController { override func commonUI() { super.commonUI() + navigationView.backgroundColor = .white + navigationController?.navigationBar.backgroundColor = .white + tableView.register( BlackListCell.self, forCellReuseIdentifier: "\(NSStringFromClass(BlackListCell.self))" diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactRemakNameViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactAliasViewController.swift similarity index 62% rename from NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactRemakNameViewController.swift rename to NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactAliasViewController.swift index f2cbc56a..b2b84307 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactRemakNameViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactAliasViewController.swift @@ -8,7 +8,15 @@ import NECoreKit import UIKit @objcMembers -open class ContactRemakNameViewController: NEBaseContactRemakNameViewController { +open class ContactAliasViewController: NEBaseContactAliasViewController { + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { + topConstant = 10 + aliasInputTopAnchor?.constant = topConstant + } + } + override func setupUI() { super.setupUI() aliasInput.layer.cornerRadius = 8 @@ -22,10 +30,11 @@ open class ContactRemakNameViewController: NEBaseContactRemakNameViewController clearItem.tintColor = UIColor(hexString: "337EFF") navigationItem.rightBarButtonItem = clearItem + aliasInputTopAnchor = aliasInput.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant + 10) + aliasInputTopAnchor?.isActive = true NSLayoutConstraint.activate([ aliasInput.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), aliasInput.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - aliasInput.topAnchor.constraint(equalTo: view.topAnchor, constant: 10 + topConstant), aliasInput.heightAnchor.constraint(equalToConstant: 50), ]) } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactSelectedPageController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactSelectedPageController.swift new file mode 100644 index 00000000..6df8f29d --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactSelectedPageController.swift @@ -0,0 +1,76 @@ +//// 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 + +@objcMembers +open class ContactSelectedPageController: NEBaseContactSelectedPageController { + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + pagingViewControllerTopAnchor?.constant = topConstant + } + + override open func getContentControllers(_ filterUsers: Set? = nil) -> [NEBaseFusionContactSelectedController]? { + if childrenControllers.count == 0 { + let userSelectController = FusionContactSelectedController(filterIds: filterUsers, type: .FusionContactTypeUser) + userSelectController.delegate = self + userSelectController.limit = limit + let aiUserSelectController = FusionContactSelectedController(filterIds: filterUsers, type: .FusionContactTypeAIUser) + aiUserSelectController.delegate = self + aiUserSelectController.limit = limit + childrenControllers.append(userSelectController) + childrenControllers.append(aiUserSelectController) + } + return childrenControllers + } + + override open func setupPageContent() { + super.setupPageContent() + view.backgroundColor = .ne_backcolor + let pagingViewController = NEPagingViewController(viewControllers: contentControllers) + addChild(pagingViewController) + view.addSubview(pagingViewController.view) + pagingViewController.view.backgroundColor = .white + pagingViewControllerTopAnchor = pagingViewController.view.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + pagingViewControllerTopAnchor?.isActive = true + pagingViewController.view.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + pagingViewController.view.leftAnchor.constraint(equalTo: view.leftAnchor), + pagingViewController.view.rightAnchor.constraint(equalTo: view.rightAnchor), + pagingViewController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + pagingViewController.selectedTextColor = .contactBlueDividerLineColor + pagingViewController.textColor = .contactNormalTextColor + pagingViewController.indicatorColor = .contactBlueDividerLineColor + pagingViewController.indicatorOptions = NEPagingIndicatorOptions.visible( + height: 2, + zIndex: Int.max, + spacing: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0), + insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + ) + pagingViewController.borderOptions = NEPagingBorderOptions.visible(height: 1, zIndex: Int.max, insets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)) + + pagingViewController.didMove(toParent: self) + selectCollectionView.register(FusionContactUnCheckCell.self, forCellWithReuseIdentifier: "\(NSStringFromClass(FusionContactUnCheckCell.self))") + } + + override open func setupNavSureItem() { + super.setupNavSureItem() + navigationView.moreButton.backgroundColor = .clear + navigationView.moreButton.setTitleColor(.contactFusionSelectButtonBGColor, for: .normal) + selectedSureButton.backgroundColor = .clear + selectedSureButton.setTitleColor(.contactFusionSelectButtonBGColor, for: .normal) + } + + override open func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let contactInfo = selectArray[indexPath.row] + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: "\(NSStringFromClass(FusionContactUnCheckCell.self))", + for: indexPath + ) as? FusionContactUnCheckCell + cell?.configure(contactInfo) + return cell ?? UICollectionViewCell() + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactSelectedViewController.swift index 028bb3d3..dcfa6692 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactSelectedViewController.swift @@ -12,9 +12,6 @@ open class ContactSelectedViewController: NEBaseContactSelectedViewController { override public init(filterUsers: Set? = nil) { super.init(filterUsers: filterUsers) customCells = [ContactCellType.ContactPerson.rawValue: ContactSelectedCell.self] - view.backgroundColor = .ne_backcolor - navigationView.backgroundColor = .white - navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { @@ -23,6 +20,9 @@ open class ContactSelectedViewController: NEBaseContactSelectedViewController { override open func setupUI() { super.setupUI() + view.backgroundColor = .ne_backcolor + navigationView.backgroundColor = .white + navigationController?.navigationBar.backgroundColor = .white collectionView.register( ContactUnCheckCell.self, diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift index b6c66265..1a2935f9 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift @@ -39,7 +39,7 @@ open class ContactUserViewController: NEBaseContactUserViewController { tableView.rowHeight = 62 } - override open func getContactRemakNameViewController() -> NEBaseContactRemakNameViewController { - ContactRemakNameViewController() + override open func getContactAliasViewController() -> NEBaseContactAliasViewController { + ContactAliasViewController() } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactViewController.swift index 0f0af79d..cd45d6c5 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactViewController.swift @@ -27,7 +27,7 @@ open class ContactViewController: NEBaseContactViewController { ), ] - if IMKitConfigCenter.shared.teamEnable { + if IMKitConfigCenter.shared.enableTeam { contactHeaders.append(ContactHeadItem( name: localizable("my_teams"), imageName: "group", @@ -36,6 +36,15 @@ open class ContactViewController: NEBaseContactViewController { )) } + if IMKitConfigCenter.shared.enableAIUser { + contactHeaders.append(ContactHeadItem( + name: localizable("my_ai_user"), + imageName: "aiUser", + router: ContactAIUserListRouter, + color: UIColor(hexString: "#BE65D9") + )) + } + if let headerDataCallback = NEKitContactConfig.shared.ui.headerData { headerDataCallback(contactHeaders) } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift index 1b27b893..8f1b1ec6 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift @@ -8,11 +8,15 @@ import UIKit open class FindFriendViewController: NEBaseFindFriendViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - navigationView.backgroundColor = .white - navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { super.init(coder: coder) } + + override open func viewDidLoad() { + super.viewDidLoad() + navigationView.backgroundColor = .white + navigationController?.navigationBar.backgroundColor = .white + } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FusionContactSelectedController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FusionContactSelectedController.swift new file mode 100644 index 00000000..ef3c36c9 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FusionContactSelectedController.swift @@ -0,0 +1,23 @@ +//// 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 + +@objcMembers +open class FusionContactSelectedController: NEBaseFusionContactSelectedController { + override open func viewDidLoad() { + fusionRegisterCellDic = [0: FusionContactSelectedCell.self] + super.viewDidLoad() + } + + /* + // 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/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift index a0942e91..644317e6 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift @@ -11,21 +11,19 @@ import UIKit open class ValidationMessageViewController: NEBaseValidationMessageViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - navigationView.backgroundColor = .white - navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { super.init(coder: coder) } - override open func setupUI() { - super.setupUI() + override func initNav() { + super.initNav() let clearItem = UIBarButtonItem( title: localizable("clear"), style: .done, target: self, - action: #selector(clearMessage) + action: #selector(toSetting) ) clearItem.tintColor = .ne_darkText var textAttributes = [NSAttributedString.Key: Any]() @@ -35,6 +33,12 @@ open class ValidationMessageViewController: NEBaseValidationMessageViewControlle navigationItem.rightBarButtonItem = clearItem navigationView.moreButton.titleLabel?.font = .systemFont(ofSize: 16) + } + + override open func setupUI() { + super.setupUI() + navigationView.backgroundColor = .white + navigationController?.navigationBar.backgroundColor = .white tableView.register( SystemNotificationCell.self, diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift index 193fa676..3da6aab0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift @@ -9,19 +9,38 @@ import UIKit @objcMembers open class NEBaseTeamListViewController: NEContactBaseViewController, UITableViewDelegate, UITableViewDataSource { - var tableView = UITableView(frame: .zero, style: .plain) - var viewModel = TeamListViewModel() var isClickCallBack = false + var viewModel = TeamListViewModel() + public var tableViewTopAnchor: NSLayoutConstraint? + + lazy var tableView: UITableView = { + var tableView = UITableView(frame: .zero, style: .plain) + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableViewTopAnchor?.constant = topConstant + } override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white - navigationController?.interactivePopGestureRecognizer?.delegate = self - if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { - navigationController?.isNavigationBarHidden = false - } else { - navigationController?.isNavigationBarHidden = true - } commonUI() loadData() @@ -30,11 +49,10 @@ open class NEBaseTeamListViewController: NEContactBaseViewController, UITableVie weakSelf?.emptyView.isHidden = (weakSelf?.viewModel.teamList.count ?? 0) > 0 weakSelf?.tableView.reloadData() } + navigationView.moreButton.isHidden = true } - func commonUI() { - title = localizable("my_teams") - navigationView.navTitle.text = title + func initNav() { let image = UIImage.ne_imageNamed(name: "backArrow")?.withRenderingMode(.alwaysOriginal) let backItem = UIBarButtonItem( image: image, @@ -45,30 +63,21 @@ open class NEBaseTeamListViewController: NEContactBaseViewController, UITableVie backItem.accessibilityIdentifier = "id.backArrow" navigationItem.leftBarButtonItem = backItem - - navigationView.translatesAutoresizingMaskIntoConstraints = false - navigationView.addBackButtonTarget(target: self, selector: #selector(backEvent)) navigationView.moreButton.isHidden = true - view.addSubview(navigationView) - NSLayoutConstraint.activate([ - navigationView.leftAnchor.constraint(equalTo: view.leftAnchor), - navigationView.rightAnchor.constraint(equalTo: view.rightAnchor), - navigationView.topAnchor.constraint(equalTo: view.topAnchor), - navigationView.heightAnchor.constraint(equalToConstant: NEConstant.navigationAndStatusHeight), - ]) + } + + func commonUI() { + title = localizable("my_teams") + initNav() - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) + tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + tableViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight), tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) emptyView.setText(localizable("team_empty")) view.addSubview(emptyView) @@ -120,8 +129,4 @@ open class NEBaseTeamListViewController: NEContactBaseViewController, UITableVie ) } } - - func backEvent() { - navigationController?.popViewController(animated: true) - } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift index 5cef0e7b..1488d8cc 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift @@ -10,6 +10,12 @@ import UIKit @objcMembers open class NEBaseValidationMessageViewController: NEContactBaseViewController { public let viewModel = ValidationMessageViewModel() + public var tableViewTopAnchor: NSLayoutConstraint? + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableViewTopAnchor?.constant = topConstant + } override open func viewDidLoad() { super.viewDidLoad() @@ -31,8 +37,8 @@ open class NEBaseValidationMessageViewController: NEContactBaseViewController { } /// 返回上一级页面 - override open func backToPrevious() { - super.backToPrevious() + override open func backEvent() { + super.backEvent() viewModel.setAddApplicationRead(nil) } @@ -55,6 +61,15 @@ open class NEBaseValidationMessageViewController: NEContactBaseViewController { tableView.backgroundColor = .clear tableView.keyboardDismissMode = .onDrag + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + tableView.mj_header = MJRefreshNormalHeader( refreshingTarget: self, refreshingAction: #selector(loadData) @@ -115,13 +130,12 @@ open class NEBaseValidationMessageViewController: NEContactBaseViewController { } } - /// 控件初始化 - open func setupUI() { + func initNav() { let clearItem = UIBarButtonItem( title: localizable("clear"), style: .done, target: self, - action: #selector(clearMessage) + action: #selector(toSetting) ) clearItem.tintColor = UIColor(hexString: "666666") var textAttributes = [NSAttributedString.Key: Any]() @@ -130,16 +144,19 @@ open class NEBaseValidationMessageViewController: NEContactBaseViewController { clearItem.setTitleTextAttributes(textAttributes, for: .normal) navigationItem.rightBarButtonItem = clearItem - title = localizable("validation_message") - navigationView.navTitle.text = title navigationView.setMoreButtonTitle(localizable("clear")) navigationView.moreButton.setTitleColor(.ne_darkText, for: .normal) - navigationView.addMoreButtonTarget(target: self, selector: #selector(clearMessage)) + } - view.addSubview(tableView) + /// 控件初始化 + open func setupUI() { + title = localizable("validation_message") + initNav() + view.addSubview(tableView) + tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + tableViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -156,7 +173,7 @@ open class NEBaseValidationMessageViewController: NEContactBaseViewController { } /// 清空好友申请 - func clearMessage() { + override open func toSetting() { NEALog.infoLog(ModuleName + " " + className(), desc: #function) viewModel.clearNotification() } diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift index 676ba3a0..f6626c74 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift @@ -14,7 +14,7 @@ public protocol ContactViewModelDelegate: NSObjectProtocol { } @objcMembers -open class ContactViewModel: NSObject, NEContactListener, NEEventListener { +open class ContactViewModel: NSObject { typealias RefreshBlock = () -> Void public var contacts: [ContactSection] = [] public var indexs: [String]? @@ -40,19 +40,20 @@ open class ContactViewModel: NSObject, NEContactListener, NEEventListener { desc: #function + ", contactHeaders.count: \(contactHeaders?.count ?? 0)" ) - contactRepo.addContactListener(self) - if let headSection = headerSection(headerItem: contactHeaders) { self.contactHeaders = headSection contacts.append(headSection) } + contactRepo.addContactListener(self) + if IMKitConfigCenter.shared.onlineStatusEnable { EventSubscribeRepo.shared.addListener(self) } } deinit { + contactRepo.removeContactListener(self) if IMKitConfigCenter.shared.onlineStatusEnable { EventSubscribeRepo.shared.removeListener(self) } @@ -95,7 +96,7 @@ open class ContactViewModel: NSObject, NEContactListener, NEEventListener { 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) + completion(contactList, error) } } @@ -219,8 +220,23 @@ open class ContactViewModel: NSObject, NEContactListener, NEEventListener { return indexs } +} - // MARK: - NEContactListener +// MARK: - NEContactListener + +extension ContactViewModel: NEContactListener { + /// 好友信息缓存更新 + /// - Parameter accountId: 用户 id + public func onContactChange(_ changeType: NEContactChangeType, _ contacts: [NEUserWithFriend]) { + for contact in contacts { + if let accid = contact.user?.accountId, + !NEFriendUserCache.shared.isBlockAccount(accid) { + loadData { [weak self] _, _ in + self?.delegate?.reloadTableView() + } + } + } + } /// 从通讯录中移除 /// - Parameter accountId: 好友 Id @@ -274,20 +290,6 @@ open class ContactViewModel: NSObject, NEContactListener, NEEventListener { 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 user: 用户信息 public func onBlockListAdded(_ user: V2NIMUser) { @@ -305,9 +307,11 @@ open class ContactViewModel: NSObject, NEContactListener, NEEventListener { } } } +} - // MARK: - NEEventListener +// MARK: - NEEventListener +extension ContactViewModel: NEEventListener { /// 订阅在线状态 public func subscribeOnlineStatus() { var subscribeList: [String] = [] diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FusionContactSelectedViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FusionContactSelectedViewModel.swift new file mode 100644 index 00000000..a7fff3a2 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FusionContactSelectedViewModel.swift @@ -0,0 +1,77 @@ +//// 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 UIKit + +@objcMembers +open class FusionContactSelectedViewModel: NSObject { + /// 数据源 + var memberDatas = [NEFusionContactCellModel]() + /// 选中数据 + var memberSelectedSet = Set() + + /// 通讯里API单例 + var contactRepo = ContactRepo.shared + + /// 获取成员数据 + /// - Parameter filters: 过滤器 + func loadMemberDatas(_ filters: Set? = nil, _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + weak var weakSelf = self + if !NEFriendUserCache.shared.isEmpty() { + var friends = NEFriendUserCache.shared.getFriendListNotInBlocklist().map(\.value) + friends.sort { model1, model2 in + if let time1 = model1.friend?.createTime, let time2 = model2.friend?.createTime { + return time2 > time1 + } + return false + } + memberDatas.removeAll() + for user in friends { + if let accountId = user.user?.accountId, let filtersSet = filters { + if filtersSet.contains(accountId) { + continue + } + } + let model = NEFusionContactCellModel() + model.user = user + memberDatas.append(model) + } + completion(nil) + return + } + contactRepo.getContactList { friends, error in + NEALog.infoLog("contact bar getFriendList", desc: "friend count:\(String(describing: friends?.count))") + weakSelf?.memberDatas.removeAll() + friends?.forEach { user in + if let accountId = user.user?.accountId, let filtersSet = filters { + if filtersSet.contains(accountId) { + return + } + } + let model = NEFusionContactCellModel() + model.user = user + weakSelf?.memberDatas.append(model) + } + completion(error as NSError?) + } + } + + /// 获取数字人数据 + func loadAIUserData(_ filters: Set? = nil) { + let aiUsers = NEAIUserManager.shared.getAllAIUsers() + for aiUser in aiUsers { + if let accountId = aiUser.accountId { + if filters?.contains(accountId) ?? false { + continue + } + let model = NEFusionContactCellModel() + model.aiUser = aiUser + memberDatas.append(model) + } + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift index c9f59061..06d4dddc 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift @@ -71,10 +71,10 @@ open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataPro } open func initSubviewsLayout() { - if NEKitContactConfig.shared.ui.contactProperties.avatarType == .rectangle { - avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius - } else if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { + if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { avatarImageView.layer.cornerRadius = 18.0 + } else if NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius > 0 { + avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius } else { avatarImageView.layer.cornerRadius = 18.0 // Normal UI } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseFusionContactSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseFusionContactSelectedCell.swift new file mode 100644 index 00000000..420badbf --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseFusionContactSelectedCell.swift @@ -0,0 +1,102 @@ +//// 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 UIKit + +@objcMembers +open class NEBaseFusionContactSelectedCell: UITableViewCell { + /// 用户头像 + public lazy var avatarImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + imageView.backgroundColor = UIColor.colorWithNumber(number: 0) + imageView.accessibilityIdentifier = "id.avatar" + return imageView + }() + + /// 没有头像的头像覆盖Label + public lazy var avatarNameLabel: UILabel = { + 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 nameLabel: UILabel = { + let label = UILabel() + label.textAlignment = .left + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 14.0) + label.textColor = UIColor(hexString: "333333") + label.accessibilityIdentifier = "id.name" + return label + }() + + /// 选中图片 + let selectedStateImage: UIImageView = { + let imageView = UIImageView() + imageView.image = UIImage.ne_imageNamed(name: "unselect") + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.accessibilityIdentifier = "id.selector" + return imageView + }() + + /// 分隔线 + public lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + return view + }() + + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + setupFusionSelectedCellUI() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// UI 初始化,在子类中实现 + open func setupFusionSelectedCellUI() {} + + /// 绑定数据 + open func configFusionModel(_ model: NEFusionContactCellModel) { + if model.user != nil { + nameLabel.text = model.user?.showName() + avatarNameLabel.text = model.user?.shortName(showAlias: false, count: 2) + if let imageUrl = model.user?.user?.avatar, !imageUrl.isEmpty { + avatarNameLabel.isHidden = true + avatarImageView.sd_setImage(with: URL(string: imageUrl), completed: nil) + } else { + avatarNameLabel.isHidden = false + avatarImageView.sd_setImage(with: nil) + avatarImageView.backgroundColor = UIColor.colorWithString(string: model.user?.user?.accountId ?? "") + } + } else if model.aiUser != nil { + nameLabel.text = model.aiUser?.showName() + avatarNameLabel.text = model.aiUser?.shortName() + if let imageUrl = model.aiUser?.avatar, !imageUrl.isEmpty { + avatarNameLabel.isHidden = true + avatarImageView.sd_setImage(with: URL(string: imageUrl), completed: nil) + } else { + avatarNameLabel.isHidden = false + avatarImageView.sd_setImage(with: nil) + avatarImageView.backgroundColor = UIColor.colorWithString(string: model.aiUser?.accountId ?? "") + } + } + + selectedStateImage.isHighlighted = model.selected + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactAliasViewController.swift similarity index 82% rename from NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift rename to NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactAliasViewController.swift index 4c5bec97..01e3bb62 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactAliasViewController.swift @@ -9,14 +9,15 @@ import NECoreKit import UIKit @objcMembers -open class NEBaseContactRemakNameViewController: NEContactBaseViewController, UITextFieldDelegate { +open class NEBaseContactAliasViewController: NEContactBaseViewController, UITextFieldDelegate { typealias ModifyBlock = (_ user: NEUserWithFriend) -> Void var completion: ModifyBlock? var user: NEUserWithFriend? let viewmodel = ContactUserViewModel() let textLimit = 15 - lazy var aliasInput: UITextField = { + public var aliasInputTopAnchor: NSLayoutConstraint? + public lazy var aliasInput: UITextField = { let textField = UITextField() textField.backgroundColor = .white textField.clipsToBounds = true @@ -43,6 +44,11 @@ open class NEBaseContactRemakNameViewController: NEContactBaseViewController, UI // return btn // }() + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + aliasInputTopAnchor?.constant = topConstant + } + override open func viewDidLoad() { super.viewDidLoad() setupUI() @@ -50,7 +56,6 @@ open class NEBaseContactRemakNameViewController: NEContactBaseViewController, UI func setupUI() { title = localizable("noteName") - navigationView.navTitle.text = title view.backgroundColor = .ne_lightBackgroundColor navigationView.setMoreButtonTitle(localizable("save")) @@ -65,10 +70,6 @@ open class NEBaseContactRemakNameViewController: NEContactBaseViewController, UI } func saveAlias() { -// guard let alais = aliasInput.text, alais.count > 0 else { -// view.makeToast("请填写备注名", duration: 2, position: .center) -// return -// } if let text = aliasInput.text, text.count > 0, text.trimmingCharacters(in: .whitespaces).isEmpty { @@ -89,7 +90,7 @@ open class NEBaseContactRemakNameViewController: NEContactBaseViewController, UI view.makeToastActivity(.center) viewmodel.updateAlias(accountId: uid, alias: alias) { error in NEALog.infoLog( - "ContactRemakNameViewController", + "ContactAliasViewController", desc: "CALLBACK update " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() @@ -109,16 +110,6 @@ open class NEBaseContactRemakNameViewController: NEContactBaseViewController, UI } } - /* - // 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. - } - */ - open func textFieldChange() { guard let _ = aliasInput.markedTextRange else { if let text = aliasInput.text, diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactSelectedPageController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactSelectedPageController.swift new file mode 100644 index 00000000..902a881f --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactSelectedPageController.swift @@ -0,0 +1,288 @@ +//// 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 NECoreKit +import NIMSDK +import UIKit + +@objcMembers +open class NEBaseContactSelectedPageController: NEContactBaseViewController, FusionContactSelectedDelegate, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + /// 选择数量限制,默认选择10, 可从外部传入 + public var limit = 10 { + didSet { + for controller in childrenControllers { + controller.limit = limit + } + } + } + + /// page view 视图控制器 + public var childrenControllers = [NEBaseFusionContactSelectedController]() + + /// 选择确定按钮 + public var selectedSureButton = UIButton(frame: CGRect(x: 0, y: 0, width: 76, height: 32)) + /// 防重变量 + var isRequesting = false + + public var userId: String? + + /// 内容controller集合 + public var contentControllers = [NEBaseFusionContactSelectedController]() + + /// 选择记录 + public var selectArray = [NEFusionContactCellModel]() + + /// 显示选中背景 + public lazy var selectCollectionBackView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + view.layer.cornerRadius = 4 + view.isHidden = true + return view + }() + + /// 显示选中列表 + public lazy var selectCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 0 + layout.minimumInteritemSpacing = 0 + 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 + }() + + var collectionBackViewTopMargin: CGFloat = 0 + var collectionBackViewHeight: CGFloat = 52 + public var collectionBackViewTopAnchor: NSLayoutConstraint? + var pagingViewControllerTopAnchor: NSLayoutConstraint? + + public init(filterUsers: Set? = nil) { + super.init(nibName: nil, bundle: nil) + if let controllers = getContentControllers(filterUsers) { + contentControllers.append(contentsOf: controllers) + } + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + + title = localizable("select") + setupNavSureItem() + setupPageContent() + } + + /// 获取page view 内容,在子类中实现 + /// - Parameter filterUsers: 用户过滤 + open func getContentControllers(_ filterUsers: Set? = nil) -> [NEBaseFusionContactSelectedController]? { + nil + } + + /// UI 初始化 + open func setupPageContent() { + view.addSubview(selectCollectionBackView) + collectionBackViewTopAnchor = selectCollectionBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant + collectionBackViewTopMargin) + collectionBackViewTopAnchor?.isActive = true + NSLayoutConstraint.activate([ + selectCollectionBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16), + selectCollectionBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16), + selectCollectionBackView.heightAnchor.constraint(equalToConstant: collectionBackViewHeight), + ]) + + selectCollectionView.backgroundColor = .clear + selectCollectionView.delegate = self + selectCollectionView.dataSource = self + selectCollectionView.allowsMultipleSelection = false + selectCollectionView.translatesAutoresizingMaskIntoConstraints = false + selectCollectionBackView.addSubview(selectCollectionView) + NSLayoutConstraint.activate([ + selectCollectionView.centerYAnchor.constraint(equalTo: selectCollectionBackView.centerYAnchor), + selectCollectionView.leftAnchor.constraint(equalTo: selectCollectionBackView.leftAnchor), + selectCollectionView.rightAnchor.constraint(equalTo: selectCollectionBackView.rightAnchor), + selectCollectionView.heightAnchor.constraint(equalToConstant: collectionBackViewHeight), + ]) + } + + /// 设置选择确定按钮样式 + open func setupNavSureItem() { + if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { + let rightItem = UIBarButtonItem(customView: selectedSureButton) + navigationItem.rightBarButtonItem = rightItem + selectedSureButton.addTarget(self, action: #selector(selectSureClick(_:)), for: .touchUpInside) + selectedSureButton.setTitle(localizable("alert_sure"), for: .normal) + selectedSureButton.setTitleColor(.white, for: .normal) + selectedSureButton.layer.cornerRadius = 4 + selectedSureButton.contentHorizontalAlignment = .center + selectedSureButton.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(selectSureClick(_:))) + selectedSureButton = navigationView.moreButton + navigationView.backgroundColor = .white + } + } + + /// 确定按钮点击 + /// - Parameter sender: 确定按钮 + open func selectSureClick(_ sender: UIButton) { + // 防止多次点击确定按钮会多次回调 + if isRequesting { + return + } + + if selectArray.count <= 0 { + showToast(localizable("select_contact")) + return + } + + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(localizable("network_error")) + return + } + + isRequesting = true + var accids = [String]() + var names = [String]() + let group = DispatchGroup() + var mine: NEUserWithFriend? + + if let mineInfo = NEFriendUserCache.shared.getFriendInfo(IMKitClient.instance.account()) { + mine = mineInfo + } else { + group.enter() + ContactRepo.shared.getUserListFromCloud(accountIds: [IMKitClient.instance.account()]) { users, error in + mine = users?.first + group.leave() + } + } + + group.notify(queue: .main) { [weak self] in + let myName = mine?.showName() ?? IMKitClient.instance.account() + names.append(myName) + var users = [V2NIMUser]() + self?.selectArray.forEach { model in + accids.append(model.getAccountId()) + + let name = model.getShowName() + let accountId = model.getAccountId() + if names.count > 0 { + names.append(name) + } else if accountId.count > 0 { + names.append(accountId) + } + if let user = model.user?.user { + users.append(user) + } else if let user = model.aiUser { + 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?.isRequesting = false + } + } + + /// 选择成员列表回调 + /// - Parameter user: 用户对象 + open func didSelectedUser(_ model: NEFusionContactCellModel) -> Bool { + if selectArray.count >= limit { + return false + } + selectArray.append(model) + didChangeSelectUser() + return true + } + + /// 取消选择成员列表回调 + /// - Parameter user: 用户对象 + open func didUnselectedUser(_ model: NEFusionContactCellModel) { + selectArray.removeAll { selectModel in + let selectAccountId = selectModel.getAccountId() + let rmAccountId = model.getAccountId() + if selectAccountId.count > 0, rmAccountId == selectAccountId { + return true + } + return false + } + didChangeSelectUser() + } + + /// 选择用户变更统一处理 + open func didChangeSelectUser() { + if selectArray.count > 0 { + selectedSureButton.setTitle("\(localizable("alert_sure"))(\(selectArray.count))", for: .normal) + } else { + selectedSureButton.setTitle(localizable("alert_sure"), for: .normal) + } + if selectArray.count <= 0 { + selectCollectionBackView.isHidden = true + pagingViewControllerTopAnchor?.constant = topConstant + } else { + selectCollectionBackView.isHidden = false + pagingViewControllerTopAnchor?.constant = topConstant + collectionBackViewHeight + collectionBackViewTopMargin * 2 + } + selectCollectionView.reloadData() + } + + /// 顶部反向取消选中 + open func didUnselect(_ model: NEFusionContactCellModel) { + for controller in childrenControllers { + controller.unselectModel(model) + } + didUnselectedUser(model) + } + + /* + // 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. + } + */ + + // MARK: Collection View Delegate + + public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + selectArray.count + } + + public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + UICollectionViewCell() + } + + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let model = selectArray[indexPath.row] + didUnselect(model) + } + + open func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + CGSize(width: 46, height: collectionBackViewHeight) + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactSelectedViewController.swift index 15744080..cf052862 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactSelectedViewController.swift @@ -24,6 +24,7 @@ open class NEBaseContactSelectedViewController: NEContactBaseViewController, UIC public let selectDic = [String: ContactInfo]() public var isCreating = false // 是否正在创建群组 + public var collectionBackViewTopAnchor: NSLayoutConstraint? public lazy var collectionBackView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -62,10 +63,16 @@ open class NEBaseContactSelectedViewController: NEContactBaseViewController, UIC tableView.translatesAutoresizingMaskIntoConstraints = false tableView.separatorStyle = .none tableView.contentInset = .init(top: -10, left: 0, bottom: 0, right: 0) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { - tableView.sectionHeaderTopPadding = 0 + tableView.sectionHeaderTopPadding = 0.0 } - return tableView }() @@ -80,10 +87,15 @@ open class NEBaseContactSelectedViewController: NEContactBaseViewController, UIC super.init(coder: coder) } + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + collectionBackViewTopAnchor?.constant = topConstant + collectionBackViewTopMargin + tableViewTopAnchor?.constant = topConstant + } + override open func viewDidLoad() { super.viewDidLoad() title = localizable("select") - navigationView.navTitle.text = title emptyView.setText(localizable("no_friend")) setupUI() setupNavRightItem() @@ -97,8 +109,9 @@ open class NEBaseContactSelectedViewController: NEContactBaseViewController, UIC open func setupUI() { view.addSubview(collectionBackView) + collectionBackViewTopAnchor = collectionBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant + collectionBackViewTopMargin) + collectionBackViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ - collectionBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant + collectionBackViewTopMargin), collectionBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16), collectionBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16), collectionBackView.heightAnchor.constraint(equalToConstant: collectionBackViewHeight), @@ -202,7 +215,7 @@ open class NEBaseContactSelectedViewController: NEContactBaseViewController, UIC mine = mineInfo } else { group.enter() - ContactRepo.shared.getUserList(accountIds: [IMKitClient.instance.account()]) { users, error in + ContactRepo.shared.getUserListFromCloud(accountIds: [IMKitClient.instance.account()]) { users, error in mine = users?.first group.leave() } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift index 11158a63..147f61c0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift @@ -18,9 +18,9 @@ open class NEBaseContactUserViewController: NEContactBaseViewController, UITable var className = "ContactUserViewController" public let viewModel = ContactUserViewModel() - public var tableView = UITableView(frame: .zero, style: .grouped) var data = [[UserItem]]() public var headerView = NEBaseUserInfoHeaderView() + public var tableViewTopAnchor: NSLayoutConstraint? /// 使用 accountId 初始化 /// - Parameter accountId: 用户 id @@ -49,10 +49,23 @@ open class NEBaseContactUserViewController: NEContactBaseViewController, UITable super.init(coder: coder) } + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + tableViewTopAnchor?.constant = topConstant + } + override open func viewDidLoad() { super.viewDidLoad() commonUI() loadData() + ContactRepo.shared.addContactListener(self) + + // 数字人无需远端查询信息 + if user?.user is V2NIMAIUser { + loadData() + return + } + if let userId = uid { weak var weakSelf = self if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { @@ -70,24 +83,40 @@ open class NEBaseContactUserViewController: NEContactBaseViewController, UITable } else if let u = user { weakSelf?.user = u weakSelf?.loadData() + NotificationCenter.default.post(name: NENotificationName.didTapHeader, object: user) } } } - - ContactRepo.shared.addContactListener(self) } + lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .grouped) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + open func commonUI() { navigationController?.navigationBar.backgroundColor = .white navigationView.backgroundColor = .white + navigationView.moreButton.isHidden = true - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) + tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + tableViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), @@ -119,9 +148,25 @@ open class NEBaseContactUserViewController: NEContactBaseViewController, UITable } func loadData() { - guard let uid = user?.user?.accountId else { return } + guard let uid = user?.user?.accountId ?? uid else { return } + + // 数字人信息从缓存中取 + if let aiUser: NEUserWithFriend = NEAIUserManager.shared.getAIUserById(uid) { + user = aiUser + } - if NEFriendUserCache.shared.isFriend(uid) { + if user?.user is V2NIMAIUser { + // 数字人仅展示【聊天】 + data = [ + [ + UserItem(title: localizable("chat"), + detailTitle: "", + value: false, + textColor: UIColor(hexString: "#337EFF"), + cellClass: CenterTextCell.self), + ], + ] + } else if NEFriendUserCache.shared.isFriend(uid) { data = [ [ UserItem(title: localizable("noteName"), @@ -244,10 +289,7 @@ open class NEBaseContactUserViewController: NEContactBaseViewController, UITable } open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - if section == 0 { - return 0 - } - return 6.0 + 0.1 } open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { @@ -257,11 +299,13 @@ open class NEBaseContactUserViewController: NEContactBaseViewController, UITable } open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - 0 + 0.1 } open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { - nil + let headerView = UIView() + headerView.backgroundColor = UIColor.clear + return headerView } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -292,12 +336,12 @@ open class NEBaseContactUserViewController: NEContactBaseViewController, UITable } } - open func getContactRemakNameViewController() -> NEBaseContactRemakNameViewController { - NEBaseContactRemakNameViewController() + open func getContactAliasViewController() -> NEBaseContactAliasViewController { + NEBaseContactAliasViewController() } func toEditRemarks() { - let remark = getContactRemakNameViewController() + let remark = getContactAliasViewController() remark.user = user remark.completion = { [weak self] u in self?.user = u @@ -371,7 +415,6 @@ open class NEBaseContactUserViewController: NEContactBaseViewController, UITable if error != nil { self.showToast(error?.localizedDescription ?? "") } else { - NEFriendUserCache.shared.removeFriendInfo(userId) self.navigationController?.popViewController(animated: true) } } @@ -421,17 +464,15 @@ open class NEBaseContactUserViewController: NEContactBaseViewController, UITable desc: "CALLBACK addFriend " + (error?.localizedDescription ?? "no error") ) if let err = error { - NEALog.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 { - if NEFriendUserCache.shared.isBlockAccount(account) { - weakSelf?.viewModel.removeBlackList(account: account) { err in - NEALog.infoLog( - self.className, - desc: #function + "CALLBACK " + (err?.localizedDescription ?? "no error") - ) - } + if NEFriendUserCache.shared.isBlockAccount(account) { + weakSelf?.viewModel.removeBlackList(account: account) { err in + NEALog.infoLog( + self.className, + desc: #function + "CALLBACK " + (err?.localizedDescription ?? "no error") + ) } } } @@ -443,47 +484,14 @@ open class NEBaseContactUserViewController: NEContactBaseViewController, UITable // MARK: - NEContactListener extension NEBaseContactUserViewController: NEContactListener { - /// 黑名单添加回调 - /// - Parameter user: 加入黑名单的好友 - public func onBlockListAdded(_ user: V2NIMUser) { - guard let accountId = user.accountId else { return } - NEFriendUserCache.shared.addBlockAccount(accountId) - if accountId == uid { - loadData() - } - } - - /// 黑名单移除回调 - /// - Parameter accountId: 移除黑名的用户账号ID - public func onBlockListRemoved(_ accountId: String) { - NEFriendUserCache.shared.removeBlockAccount(accountId) - if accountId == uid { - loadData() - } - } - - /// 添加好友通知 - /// - Parameter friendInfo: 好友信息 - public func onFriendAdded(_ friendInfo: V2NIMFriend) { - if friendInfo.accountId == uid { - user?.friend = friendInfo - loadData() - } - } - - public func onFriendDeleted(_ accountId: String, deletionType: V2NIMFriendDeletionType) { - NEFriendUserCache.shared.removeFriendInfo(accountId) - if accountId == uid { - loadData() - } - } - - /// 好友信息变更回调 - /// - Parameter friendInfo: 好友信息 - public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { - if friendInfo.accountId == uid { - user?.friend = friendInfo - loadData() + /// 好友信息缓存更新 + /// - Parameter accountId: 用户 id + public func onContactChange(_ changeType: NEContactChangeType, _ contacts: [NEUserWithFriend]) { + for contact in contacts { + if contact.user?.accountId == uid { + user = contact + loadData() + } } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactViewController.swift index 234b383a..f8e90228 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactViewController.swift @@ -110,8 +110,17 @@ open class NEBaseContactViewController: UIViewController, UITableViewDelegate, U 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)) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() @@ -148,6 +157,13 @@ open class NEBaseContactViewController: UIViewController, UITableViewDelegate, U } } } + + if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { + navigationController?.isNavigationBarHidden = false + } else { + navigationController?.isNavigationBarHidden = true + } + loadData() viewModel.getAddApplicationUnreadCount(nil) } @@ -165,7 +181,7 @@ open class NEBaseContactViewController: UIViewController, UITableViewDelegate, U 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) + NotificationCenter.default.addObserver(self, selector: #selector(reloadData), name: NENotificationName.friendCacheInit, object: nil) } /// 清除未读数 @@ -263,6 +279,14 @@ open class NEBaseContactViewController: UIViewController, UITableViewDelegate, U } } + /// 重新加载数据 + func reloadData() { + // 从缓存中取 + if !NEFriendUserCache.shared.isEmpty() { + loadData() + } + } + // UITableViewDataSource open func numberOfSections(in tableView: UITableView) -> Int { viewModel.contacts.count @@ -343,6 +367,7 @@ open class NEBaseContactViewController: UIViewController, UITableViewDelegate, U Router.shared.use(ValidationMessageRouter, parameters: ["nav": navigationController as Any], closure: nil) + case ContactBlackListRouter: Router.shared.use(ContactBlackListRouter, parameters: ["nav": navigationController as Any], @@ -361,7 +386,11 @@ open class NEBaseContactViewController: UIViewController, UITableViewDelegate, U break default: - break + if info.router.count > 0 { + Router.shared.use(info.router, + parameters: ["nav": navigationController as Any], + closure: nil) + } } } else { if let friendItemClick = NEKitContactConfig.shared.ui.friendItemClick { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift index e9c8a718..73552ea5 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift @@ -28,6 +28,8 @@ open class NEBaseFindFriendViewController: NEContactBaseViewController, UITextFi public var isRequesting = false + public var searchBackViewTopAnchor: NSLayoutConstraint? + /// 搜索背景 public lazy var searchBackView: UIView = { let searchBackView = UIView() @@ -46,10 +48,15 @@ open class NEBaseFindFriendViewController: NEContactBaseViewController, UITextFi return searchImageView }() + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + searchBackViewTopAnchor?.constant = 20 + topConstant + } + override open func viewDidLoad() { super.viewDidLoad() title = localizable("add_friend") - navigationView.navTitle.text = title + navigationView.moreButton.isHidden = true emptyView.setText(localizable("user_not_exist")) setupUI() @@ -61,10 +68,11 @@ open class NEBaseFindFriendViewController: NEContactBaseViewController, UITextFi /// UI 初始化 open func setupUI() { view.addSubview(searchBackView) + searchBackViewTopAnchor = searchBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20 + topConstant) + searchBackViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ 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), ]) diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFusionContactSelectedController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFusionContactSelectedController.swift new file mode 100644 index 00000000..f14b1bb7 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFusionContactSelectedController.swift @@ -0,0 +1,212 @@ +//// 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 NECoreIM2Kit +import UIKit + +@objc public enum FusionContactType: NSInteger { + case FusionContactTypeUser + case FusionContactTypeAIUser +} + +@objc public protocol FusionContactSelectedDelegate: NSObjectProtocol { + /// 选择成员列表回调 + /// - Parameter model: 成员model + @objc func didSelectedUser(_ model: NEFusionContactCellModel) -> Bool + /// 取消选择成员列表回调 + /// - Parameter model: 成员model + @objc func didUnselectedUser(_ model: NEFusionContactCellModel) +} + +@objcMembers +open class NEBaseFusionContactSelectedController: UIViewController, UITableViewDelegate, UITableViewDataSource { + /// 数据管理 + let viewModel = FusionContactSelectedViewModel() + + /// cell 注册表 + public var fusionRegisterCellDic = [0: NEBaseFusionContactSelectedCell.self] + + /// 选择器代理 + public weak var delegate: FusionContactSelectedDelegate? + + /// 选择人数限制 + var limit = 10 + /// 当前类型 + var fusionType: FusionContactType? + /// 过滤存在用户 + var filterSet: Set? + + public init(filterIds: Set? = nil, type: FusionContactType) { + super.init(nibName: nil, bundle: nil) + fusionType = type + filterSet = filterIds + if fusionType == .FusionContactTypeUser { + title = localizable("contact_friend") + } else if fusionType == .FusionContactTypeAIUser { + title = localizable("contact_ai_user") + } + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// 选择器成员列表 + public lazy var fusionContactTableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.backgroundColor = .clear + tableView.delegate = self + tableView.dataSource = self + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + + /// 空占位图 + public lazy var fusionEmptyView: NEEmptyDataView = { + let emptyView = NEEmptyDataView( + imageName: "user_empty", + content: "", + frame: CGRect.zero + ) + emptyView.setText(localizable("no_friend")) + emptyView.translatesAutoresizingMaskIntoConstraints = false + emptyView.isUserInteractionEnabled = false + emptyView.isHidden = true + return emptyView + }() + + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + setupFusionContactSelectedUI() + } + + /// UI 初始化 + open func setupFusionContactSelectedUI() { + view.addSubview(fusionContactTableView) + NSLayoutConstraint.activate([ + fusionContactTableView.topAnchor.constraint(equalTo: view.topAnchor), + fusionContactTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + fusionContactTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + fusionContactTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + for (key, value) in fusionRegisterCellDic { + fusionContactTableView.register(value, forCellReuseIdentifier: "\(key)") + } + + view.addSubview(fusionEmptyView) + NSLayoutConstraint.activate([ + fusionEmptyView.topAnchor.constraint(equalTo: fusionContactTableView.topAnchor), + fusionEmptyView.bottomAnchor.constraint(equalTo: fusionContactTableView.bottomAnchor), + fusionEmptyView.leftAnchor.constraint(equalTo: fusionContactTableView.leftAnchor), + fusionEmptyView.rightAnchor.constraint(equalTo: fusionContactTableView.rightAnchor), + ]) + + weak var weakSelf = self + if fusionType == .FusionContactTypeUser { + viewModel.loadMemberDatas(filterSet) { error in + if let err = error { + weakSelf?.view.makeToast(err.localizedDescription) + } else { + weakSelf?.fusionContactTableView.reloadData() + if weakSelf?.viewModel.memberDatas.count ?? 0 <= 0 { + weakSelf?.fusionEmptyView.isHidden = false + } + } + } + } else if fusionType == .FusionContactTypeAIUser { + fusionEmptyView.setText(localizable("no_ai_user")) + viewModel.loadAIUserData(filterSet) + fusionContactTableView.reloadData() + if viewModel.memberDatas.count <= 0 { + fusionEmptyView.isHidden = false + } + } + + view.backgroundColor = .white + } + + open func setupFusionContactUI() {} + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + viewModel.memberDatas.count + } + + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cellModel = viewModel.memberDatas[indexPath.row] + if let cell = tableView.dequeueReusableCell(withIdentifier: "\(cellModel.type)", for: indexPath) as? NEBaseFusionContactSelectedCell { + cell.configFusionModel(cellModel) + return cell + } + return UITableViewCell() + } + + open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 56 + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let cellModel = viewModel.memberDatas[indexPath.row] + if cellModel.selected == false { + if delegate?.didSelectedUser(cellModel) == true { + cellModel.selected = !cellModel.selected + if let cell = tableView.cellForRow(at: indexPath) as? NEBaseFusionContactSelectedCell { + cell.configFusionModel(cellModel) + } + } else { + view.makeToast(String(format: localizable("exceeded_limit"), limit)) + } + } else { + delegate?.didUnselectedUser(cellModel) + cellModel.selected = !cellModel.selected + if let cell = tableView.cellForRow(at: indexPath) as? NEBaseFusionContactSelectedCell { + cell.configFusionModel(cellModel) + } + } + } + + func getCellModelUser(_ cellModel: NEFusionContactCellModel) -> V2NIMUser? { + if fusionType == .FusionContactTypeUser { + return cellModel.user?.user + } else if fusionType == .FusionContactTypeAIUser { + return cellModel.aiUser + } + return nil + } + + /// 外部触发反选操作 + /// - Parameter model: 数据模型 + public func unselectModel(_ model: NEFusionContactCellModel) { + for memberModel in viewModel.memberDatas { + if memberModel.getAccountId() == model.getAccountId() { + memberModel.selected = false + fusionContactTableView.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/NEContactUIKit/NEContactUIKit/Classes/Views/NEContactBaseViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEContactBaseViewController.swift index 9e402be0..bdedb023 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEContactBaseViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEContactBaseViewController.swift @@ -8,9 +8,37 @@ import UIKit /// 通讯录模块 ViewController 基类 @objcMembers open class NEContactBaseViewController: UIViewController, UIGestureRecognizerDelegate { - var topConstant: CGFloat = 0 + public var topConstant: CGFloat = 0 { + didSet { + navigationViewHeightAnchor?.constant = topConstant + } + } + + // 自定义导航栏高度布局约束 + public var navigationViewHeightAnchor: NSLayoutConstraint? + + // 自定义导航栏 public let navigationView = NENavigationView() + override open var title: String? { + get { + super.title + } + + set { + super.title = newValue + navigationView.navTitle.text = newValue + } + } + + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + public lazy var emptyView: NEEmptyDataView = { let view = NEEmptyDataView( imageName: "user_empty", @@ -23,30 +51,45 @@ open class NEContactBaseViewController: UIViewController, UIGestureRecognizerDel return view }() + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // 配置项:会话界面是否展示标题栏 + if !NEKitContactConfig.shared.ui.showTitleBar { + navigationController?.isNavigationBarHidden = true + navigationView.removeFromSuperview() + return + } + + if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { + navigationController?.isNavigationBarHidden = false + navigationView.removeFromSuperview() + setupBackUI() + } else { + navigationController?.isNavigationBarHidden = true + } + } + override open func viewDidLoad() { super.viewDidLoad() - - // Do any additional setup after loading the view. view.backgroundColor = .white edgesForExtendedLayout = [] - navigationController?.interactivePopGestureRecognizer?.delegate = self if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { - navigationController?.isNavigationBarHidden = false - setupBackUI() topConstant = 0 } else { - navigationController?.isNavigationBarHidden = true topConstant = NEConstant.navigationAndStatusHeight navigationView.translatesAutoresizingMaskIntoConstraints = false - navigationView.addBackButtonTarget(target: self, selector: #selector(backToPrevious)) - navigationView.moreButton.isHidden = true + navigationView.addBackButtonTarget(target: self, selector: #selector(backEvent)) + navigationView.addMoreButtonTarget(target: self, selector: #selector(toSetting)) + view.addSubview(navigationView) + navigationViewHeightAnchor = navigationView.heightAnchor.constraint(equalToConstant: topConstant) + navigationViewHeightAnchor?.isActive = true NSLayoutConstraint.activate([ navigationView.leftAnchor.constraint(equalTo: view.leftAnchor), navigationView.rightAnchor.constraint(equalTo: view.rightAnchor), navigationView.topAnchor.constraint(equalTo: view.topAnchor), - navigationView.heightAnchor.constraint(equalToConstant: topConstant), ]) } } @@ -57,23 +100,16 @@ open class NEContactBaseViewController: UIViewController, UIGestureRecognizerDel image: UIImage.ne_imageNamed(name: "backArrow"), style: .plain, target: self, - action: #selector(backToPrevious) + action: #selector(backEvent) ) backItem.accessibilityIdentifier = "id.backArrow" backItem.tintColor = UIColor(hexString: "333333") navigationItem.leftBarButtonItem = backItem } - open func backToPrevious() { + open func backEvent() { navigationController?.popViewController(animated: true) } - /* - // 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. - } - */ + + open func toSetting() {} } diff --git a/NEConversationUIKit/NEConversationUIKit.podspec b/NEConversationUIKit/NEConversationUIKit.podspec index c28da51c..4398fb6f 100644 --- a/NEConversationUIKit/NEConversationUIKit.podspec +++ b/NEConversationUIKit/NEConversationUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEConversationUIKit' - s.version = '10.2.1' + s.version = '10.3.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. @@ -27,7 +27,7 @@ TODO: Add long description of the pod here. s.source = { :git => 'ssh://git@g.hz.netease.com:22222/yunxin-app/xkit-ios.git', :tag => s.version.to_s } # s.social_media_url = 'https://twitter.com/' - s.ios.deployment_target = '11.0' + s.ios.deployment_target = '12.0' s.swift_version = '5.0' s.source_files = 'NEConversationUIKit/Classes/**/*' diff --git a/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings b/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings index 846b1d14..f7a9e63d 100644 --- a/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings +++ b/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings @@ -46,3 +46,6 @@ "tip"="[tip message]"; "leave_team"="离开群聊"; "leave_team_desc"="您已被移除群聊或群聊已解散"; + +"ai_user_pin_top"="Pin to top"; +"ai_user_cancel_pin_top"="Remove Pin top"; diff --git a/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings index 687ba504..b0a66073 100644 --- a/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -46,3 +46,6 @@ "tip"="[提醒消息]"; "leave_team"="离开群聊"; "leave_team_desc"="您已被移除群聊或群聊已解散"; + +"ai_user_pin_top"="PIN置顶"; +"ai_user_cancel_pin_top"="取消PIN置顶"; diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoader.h b/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoader.h new file mode 100644 index 00000000..497a8adc --- /dev/null +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoader.h @@ -0,0 +1,12 @@ +//// 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. + +#ifndef NEConversationLoader_h +#define NEConversationLoader_h + +@interface NEConversationLoader : NSObject + +@end + +#endif /* NEConversationLoader_h */ diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoader.m b/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoader.m new file mode 100644 index 00000000..b947b49e --- /dev/null +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoader.m @@ -0,0 +1,31 @@ +//// 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 "NEConversationLoader.h" +#import + +#if __has_include() +#import +#else +#import "NEConversationUIKit-Swift.h" +#endif + +@implementation NEConversationLoader + +static id gShareInstance = nil; + ++ (instancetype)shareInstance { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + gShareInstance = [[self alloc] init]; + }); + return gShareInstance; +} + ++ (void)load { + NSLog(@"NEConversationLoader load"); + [NEConversationLoaderService.shared setupInit]; +} + +@end diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoaderService.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoaderService.swift new file mode 100644 index 00000000..91e5e59d --- /dev/null +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationLoaderService.swift @@ -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 Foundation +import NEChatKit + +@objcMembers +public class NEConversationLoaderService: NSObject { + public static let shared = NEConversationLoaderService() + + override private init() { + super.init() + } + + /// 初始化方法 + /// 此方法会在模块被加载时调用 + public func setupInit() { + ChatKitClient.shared.registerInit(NEConversationService.shared) + } +} diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationService.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationService.swift new file mode 100644 index 00000000..b4771cf5 --- /dev/null +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Common/NEConversationService.swift @@ -0,0 +1,36 @@ +//// 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 + +@objcMembers +public class NEConversationService: NSObject, ChatServiceDelegate { + public static let shared = NEConversationService() + + override private init() { + super.init() + } + + /// 注册 NEConversationUIKit 初始化协议 + /// - Parameter params: 初始化参数 + public func setupInit(_ params: [String: Any]?) { + registerRouter(params) + } + + /// 注册路由 + /// - Parameter param: 参数 + public func registerRouter(_ param: [String: Any]?) { + // @功能初始化 + if IMKitConfigCenter.shared.enableAtMessage { + NEAtMessageManager.setupInstance() + } + + if let isFun = param?["isFun"] as? Bool, isFun { + ConversationRouter.registerFun() + } else { + ConversationRouter.register() + } + } +} diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift index 0e754d59..3acd0e9b 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift @@ -132,9 +132,14 @@ open class NEBaseConversationListCell: UITableViewCell { // last message if let lastMessage = conversationModel.conversation?.lastMessage { let text = contentForConversation(lastMessage: lastMessage) - let mutaAttri = NSMutableAttributedString(string: text) + let mutaAttri = NSMutableAttributedString() + if let lastContent = conversationModel.lastMessageConent { + mutaAttri.append(lastContent) + } else { + mutaAttri.append(NSAttributedString(string: text)) + } if let sessionId = conversationModel.conversation?.conversationId { - let isAtMessage = NEAtMessageManager.instance?.isAtCurrentUser(sessionId: sessionId) + let isAtMessage = NEAtMessageManager.instance?.isAtCurrentUser(conversationId: sessionId) if isAtMessage == true { let atStr = localizable("you_were_mentioned") mutaAttri.insert(NSAttributedString(string: atStr), at: 0) diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseStickTopCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseStickTopCell.swift new file mode 100644 index 00000000..39f329fb --- /dev/null +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseStickTopCell.swift @@ -0,0 +1,75 @@ +//// 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 NIMSDK +import UIKit + +@objcMembers +open class NEBaseStickTopCell: UICollectionViewCell { + /// 置顶会话头像 + public lazy var stickTopHeadImageView: NEUserHeaderView = { + let headView = NEUserHeaderView(frame: .zero) + headView.titleLabel.textColor = .white + headView.titleLabel.font = NEConstant.defaultTextFont(14) + headView.translatesAutoresizingMaskIntoConstraints = false + headView.layer.cornerRadius = 21 + headView.clipsToBounds = true + return headView + }() + + /// 置顶会话名称 + public lazy var stickTopNameLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = NEConstant.defaultTextFont(14) + label.textColor = UIColor.ne_greyText + label.accessibilityIdentifier = "id.name" + label.textAlignment = .center + return label + }() + + override public init(frame: CGRect) { + super.init(frame: frame) + setupStickTopCellUI() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// 初始化UI + open func setupStickTopCellUI() { + contentView.addSubview(stickTopHeadImageView) + contentView.addSubview(stickTopNameLabel) + } + + /// 绑定会话数据 + /// - Parameter 会话数据模型 + open func configAIUserCellData(_ model: NEAIUserModel?) { + guard let user = model?.aiUser else { return } + if let imageUrl = user.avatar, !imageUrl.isEmpty { + stickTopHeadImageView.setTitle("") + stickTopHeadImageView.sd_setImage(with: URL(string: imageUrl), completed: nil) + stickTopHeadImageView.backgroundColor = .clear + } else { + if let name = user.shortName() { + stickTopHeadImageView.setTitle(name) + } else if let showId = user.shortName() { + stickTopHeadImageView.setTitle(showId) + } + stickTopHeadImageView.sd_setImage(with: nil, completed: nil) + if let uid = user.accountId { + stickTopHeadImageView.backgroundColor = UIColor + .colorWithString(string: uid) + } + } + + if let name = user.name { + stickTopNameLabel.text = name + } else if let accountId = user.accountId { + stickTopNameLabel.text = accountId + } + } +} diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift index 536321d2..f74dc83f 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift @@ -46,6 +46,8 @@ open class NEBaseConversationController: UIViewController, UIGestureRecognizerDe } public var cellRegisterDic = [0: NEBaseConversationListCell.self] + /// 置顶l列表样式注册表 + public var stickTopCellRegisterDic = [0: NEBaseStickTopCell.self] public let viewModel = ConversationViewModel() public lazy var navigationView: TabNavigationView = { @@ -166,6 +168,17 @@ open class NEBaseConversationController: UIViewController, UIGestureRecognizerDe tableView.delegate = self tableView.dataSource = self tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + tableView.mj_footer = MJRefreshBackNormalFooter( refreshingTarget: self, refreshingAction: #selector(loadMoreData) @@ -180,6 +193,26 @@ open class NEBaseConversationController: UIViewController, UIGestureRecognizerDe return view }() + /// 置顶内容展示列表 + lazy var stickTopCollcetionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 0 + layout.minimumInteritemSpacing = 0 + let collcetionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collcetionView.backgroundColor = UIColor.clear + collcetionView.translatesAutoresizingMaskIntoConstraints = false + collcetionView.dataSource = self + collcetionView.delegate = self + collcetionView.isUserInteractionEnabled = true + collcetionView.isPagingEnabled = true + collcetionView.showsHorizontalScrollIndicator = false + collcetionView.showsVerticalScrollIndicator = false + collcetionView.alwaysBounceHorizontal = true + collcetionView.clipsToBounds = false + return collcetionView + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nil, bundle: nil) } @@ -222,12 +255,6 @@ open class NEBaseConversationController: UIViewController, UIGestureRecognizerDe setupSubviews() requestData() initialConfig() - - // 拉取好友信息 - DispatchQueue.global().async { - ContactRepo.shared.getUserList(accountIds: [IMKitClient.instance.account()]) { _, _ in } - ContactRepo.shared.getContactList { _, _ in } - } } override open func viewWillDisappear(_ animated: Bool) { @@ -327,6 +354,10 @@ open class NEBaseConversationController: UIViewController, UIGestureRecognizerDe tableView.register(value, forCellReuseIdentifier: "\(key)") } + for (key, value) in stickTopCellRegisterDic { + stickTopCollcetionView.register(value, forCellWithReuseIdentifier: "\(key)") + } + if let customController = NEKitConversationConfig.shared.ui.customController { customController(self) } @@ -354,13 +385,14 @@ open class NEBaseConversationController: UIViewController, UIGestureRecognizerDe func requestData() { viewModel.getConversationListByPage { [weak self] error, finished in + self?.viewModel.getAIUserList() 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!)" + desc: "CALLBACK requestData failed,error = \(error!)" ) } else { if let end = finished, end == true { @@ -448,7 +480,7 @@ extension NEBaseConversationController: TabNavigationViewDelegate { return } - if IMKitConfigCenter.shared.teamEnable { + if IMKitConfigCenter.shared.enableTeam { popListView.itemDatas = getPopListItems() popListView.frame = CGRect(origin: .zero, size: view.frame.size) popListView.removeSelf() @@ -473,13 +505,24 @@ extension NEBaseConversationController: TabNavigationViewDelegate { var filters = Set() filters.insert(IMKitClient.instance.account()) - Router.shared.use( - ContactUserSelectRouter, - parameters: ["nav": navigationController as Any, - "limit": inviteNumberLimit, - "filters": filters], - closure: nil - ) + if IMKitConfigCenter.shared.enableAIUser { + Router.shared.use( + ContactFusionSelectRouter, + parameters: ["nav": navigationController as Any, + "limit": inviteNumberLimit, + "filters": filters], + closure: nil + ) + } else { + Router.shared.use( + ContactUserSelectRouter, + parameters: ["nav": navigationController as Any, + "limit": inviteNumberLimit, + "filters": filters], + closure: nil + ) + } + weak var weakSelf = self Router.shared.register(TeamCreateDiscussResult) { param in print("create discuss ", param) @@ -508,13 +551,24 @@ extension NEBaseConversationController: TabNavigationViewDelegate { var filters = Set() filters.insert(IMKitClient.instance.account()) - Router.shared.use( - ContactUserSelectRouter, - parameters: ["nav": navigationController as Any, - "limit": inviteNumberLimit, - "filters": filters], - closure: nil - ) + if IMKitConfigCenter.shared.enableAIUser { + Router.shared.use( + ContactFusionSelectRouter, + parameters: ["nav": navigationController as Any, + "limit": inviteNumberLimit, + "filters": filters], + closure: nil + ) + } else { + Router.shared.use( + ContactUserSelectRouter, + parameters: ["nav": navigationController as Any, + "limit": inviteNumberLimit, + "filters": filters], + closure: nil + ) + } + weak var weakSelf = self Router.shared.register(TeamCreateSeniorResult) { param in print("create senior : ", param) @@ -534,6 +588,48 @@ extension NEBaseConversationController: TabNavigationViewDelegate { } } +extension NEBaseConversationController: UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { + public func numberOfSections(in collectionView: UICollectionView) -> Int { + 1 + } + + /// 置顶分区 + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + viewModel.aiUserListData.count + } + + /// 置顶数据源绑定 + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + let model = viewModel.aiUserListData[indexPath.row] + + let reusedId = "\(model.customType)" + let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reusedId, for: indexPath) + + if let c = cell as? NEBaseStickTopCell { + c.configAIUserCellData(model) + } + return cell + } + + /// 置顶cell大小,因为两套皮肤不同,在子类中具体实现 + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + CGSize.zero + } + + /// 置顶点击 + open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + let conversationModel = viewModel.aiUserListData[indexPath.row] + + if let accountId = conversationModel.aiUser?.accountId, let conversationId = V2NIMConversationIdUtil.p2pConversationId(accountId) { + Router.shared.use( + PushP2pChatVCRouter, + parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any], + closure: nil + ) + } + } +} + extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSource { public func numberOfSections(in tableView: UITableView) -> Int { 2 @@ -543,13 +639,7 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour if section == 0 { return viewModel.stickTopConversations.count } - - if section == 1 { - let conversationCount = viewModel.conversationListData.count - return conversationCount - } - - return 0 + return viewModel.conversationListData.count } open func tableView(_ tableView: UITableView, @@ -612,6 +702,28 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour rowActions.append(deleteAction) rowActions.append(topAction) + /* 会话数字人pin到顶部处理 + var model: NEConversationListModel? + if indexPath.section == 0 { + model = viewModel.stickTopConversations[indexPath.row] + } else if indexPath.section == 1 { + model = viewModel.conversationListData[indexPath.row] + } + if model?.conversation?.type == .CONVERSATION_TYPE_P2P { + if let conversationId = model?.conversation?.conversationId { + if let accountId = V2NIMConversationIdUtil.conversationTargetId(conversationId) { + if let enalbeAIUser = NEAIUserPinManager.shared.checkoutPinEnable(accountId) { + let pinToTop = NEAIUserPinManager.shared.checkoutUnPinAIUser(enalbeAIUser) + let pinToTopAction = UITableViewRowAction(style: .destructive, title: pinToTop ? localizable("ai_user_cancel_pin_top") : localizable("ai_user_pin_top")) { action, indexPath in + weakSelf?.pinToTopActionHandler(user: enalbeAIUser, pinTop: pinToTop) + } + pinToTopAction.backgroundColor = .green + rowActions.append(pinToTopAction) + } + } + } + } */ + return rowActions } @@ -646,6 +758,23 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour } } + /// pin 置顶 + open func pinToTopActionHandler(user: V2NIMUser, pinTop: Bool) { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(localizable("network_error")) + return + } + if let accountId = user.accountId { + if pinTop { + NEAIUserPinManager.shared.unpinAIUser(accountId) { error, finish in + } + } else { + NEAIUserPinManager.shared.pinAIUser(accountId) { error, finish in + } + } + } + } + /// 点击会话 open func topActionHandler(action: UITableViewRowAction?, indexPath: IndexPath, isTop: Bool) { if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { @@ -730,10 +859,10 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour _ completion: @escaping (NSError?) -> Void) { weak var weakSelf = self - if indexPath.section == 0 { + if isTop == true { viewModel.removeStickTop(conversation: conversation) { error in if let err = error { - NEALog.errorLog(ModuleName + " " + (weakSelf?.className ?? "ConversationController"), desc: "❌CALLBACK removeStickTopSession failed,error = \(err)") + NEALog.errorLog(ModuleName + " " + (weakSelf?.className ?? "ConversationController"), desc: "CALLBACK removeStickTopSession failed,error = \(err)") completion(error) return @@ -741,8 +870,6 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour NEALog.infoLog( ModuleName + " " + (weakSelf?.className ?? "ConversationController"), desc: "✅CALLBACK removeStickTopSession SUCCESS" ) - weakSelf?.moveTopToNormalConversation(conversation: conversation) - weakSelf?.reloadTableView() completion(nil) } @@ -753,14 +880,13 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour if let err = error { NEALog.errorLog( ModuleName + " " + (weakSelf?.className ?? "ConversationController"), - desc: "❌CALLBACK addStickTopSession failed,error = \(err)" + desc: "CALLBACK addStickTopSession failed,error = \(err)" ) completion(error) return } else { NEALog.infoLog(ModuleName + " " + (weakSelf?.className ?? "ConversationController"), desc: "✅CALLBACK addStickTopSession callback SUCCESS") - weakSelf?.moveNormalConversationToTop(conversation: conversation) weakSelf?.reloadTableView() completion(nil) } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift index ba45c942..02471e89 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift @@ -19,7 +19,6 @@ open class NEBaseConversationSearchController: NEConversationBaseViewController, 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 @@ -27,9 +26,20 @@ open class NEBaseConversationSearchController: NEConversationBaseViewController, tableView.sectionHeaderHeight = 30 tableView.sectionFooterHeight = 0 tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() + public var searchTextFieldTopAnchor: NSLayoutConstraint? public lazy var searchTextField: SearchTextField = { let textField = SearchTextField() let leftImageView = UIImageView(image: UIImage diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEConversationBaseViewController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEConversationBaseViewController.swift index e953378a..6e76a364 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEConversationBaseViewController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEConversationBaseViewController.swift @@ -8,10 +8,19 @@ import UIKit /// 会话模块 ViewController 基类 @objcMembers open class NEConversationBaseViewController: UIViewController, UIGestureRecognizerDelegate { - var topConstant: CGFloat = 0 + public var topConstant: CGFloat = 0 { + didSet { + navigationViewHeightAnchor?.constant = topConstant + } + } + + // 自定义导航栏高度布局约束 + public var navigationViewHeightAnchor: NSLayoutConstraint? + + // 自定义导航栏 public let navigationView = NENavigationView() - override public var title: String? { + override open var title: String? { get { super.title } @@ -22,26 +31,51 @@ open class NEConversationBaseViewController: UIViewController, UIGestureRecogniz } } + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // 配置项:会话界面是否展示标题栏 + if !NEKitConversationConfig.shared.ui.showTitleBar { + navigationController?.isNavigationBarHidden = true + navigationView.removeFromSuperview() + return + } + + if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { + navigationController?.isNavigationBarHidden = false + navigationView.removeFromSuperview() + setupBackUI() + } else { + navigationController?.isNavigationBarHidden = true + } + } + override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white - navigationController?.interactivePopGestureRecognizer?.delegate = self - setupBackUI() if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { - navigationController?.isNavigationBarHidden = false + topConstant = NEConstant.navigationAndStatusHeight } else { - navigationController?.isNavigationBarHidden = true topConstant = NEConstant.navigationAndStatusHeight navigationView.translatesAutoresizingMaskIntoConstraints = false navigationView.addBackButtonTarget(target: self, selector: #selector(backEvent)) - navigationView.moreButton.isHidden = true + view.addSubview(navigationView) + navigationViewHeightAnchor = navigationView.heightAnchor.constraint(equalToConstant: topConstant) + navigationViewHeightAnchor?.isActive = true NSLayoutConstraint.activate([ navigationView.leftAnchor.constraint(equalTo: view.leftAnchor), navigationView.rightAnchor.constraint(equalTo: view.rightAnchor), navigationView.topAnchor.constraint(equalTo: view.topAnchor), - navigationView.heightAnchor.constraint(equalToConstant: topConstant), ]) } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Model/NEConversationListModel.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Model/NEConversationListModel.swift new file mode 100644 index 00000000..2dd2db13 --- /dev/null +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Model/NEConversationListModel.swift @@ -0,0 +1,33 @@ +//// 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 +public class NEConversationListModel: NSObject, Comparable { + public static func < (lhs: NEConversationListModel, rhs: NEConversationListModel) -> Bool { + let time1 = lhs.conversation?.lastMessage?.messageRefer.createTime ?? lhs.conversation?.updateTime ?? 0 + let time2 = rhs.conversation?.lastMessage?.messageRefer.createTime ?? rhs.conversation?.updateTime ?? 0 + return time1 > time2 + } + + /// 会话 + public var conversation: V2NIMConversation? { + didSet { + if let lastMessage = conversation?.lastMessage, lastMessage.messageType == .MESSAGE_TYPE_TEXT, let text = lastMessage.text { + lastMessageConent = NEChatKitClient.instance.getEmojString(text, NEKitConversationConfig.shared.ui.conversationProperties.itemContentSize > 0 ? NEKitConversationConfig.shared.ui.conversationProperties.itemContentSize : 13) + } else { + lastMessageConent = nil + } + } + } + + /// 自定义类型 + public var customType = 0 + + /// 最后一条消息内容(包含表情解析) + var lastMessageConent: NSAttributedString? +} diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift index 000dba35..30beb204 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift @@ -8,7 +8,7 @@ import NIMSDK import UIKit @objcMembers -open class ConversationSearchViewModel: NSObject, NETeamListener, NEContactListener, NEIMKitClientListener { +open class ConversationSearchViewModel: NSObject, NETeamListener, NEIMKitClientListener { let conversationRepo = ConversationRepo.shared /// 群数据缓存 @@ -130,18 +130,6 @@ open class ConversationSearchViewModel: NSObject, NETeamListener, NEContactListe } } - // MARK: - NEContactListener - - /// 好友信息更新 - /// - Parameter friendInfo: 好友信息 - public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { - if let uid = friendInfo.accountId { - let model = ConversationSearchListModel() - model.userInfo = NEFriendUserCache.shared.getFriendInfo(uid) - friendDic[uid] = model - } - } - // MARK: - V2NIMTeamListener /// 群信息更新回调 @@ -204,3 +192,23 @@ open class ConversationSearchViewModel: NSObject, NETeamListener, NEContactListe } } } + +// MARK: - NEContactListener + +extension ConversationSearchViewModel: NEContactListener { + /// 好友信息缓存更新 + /// - Parameter accountId: 用户 id + public func onContactChange(_ changeType: NEContactChangeType, _ contacts: [NEUserWithFriend]) { + guard changeType == .update else { + return + } + + for contact in contacts { + if let accid = contact.user?.accountId { + let model = ConversationSearchListModel() + model.userInfo = contact + friendDic[accid] = model + } + } + } +} diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift index ef36ad3a..bfa91360 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift @@ -18,7 +18,7 @@ public protocol ConversationViewModelDelegate: NSObjectProtocol { public typealias ConversationCallBack = (NSError?, Bool?) -> Void @objcMembers -open class ConversationViewModel: NSObject, NEConversationListener, NETeamListener, NEChatListener, NEContactListener, NEIMKitClientListener { +open class ConversationViewModel: NSObject, NEConversationListener, NETeamListener, NEChatListener, NEContactListener, NEIMKitClientListener, AIUserPinListener, AIUserChangeListener { public weak var delegate: ConversationViewModelDelegate? private let className = "ConversationViewModel" @@ -37,6 +37,9 @@ open class ConversationViewModel: NSObject, NEConversationListener, NETeamListen /// 置顶会话数据 public var stickTopConversations = [NEConversationListModel]() + /// AI 数字人列表 + public var aiUserListData = [NEAIUserModel]() + /// 所有会话数据记录 public var conversationDic = [String: NEConversationListModel]() @@ -58,21 +61,25 @@ open class ConversationViewModel: NSObject, NEConversationListener, NETeamListen super.init() NotificationCenter.default.addObserver(self, selector: #selector(atMessageChange), name: Notification.Name(AtMessageChangeNoti), object: nil) NotificationCenter.default.addObserver(self, selector: #selector(deleteConversationNoti), name: NENotificationName.deleteConversationNotificationName, object: nil) - conversationRepo.addListener(self) + conversationRepo.addConversationListener(self) ChatRepo.shared.addChatListener(self) TeamRepo.shared.addTeamListener(self) ContactRepo.shared.addContactListener(self) IMKitClient.instance.addLoginListener(self) + NEAIUserPinManager.shared.addPinManagerListener(self) + NEAIUserManager.shared.addAIUserChangeListener(listener: self) } deinit { NEALog.infoLog(ModuleName + className(), desc: #function) NotificationCenter.default.removeObserver(self) - conversationRepo.removeListener(self) + conversationRepo.removeConversationListener(self) ChatRepo.shared.removeChatListener(self) TeamRepo.shared.removeTeamListener(self) ContactRepo.shared.removeContactListener(self) IMKitClient.instance.removeLoginListener(self) + NEAIUserPinManager.shared.removePinManagerListener(self) + NEAIUserManager.shared.removeAIUserChangeListener(listener: self) } func atMessageChange() { @@ -89,6 +96,10 @@ open class ConversationViewModel: NSObject, NEConversationListener, NETeamListen } } + open func getAIUserList() { + NEAIUserManager.shared.getAIUserList() + } + /// 分页获取会话列表 open func getConversationListByPage(_ completion: @escaping (NSError?, Bool?) -> Void) { if syncFinished == false { @@ -188,7 +199,9 @@ open class ConversationViewModel: NSObject, NEConversationListener, NETeamListen /// 处理置顶变更逻辑 public func filterStickTopData(_ conversations: [V2NIMConversation]) { + // 记录置顶 var changeTostickTopSet = Set() + // 记录移除置顶 var changeToUnStickTopDic = Set() for conversation in conversations { if let model = conversationDic[conversation.conversationId] { @@ -242,6 +255,12 @@ open class ConversationViewModel: NSObject, NEConversationListener, NETeamListen filterStickTopData(conversations) for conversation in conversations { + if let manager = NEAtMessageManager.instance { + if conversation.unreadCount == 0, manager.isAtCurrentUser(conversationId: conversation.conversationId) { + NEAtMessageManager.instance?.clearAtRecord(conversation.conversationId) + } + } + if checkDismissTeamNoti(conversation) { continue } @@ -277,7 +296,7 @@ open class ConversationViewModel: NSObject, NEConversationListener, NETeamListen /// 检查会话是否包含解散通知的变更 /// - Parameter conversation: 会话 public func checkDismissTeamNoti(_ conversation: V2NIMConversation) -> Bool { - if IMKitConfigCenter.shared.dismissTeamDeleteConversation == false { + if IMKitConfigCenter.shared.enabledismissTeamDeleteConversation == false { return false } @@ -403,7 +422,7 @@ open class ConversationViewModel: NSObject, NEConversationListener, NETeamListen /// - Parameter team: 群信息 public func onTeamDismissed(_ team: V2NIMTeam) { NEALog.infoLog(className(), desc: "onTeamDismissed team id : \(team.teamId) team name: \(team.name)") - if IMKitConfigCenter.shared.dismissTeamDeleteConversation { + if IMKitConfigCenter.shared.enabledismissTeamDeleteConversation { if let cid = V2NIMConversationIdUtil.teamConversationId(team.teamId) { didDeleteConversation(cid) } @@ -411,7 +430,7 @@ open class ConversationViewModel: NSObject, NEConversationListener, NETeamListen } private func didDeleteConversation(_ cid: String) { - if IMKitConfigCenter.shared.dismissTeamDeleteConversation == false { + if IMKitConfigCenter.shared.enabledismissTeamDeleteConversation == false { return } conversationRepo.deleteConversation(cid) { [weak self] error in @@ -452,6 +471,7 @@ open class ConversationViewModel: NSObject, NEConversationListener, NETeamListen getConversationListByPage(completion) /// 回调置空 callBack = nil + } else { NEALog.infoLog(className(), desc: #function + " retrieveConversationDatas") retrieveConversationDatas() @@ -501,14 +521,30 @@ open class ConversationViewModel: NSObject, NEConversationListener, NETeamListen NEALog.infoLog(className(), desc: "onConversationSyncFailed : \(error.desc)") } + /// 好友信息缓存更新 + /// - Parameter accountId: 用户 id public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { - NEALog.infoLog(className(), desc: "onFriendInfoChanged : \(friendInfo.accountId ?? "")") + NEALog.infoLog(className(), desc: "onFriendInfoUpdate : \(String(describing: friendInfo.accountId))") delegate?.reloadTableView() } - /// 用户信息变更回调 - /// - Parameter users: 用户信息列表 - public func onUserProfileChanged(_ users: [V2NIMUser]) { + // MARK: Pin Manager Listener + + public func userInfoDidChange() { + NEALog.infoLog(className(), desc: #function + "" + "conversaion view model userInfoDidChange") + getAIUserList() + } + + public func onAIUserChanged(aiUsers: [V2NIMAIUser]) { + aiUserListData.removeAll() + weak var weakSelf = self + for aiUser in aiUsers { + if NEAIUserPinManager.shared.checkoutUnPinAIUser(aiUser) == true { + let model = NEAIUserModel() + model.aiUser = aiUser + weakSelf?.aiUserListData.append(model) + } + } delegate?.reloadTableView() } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift b/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift index fa0a422f..89910cf4 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift @@ -80,10 +80,10 @@ public class ConversationUIConfig: NSObject { @objcMembers public class ConversationProperties: NSObject { /// 头像圆角大小 - public var avatarCornerRadius = 4.0 + public var avatarCornerRadius = 0.0 /// 头像类型 - public var avatarType: NEConversationAvatarType? + public var avatarType: NEConversationAvatarType = .rectangle /// 未被置顶的会话项的背景色 public var itemBackground: UIColor? diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift index 9dda6639..2f9ebdfd 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift @@ -9,7 +9,7 @@ import UIKit open class FunConversationListCell: NEBaseConversationListCell { var contentModel: NEConversationListModel? - /// 分割线视图 + /// 分隔线视图 public lazy var bottomLine: UIView = { let bottomLine = UIView() bottomLine.translatesAutoresizingMaskIntoConstraints = false @@ -54,10 +54,10 @@ open class FunConversationListCell: NEBaseConversationListCell { } override func initSubviewsLayout() { - if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .rectangle { - headImageView.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius - } else if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .cycle { + if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .cycle { headImageView.layer.cornerRadius = 24.0 + } else if NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius > 0 { + headImageView.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius } else { headImageView.layer.cornerRadius = 4.0 } @@ -66,6 +66,7 @@ open class FunConversationListCell: NEBaseConversationListCell { override open func configureData(_ sessionModel: NEConversationListModel?) { super.configureData(sessionModel) contentModel = sessionModel + if sessionModel?.conversation?.stickTop == true { contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemStickTopBackground ?? .funConversationBackgroundColor } else { diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift index d8b33c86..dcd3a5c4 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift @@ -7,7 +7,7 @@ import UIKit @objcMembers open class FunConversationSearchCell: NEBaseConversationSearchCell { - /// 分割线视图 + /// 分隔线视图 lazy var bottomLine: UIView = { let bottomLine = UIView() bottomLine.translatesAutoresizingMaskIntoConstraints = false diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunStickTopCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunStickTopCell.swift new file mode 100644 index 00000000..f96b13da --- /dev/null +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunStickTopCell.swift @@ -0,0 +1,26 @@ +//// 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 + +@objcMembers +open class FunStickTopCell: NEBaseStickTopCell { + override open func setupStickTopCellUI() { + super.setupStickTopCellUI() + stickTopHeadImageView.layer.cornerRadius = 4.0 + NSLayoutConstraint.activate([ + stickTopHeadImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + stickTopHeadImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15), + stickTopHeadImageView.widthAnchor.constraint(equalToConstant: 48), + stickTopHeadImageView.heightAnchor.constraint(equalToConstant: 48), + ]) + + NSLayoutConstraint.activate([ + stickTopNameLabel.topAnchor.constraint(equalTo: stickTopHeadImageView.bottomAnchor, constant: 6), + stickTopNameLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + stickTopNameLabel.leftAnchor.constraint(equalTo: stickTopHeadImageView.leftAnchor), + stickTopNameLabel.rightAnchor.constraint(equalTo: stickTopHeadImageView.rightAnchor), + ]) + } +} diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift index 9732c3fc..3488f113 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift @@ -24,6 +24,7 @@ open class FunConversationController: NEBaseConversationController { className = "FunConversationController" deleteButtonBackgroundColor = .funConversationdeleteActionColor cellRegisterDic = [0: FunConversationListCell.self] + stickTopCellRegisterDic = [0: FunStickTopCell.self] brokenNetworkViewHeight = 48 brokenNetworkView.errorIconView.isHidden = false brokenNetworkView.backgroundColor = .funConversationNetworkBrokenBackgroundColor @@ -86,6 +87,8 @@ open class FunConversationController: NEBaseConversationController { tableView.rowHeight = 72 tableView.backgroundColor = .funConversationBackgroundColor + + stickTopCollcetionView.frame = CGRect(x: 4, y: 0, width: view.frame.size.width - 8.0, height: 104) } override open func getPopListItems() -> [PopListItem] { @@ -104,4 +107,35 @@ open class FunConversationController: NEBaseConversationController { return items } + + /// 置顶cell大小 + override open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + CGSize(width: 72, height: 104) + } + + /// 置顶显示隐藏(根据是否有置顶数据) + open func setupFunStickTopView() { + if viewModel.aiUserListData.count > 0 { + if let headerView = tableView.tableHeaderView { + if headerView.isKind(of: UICollectionView.self) == false { + NEALog.infoLog(className(), desc: #function + " set top conversation header \(stickTopCollcetionView)") + tableView.tableHeaderView = stickTopCollcetionView + } + } else { + NEALog.infoLog(className(), desc: #function + " set top conversation header \(stickTopCollcetionView)") + tableView.tableHeaderView = stickTopCollcetionView + } + stickTopCollcetionView.reloadData() + } else { + if tableView.tableHeaderView != nil { + tableView.tableHeaderView = nil + } + } + } + + override open func reloadTableView() { + super.reloadTableView() + NEALog.infoLog(className(), desc: #function + " reloadTableView in fun conversation controller stick top count \(viewModel.stickTopConversations.count)") + setupFunStickTopView() + } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift index f5305c33..96ea80a6 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift @@ -32,9 +32,13 @@ open class FunConversationSearchController: NEBaseConversationSearchController { override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funConversationBackgroundColor + emptyView.setEmptyImage(name: "fun_user_empty") + } + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) navigationController?.isNavigationBarHidden = true navigationView.isHidden = true - emptyView.setEmptyImage(name: "fun_user_empty") } /// 初始化子视图 @@ -47,8 +51,9 @@ open class FunConversationSearchController: NEBaseConversationSearchController { searchTextField.textColor = .black searchTextField.layer.cornerRadius = 4 searchTextField.backgroundColor = .white + searchTextFieldTopAnchor = searchTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.statusBarHeight + 12) + searchTextFieldTopAnchor?.isActive = true NSLayoutConstraint.activate([ - searchTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.statusBarHeight + 12), searchTextField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 8), searchTextField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -72), searchTextField.heightAnchor.constraint(equalToConstant: 36), diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift index 267dcacd..d55803a7 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift @@ -45,7 +45,9 @@ open class NEAtMessageManager: NSObject, NEIMKitClientListener, NEChatListener { /// 初始化 public static func setupInstance() { - NEAtMessageManager.instance = NEAtMessageManager() + if NEAtMessageManager.instance == nil { + NEAtMessageManager.instance = NEAtMessageManager() + } } /// 登录状态变更 @@ -98,10 +100,10 @@ open class NEAtMessageManager: NSObject, NEIMKitClientListener, NEChatListener { /// 判断是否是当前用户 /// - Parameter sessionId: 会话id /// - Returns: 是否是当前用户 - open func isAtCurrentUser(sessionId: String) -> Bool { + open func isAtCurrentUser(conversationId: String) -> Bool { let dic = getMessageDic() - if let model = dic[sessionId], model.isRead == false { + if let model = dic[conversationId], model.isRead == false { NEALog.infoLog(className(), desc: "read == false") return true } @@ -159,12 +161,12 @@ open class NEAtMessageManager: NSObject, NEIMKitClientListener, NEChatListener { 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) + weakSelf?.addAtRecordWithCompare(message: message, record: &temDic) isExistAtMessage = true continue } if dic[currentAccid] != nil { - weakSelf?.addAtRecord(message: message, record: &temDic) + weakSelf?.addAtRecordWithCompare(message: message, record: &temDic) isExistAtMessage = true continue } @@ -204,12 +206,12 @@ open class NEAtMessageManager: NSObject, NEIMKitClientListener, NEChatListener { 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) + weakSelf?.addAtRecordWithCompare(message: message, record: &temDic) isExistAtMessage = true return } if dic[accid] != nil { - weakSelf?.addAtRecord(message: message, record: &temDic) + weakSelf?.addAtRecordWithCompare(message: message, record: &temDic) isExistAtMessage = true return } @@ -315,6 +317,60 @@ open class NEAtMessageManager: NSObject, NEIMKitClientListener, NEChatListener { return didRemove } + /// 添加at消息记录(有时间错比较) + /// - Parameter message: 消息 + /// - Parameter record: at 消息记录缓存 + private func addAtRecordWithCompare(message: V2NIMMessage, record: inout [String: AtMEMessageRecord]) { + guard let conversationId = message.conversationId else { + NEALog.infoLog(className(), desc: #function + " addAtRecord conversationId nil ") + return + } + + let semaphore = DispatchSemaphore(value: 0) + var compareTimestap: Double = 0 + ConversationRepo.shared.getConversationReadTime(conversationId) { [weak self] timestap, error in + if error != nil { + NEALog.infoLog(self?.className() ?? "", desc: #function + "getConversationReadTime error : \(error?.localizedDescription ?? "")") + } + + if let t = timestap { + compareTimestap = t + NEALog.infoLog(self?.className() ?? "", desc: #function + "getConversationReadTime time \(t)") + } + semaphore.signal() + } + + semaphore.wait() + + if compareTimestap > 0, message.createTime > compareTimestap { + if let atMeRecord = record[message.conversationId ?? ""] { + let lastTime = atMeRecord.lastTime?.doubleValue ?? 0 + if lastTime < message.createTime { + let atMessage = AtMessageModel() + atMeRecord.isRead = false + atMessage.messageId = message.messageClientId + atMeRecord.lastTime = NSNumber(value: message.createTime) + atMessage.messageTime = NSNumber(value: message.createTime) + atMeRecord.atMessages[message.messageClientId ?? ""] = NSNumber(value: message.createTime) + if let conversationId = message.conversationId { + record[conversationId] = atMeRecord + } + } + } else { + let atMeRecord = AtMEMessageRecord() + let atMessage = AtMessageModel() + atMeRecord.isRead = false + atMessage.messageId = message.messageClientId + atMeRecord.lastTime = NSNumber(value: message.createTime) + atMessage.messageTime = NSNumber(value: message.createTime) + atMeRecord.atMessages[message.messageClientId ?? ""] = NSNumber(value: message.createTime) + if let conversationId = message.conversationId { + record[conversationId] = atMeRecord + } + } + } + } + /// 添加at消息记录 /// - Parameter message: 消息 /// - Parameter record: at 消息记录缓存 diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift index 54a514fa..a633ddfb 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift @@ -36,10 +36,10 @@ open class ConversationListCell: NEBaseConversationListCell { } override func initSubviewsLayout() { - if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .rectangle { - headImageView.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius - } else if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .cycle { + if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .cycle { headImageView.layer.cornerRadius = 21.0 + } else if NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius > 0 { + headImageView.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius } else { headImageView.layer.cornerRadius = 21.0 } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/StickTopCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/StickTopCell.swift new file mode 100644 index 00000000..a2fb5d4e --- /dev/null +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/StickTopCell.swift @@ -0,0 +1,24 @@ +//// 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 + +@objcMembers +open class StickTopCell: NEBaseStickTopCell { + override open func setupStickTopCellUI() { + super.setupStickTopCellUI() + NSLayoutConstraint.activate([ + stickTopHeadImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + stickTopHeadImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15), + stickTopHeadImageView.widthAnchor.constraint(equalToConstant: 42), + stickTopHeadImageView.heightAnchor.constraint(equalToConstant: 42), + ]) + NSLayoutConstraint.activate([ + stickTopNameLabel.topAnchor.constraint(equalTo: stickTopHeadImageView.bottomAnchor, constant: 6), + stickTopNameLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + stickTopNameLabel.leftAnchor.constraint(equalTo: stickTopHeadImageView.leftAnchor), + stickTopNameLabel.rightAnchor.constraint(equalTo: stickTopHeadImageView.rightAnchor), + ]) + } +} diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift index 5d141c98..d3008e10 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift @@ -20,6 +20,14 @@ open class ConversationController: NEBaseConversationController { return searchBarButton }() + /// 数字人分割线 + public lazy var pinUserDividerLine: UIView = { + let line = UIView() + line.backgroundColor = UIColor.ne_navLineColor + line.translatesAutoresizingMaskIntoConstraints = false + return line + }() + /// 添加按钮 public lazy var addBarButton: UIButton = { let addBarButton = UIButton() @@ -33,6 +41,7 @@ open class ConversationController: NEBaseConversationController { super.init(nibName: nil, bundle: nil) className = "ConversationController" cellRegisterDic = [0: ConversationListCell.self] + stickTopCellRegisterDic = [0: StickTopCell.self] } public required init?(coder: NSCoder) { @@ -57,6 +66,7 @@ open class ConversationController: NEBaseConversationController { navigationView.searchBtn.isHidden = true navigationItem.rightBarButtonItems = [addBarItem] } + if !NEKitConversationConfig.shared.ui.showTitleBarRightIcon { navigationView.addBtn.isHidden = true navigationItem.rightBarButtonItems = [searchBarItem] @@ -70,5 +80,42 @@ open class ConversationController: NEBaseConversationController { tableView.rowHeight = 62 tableView.backgroundColor = .white + + // 设置置顶列表宽高 + stickTopCollcetionView.frame = CGRect(x: 10, y: 0, width: view.frame.size.width - 20.0, height: 181 - NEConstant.navigationAndStatusHeight) + + stickTopCollcetionView.addSubview(pinUserDividerLine) + pinUserDividerLine.frame = CGRect(x: -10, y: 180 - NEConstant.navigationAndStatusHeight, width: view.frame.size.width + 20.0, height: 1.0) + } + + /// 置顶cell大小 + override open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + CGSize(width: 62, height: 181 - NEConstant.navigationAndStatusHeight) + } + + /// 置顶显示隐藏(根据是否有置顶数据) + open func setupNormalStickTopView() { + if viewModel.aiUserListData.count > 0 { + if let headerView = tableView.tableHeaderView { + if headerView.isKind(of: UICollectionView.self) == false { + tableView.tableHeaderView = stickTopCollcetionView + } + } else { + tableView.tableHeaderView = stickTopCollcetionView + } + stickTopCollcetionView.reloadData() + navigationView.titleBarBottomLine.isHidden = true + } else { + if tableView.tableHeaderView != nil { + tableView.tableHeaderView = nil + } + navigationView.titleBarBottomLine.isHidden = false + } + } + + override open func reloadTableView() { + super.reloadTableView() + NEALog.infoLog(className(), desc: #function + " reloadTableView in conversation controller stick top count \(viewModel.stickTopConversations.count)") + setupNormalStickTopView() } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift index 7ec2bd04..73fbf067 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift @@ -29,23 +29,28 @@ open class ConversationSearchController: NEBaseConversationSearchController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) tag = "ConversationSearchController" - navigationView.backgroundColor = .white - navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { super.init(coder: coder) } + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + searchTextFieldTopAnchor?.constant = topConstant + 20 + } + override open func setupSubviews() { super.setupSubviews() + title = commonLocalizable("search") + navigationController?.navigationBar.backgroundColor = .white + navigationView.backgroundColor = .white + navigationView.moreButton.isHidden = true searchTextField.placeholder = localizable("search_keyword") + searchTextFieldTopAnchor = searchTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant + 20) + searchTextFieldTopAnchor?.isActive = true NSLayoutConstraint.activate([ - searchTextField.topAnchor.constraint( - equalTo: view.topAnchor, - constant: NEConstant.navigationHeight + NEConstant.statusBarHeight + 20 - ), searchTextField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), searchTextField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), searchTextField.heightAnchor.constraint(equalToConstant: 32), diff --git a/NEMapKit/NEMapKit.podspec b/NEMapKit/NEMapKit.podspec index e3e9e35f..bba16931 100644 --- a/NEMapKit/NEMapKit.podspec +++ b/NEMapKit/NEMapKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEMapKit' - s.version = '10.2.1' + s.version = '10.3.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. @@ -28,7 +28,7 @@ TODO: Add long description of the pod here. s.source = { :git => 'ssh://git@g.hz.netease.com:22222/yunxin-app/xkit-ios.git', :tag => s.version.to_s } # s.social_media_url = 'https://twitter.com/' - s.ios.deployment_target = '11.0' + s.ios.deployment_target = '12.0' s.source_files = 'NEMapKit/Classes/**/*' # s.resource = 'NEMapKit/Assets/**/*' diff --git a/NEMapKit/NEMapKit/Classes/Controller/NELocationViewController.swift b/NEMapKit/NEMapKit/Classes/Controller/NELocationViewController.swift index 83577b2f..bc8373b2 100644 --- a/NEMapKit/NEMapKit/Classes/Controller/NELocationViewController.swift +++ b/NEMapKit/NEMapKit/Classes/Controller/NELocationViewController.swift @@ -132,6 +132,15 @@ open class NELocationViewController: UIViewController, NELocationBottomViewDeleg tableView.rowHeight = 72 tableView.backgroundColor = .white tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() diff --git a/NETeamUIKit/NETeamUIKit.podspec b/NETeamUIKit/NETeamUIKit.podspec index fd795100..88c3b5a4 100644 --- a/NETeamUIKit/NETeamUIKit.podspec +++ b/NETeamUIKit/NETeamUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NETeamUIKit' - s.version = '10.2.1' + s.version = '10.3.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. @@ -31,12 +31,11 @@ TODO: Add long description of the pod here. 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' } - s.ios.deployment_target = '11.0' + s.ios.deployment_target = '12.0' s.swift_version = '5.0' s.source_files = 'NETeamUIKit/Classes/**/*' s.resource = 'NETeamUIKit/Assets/**/*' - s.dependency 'NECommonUIKit' - s.dependency 'NEChatKit' + s.dependency 'NEChatUIKit' end diff --git a/NETeamUIKit/NETeamUIKit/Classes/Common/NEBaseTeamRouter.swift b/NETeamUIKit/NETeamUIKit/Classes/Common/NEBaseTeamRouter.swift index dfdb2b81..4ad42d83 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Common/NEBaseTeamRouter.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Common/NEBaseTeamRouter.swift @@ -94,12 +94,26 @@ open class TeamRouter: NSObject { result["teamId"] = creatResult?.team?.teamId // 先插入【成功创建群聊】消息 - ChatRepo.shared.sendCreateAdavanceNoti(creatResult?.team?.teamId ?? "", .TEAM_TYPE_NORMAL, localizable("create_senior_team_noti")) { error in - print("send noti message : ", error as Any) - - // 再邀请用户 - repo.inviteMembers(creatResult?.team?.teamId ?? "", .TEAM_TYPE_NORMAL, accids) { error, members in - print("invite users : \(accids)", error as Any) + if let tid = creatResult?.team?.teamId, let cid = V2NIMConversationIdUtil.teamConversationId(tid) { + TeamProvider.shared.getTeamInfo(tid, .TEAM_TYPE_NORMAL) { retTeam, error in + print("getTeamInfo : ", error as Any) + var createTime: TimeInterval = 0 + if let team = retTeam { + createTime = team.createTime + } else { + createTime = Date().timeIntervalSince1970 + } + + let message = V2NIMMessageCreator.createTipsMessage(localizable("create_senior_team_noti")) + ChatRepo.shared.insertMessageToLocal(message: message, + conversationId: cid, + createTime: createTime) { _, error in + print("send noti message : ", error as Any) + // 再邀请用户 + repo.inviteTeamMembers(tid, .TEAM_TYPE_NORMAL, accids) { error, members in + print("invite users : \(accids)", error as Any) + } + } } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoader.h b/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoader.h new file mode 100644 index 00000000..7eb27690 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoader.h @@ -0,0 +1,12 @@ +//// 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. + +#ifndef NETeamLoader_h +#define NETeamLoader_h + +@interface NETeamLoader : NSObject + +@end + +#endif /* NETeamLoader_h */ diff --git a/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoader.m b/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoader.m new file mode 100644 index 00000000..e225a435 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoader.m @@ -0,0 +1,31 @@ +//// 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 "NETeamLoader.h" +#import + +#if __has_include() +#import +#else +#import "NETeamUIKit-Swift.h" +#endif + +@implementation NETeamLoader + +static id gShareInstance = nil; + ++ (instancetype)shareInstance { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + gShareInstance = [[self alloc] init]; + }); + return gShareInstance; +} + ++ (void)load { + NSLog(@"NETeamUIKit load"); + [NETeamLoaderService.shared setupInit]; +} + +@end diff --git a/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoaderService.swift b/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoaderService.swift new file mode 100644 index 00000000..fbedd9ec --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamLoaderService.swift @@ -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 Foundation +import NEChatKit + +@objcMembers +public class NETeamLoaderService: NSObject { + public static let shared = NETeamLoaderService() + + override private init() { + super.init() + } + + /// 初始化方法 + /// 此方法会在模块被加载时调用 + public func setupInit() { + ChatKitClient.shared.registerInit(NETeamService.shared) + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamService.swift b/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamService.swift new file mode 100644 index 00000000..af64a325 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Common/NETeamService.swift @@ -0,0 +1,30 @@ +//// 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 + +@objcMembers +public class NETeamService: NSObject, ChatServiceDelegate { + public static let shared = NETeamService() + + override private init() { + super.init() + } + + /// 注册 NETeamUIKit 初始化协议 + /// - Parameter params: 初始化参数 + public func setupInit(_ params: [String: Any]?) { + registerRouter(params) + } + + /// 注册路由 + /// - Parameter param: 参数 + public func registerRouter(_ param: [String: Any]?) { + if let isFun = param?["isFun"] as? Bool, isFun { + TeamRouter.registerFun() + } else { + TeamRouter.register() + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift index c0522288..2d3df7a3 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift @@ -30,7 +30,7 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { return arrowImageView }() - /// 分割线 + /// 分隔线 lazy var dividerLineView: UIView = { let dividerLineView = UIView() dividerLineView.translatesAutoresizingMaskIntoConstraints = false diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift index 0ccf7ec0..33889034 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift @@ -11,8 +11,6 @@ open class TeamHistoryMessageController: NEBaseTeamHistoryMessageController { override public init(teamId: String?) { super.init(teamId: teamId) tag = "TeamHistoryMessageController" - navigationView.backgroundColor = .white - navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { @@ -21,6 +19,9 @@ open class TeamHistoryMessageController: NEBaseTeamHistoryMessageController { override open func setupSubviews() { super.setupSubviews() + navigationView.backgroundColor = .white + navigationController?.navigationBar.backgroundColor = .white + NSLayoutConstraint.activate([ searchTextField.topAnchor.constraint( equalTo: view.topAnchor, diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift index 70a3fb2d..07c5ecdb 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift @@ -14,9 +14,6 @@ open class TeamInfoViewController: NEBaseTeamInfoViewController { SettingCellType.SettingArrowCell.rawValue: TeamArrowSettingCell.self, SettingCellType.SettingHeaderCell.rawValue: TeamSettingHeaderCell.self, ] - view.backgroundColor = .ne_lightBackgroundColor - navigationView.backgroundColor = .ne_lightBackgroundColor - navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor } public required init?(coder: NSCoder) { @@ -26,6 +23,8 @@ open class TeamInfoViewController: NEBaseTeamInfoViewController { override open func setupUI() { super.setupUI() view.backgroundColor = .ne_lightBackgroundColor + navigationView.backgroundColor = .ne_lightBackgroundColor + navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor } // MARK: UITableViewDelegate, UITableViewDataSource diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerController.swift index 82568a93..4223f6f8 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerController.swift @@ -9,8 +9,6 @@ import UIKit 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, @@ -24,6 +22,8 @@ open class TeamManagerController: NEBaseTeamManagerController { override open func viewDidLoad() { super.viewDidLoad() + navigationView.backgroundColor = .ne_lightBackgroundColor + navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor } override open func didManagerClick() { diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift index 67fb2011..079e8d44 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift @@ -12,8 +12,6 @@ import UIKit open class TeamSettingViewController: NEBaseTeamSettingViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - navigationView.backgroundColor = .ne_lightBackgroundColor - navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor className = "TeamSettingViewController" cellClassDic = [ SettingCellType.SettingArrowCell.rawValue: TeamArrowSettingCell.self, @@ -47,7 +45,7 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { return arrowImageView }() - /// 分割线 + /// 分隔线 lazy var dividerLineView: UIView = { let dividerLineView = UIView() dividerLineView.translatesAutoresizingMaskIntoConstraints = false @@ -83,6 +81,9 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { override open func setupUI() { super.setupUI() + navigationView.backgroundColor = .ne_lightBackgroundColor + navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor + teamHeaderView.layer.cornerRadius = 21.0 addButton.setImage(coreLoader.loadImage("add"), for: .normal) } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/Common/NETeamMemberCache.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/Common/NETeamMemberCache.swift index d1accdc9..e0436ffe 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/Common/NETeamMemberCache.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/Common/NETeamMemberCache.swift @@ -44,6 +44,7 @@ open class NETeamMemberCache: NSObject, NETeamListener, NEIMKitClientListener, N NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didTapHeader), name: NENotificationName.didTapHeader, object: nil) } deinit { @@ -76,6 +77,17 @@ open class NETeamMemberCache: NSObject, NETeamListener, NEIMKitClientListener, N clearCache() } + /// 点击消息发送者头像 + /// 拉取最新用户信息后刷新消息发送者信息 + /// - Parameter noti: 通知对象 + func didTapHeader(_ noti: Notification) { + if let user = noti.object as? NEUserWithFriend, + let accid = user.user?.accountId { + cacheDic[accid]?.nimUser = user + updateFinish() + } + } + /// 设置缓存(对一个新群设置缓存会移除之前群的缓存,单例只保存一个群的成员缓存) /// - Parameter teamId: 群id /// - Parameter members: 群成员数据对象列表 @@ -109,6 +121,15 @@ open class NETeamMemberCache: NSObject, NETeamListener, NEIMKitClientListener, N return nil } + /// 判断是否是当前群成员 + public func isCurrentMember(_ accountId: String) -> Bool { + if cacheDic[accountId] != nil { + return true + } else { + return false + } + } + /// 好友信息更新 /// - Parameter friendInfo: 好友信息 public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamAvatarViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamAvatarViewController.swift index ca9b0e6d..77285788 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamAvatarViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamAvatarViewController.swift @@ -9,7 +9,7 @@ import UIKit @objcMembers open class NEBaseTeamAvatarViewController: NEBaseViewController, UICollectionViewDelegate, - UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate { + UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate, UIImagePickerControllerDelegate { public typealias SaveCompletion = () -> Void public var block: SaveCompletion? public var team: V2NIMTeam? diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift index ee11f73d..1a9b408e 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift @@ -22,7 +22,6 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField let tableView = UITableView(frame: .zero, style: .plain) tableView.translatesAutoresizingMaskIntoConstraints = false tableView.separatorStyle = .none - tableView.keyboardDismissMode = .onDrag tableView.delegate = self tableView.dataSource = self tableView.register( @@ -33,6 +32,16 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField tableView.backgroundColor = .white tableView.sectionHeaderHeight = 30 tableView.sectionFooterHeight = 0 + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() @@ -112,6 +121,8 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), emptyView.topAnchor.constraint(equalTo: tableView.topAnchor), ]) + + navigationView.moreButton.isHidden = true } open func initialConfig() { @@ -154,7 +165,7 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField } else { NEALog.errorLog( ModuleName + " " + (weakSelf?.tag ?? "TeamHistoryMessageController"), - desc: "❌searchMessages failed, error = \(error!)" + desc: "searchMessages failed, error = \(error!)" ) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamInfoViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamInfoViewController.swift index 3dfb1ee2..049043b6 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamInfoViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamInfoViewController.swift @@ -24,6 +24,16 @@ open class NEBaseTeamInfoViewController: NEBaseViewController, UITableViewDelega tableView.delegate = self tableView.separatorStyle = .none tableView.sectionHeaderHeight = 0 + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() @@ -66,6 +76,7 @@ open class NEBaseTeamInfoViewController: NEBaseViewController, UITableViewDelega for (key, value) in registerCellDic { contentTableView.register(value, forCellReuseIdentifier: "\(key)") } + navigationView.moreButton.isHidden = true } // MARK: UITableViewDelegate, UITableViewDataSource diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerController.swift index c7110585..7a9646d3 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerController.swift @@ -27,6 +27,13 @@ open class NEBaseTeamManagerController: NEBaseViewController, UITableViewDelegat tableView .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = 0.0 } @@ -56,6 +63,7 @@ open class NEBaseTeamManagerController: NEBaseViewController, UITableViewDelegat for (key, value) in cellClassDic { contentTableView.register(value, forCellReuseIdentifier: "\(key)") } + navigationView.moreButton.isHidden = true } /// 页面出现回到(系统类生命周期函数) diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift index bea3795a..1f498c23 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift @@ -22,11 +22,17 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel 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)) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = 0.0 } @@ -65,6 +71,7 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel for (key, value) in cellClassDic { contentTableView.register(value, forCellReuseIdentifier: "\(key)") } + navigationView.moreButton.isHidden = true } open func numberOfSections(in tableView: UITableView) -> Int { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift index f8f4318d..401db6c4 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift @@ -43,10 +43,16 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe 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)) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = 0.0 } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift index 13902ddc..22180fef 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift @@ -60,10 +60,16 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat tableView .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = 0.0 } - tableView.keyboardDismissMode = .onDrag return tableView }() @@ -99,7 +105,7 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat addObserver() viewModel.delegate = self viewModel.teamId = teamId - + navigationView.moreButton.isHidden = true weak var weakSelf = self if let tid = teamId { weakSelf?.viewModel.getTeamInfo(tid) { teamInfo, error in @@ -209,17 +215,12 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat viewModel.searchDatas.removeAll() if let text = searchTextField.text, text.count > 0 { 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) + if let teamName = model.atNameInTeam() { + if teamName.contains(text) { + viewModel.searchDatas.append(model) + } } } - } else { emptyView.isHidden = true } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamSettingViewController.swift index de40f88e..81f027a8 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamSettingViewController.swift @@ -43,6 +43,13 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi tableView .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = 0.0 } @@ -124,6 +131,7 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi title = localizable("setting") weak var weakSelf = self viewModel.delegate = self + navigationView.moreButton.isHidden = true if let tid = teamId { viewModel.getCurrentMember(IMKitClient.instance.account(), tid) { member, error in if let currentMember = member { @@ -284,7 +292,12 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } param["limit"] = (viewModel.teamInfoModel?.team?.memberLimit ?? inviteNumberLimit + filters.count) - filters.count - Router.shared.use(ContactUserSelectRouter, parameters: param, closure: nil) + + if IMKitConfigCenter.shared.enableAIUser { + Router.shared.use(ContactFusionSelectRouter, parameters: param, closure: nil) + } else { + Router.shared.use(ContactUserSelectRouter, parameters: param, closure: nil) + } } /// 退出/解散群聊 diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift index bc686dba..93abb2ae 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.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 NEChatUIKit import NECommonKit import NIMSDK import UIKit @@ -46,7 +47,7 @@ open class NEBaseHistoryMessageCell: UITableViewCell { return label }() - /// 分割线 + /// 分隔线 public lazy var bottomLine: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -85,7 +86,7 @@ open class NEBaseHistoryMessageCell: UITableViewCell { open func configData(message: HistoryMessageModel?) { if message?.fullName?.count ?? 0 <= 0 { - message?.fullName = message?.imMessage?.senderId + message?.fullName = ChatMessageHelper.getSenderId(message?.imMessage) } titleLabel.text = message?.fullName timeLabel.text = message?.time @@ -96,7 +97,7 @@ open class NEBaseHistoryMessageCell: UITableViewCell { } else { headView.setTitle(message?.shortName ?? "") headView.sd_setImage(with: nil, completed: nil) - headView.backgroundColor = UIColor.colorWithString(string: message?.imMessage?.senderId) + headView.backgroundColor = UIColor.colorWithString(string: ChatMessageHelper.getSenderId(message?.imMessage)) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamHistoryMessageViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamHistoryMessageViewModel.swift index d052795d..c2bff0bc 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamHistoryMessageViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamHistoryMessageViewModel.swift @@ -2,12 +2,13 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatUIKit import NECoreIM2Kit import NIMSDK import UIKit @objcMembers -open class TeamHistoryMessageViewModel: NSObject, NETeamListener, NEContactListener { +open class TeamHistoryMessageViewModel: NSObject, NETeamListener { /// 群信息 public var teamInfoModel: NETeamInfoModel? /// 搜索结果 @@ -31,7 +32,10 @@ open class TeamHistoryMessageViewModel: NSObject, NETeamListener, NEContactListe contactRepo.addContactListener(self) } - deinit {} + deinit { + teamRepo.removeTeamListener(self) + contactRepo.removeContactListener(self) + } /// 设置从上一个页面传入的成员 public func setupCache() { @@ -64,17 +68,21 @@ open class TeamHistoryMessageViewModel: NSObject, NETeamListener, NEContactListe param.teamIds = [teamId] weak var weakSelf = self - chatRepo.searchMessages(params: param) { error, messages in + chatRepo.searchMessages(params: param) { messages, error in if error == nil { // 未找到用户信息信息记录 var noFindUserSet = Set() for message in messages ?? [] { - if let uid = message.imMessage?.senderId { + if let uid = ChatMessageHelper.getSenderId(message.imMessage) { if let member = infoDic[uid] { message.avatar = member.nimUser?.user?.avatar message.fullName = member.atNameInTeam() message.shortName = member.getShortName(member.showNickInTeam() ?? "") + } else if let aiUser: V2NIMAIUser = NEAIUserManager.shared.getAIUserById(uid) { + message.avatar = aiUser.avatar + message.fullName = aiUser.showName() + message.shortName = aiUser.shortName() } else { noFindUserSet.insert(uid) } @@ -112,7 +120,7 @@ open class TeamHistoryMessageViewModel: NSObject, NETeamListener, NEContactListe /// 获取消息对应的用户信息 public func bindMessageUserInfo(_ messages: [HistoryMessageModel], _ infoDic: [String: NETeamMemberInfoModel]) { for message in messages { - if let uid = message.imMessage?.senderId { + if let uid = ChatMessageHelper.getSenderId(message.imMessage) { if let member = infoDic[uid] { message.avatar = member.nimUser?.user?.avatar message.fullName = member.atNameInTeam() @@ -196,14 +204,6 @@ open class TeamHistoryMessageViewModel: NSObject, NETeamListener, NEContactListe } } - /// 好友变更回调 - /// - parameter friendInfo: 好友信息对象 - public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { - if let accountId = friendInfo.accountId { - memberModelCacheDic[accountId]?.nimUser?.friend = friendInfo - } - } - /// 群成员变更回调 /// - parameter teamMembers: 群成员信息对象列表 public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { @@ -222,3 +222,18 @@ open class TeamHistoryMessageViewModel: NSObject, NETeamListener, NEContactListe } } } + +// MARK: - NEContactListener + +extension TeamHistoryMessageViewModel: NEContactListener { + /// 好友信息缓存更新 + /// - Parameter accountId: 用户 id + public func onContactChange(_ changeType: NEContactChangeType, _ contacts: [NEUserWithFriend]) { + for contact in contacts { + if let accid = contact.user?.accountId, + memberModelCacheDic[accid] != nil { + memberModelCacheDic[accid]?.nimUser = contact + } + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift index eb8b8a36..8a644f4f 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift @@ -11,7 +11,7 @@ public protocol TeamManagerListViewModelDelegate: NSObject { } @objcMembers -open class TeamManagerListViewModel: NSObject, NETeamListener, NEContactListener { +open class TeamManagerListViewModel: NSObject, NETeamListener { /// 群API 单例 public let teamRepo = TeamRepo.shared /// 当前用户的群成员对象 @@ -30,6 +30,7 @@ open class TeamManagerListViewModel: NSObject, NETeamListener, NEContactListener super.init() teamRepo.addTeamListener(self) ContactRepo.shared.addContactListener(self) + NotificationCenter.default.addObserver(self, selector: #selector(didTapHeader), name: NENotificationName.didTapHeader, object: nil) } deinit { @@ -37,6 +38,27 @@ open class TeamManagerListViewModel: NSObject, NETeamListener, NEContactListener ContactRepo.shared.removeContactListener(self) } + /// 点击消息发送者头像 + /// 拉取最新用户信息后刷新消息发送者信息 + /// - Parameter noti: 通知对象 + func didTapHeader(_ noti: Notification) { + if let user = noti.object as? NEUserWithFriend, + let accid = user.user?.accountId { + if NETeamMemberCache.shared.isCurrentMember(accid) { + var isDidFind = false + for model in managers { + if let accountId = model.nimUser?.user?.accountId, accountId == accid { + model.nimUser = user + isDidFind = true + } + } + if isDidFind == true { + delegate?.didNeedReloadData() + } + } + } + } + /// 获取群成员信息 /// - Parameter teamId: 群id /// - Parameter completion: 结果回调 @@ -157,18 +179,6 @@ open class TeamManagerListViewModel: NSObject, NETeamListener, NEContactListener } } - /// 好友信息变更 - /// - 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: 完成回调 @@ -332,3 +342,20 @@ open class TeamManagerListViewModel: NSObject, NETeamListener, NEContactListener } } } + +// MARK: - NEContactListener + +extension TeamManagerListViewModel: NEContactListener { + /// 好友信息缓存更新 + /// - Parameter accountId: 用户 id + public func onContactChange(_ changeType: NEContactChangeType, _ contacts: [NEUserWithFriend]) { + for contact in contacts { + for memberInfo in managers { + if memberInfo.teamMember?.accountId == contact.user?.accountId { + memberInfo.nimUser = contact + delegate?.didNeedReloadData() + } + } + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerViewModel.swift index 6c824d64..2f31612e 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerViewModel.swift @@ -195,7 +195,7 @@ open class TeamManagerViewModel: NSObject, NETeamListener { model.cellModels.append(contentsOf: [editTeamPermission, invitePermission, atAllPermission]) - if IMKitConfigCenter.shared.topEnable { + if IMKitConfigCenter.shared.enableTopMessage { // 谁可以置顶消息 let topMessagePermission = SettingCellModel() topMessagePermission.cellName = localizable("who_can_top_message") diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift index 5ccf0f25..28bcb73c 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift @@ -11,6 +11,7 @@ public protocol TeamMemberSelectViewModelDelegate: NSObject { func didNeedRefresh() } +@objcMembers class TeamMemberSelectViewModel: NSObject, NETeamListener, NETeamMemberCacheListener { /// 群API单例 let teamRepo = TeamRepo.shared @@ -33,6 +34,7 @@ class TeamMemberSelectViewModel: NSObject, NETeamListener, NETeamMemberCacheList super.init() teamRepo.addTeamListener(self) NETeamMemberCache.shared.addTeamCacheListener(self) + NotificationCenter.default.addObserver(self, selector: #selector(didTapHeader), name: NENotificationName.didTapHeader, object: nil) } deinit { @@ -40,6 +42,27 @@ class TeamMemberSelectViewModel: NSObject, NETeamListener, NETeamMemberCacheList NETeamMemberCache.shared.removeTeamCacheListener(self) } + // 点击消息发送者头像 + /// 拉取最新用户信息后刷新消息发送者信息 + /// - Parameter noti: 通知对象 + func didTapHeader(_ noti: Notification) { + if let user = noti.object as? NEUserWithFriend, + let accid = user.user?.accountId { + if NETeamMemberCache.shared.isCurrentMember(accid) { + var isDidFind = false + for model in showDatas { + if let accountId = model.member?.nimUser?.user?.accountId, accountId == accid { + model.member?.nimUser = user + isDidFind = true + } + } + if isDidFind == true { + delegate?.didNeedRefresh() + } + } + } + } + /// 群信息(包含群成员) /// - Parameter teamId: 群id /// - Parameter completion: 完成回调 @@ -56,7 +79,15 @@ class TeamMemberSelectViewModel: NSObject, NETeamListener, NETeamMemberCacheList } else { let teamInfo = NETeamInfoModel() teamInfo.team = team - if let members = NETeamMemberCache.shared.getTeamMemberCache(teamId), team?.memberCount == members.count { + if var members = NETeamMemberCache.shared.getTeamMemberCache(teamId), team?.memberCount == members.count { + members.removeAll { model in + if let account = model.nimUser?.user?.accountId { + if NEAIUserManager.shared.isAIUser(account) { + return true + } + } + return false + } teamInfo.users = members weakSelf?.teamInfoModel = teamInfo weakSelf?.datas.removeAll() @@ -75,9 +106,18 @@ class TeamMemberSelectViewModel: NSObject, NETeamListener, NETeamMemberCacheList } else { if let members = ms { weakSelf?.splitSelectMembers(members, teamInfo, 150) { error, model in - if let users = model?.users, users.count > 0 { + if var users = model?.users, users.count > 0 { NEALog.infoLog(weakSelf?.className() ?? "", desc: "set team member cache success.") NETeamMemberCache.shared.setCacheMembers(teamId, users) + + users.removeAll { model in + if let account = model.nimUser?.user?.accountId { + if NEAIUserManager.shared.isAIUser(account) { + return true + } + } + return false + } model?.users = users } weakSelf?.teamInfoModel = model diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift index fecb2744..86866d35 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift @@ -11,7 +11,7 @@ protocol TeamMembersViewModelDelegate: NSObject { func didNeedRefreshUI() } -class TeamMembersViewModel: NSObject, NETeamListener, NEContactListener, NEConversationListener, NETeamMemberCacheListener, NEEventListener { +class TeamMembersViewModel: NSObject, NETeamListener, NEConversationListener, NETeamMemberCacheListener, NEEventListener { /// 是否正在请求数据 public var isRequest = false /// 群id @@ -34,8 +34,7 @@ class TeamMembersViewModel: NSObject, NETeamListener, NEContactListener, NEConve override init() { super.init() teamRepo.addTeamListener(self) - ContactRepo.shared.addContactListener(self) - ConversationRepo.shared.addListener(self) + ConversationRepo.shared.addConversationListener(self) NETeamMemberCache.shared.addTeamCacheListener(self) if IMKitConfigCenter.shared.onlineStatusEnable { EventSubscribeRepo.shared.addListener(self) @@ -44,8 +43,7 @@ class TeamMembersViewModel: NSObject, NETeamListener, NEContactListener, NEConve deinit { teamRepo.removeTeamListener(self) - ContactRepo.shared.removeContactListener(self) - ConversationRepo.shared.removeListener(self) + ConversationRepo.shared.removeConversationListener(self) NETeamMemberCache.shared.removeTeamCacheListener(self) if IMKitConfigCenter.shared.onlineStatusEnable { EventSubscribeRepo.shared.removeListener(self) @@ -67,7 +65,7 @@ class TeamMembersViewModel: NSObject, NETeamListener, NEContactListener, NEConve /// - Parameter teamdId: 群id /// - Parameter uids: 用户id func removeTeamMember(_ teamdId: String, _ uids: [String], _ completion: @escaping (NSError?) -> Void) { - teamRepo.removeMembers(teamdId, .TEAM_TYPE_NORMAL, uids) { error in + teamRepo.removeTeamMembers(teamdId, .TEAM_TYPE_NORMAL, uids) { error in completion(error as NSError?) } } @@ -128,19 +126,6 @@ class TeamMembersViewModel: NSObject, NETeamListener, NEContactListener, NEConve }) } - // 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 {} - return - } - } - } - /// 群成员信息更新 /// - Parameter teamMembers: 群成员信息 func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) {} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift index 7bccb75f..2fc8926e 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift @@ -52,12 +52,34 @@ open class TeamSettingViewModel: NSObject, NETeamListener, NEConversationListene override public init() { super.init() teamRepo.addTeamListener(self) - conversationRepo.addListener(self) + conversationRepo.addConversationListener(self) + NotificationCenter.default.addObserver(self, selector: #selector(didTapHeader), name: NENotificationName.didTapHeader, object: nil) + } + + /// 点击消息发送者头像 + /// 拉取最新用户信息后刷新消息发送者信息 + /// - Parameter noti: 通知对象 + func didTapHeader(_ noti: Notification) { + if let user = noti.object as? NEUserWithFriend, + let accid = user.user?.accountId { + if NETeamMemberCache.shared.isCurrentMember(accid) { + var isDidFind = false + teamInfoModel?.users.forEach { model in + if let accountId = model.nimUser?.user?.accountId, accountId == accid { + model.nimUser = user + isDidFind = true + } + } + if isDidFind == true { + delegate?.didNeedRefreshUI() + } + } + } } func clear() { teamRepo.removeTeamListener(self) - conversationRepo.removeListener(self) + conversationRepo.removeConversationListener(self) NETeamMemberCache.shared.trigerTimer() } @@ -206,7 +228,7 @@ open class TeamSettingViewModel: NSObject, NETeamListener, NEConversationListene nick.cellClick = { weakSelf?.delegate?.didClickChangeNick() } - if IMKitConfigCenter.shared.pinEnable { + if IMKitConfigCenter.shared.enablePinMessage { model.cellModels.append(mark) } model.cellModels.append(history) @@ -520,7 +542,9 @@ open class TeamSettingViewModel: NSObject, NETeamListener, NEConversationListene } for model in sortArr { - if model.teamMember?.accountId != owner { + if let accid = model.teamMember?.accountId, + accid != owner, + !NEAIUserManager.shared.isAIUser(accid) { return model.teamMember?.accountId } } @@ -535,11 +559,19 @@ open class TeamSettingViewModel: NSObject, NETeamListener, NEConversationListene return } - guard let members = teamInfoModel?.users, let teamId = teamInfoModel?.team?.teamId else { + guard var members = teamInfoModel?.users, let teamId = teamInfoModel?.team?.teamId else { completion(NSError(domain: "imuikit", code: -1, userInfo: [NSLocalizedDescriptionKey: "team info error"])) return } - + // 移除数字人 + members.removeAll { model in + if let accountId = model.nimUser?.user?.accountId { + if NEAIUserManager.shared.isAIUser(accountId) { + return true + } + } + return false + } var userId = IMKitClient.instance.account() if members.count == 1 { dismissTeam(teamId, completion) @@ -664,7 +696,7 @@ open class TeamSettingViewModel: NSObject, NETeamListener, NEConversationListene /// - Parameter teamId: 群id /// - Parameter completion: 完成回调 open func inviteUsers(_ members: [String], _ teamId: String, _ completion: @escaping (NSError?, [V2NIMTeamMember]?) -> Void) { - teamRepo.inviteMembers(teamId, .TEAM_TYPE_NORMAL, members) { error, members in + teamRepo.inviteTeamMembers(teamId, .TEAM_TYPE_NORMAL, members) { error, members in completion(error, members) } } diff --git a/Podfile b/Podfile index 8b1f5a94..f5220354 100644 --- a/Podfile +++ b/Podfile @@ -7,19 +7,22 @@ target 'app' do use_frameworks! # 基础库 - pod 'NIMSDK_LITE','10.2.6-beta' - pod 'NEChatKit', '10.2.1' + pod 'NIMSDK_LITE','10.3.1-beta' + pod 'NEChatKit', '10.3.0' # UI 组件,依次为通讯录组件、会话列表组件、会话(聊天)组件、群相关设置组件 - pod 'NEChatUIKit', '10.2.1' - pod 'NEContactUIKit', '10.2.1' - pod 'NEConversationUIKit', '10.2.1' - pod 'NETeamUIKit', '10.2.1' - -# 扩展库-地理位置组件 - pod 'NEMapKit', '10.2.1' - - # 扩展库-呼叫组件 + pod 'NEChatUIKit', '10.3.0' + pod 'NEContactUIKit', '10.3.0' + pod 'NEConversationUIKit', '10.3.0' + pod 'NETeamUIKit', '10.3.0' + + # 扩展库 - 地理位置组件 + pod 'NEMapKit', '10.3.0' + + # 扩展库 - AI 划词搜索 + pod 'NEAISearchKit', '1.0.0' + + # 扩展库 - 呼叫组件 pod 'NERtcCallKit/NOS_Special', '2.4.0' pod 'NERtcCallUIKit/NOS_Special', '2.4.0' @@ -35,6 +38,7 @@ target 'app' do # pod 'NETeamUIKit', :path => 'NETeamUIKit/NETeamUIKit.podspec' # pod 'NEChatUIKit', :path => 'NEChatUIKit/NEChatUIKit.podspec' # pod 'NEMapKit', :path => 'NEMapKit/NEMapKit.podspec' +# pod 'NEAISearchKit', :path => 'NEAISearchKit/NEAISearchKit.podspec' # pod 'NERtcCallUIKit', :path => 'NERtcCallUIKit/NERtcCallUIKit.podspec' diff --git a/app.xcodeproj/project.pbxproj b/app.xcodeproj/project.pbxproj index ef1dd334..c7fd716f 100644 --- a/app.xcodeproj/project.pbxproj +++ b/app.xcodeproj/project.pbxproj @@ -651,7 +651,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 10.2.1; + MARKETING_VERSION = 10.3.0; PRODUCT_BUNDLE_IDENTIFIER = com.netease.yunxin.app.im; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -700,7 +700,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 10.2.1; + MARKETING_VERSION = 10.3.0; PRODUCT_BUNDLE_IDENTIFIER = com.netease.yunxin.app.im; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = inHouseYunxin; diff --git a/app/Main/AppDelegate.swift b/app/Main/AppDelegate.swift index f6dcfbf1..6758bdf7 100644 --- a/app/Main/AppDelegate.swift +++ b/app/Main/AppDelegate.swift @@ -29,7 +29,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return true } - + func setupInit(){ // 初始化NIMSDK @@ -39,8 +39,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD option.apnsCername = AppKey.pushCerName IMKitClient.instance.setupIM(option) - // 登录IM之前先初始化 @ 消息监听mananger - NEAtMessageManager.setupInstance() + NEAIUserManager.shared.setProvider(provider: self) let account = "<#account#>" let token = "<#token#>" @@ -50,8 +49,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if let err = error { print("login error in app : ", err.localizedDescription) }else { - let _ = NEAtMessageManager.instance - ChatRouter.setupInit() + weakSelf?.initConfig() weakSelf?.initializePage() } } @@ -101,59 +99,28 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func initializePage() { self.window?.rootViewController = NETabBarController(true) - loadService() } -// regist router - func loadService() { - - ContactRouter.register() - ChatRouter.register() - TeamRouter.register() - ConversationRouter.register() - if NEStyleManager.instance.isNormalStyle() == false { - ContactRouter.registerFun() - ChatRouter.registerFun() - TeamRouter.registerFun() - ConversationRouter.registerFun() - } - - // 自定义示例 - customVerification() - - //地图map初始化 + func initConfig() { + //地图组件初始化 NEMapClient.shared().setupMapClient(withAppkey: AppKey.gaodeMapAppkey, withServerKey: AppKey.gaodeMapServerAppkey) - /* 聊天面板外部扩展示例 - // 新增未知类型 - let item = NEMoreItemModel() - item.customDelegate = self - item.action = #selector(testLog) - item.customImage = UIImage(named: "chatSelect") - NEChatUIKitClient.instance.moreAction.append(item) - - // 覆盖已有类型 - let item = NEMoreItemModel() - item.customImage = UIImage(named: "chatSelect") - item.type = .rtc - item.title = "测试" - NEChatUIKitClient.instance.moreAction.append(item) - - // 移除已有类型 - // 遍历 NEChatUIKitClient.instance.moreAction, 根据type 移除已有类型 - */ - //呼叫组件初始化 - let option = NERtcCallOptions() - option.apnsCerName = AppKey.pushCerName - option.isDisableLog = true - option.supportAutoJoinWhenCalled = false - NERtcCallKit.sharedInstance().setupAppKey(AppKey.appKey, options: option) + let setupConfig = NESetupConfig(appkey: AppKey.appKey) + NECallEngine.sharedInstance().setup(setupConfig) + NECallEngine.sharedInstance().setTimeout(30) + let uiConfig = NECallUIKitConfig() - uiConfig.appKey = AppKey.appKey - uiConfig.uiConfig.showCallingSwitchCallType = option.supportAutoJoinWhenCalled - NERtcCallKit.sharedInstance().timeOutSeconds = 30 NERtcCallUIKit.sharedInstance().setup(with: uiConfig) + } + + // regist router + func loadService() { + + ChatKitClient.shared.setupInit(isFun: !NEStyleManager.instance.isNormalStyle()) + + // 自定义示例 + customVerification() Router.shared.register(MeSettingRouter) { param in if let nav = param["nav"] as? UINavigationController { @@ -170,52 +137,75 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func customVerification(){ if NEStyleManager.instance.isNormalStyle() { Router.shared.register(PushP2pChatVCRouter) { param in - print("param:\(param)") - let nav = param["nav"] as? UINavigationController - guard let conversationId = param["conversationId"] as? String else { - return - } - let anchor = param["anchor"] as? V2NIMMessage - let p2pChatVC = CustomNormalChatViewController(conversationId: conversationId, anchor: anchor) + print("param:\(param)") + let nav = param["nav"] as? UINavigationController + guard let conversationId = param["conversationId"] as? String else { + return + } + 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) { - nav?.viewControllers[i] = p2pChatVC - nav?.popToViewController(p2pChatVC, animated: true) - return + for (i, vc) in (nav?.viewControllers ?? []).enumerated() { + if vc.isKind(of: ChatViewController.self) { + nav?.viewControllers[i] = p2pChatVC + nav?.popToViewController(p2pChatVC, animated: true) + return + } } - } if let remove = param["removeUserVC"] as? Bool, remove { nav?.viewControllers.removeLast() } - nav?.pushViewController(p2pChatVC, animated: true) + nav?.pushViewController(p2pChatVC, animated: true) } } else { Router.shared.register(PushP2pChatVCRouter) { param in - print("param:\(param)") - let nav = param["nav"] as? UINavigationController - guard let conversationId = param["conversationId"] as? String else { - return - } - let anchor = param["anchor"] as? V2NIMMessage - let p2pChatVC = CustomFunChatViewController(conversationId: conversationId, anchor: anchor) + print("param:\(param)") + let nav = param["nav"] as? UINavigationController + guard let conversationId = param["conversationId"] as? String else { + return + } + 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) { - nav?.viewControllers[i] = p2pChatVC - nav?.popToViewController(p2pChatVC, animated: true) - return + for (i, vc) in (nav?.viewControllers ?? []).enumerated() { + if vc.isKind(of: ChatViewController.self) { + nav?.viewControllers[i] = p2pChatVC + nav?.popToViewController(p2pChatVC, animated: true) + return + } } - } if let remove = param["removeUserVC"] as? Bool, remove { nav?.viewControllers.removeLast() } - nav?.pushViewController(p2pChatVC, animated: true) + nav?.pushViewController(p2pChatVC, animated: true) } } } } +extension AppDelegate: AIUserAgentProvider { + public func getAISearchUser(_ users: [V2NIMAIUser]) -> V2NIMAIUser? { + for user in users { + if user.accountId == "search" { + return user + } + } + return nil + } + + public func getAITranslateUser(_ users: [V2NIMAIUser]) -> V2NIMAIUser? { + for user in users { + if user.accountId == "translation" { + return user + } + } + return nil + } + + public func getAITranslateLangs(_ users: [V2NIMAIUser]) -> [String] { + ["英语", "日语", "韩语", "俄语", "法语", "德语"] + } +} diff --git a/app/Main/NETabBarController.swift b/app/Main/NETabBarController.swift index 6160aa82..802724eb 100644 --- a/app/Main/NETabBarController.swift +++ b/app/Main/NETabBarController.swift @@ -33,13 +33,13 @@ class NETabBarController: UITabBarController, NEConversationListener, NEContactL setUpControllers() setUpSessionBadgeValue() setUpContactBadgeValue() - ConversationRepo.shared.addListener(self) + ConversationRepo.shared.addConversationListener(self) NotificationCenter.default.addObserver(self, selector: #selector(clearValidationUnreadCount), name: NENotificationName.clearValidationUnreadCount, object: nil) } deinit { - ConversationRepo.shared.removeListener(self) + ConversationRepo.shared.removeConversationListener(self) ContactRepo.shared.removeContactListener(self) } diff --git a/app/Mine/Controller/ConfigTestViewController.swift b/app/Mine/Controller/ConfigTestViewController.swift index 07469db0..18f645f6 100644 --- a/app/Mine/Controller/ConfigTestViewController.swift +++ b/app/Mine/Controller/ConfigTestViewController.swift @@ -23,6 +23,13 @@ class ConfigTestViewController: NEBaseViewController, UITableViewDelegate, tableView.separatorColor = .clear tableView.separatorStyle = .none tableView.sectionHeaderHeight = 12.0 + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = 0.0 } @@ -51,36 +58,36 @@ class ConfigTestViewController: NEBaseViewController, UITableViewDelegate, let showTeam = SettingCellModel() showTeam.cellName = "显示群聊" showTeam.type = SettingCellType.SettingSwitchCell.rawValue - showTeam.switchOpen = IMKitConfigCenter.shared.teamEnable + showTeam.switchOpen = IMKitConfigCenter.shared.enableTeam showTeam.swichChange = { isOpen in - IMKitConfigCenter.shared.teamEnable = isOpen + IMKitConfigCenter.shared.enableTeam = isOpen } model.cellModels.append(showTeam) let showMessageCollection = SettingCellModel() showMessageCollection.cellName = "显示收藏" showMessageCollection.type = SettingCellType.SettingSwitchCell.rawValue - showMessageCollection.switchOpen = IMKitConfigCenter.shared.collectionEnable + showMessageCollection.switchOpen = IMKitConfigCenter.shared.enableCollectionMessage showMessageCollection.swichChange = { isOpen in -// IMKitConfigCenter.shared.collectionEnable = isOpen + IMKitConfigCenter.shared.enableCollectionMessage = isOpen } model.cellModels.append(showMessageCollection) let showMessagePin = SettingCellModel() showMessagePin.cellName = "显示标记" showMessagePin.type = SettingCellType.SettingSwitchCell.rawValue - showMessagePin.switchOpen = IMKitConfigCenter.shared.pinEnable + showMessagePin.switchOpen = IMKitConfigCenter.shared.enablePinMessage showMessagePin.swichChange = { isOpen in - IMKitConfigCenter.shared.pinEnable = isOpen + IMKitConfigCenter.shared.enablePinMessage = isOpen } model.cellModels.append(showMessagePin) let showMessageTop = SettingCellModel() showMessageTop.cellName = "显示置顶" showMessageTop.type = SettingCellType.SettingSwitchCell.rawValue - showMessageTop.switchOpen = IMKitConfigCenter.shared.topEnable + showMessageTop.switchOpen = IMKitConfigCenter.shared.enableTopMessage showMessageTop.swichChange = { isOpen in -// IMKitConfigCenter.shared.topEnable = isOpen + IMKitConfigCenter.shared.enableTopMessage = isOpen } model.cellModels.append(showMessageTop) @@ -89,18 +96,18 @@ class ConfigTestViewController: NEBaseViewController, UITableViewDelegate, showOnlineStatus.type = SettingCellType.SettingSwitchCell.rawValue showOnlineStatus.switchOpen = IMKitConfigCenter.shared.onlineStatusEnable showOnlineStatus.swichChange = { isOpen in -// IMKitConfigCenter.shared.onlineStatusEnable = isOpen + IMKitConfigCenter.shared.onlineStatusEnable = isOpen } model.cellModels.append(showOnlineStatus) - let strangerCallEnable = SettingCellModel() - strangerCallEnable.cellName = "是否允许陌生人音视频通话" - strangerCallEnable.type = SettingCellType.SettingSwitchCell.rawValue - strangerCallEnable.switchOpen = IMKitConfigCenter.shared.strangerCallEnable - strangerCallEnable.swichChange = { isOpen in - IMKitConfigCenter.shared.strangerCallEnable = isOpen + let strangerCallModel = SettingCellModel() + strangerCallModel.cellName = "好友能否进行音视频通话" + strangerCallModel.type = SettingCellType.SettingSwitchCell.rawValue + strangerCallModel.switchOpen = IMKitConfigCenter.shared.enableOnlyFriendCall + strangerCallModel.swichChange = { isOpen in + IMKitConfigCenter.shared.enableOnlyFriendCall = isOpen } - model.cellModels.append(strangerCallEnable) + model.cellModels.append(strangerCallModel) model.setCornerType() return model diff --git a/app/Mine/Controller/IMSDKConfigViewController.swift b/app/Mine/Controller/IMSDKConfigViewController.swift new file mode 100644 index 00000000..36a535b2 --- /dev/null +++ b/app/Mine/Controller/IMSDKConfigViewController.swift @@ -0,0 +1,331 @@ +//// 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 NECoreKit +import NETeamUIKit +import NIMSDK +import UIKit + +class IMSDKConfigViewController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource { + public var cellClassDic = + [SettingCellType.SettingSwitchCell.rawValue: CustomTeamSettingSwitchCell.self, SettingCellType.SettingArrowCell.rawValue: CustomTeamArrowSettingCell.self, SettingCellType.SettingSubtitleCell.rawValue: CustomSettingInputCell.self, SettingCellType.SettingSubtitleCustomCell.rawValue: CustomSettingTextviewCell.self] + + var sectionData = [SettingSectionModel]() + + lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.separatorColor = .clear + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + + var configModel: IMSDKConfigModel + + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + configModel = IMSDKConfigManager.instance.getConfig() + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + required init?(coder: NSCoder) { + configModel = IMSDKConfigManager.instance.getConfig() + super.init(coder: coder) + } + + /// 是否是自动解析 + public var isCustomJsonParse = false + + override func viewDidLoad() { + super.viewDidLoad() + title = "私有云环境配置" + setupCellData() + setupUI() + } + + public func setupUI() { + navigationView.moreButton.isHidden = true + view.addSubview(contentTableView) + NSLayoutConstraint.activate([ + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.statusBarHeight + NEConstant.navigationHeight), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + contentTableView.tableFooterView = getFooterView() + + for (key, value) in cellClassDic { + contentTableView.register(value, forCellReuseIdentifier: "\(key)") + } + view.backgroundColor = navigationView.backgroundColor + navigationView.moreButton.isHidden = true + } + + let twoSectionModel = SettingSectionModel() + + let customSectionModel = SettingSectionModel() + + let autoConfig = CustomSettingCellModel() + + func setupCellData() { + let oneSectionModel = SettingSectionModel() + sectionData.append(oneSectionModel) + + let usePrivateConfigModel = SettingCellModel() + usePrivateConfigModel.cellName = "私有化环境配置生效" + usePrivateConfigModel.type = SettingCellType.SettingSwitchCell.rawValue + usePrivateConfigModel.swichChange = { isOpen in + } + oneSectionModel.cellModels.append(usePrivateConfigModel) + oneSectionModel.setCornerType() + + sectionData.append(twoSectionModel) + + let configType = SettingCellModel() + configType.cellName = "配置方式" + configType.type = SettingCellType.SettingArrowCell.rawValue + twoSectionModel.cellModels.append(configType) + + let linkModel = CustomSettingCellModel() + linkModel.cellName = "服务器Link地址" + linkModel.type = SettingCellType.SettingSubtitleCell.rawValue + linkModel.inputKey = #keyPath(NIMServerSetting.linkAddress) + if let linkValue = configModel.configMap[linkModel.inputKey] as? String { + linkModel.customInputText = linkValue + } + twoSectionModel.cellModels.append(linkModel) + + let lbsModel = CustomSettingCellModel() + lbsModel.cellName = "LBS服务器地址" + lbsModel.type = SettingCellType.SettingSubtitleCell.rawValue + lbsModel.inputKey = #keyPath(NIMServerSetting.lbsAddress) + if let lbsValue = configModel.configMap[lbsModel.inputKey] as? String { + lbsModel.customInputText = lbsValue + } + twoSectionModel.cellModels.append(lbsModel) + + let nosToLBSModel = CustomSettingCellModel() + nosToLBSModel.cellName = "NOS上传LBS服务器地址" + nosToLBSModel.type = SettingCellType.SettingSubtitleCell.rawValue + nosToLBSModel.inputKey = #keyPath(NIMServerSetting.nosLbsAddress) + if let nosLbsAddress = configModel.configMap[nosToLBSModel.inputKey] as? String { + nosToLBSModel.customInputText = nosLbsAddress + } + twoSectionModel.cellModels.append(nosToLBSModel) + + let nosToLBSLinkModel = CustomSettingCellModel() + nosToLBSLinkModel.cellName = "NOS上传LBS服务器默认Link服务器地址" + nosToLBSLinkModel.type = SettingCellType.SettingSubtitleCell.rawValue + nosToLBSLinkModel.inputKey = #keyPath(NIMServerSetting.nosUploadAddress) + if let nosUploadAddress = configModel.configMap[nosToLBSLinkModel.inputKey] as? String { + nosToLBSLinkModel.customInputText = nosUploadAddress + } + twoSectionModel.cellModels.append(nosToLBSLinkModel) + + let nosSplicingModel = CustomSettingCellModel() + nosSplicingModel.cellName = "NOS拼接下载地址" + nosSplicingModel.type = SettingCellType.SettingSubtitleCell.rawValue + nosSplicingModel.inputKey = #keyPath(NIMServerSetting.nosDownloadAddress) + if let nosDownloadAddress = configModel.configMap[nosSplicingModel.inputKey] as? String { + nosSplicingModel.customInputText = nosDownloadAddress + } + twoSectionModel.cellModels.append(nosSplicingModel) + + let nosUpladModel = CustomSettingCellModel() + nosUpladModel.cellName = "NOS上传服务器主机地址" + nosUpladModel.type = SettingCellType.SettingSubtitleCell.rawValue + nosUpladModel.inputKey = #keyPath(NIMServerSetting.nosUploadHost) + if let nosUploadHost = configModel.configMap[nosUpladModel.inputKey] as? String { + nosUpladModel.customInputText = nosUploadHost + } + twoSectionModel.cellModels.append(nosUpladModel) + + twoSectionModel.setCornerType() + + let autoconfigType = CustomSettingCellModel() + autoconfigType.cellName = "配置方式" + autoconfigType.type = SettingCellType.SettingArrowCell.rawValue + autoconfigType.rowHeight = 49.0 + customSectionModel.cellModels.append(autoconfigType) + + autoConfig.cellName = "填写私有化配置内容" + autoConfig.type = SettingCellType.SettingSubtitleCustomCell.rawValue + autoConfig.rowHeight = 200 + autoConfig.customInputText = configModel.customJson + + customSectionModel.cellModels.append(autoConfig) + + customSectionModel.setCornerType() + } + + func didSave() { + for sectionModel in sectionData { + for model in sectionModel.cellModels { + if let customModel = model as? CustomSettingCellModel, customModel.inputKey.count > 0 { + if let customText = customModel.customInputText, customText.count > 0 { + configModel.configMap[customModel.inputKey] = customText + } else { + configModel.configMap.removeValue(forKey: customModel.inputKey) + } + } + } + } + if let custom = autoConfig.customInputText, custom.count > 0 { + configModel.customJson = custom + } else { + configModel.customJson = nil + } + UIApplication.shared.keyWindow?.ne_makeToast("保存成功") + IMSDKConfigManager.instance.saveConfig(model: configModel) + navigationController?.popViewController(animated: true) + } + + // MARK: UITableViewDelegate, UITableViewDataSource + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if sectionData.count > section { + let model = sectionData[section] + return model.cellModels.count + } + return 0 + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let model = 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() + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let model = sectionData[indexPath.section].cellModels[indexPath.row] + if indexPath.section == 1 { + if indexPath.row == 0 { + print("height for row : ", model.rowHeight) + } + } + return model.rowHeight + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + 10 + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + if section == 0 { + return 40 + } + return 0 + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView = UIView() + headerView.backgroundColor = .clear + return headerView + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if indexPath.section == 1 { + if indexPath.row == 0 { + changeParseModel() + } + } + } + + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + let footerView = UIView() + footerView.backgroundColor = .clear + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor.ne_darkText + label.font = UIFont.systemFont(ofSize: 16) + footerView.addSubview(label) + + if NEStyleManager.instance.isNormalStyle() { + NSLayoutConstraint.activate([ + label.leftAnchor.constraint(equalTo: footerView.leftAnchor, constant: 36), + label.rightAnchor.constraint(equalTo: footerView.rightAnchor, constant: -36), + label.bottomAnchor.constraint(equalTo: footerView.bottomAnchor), + ]) + } else { + NSLayoutConstraint.activate([ + label.leftAnchor.constraint(equalTo: footerView.leftAnchor, constant: 20), + label.rightAnchor.constraint(equalTo: footerView.rightAnchor, constant: -20), + label.bottomAnchor.constraint(equalTo: footerView.bottomAnchor), + ]) + } + + label.text = "配置私有化参数配置" + return footerView + } + + func numberOfSections(in tableView: UITableView) -> Int { + sectionData.count + } + + public func changeParseModel() { + if isCustomJsonParse == false { + if sectionData.count == 2 { + sectionData.remove(at: 1) + sectionData.append(customSectionModel) + } + } else { + if sectionData.count == 2 { + sectionData.remove(at: 1) + sectionData.append(twoSectionModel) + } + } + contentTableView.reloadData() + isCustomJsonParse = !isCustomJsonParse + } + + public func getFooterView() -> UIView { + let back = UIView() + back.backgroundColor = .clear + back.frame = CGRectMake(0, 0, view.frame.width, 60) + let button = ExpandButton() + back.addSubview(button) + button.setTitle("保存", for: .normal) + button.setTitleColor(.white, for: .normal) + if NEStyleManager.instance.isNormalStyle() { + button.backgroundColor = UIColor.ne_blueText + button.frame = CGRectMake(20, 10, view.frame.width - 40, 40) + } else { + button.frame = CGRectMake(0, 10, view.frame.width, 40) + button.backgroundColor = UIColor.ne_funTheme + } + button.addTarget(self, action: #selector(didSave), for: .touchUpInside) + return back + } + + /* + // 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/Mine/Controller/InputPersonInfoController.swift b/app/Mine/Controller/InputPersonInfoController.swift index 7727ee2d..16e92e11 100644 --- a/app/Mine/Controller/InputPersonInfoController.swift +++ b/app/Mine/Controller/InputPersonInfoController.swift @@ -93,10 +93,10 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { /// 初始化UI(导航栏) func initialConfig() { - addRightAction(NSLocalizedString("save", comment: ""), #selector(saveName), self) + addRightAction(commonLocalizable("complete"), #selector(saveName), self) view.backgroundColor = NEStyleManager.instance.isNormalStyle() ? UIColor(hexString: "#EFF1F4") : UIColor(hexString: "#EDEDED") - navigationView.setMoreButtonTitle(NSLocalizedString("save", comment: "")) + navigationView.setMoreButtonTitle(commonLocalizable("complete")) navigationView.addMoreButtonTarget(target: self, selector: #selector(saveName)) if NEStyleManager.instance.isNormalStyle() { diff --git a/app/Mine/Controller/IntroduceBrandViewController.swift b/app/Mine/Controller/IntroduceBrandViewController.swift index d9ea27ef..e6e74537 100644 --- a/app/Mine/Controller/IntroduceBrandViewController.swift +++ b/app/Mine/Controller/IntroduceBrandViewController.swift @@ -39,6 +39,13 @@ class IntroduceBrandViewController: NEBaseViewController, UITableViewDelegate, tableView.delegate = self tableView.separatorStyle = .none tableView.register(VersionCell.self, forCellReuseIdentifier: "VersionCell") + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = 0.0 } @@ -58,7 +65,7 @@ class IntroduceBrandViewController: NEBaseViewController, UITableViewDelegate, view.addSubview(tableView) navigationController?.navigationBar.backgroundColor = .white navigationView.backgroundColor = .white - + navigationView.moreButton.isHidden = true NSLayoutConstraint.activate([ headImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), headImageView.topAnchor.constraint( diff --git a/app/Mine/Controller/MeViewController.swift b/app/Mine/Controller/MeViewController.swift index 3e878577..bcaf58c3 100644 --- a/app/Mine/Controller/MeViewController.swift +++ b/app/Mine/Controller/MeViewController.swift @@ -28,6 +28,16 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { forCellReuseIdentifier: "\(NSStringFromClass(MineTableViewCell.self))" ) tableView.rowHeight = 52 + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } return tableView }() @@ -86,7 +96,7 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { super.viewDidLoad() view.backgroundColor = NEStyleManager.instance.isNormalStyle() ? UIColor(hexString: "#EFF1F4") : UIColor(hexString: "#EDEDED") setupSubviews() - if IMKitConfigCenter.shared.collectionEnable { + if IMKitConfigCenter.shared.enableCollectionMessage { mineData.insert([NSLocalizedString("mine_collection", comment: ""): "mine_collection"], at: 1) } } @@ -206,7 +216,7 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { if let userFriend = NEFriendUserCache.shared.getFriendInfo(IMKitClient.instance.account()) { setupUserInfo(userFriend) } else { - ContactRepo.shared.getUserList(accountIds: [IMKitClient.instance.account()]) { [weak self] users, error in + ContactRepo.shared.getUserListFromCloud(accountIds: [IMKitClient.instance.account()]) { [weak self] users, error in self?.setupUserInfo(users?.first) } } @@ -232,7 +242,7 @@ extension MeViewController: UITableViewDelegate, UITableViewDataSource { } public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if IMKitConfigCenter.shared.collectionEnable { + if IMKitConfigCenter.shared.enableCollectionMessage { if indexPath.row == 0 { let ctrl = MineSettingViewController() navigationController?.pushViewController(ctrl, animated: true) diff --git a/app/Mine/Controller/MessageRemindViewController.swift b/app/Mine/Controller/MessageRemindViewController.swift index 1c860782..363c8048 100644 --- a/app/Mine/Controller/MessageRemindViewController.swift +++ b/app/Mine/Controller/MessageRemindViewController.swift @@ -23,6 +23,13 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, tableView.separatorColor = .clear tableView.separatorStyle = .none tableView.sectionHeaderHeight = 12.0 + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = 0.0 } @@ -64,6 +71,7 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, for (key, value) in cellClassDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } + navigationView.moreButton.isHidden = true } // MARK: UITableViewDelegate, UITableViewDataSource diff --git a/app/Mine/Controller/MineSettingViewController.swift b/app/Mine/Controller/MineSettingViewController.swift index cb31f39e..df5dc634 100644 --- a/app/Mine/Controller/MineSettingViewController.swift +++ b/app/Mine/Controller/MineSettingViewController.swift @@ -10,204 +10,217 @@ import NIMSDK import UIKit class MineSettingViewController: NEBaseViewController, UITableViewDataSource, UITableViewDelegate { - private var viewModel = MineSettingViewModel() - public var cellClassDic = [ - SettingCellType.SettingArrowCell.rawValue: CustomTeamArrowSettingCell.self, - SettingCellType.SettingSwitchCell.rawValue: CustomTeamSettingSwitchCell.self, - ] - private var tag = "MineSettingViewController" - private let userDefault = UserDefaults.standard - - /// 设置列表 - lazy var tableView: UITableView = { - 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, *) { - tableView.sectionHeaderTopPadding = 0.0 - } - return tableView - }() - - /// 退出登录按钮 - let logoutButton = UIButton() - - override func viewDidLoad() { - super.viewDidLoad() - viewModel.delegate = self - viewModel.getData() - setupSubviews() - initialConfig() + private var viewModel = MineSettingViewModel() + public var cellClassDic = [ + SettingCellType.SettingArrowCell.rawValue: CustomTeamArrowSettingCell.self, + SettingCellType.SettingSwitchCell.rawValue: CustomTeamSettingSwitchCell.self, + ] + private var tag = "MineSettingViewController" + private let userDefault = UserDefaults.standard + + /// 设置列表 + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.tableFooterView = getFooterView() + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 } - - func initialConfig() { - title = NSLocalizedString("setting", comment: "") - - if NEStyleManager.instance.isNormalStyle() { - view.backgroundColor = .ne_backgroundColor - navigationView.backgroundColor = .ne_backgroundColor - navigationController?.navigationBar.backgroundColor = .ne_backgroundColor - } else { - view.backgroundColor = .funChatBackgroundColor - } + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 } - - func setupSubviews() { - view.addSubview(tableView) - if NEStyleManager.instance.isNormalStyle() { - topConstant += 12 - } - NSLayoutConstraint.activate([ - tableView.leftAnchor.constraint(equalTo: view.leftAnchor), - tableView.rightAnchor.constraint(equalTo: view.rightAnchor), - tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - for (key, value) in cellClassDic { - tableView.register(value, forCellReuseIdentifier: "\(key)") - } + return tableView + }() + + /// 退出登录按钮 + let logoutButton = UIButton() + + override func viewDidLoad() { + super.viewDidLoad() + viewModel.delegate = self + viewModel.getData() + setupSubviews() + initialConfig() + } + + func initialConfig() { + title = NSLocalizedString("setting", comment: "") + + if NEStyleManager.instance.isNormalStyle() { + view.backgroundColor = .ne_backgroundColor + navigationView.backgroundColor = .ne_backgroundColor + navigationController?.navigationBar.backgroundColor = .ne_backgroundColor + } else { + view.backgroundColor = .funChatBackgroundColor + } + } + + func setupSubviews() { + view.addSubview(tableView) + if NEStyleManager.instance.isNormalStyle() { + topConstant += 12 + } + NSLayoutConstraint.activate([ + tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.rightAnchor.constraint(equalTo: view.rightAnchor), + tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + for (key, value) in cellClassDic { + tableView.register(value, forCellReuseIdentifier: "\(key)") } - - func getFooterView() -> UIView? { - let footerView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) - footerView.addSubview(logoutButton) - logoutButton.backgroundColor = .white - logoutButton.clipsToBounds = true - logoutButton.setTitleColor(UIColor(hexString: "0xE6605C"), for: .normal) - logoutButton.titleLabel?.font = UIFont.systemFont(ofSize: 16) - logoutButton.setTitle(title, for: .normal) - logoutButton.addTarget(self, action: #selector(loginOutAction), for: .touchUpInside) - logoutButton.setTitle(NSLocalizedString("logout", comment: ""), for: .normal) - logoutButton.accessibilityIdentifier = "id.logout" - if NEStyleManager.instance.isNormalStyle() { - logoutButton.layer.cornerRadius = 8.0 - logoutButton.frame = CGRect(x: 20, y: 12, width: view.frame.size.width - 40, height: 40) - } else { - logoutButton.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - logoutButton.leftAnchor.constraint(equalTo: footerView.leftAnchor, constant: 0), - logoutButton.rightAnchor.constraint(equalTo: footerView.rightAnchor, constant: 0), - logoutButton.topAnchor.constraint(equalTo: footerView.topAnchor, constant: 12), - logoutButton.heightAnchor.constraint(equalToConstant: 40), - ]) - } - - return footerView + + navigationView.moreButton.isHidden = true + } + + func getFooterView() -> UIView? { + let footerView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) + footerView.addSubview(logoutButton) + logoutButton.backgroundColor = .white + logoutButton.clipsToBounds = true + logoutButton.setTitleColor(UIColor(hexString: "0xE6605C"), for: .normal) + logoutButton.titleLabel?.font = UIFont.systemFont(ofSize: 16) + logoutButton.setTitle(title, for: .normal) + logoutButton.addTarget(self, action: #selector(loginOutAction), for: .touchUpInside) + logoutButton.setTitle(NSLocalizedString("logout", comment: ""), for: .normal) + logoutButton.accessibilityIdentifier = "id.logout" + if NEStyleManager.instance.isNormalStyle() { + logoutButton.layer.cornerRadius = 8.0 + logoutButton.frame = CGRect(x: 20, y: 12, width: view.frame.size.width - 40, height: 40) + } else { + logoutButton.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + logoutButton.leftAnchor.constraint(equalTo: footerView.leftAnchor, constant: 0), + logoutButton.rightAnchor.constraint(equalTo: footerView.rightAnchor, constant: 0), + logoutButton.topAnchor.constraint(equalTo: footerView.topAnchor, constant: 12), + logoutButton.heightAnchor.constraint(equalToConstant: 40), + ]) } - + + return footerView + } + @objc func loginOutAction() { - weak var weakSelf = self - logoutButton.isEnabled = false - IMKitClient.instance.logoutIM { error in - weakSelf?.logoutButton.isEnabled = true - if error != nil { - NEALog.infoLog(weakSelf?.className() ?? "", desc: "logout im error : \(error?.localizedDescription ?? "")") - weakSelf?.view.makeToast(error?.localizedDescription) - NEALog.errorLog( - weakSelf?.tag ?? "", - desc: "CALLBACK logout SUCCESS = \(error!)" - ) - } else { - NEALog.infoLog(weakSelf?.className() ?? "", desc: "logout im success ") - NotificationCenter.default.post( - name: Notification.Name("logout"), - object: nil - ) - NEALog.infoLog( - weakSelf?.tag ?? "", - desc: "CALLBACK logout SUCCESS" - ) - NEFriendUserCache.shared.removeAllFriendInfo() - } - } - } - - // MARK: UITableViewDataSource, UITableViewDelegate - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if viewModel.sectionData.count > section { - let model = viewModel.sectionData[section] - return model.cellModels.count - } - return 0 - } - - func numberOfSections(in tableView: UITableView) -> Int { - viewModel.sectionData.count - } - - 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() - } - - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] - if let block = model.cellClick { - block() - } - } - - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] - return model.rowHeight - } - - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - if section == 0 { - return 0 - } - if viewModel.sectionData.count > section { - let model = viewModel.sectionData[section] - if model.cellModels.count > 0 { - return 12.0 + weak var weakSelf = self + logoutButton.isEnabled = false + IMKitClient.instance.logoutIM { error in + weakSelf?.logoutButton.isEnabled = true + if error != nil { + NEALog.infoLog(weakSelf?.className() ?? "", desc: "logout im error : \(error?.localizedDescription ?? "")") + weakSelf?.view.makeToast(error?.localizedDescription) + NEALog.errorLog( + weakSelf?.tag ?? "", + desc: "CALLBACK logout SUCCESS = \(error!)" + ) + } else { + NEALog.infoLog(weakSelf?.className() ?? "", desc: "logout im success ") + NotificationCenter.default.post( + name: Notification.Name("logout"), + object: nil + ) + NEALog.infoLog( + weakSelf?.tag ?? "", + desc: "CALLBACK logout SUCCESS" + ) + NEFriendUserCache.shared.removeAllFriendInfo() + } } } - return 0 + + // MARK: UITableViewDataSource, UITableViewDelegate + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if viewModel.sectionData.count > section { + let model = viewModel.sectionData[section] + return model.cellModels.count } - - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let headerView = UIView() - headerView.backgroundColor = .clear - return headerView + return 0 + } + + func numberOfSections(in tableView: UITableView) -> Int { + viewModel.sectionData.count + } + + 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() + } + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] + if let block = model.cellClick { + block() } - - func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - if section == viewModel.sectionData.count - 1 { - return 12.0 - } - return 0 + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] + return model.rowHeight + } + + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + if section == 0 { + return 0 + } + if viewModel.sectionData.count > section { + let model = viewModel.sectionData[section] + if model.cellModels.count > 0 { + return 12.0 + } + } + return 0 + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + let headerView = UIView() + headerView.backgroundColor = .clear + return headerView + } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + if section == viewModel.sectionData.count - 1 { + return 12.0 } + return 0 + } } extension MineSettingViewController: MineSettingViewModelDelegate { - func didMessageRemindClick() { - let messageRemindCtrl = MessageRemindViewController() - navigationController?.pushViewController(messageRemindCtrl, animated: true) - } - - func didStyleClick() { - let styleSelectionCtrl = StyleSelectionViewController() - navigationController?.pushViewController(styleSelectionCtrl, animated: true) - } - - func didClickCleanCache() {} + func didMessageRemindClick() { + let messageRemindCtrl = MessageRemindViewController() + navigationController?.pushViewController(messageRemindCtrl, animated: true) + } + + func didStyleClick() { + let styleSelectionCtrl = StyleSelectionViewController() + navigationController?.pushViewController(styleSelectionCtrl, animated: true) + } + + func didClickCleanCache() {} + + func didClickConfigTest() { + let configTestVC = ConfigTestViewController() + navigationController?.pushViewController(configTestVC, animated: true) + } + + func didClickSDKConfig() { - func didClickConfigTest() { - let configTestVC = ConfigTestViewController() - navigationController?.pushViewController(configTestVC, animated: true) - } + } } diff --git a/app/Mine/Controller/NEAboutWebViewController.swift b/app/Mine/Controller/NEAboutWebViewController.swift index 41065b61..6c15bd3d 100644 --- a/app/Mine/Controller/NEAboutWebViewController.swift +++ b/app/Mine/Controller/NEAboutWebViewController.swift @@ -28,6 +28,7 @@ class NEAboutWebViewController: NEBaseViewController { func setUpSubViews() { title = NSLocalizedString("product_intro", comment: "") navigationView.backgroundColor = .white + navigationView.moreButton.isHidden = true navigationController?.navigationBar.backgroundColor = .white guard let requestUrl = URL(string: loadUrl) else { diff --git a/app/Mine/Controller/NELoginViewController.swift b/app/Mine/Controller/NELoginViewController.swift new file mode 100644 index 00000000..1541948f --- /dev/null +++ b/app/Mine/Controller/NELoginViewController.swift @@ -0,0 +1,226 @@ + +// 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 NECommonKit +import NECoreIM2Kit +import NIMSDK +import UIKit +import YXLogin + +public class NELoginViewController: UIViewController { + // 登录成功 + public typealias LoginBlock = () -> Void + + 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_normalTheme + 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() + } + + override public func viewWillAppear(_ animated: Bool) { + navigationController?.navigationBar.isHidden = true + } + + override public func viewWillDisappear(_ animated: Bool) { + navigationController?.navigationBar.isHidden = false + } + + func setupUI() { + view.addSubview(launchIconView) + view.addSubview(launchIconLabel) + view.addSubview(loginButton) + view.addSubview(emailLoginButton) + view.addSubview(dividerLineView) + view.addSubview(nodeButton) + + if #available(iOS 11.0, *) { + NSLayoutConstraint.activate([ + launchIconView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + launchIconView.topAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.topAnchor, + constant: 145.0 + ), + ]) + } else { + NSLayoutConstraint.activate([ + 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: launchIconView.bottomAnchor, constant: -12.0), + ]) + + NSLayoutConstraint.activate([ + 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([ + dividerLineView.bottomAnchor.constraint( + equalTo: view.bottomAnchor, + constant: -10 - NEConstant.statusBarHeight + ), + dividerLineView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + dividerLineView.widthAnchor.constraint(equalToConstant: 1), + dividerLineView.heightAnchor.constraint(equalToConstant: 10), + ]) + + NSLayoutConstraint.activate([ + emailLoginButton.centerYAnchor.constraint(equalTo: dividerLineView.centerYAnchor), + emailLoginButton.rightAnchor.constraint(equalTo: dividerLineView.leftAnchor, constant: -8), + ]) + + NSLayoutConstraint.activate([ + 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 = ServerAddresses.getAppkey() + 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 { + print("login error : ", err) + } else { + weakSelf?.setupSuccessLogic(user) + } + }) + } + + @objc func emailLoginBtnClick(sender: UIButton) { + // login to business server + let config = YXConfig() + config.appKey = ServerAddresses.getAppkey() + config.parentScope = NSNumber(integerLiteral: 2) + config.scope = NSNumber(integerLiteral: 7) + config.supportInternationalize = false + config.type = .email + #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 { + print("login error : ", err) + } else { + weakSelf?.setupSuccessLogic(user) + } + }) + } + + @objc func nodeBtnClick(sender: UIButton) { + let ctrl = NENodeViewController() + navigationController?.pushViewController(ctrl, animated: true) + } + + private func setupSuccessLogic(_ user: YXUserInfo?) { + if let token = user?.imToken, let account = user?.imAccid { + weak var weakSelf = self + print("login accid : ", account) + print("login token : ", token) + + let option = V2NIMLoginOption() + IMKitClient.instance.login(account, token, option) { error in + if let err = error { + 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") + if let block = weakSelf?.successLogin { + block() + } + } + } + } + } +} diff --git a/app/Mine/Controller/NENodeViewController.swift b/app/Mine/Controller/NENodeViewController.swift index 00e6587d..1a4d2b58 100644 --- a/app/Mine/Controller/NENodeViewController.swift +++ b/app/Mine/Controller/NENodeViewController.swift @@ -21,6 +21,13 @@ class NENodeViewController: NEBaseViewController, UITableViewDataSource, UITable tableView.separatorColor = .clear tableView.separatorStyle = .none tableView.sectionHeaderHeight = 12.0 + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = 0.0 } diff --git a/app/Mine/Controller/PersonInfoViewController.swift b/app/Mine/Controller/PersonInfoViewController.swift index 93d6f8ee..c51c1e77 100644 --- a/app/Mine/Controller/PersonInfoViewController.swift +++ b/app/Mine/Controller/PersonInfoViewController.swift @@ -3,15 +3,16 @@ // found in the LICENSE file. import NEChatUIKit +import NECoreIM2Kit import NECoreKit import NETeamUIKit import NIMSDK import UIKit @objcMembers -class PersonInfoViewController: NEBaseViewController, +public class PersonInfoViewController: NEBaseViewController, UINavigationControllerDelegate, PersonInfoViewModelDelegate, UITableViewDelegate, - UITableViewDataSource, NEContactListener { + UITableViewDataSource, UIImagePickerControllerDelegate, NEContactListener { public var cellClassDic = [ SettingCellType.SettingSubtitleCell.rawValue: CustomTeamSettingSubtitleCell.self, SettingCellType.SettingHeaderCell.rawValue: CustomTeamSettingHeaderCell.self, @@ -28,6 +29,13 @@ class PersonInfoViewController: NEBaseViewController, tableView.delegate = self tableView.separatorColor = .clear tableView.separatorStyle = .none + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } if #available(iOS 15.0, *) { tableView.sectionHeaderTopPadding = 0.0 } @@ -40,7 +48,7 @@ class PersonInfoViewController: NEBaseViewController, return picker }() - override func viewDidLoad() { + override public func viewDidLoad() { super.viewDidLoad() ContactRepo.shared.addContactListener(self) initialConfig() @@ -52,7 +60,6 @@ class PersonInfoViewController: NEBaseViewController, func initialConfig() { title = NSLocalizedString("person_info", comment: "") - navigationView.navTitle.text = title if NEStyleManager.instance.isNormalStyle() { view.backgroundColor = .ne_backgroundColor @@ -61,6 +68,8 @@ class PersonInfoViewController: NEBaseViewController, } else { view.backgroundColor = .funChatBackgroundColor } + + navigationView.moreButton.isHidden = true viewModel.delegate = self } @@ -147,25 +156,11 @@ class PersonInfoViewController: NEBaseViewController, ]) } - /// 用户信息变更回调 - /// - 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 - } - } - } - // MARK: UIImagePickerControllerDelegate - func imagePickerController(_ picker: UIImagePickerController, - didFinishPickingMediaWithInfo info: [UIImagePickerController - .InfoKey: Any]) { + public func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController + .InfoKey: Any]) { let image: UIImage = info[UIImagePickerController.InfoKey.editedImage] as! UIImage uploadHeadImage(image: image) dismiss(animated: true, completion: nil) @@ -195,7 +190,7 @@ class PersonInfoViewController: NEBaseViewController, } else { NEALog.errorLog( weakSelf?.className ?? "", - desc: "❌CALLBACK upload image failed,error = \(error!)" + desc: "CALLBACK upload image failed,error = \(error!)" ) } self?.view.hideToastActivity() @@ -221,7 +216,11 @@ class PersonInfoViewController: NEBaseViewController, weak var weakSelf = self ctrl.callBack = { editText in weakSelf?.viewModel.updateSelfNickName(editText) { [weak self] error in - if error != nil { + if let err = error { + if err.code == antiErrorCode { + self?.showToastInWindow(NSLocalizedString("anti_error", comment: "")) + return + } self?.showToastInWindow(NSLocalizedString("setting_nickname_failure", comment: "")) } else { self?.navigationController?.popViewController(animated: true) @@ -239,7 +238,11 @@ class PersonInfoViewController: NEBaseViewController, gender = value == 0 ? .GENDER_MALE : .GENDER_FEMALE weakSelf?.viewModel.updateSelfSex(gender) { [weak self] error in - if error != nil { + if let err = error { + if err.code == antiErrorCode { + self?.showToastInWindow(NSLocalizedString("anti_error", comment: "")) + return + } if error?.code == protocolSendFailed { self?.showToast(commonLocalizable("network_error")) } else { @@ -275,7 +278,11 @@ class PersonInfoViewController: NEBaseViewController, weak var weakSelf = self ctrl.callBack = { editText in weakSelf?.viewModel.updateSelfMobile(editText) { [weak self] error in - if error != nil { + if let err = error { + if err.code == antiErrorCode { + self?.showToastInWindow(NSLocalizedString("anti_error", comment: "")) + return + } self?.showToastInWindow(NSLocalizedString("change_phone_failure", comment: "")) } else { self?.navigationController?.popViewController(animated: true) @@ -303,7 +310,11 @@ class PersonInfoViewController: NEBaseViewController, } weakSelf?.viewModel.updateSelfEmail(editText) { [weak self] error in - if error != nil { + if let err = error { + if err.code == antiErrorCode { + self?.showToastInWindow(NSLocalizedString("anti_error", comment: "")) + return + } self?.showToastInWindow(NSLocalizedString("change_email_failure", comment: "")) } else { self?.navigationController?.popViewController(animated: true) @@ -320,7 +331,11 @@ class PersonInfoViewController: NEBaseViewController, weak var weakSelf = self ctrl.callBack = { editText in weakSelf?.viewModel.updateSelfSign(editText) { [weak self] error in - if error != nil { + if let err = error { + if err.code == antiErrorCode { + self?.showToastInWindow(NSLocalizedString("anti_error", comment: "")) + return + } self?.showToastInWindow(NSLocalizedString("change_sign_failure", comment: "")) } else { self?.navigationController?.popViewController(animated: true) @@ -337,7 +352,7 @@ class PersonInfoViewController: NEBaseViewController, // MARK: UITableViewDelegate, UITableViewDataSource - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if viewModel.sectionData.count > section { let model = viewModel.sectionData[section] return model.cellModels.count @@ -345,11 +360,11 @@ class PersonInfoViewController: NEBaseViewController, return 0 } - func numberOfSections(in tableView: UITableView) -> Int { + public func numberOfSections(in tableView: UITableView) -> Int { viewModel.sectionData.count } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + public 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)", @@ -361,19 +376,19 @@ class PersonInfoViewController: NEBaseViewController, return UITableViewCell() } - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] if let block = model.cellClick { block() } } - func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] return model.rowHeight } - func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { if section == 0 { return 0 } @@ -386,9 +401,23 @@ class PersonInfoViewController: NEBaseViewController, return 0 } - func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let headerView = UIView() headerView.backgroundColor = .ne_lightBackgroundColor return headerView } + + // MARK: - NEContactListener + + /// 好友信息缓存更新 + /// - Parameter accountId: 用户 id + public func onContactChange(_ changeType: NEContactChangeType, _ contacts: [NEUserWithFriend]) { + for contact in contacts { + if contact.user?.accountId == IMKitClient.instance.account() { + viewModel.userInfo = contact + viewModel.refreshData() + tableView.reloadData() + } + } + } } diff --git a/app/Mine/Controller/StyleSelectionViewController.swift b/app/Mine/Controller/StyleSelectionViewController.swift index a9f333bd..0c9c466b 100644 --- a/app/Mine/Controller/StyleSelectionViewController.swift +++ b/app/Mine/Controller/StyleSelectionViewController.swift @@ -64,6 +64,7 @@ open class StyleSelectionViewController: NEBaseViewController, UICollectionViewD } getData() setupSubviews() + navigationView.moreButton.isHidden = true } func getData() { diff --git a/app/Mine/Model/CustomSettingCellModel.swift b/app/Mine/Model/CustomSettingCellModel.swift new file mode 100644 index 00000000..192462c6 --- /dev/null +++ b/app/Mine/Model/CustomSettingCellModel.swift @@ -0,0 +1,20 @@ +//// 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 NETeamUIKit +import UIKit + +class CustomSettingCellModel: SettingCellModel { + public var placeholder = "请填写" + + public var customInputText: String? + + /// 输入内容对应SDK配置字段的key,便于自动解析映射 + public var inputKey = "" + + override init() { + super.init() + rowHeight = 100 + } +} diff --git a/app/Mine/View/BirthdayDatePickerView.swift b/app/Mine/View/BirthdayDatePickerView.swift index ed76adf4..c6a1bd16 100644 --- a/app/Mine/View/BirthdayDatePickerView.swift +++ b/app/Mine/View/BirthdayDatePickerView.swift @@ -24,7 +24,7 @@ public class BirthdayDatePickerView: UIView { lazy var sureButton: UIButton = { let button = UIButton(type: .custom) button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(NSLocalizedString("confirm", comment: ""), for: .normal) + button.setTitle(commonLocalizable("ok"), for: .normal) button.setTitleColor(UIColor.ne_blueText, for: .normal) button.titleLabel?.font = UIFont.systemFont(ofSize: 14) button.addTarget(self, action: #selector(sureBtnClick), for: .touchUpInside) diff --git a/app/Mine/View/MineTableViewCell.swift b/app/Mine/View/MineTableViewCell.swift index 076a997c..52c7f0db 100644 --- a/app/Mine/View/MineTableViewCell.swift +++ b/app/Mine/View/MineTableViewCell.swift @@ -25,7 +25,7 @@ public class MineTableViewCell: UITableViewCell { return nameLabel }() - /// 分割线 + /// 分隔线 private lazy var bottomLine: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false diff --git a/app/Mine/View/Theme/CustomSettingInputCell.swift b/app/Mine/View/Theme/CustomSettingInputCell.swift new file mode 100644 index 00000000..f94d439a --- /dev/null +++ b/app/Mine/View/Theme/CustomSettingInputCell.swift @@ -0,0 +1,103 @@ +//// 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 NETeamUIKit +import UIKit + +class CustomSettingInputCell: TeamSettingSubtitleCell, UITextFieldDelegate { + public var subCornerType: CornerType { + get { cornerType } + set { + if cornerType != newValue { + cornerType = newValue + setNeedsDisplay() + } + } + } + + var dataModel: CustomSettingCellModel? + + public lazy var inputTextField: UITextField = { + let textField = UITextField() + textField.translatesAutoresizingMaskIntoConstraints = false + textField.font = UIFont.systemFont(ofSize: 14) + textField.textColor = UIColor.black + textField.textAlignment = .right + textField.borderStyle = .roundedRect + textField.textAlignment = .left + return textField + }() + + /// UI 初始化 + override func setupUI() { + contentView.addSubview(titleLabel) + contentView.addSubview(inputTextField) + + if NEStyleManager.instance.isNormalStyle() { + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 36), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15), + titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + ]) + titleWidthAnchor = titleLabel.widthAnchor.constraint(equalToConstant: 0) + titleWidthAnchor?.isActive = true + + NSLayoutConstraint.activate([ + inputTextField.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 36), + inputTextField.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + inputTextField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10), + inputTextField.heightAnchor.constraint(equalToConstant: 40), + ]) + + } else { + let whiteBgView = UIView() + whiteBgView.backgroundColor = UIColor.white + whiteBgView.translatesAutoresizingMaskIntoConstraints = false + contentView.insertSubview(whiteBgView, belowSubview: dividerLine) + NSLayoutConstraint.activate([ + whiteBgView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + whiteBgView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + whiteBgView.topAnchor.constraint(equalTo: contentView.topAnchor), + whiteBgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15), + titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + ]) + titleWidthAnchor = titleLabel.widthAnchor.constraint(equalToConstant: 0) + titleWidthAnchor?.isActive = true + + NSLayoutConstraint.activate([ + inputTextField.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + inputTextField.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + inputTextField.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10), + inputTextField.heightAnchor.constraint(equalToConstant: 40), + ]) + + dividerLineLeftMargin?.constant = 20 + dividerLineRightMargin?.constant = 0 + } + } + + /// 绑定数据 + override func configure(_ anyModel: Any) { + super.configure(anyModel) + if let model = anyModel as? CustomSettingCellModel { + inputTextField.placeholder = model.placeholder + titleLabel.text = model.cellName + subCornerType = model.cornerType + dataModel = model + } + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if let textString = textField.text as? NSString { + let changeString = textString.replacingCharacters(in: range, with: string) + dataModel?.customInputText = changeString + } + return true + } +} diff --git a/app/Mine/View/Theme/CustomSettingTextviewCell.swift b/app/Mine/View/Theme/CustomSettingTextviewCell.swift new file mode 100644 index 00000000..4f08d848 --- /dev/null +++ b/app/Mine/View/Theme/CustomSettingTextviewCell.swift @@ -0,0 +1,104 @@ +//// 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 NETeamUIKit +import UIKit + +class CustomSettingTextviewCell: TeamSettingSubtitleCell, UITextViewDelegate { + public var subCornerType: CornerType { + get { cornerType } + set { + if cornerType != newValue { + cornerType = newValue + setNeedsDisplay() + } + } + } + + var dataModel: CustomSettingCellModel? + + /// 自定义json配置输入 + public lazy var inputTextView: UITextView = { + let textView = UITextView() + textView.translatesAutoresizingMaskIntoConstraints = false + textView.font = UIFont.systemFont(ofSize: 14) + textView.textColor = UIColor.ne_darkText + textView.textAlignment = .left + textView.layer.cornerRadius = 4 + textView.layer.borderWidth = 1 + textView.layer.borderColor = UIColor.ne_outlineColor.cgColor + textView.delegate = self + return textView + }() + + override func setupUI() { + contentView.addSubview(titleLabel) + contentView.addSubview(inputTextView) + + if NEStyleManager.instance.isNormalStyle() { + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 36), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15), + titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + ]) + titleWidthAnchor = titleLabel.widthAnchor.constraint(equalToConstant: 0) + titleWidthAnchor?.isActive = true + + NSLayoutConstraint.activate([ + inputTextView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 36), + inputTextView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + inputTextView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10), + inputTextView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), + ]) + + } else { + let whiteBgView = UIView() + whiteBgView.backgroundColor = UIColor.white + whiteBgView.translatesAutoresizingMaskIntoConstraints = false + contentView.insertSubview(whiteBgView, belowSubview: dividerLine) + NSLayoutConstraint.activate([ + whiteBgView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + whiteBgView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + whiteBgView.topAnchor.constraint(equalTo: contentView.topAnchor), + whiteBgView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 15), + titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + ]) + titleWidthAnchor = titleLabel.widthAnchor.constraint(equalToConstant: 0) + titleWidthAnchor?.isActive = true + + NSLayoutConstraint.activate([ + inputTextView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + inputTextView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + inputTextView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 10), + inputTextView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -10), + ]) + + dividerLineLeftMargin?.constant = 20 + dividerLineRightMargin?.constant = 0 + } + } + + override func configure(_ anyModel: Any) { + super.configure(anyModel) + if let model = anyModel as? CustomSettingCellModel { + titleLabel.text = model.cellName + subCornerType = model.cornerType + inputTextView.text = model.customInputText + dataModel = model + } + } + + func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if let textString = textView.text as? NSString { + let changeString = textString.replacingCharacters(in: range, with: text) + dataModel?.customInputText = changeString + } + return true + } +} diff --git a/app/Mine/ViewModel/MineSettingViewModel.swift b/app/Mine/ViewModel/MineSettingViewModel.swift index 3e578fb0..7e3bc266 100644 --- a/app/Mine/ViewModel/MineSettingViewModel.swift +++ b/app/Mine/ViewModel/MineSettingViewModel.swift @@ -11,6 +11,7 @@ public protocol MineSettingViewModelDelegate: NSObjectProtocol { func didStyleClick() func didClickCleanCache() func didClickConfigTest() + func didClickSDKConfig() } @objcMembers @@ -63,6 +64,16 @@ public class MineSettingViewModel: NSObject { model.cellModels.append(configTest) #endif + /* + let sdkConfigModel = SettingCellModel() + sdkConfigModel.cellName = "私有云环境配置" + sdkConfigModel.type = SettingCellType.SettingArrowCell.rawValue + sdkConfigModel.cellClick = { + weakSelf?.delegate?.didClickSDKConfig() + } + model.cellModels.append(sdkConfigModel) + */ + model.setCornerType() return model } diff --git a/app/Mine/ViewModel/PersonInfoViewModel.swift b/app/Mine/ViewModel/PersonInfoViewModel.swift index 564d1f19..e6b7dfb1 100644 --- a/app/Mine/ViewModel/PersonInfoViewModel.swift +++ b/app/Mine/ViewModel/PersonInfoViewModel.swift @@ -36,7 +36,7 @@ public class PersonInfoViewModel: NSObject { sectionData.append(getSecondSection()) completion() } else { - ContactRepo.shared.getUserList(accountIds: [IMKitClient.instance.account()]) { [weak self] userFriend, error in + ContactRepo.shared.getUserListFromCloud(accountIds: [IMKitClient.instance.account()]) { [weak self] userFriend, error in guard let self = self else { return } self.userInfo = userFriend?.first self.sectionData.append(self.getFirstSection()) From c95a704c0a1a97797a5897c78f02ffb10287e75f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=AF=97=E6=96=87?= Date: Thu, 18 Jul 2024 13:45:58 +0800 Subject: [PATCH 4/5] loadService --- app/Main/AppDelegate.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Main/AppDelegate.swift b/app/Main/AppDelegate.swift index 6758bdf7..c0bf1a31 100644 --- a/app/Main/AppDelegate.swift +++ b/app/Main/AppDelegate.swift @@ -43,6 +43,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let account = "<#account#>" let token = "<#token#>" + loadService() weak var weakSelf = self IMKitClient.instance.login(account, token, nil) { error in From 2d4c65ef42fc6d4614f2170f45dfe1dea93b03b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=AF=97=E6=96=87?= Date: Mon, 12 Aug 2024 15:20:13 +0800 Subject: [PATCH 5/5] update podspec --- NEAISearchKit/NEAISearchKit.podspec | 2 +- NEChatUIKit/NEChatUIKit.podspec | 3 +-- NEContactUIKit/NEContactUIKit.podspec | 2 +- NEConversationUIKit/NEConversationUIKit.podspec | 2 +- NEMapKit/NEMapKit.podspec | 6 ++---- NETeamUIKit/NETeamUIKit.podspec | 2 +- 6 files changed, 7 insertions(+), 10 deletions(-) diff --git a/NEAISearchKit/NEAISearchKit.podspec b/NEAISearchKit/NEAISearchKit.podspec index 3b1c3878..302edfa6 100644 --- a/NEAISearchKit/NEAISearchKit.podspec +++ b/NEAISearchKit/NEAISearchKit.podspec @@ -38,7 +38,7 @@ TODO: Add long description of the pod here. s.resource = 'NEAISearchKit/Assets/**/*' s.dependency 'NEChatKit' - s.dependency 'NECommonUIKit' + s.dependency 'NECommonUIKit', '9.7.0' s.dependency 'lottie-ios','4.4.0' end diff --git a/NEChatUIKit/NEChatUIKit.podspec b/NEChatUIKit/NEChatUIKit.podspec index 133b6af4..46d7d6d5 100644 --- a/NEChatUIKit/NEChatUIKit.podspec +++ b/NEChatUIKit/NEChatUIKit.podspec @@ -43,8 +43,7 @@ TODO: Add long description of the pod here. s.resource = 'NEChatUIKit/Assets/**/*' s.dependency 'NEChatKit' - s.dependency 'NECommonUIKit' - s.dependency 'NECommonKit' + s.dependency 'NECommonUIKit', '9.7.0' s.dependency 'MJRefresh' s.dependency 'SDWebImageWebPCoder' s.dependency 'SDWebImageSVGKitPlugin' diff --git a/NEContactUIKit/NEContactUIKit.podspec b/NEContactUIKit/NEContactUIKit.podspec index dbb4b5eb..025ad722 100644 --- a/NEContactUIKit/NEContactUIKit.podspec +++ b/NEContactUIKit/NEContactUIKit.podspec @@ -33,6 +33,6 @@ Pod::Spec.new do |s| s.source_files = 'NEContactUIKit/Classes/**/*' s.resource = 'NEContactUIKit/Assets/**/*' s.dependency 'NEChatKit' - s.dependency 'NECommonUIKit' + s.dependency 'NECommonUIKit', '9.7.0' s.dependency 'MJRefresh' end diff --git a/NEConversationUIKit/NEConversationUIKit.podspec b/NEConversationUIKit/NEConversationUIKit.podspec index 4398fb6f..49f67d69 100644 --- a/NEConversationUIKit/NEConversationUIKit.podspec +++ b/NEConversationUIKit/NEConversationUIKit.podspec @@ -38,7 +38,7 @@ TODO: Add long description of the pod here. 'BUILD_LIBRARY_FOR_DISTRIBUTION' => 'YES' } - s.dependency 'NECommonUIKit' + s.dependency 'NECommonUIKit', '9.7.0' s.dependency 'NEChatKit' s.dependency 'MJRefresh' end diff --git a/NEMapKit/NEMapKit.podspec b/NEMapKit/NEMapKit.podspec index bba16931..b13eb79f 100644 --- a/NEMapKit/NEMapKit.podspec +++ b/NEMapKit/NEMapKit.podspec @@ -29,14 +29,12 @@ TODO: Add long description of the pod here. # s.social_media_url = 'https://twitter.com/' s.ios.deployment_target = '12.0' - + s.static_framework = true s.source_files = 'NEMapKit/Classes/**/*' # s.resource = 'NEMapKit/Assets/**/*' s.dependency 'AMap3DMap' s.dependency 'AMapSearch' s.dependency 'AMapLocation' - s.static_framework = true - s.dependency 'NEChatUIKit' - s.dependency 'NEChatKit' + s.dependency 'NEChatUIKit', '10.3.0' end diff --git a/NETeamUIKit/NETeamUIKit.podspec b/NETeamUIKit/NETeamUIKit.podspec index 88c3b5a4..4be2243e 100644 --- a/NETeamUIKit/NETeamUIKit.podspec +++ b/NETeamUIKit/NETeamUIKit.podspec @@ -36,6 +36,6 @@ TODO: Add long description of the pod here. s.source_files = 'NETeamUIKit/Classes/**/*' s.resource = 'NETeamUIKit/Assets/**/*' - s.dependency 'NEChatUIKit' + s.dependency 'NEChatUIKit', '10.3.0' end