From 0aa7d374b48c07ae5873314162044fd5ea972eef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=AF=97=E6=96=87?= Date: Mon, 11 Dec 2023 19:45:04 +0800 Subject: [PATCH 1/4] update Podfile --- .../IMUIKitOCExample/Main/AppDelegate.m | 8 +- IMUIKitOC/IMUIKitOCExample/Podfile | 75 ++++++++++--------- Podfile | 27 +++---- app/Main/AppDelegate.swift | 9 ++- app/Main/AppKey.swift | 2 +- 5 files changed, 64 insertions(+), 57 deletions(-) diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m index 28d53e7e..2915938b 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m @@ -8,11 +8,12 @@ #import "AppKey.h" #import "NETabbarController.h" +#import +#import #import #import #import -#import -#import +#import //#import #import "CustomRouterViewController.h" @@ -44,7 +45,7 @@ - (void)setupInit { [[IMKitClient instance] setupCoreKitIM:option]; // 登录IM之前先初始化 @ 消息监听mananger - NEAtMessageManager * _ = [NEAtMessageManager instance]; + [NEAtMessageManager setupInstance]; [[IMKitClient instance] loginIM:@"imaccid" :@"imToken" :^(NSError * _Nullable error) { if (error != nil) { @@ -65,6 +66,7 @@ - (void)registerRouter { [ChatRouter register]; [ConversationRouter register]; [ContactRouter register]; + [TeamRouter register]; [[Router shared] register:@"imkit://chat/p2pChat.page" closure:^(NSDictionary *_Nonnull param) { diff --git a/IMUIKitOC/IMUIKitOCExample/Podfile b/IMUIKitOC/IMUIKitOCExample/Podfile index 30342eee..61b957b6 100644 --- a/IMUIKitOC/IMUIKitOCExample/Podfile +++ b/IMUIKitOC/IMUIKitOCExample/Podfile @@ -1,56 +1,57 @@ # Uncomment the next line to define a global platform for your project -# platform :ios, '9.0' - -platform :ios, '9.0' + platform :ios, '11.0' source 'https://github.com/CocoaPods/Specs.git' target 'IMUIKitOCExample' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! + #登录组件 pod 'YXLogin', '1.0.0' - + #可选UI库 - pod 'NEContactUIKit', '9.5.0' -# pod 'NEQChatUIKit', '9.5.0' - pod 'NEConversationUIKit', '9.5.0' - pod 'NEChatUIKit', '9.5.0' - pod 'NETeamUIKit', '9.5.0' - +# pod 'NEContactUIKit', '9.6.5' +# pod 'NEConversationUIKit', '9.6.5' +# pod 'NEChatUIKit', '9.6.5' +# pod 'NETeamUIKit', '9.6.5' #可选Kit库(和UIKit对应) - pod 'NEContactKit', '9.5.0' -# pod 'NEQChatKit', '9.5.0' - pod 'NEConversationKit', '9.5.0' - pod 'NEChatKit', '9.5.0' - pod 'NETeamKit', '9.5.0' + pod 'NEChatKit', '9.6.5' #基础kit库 - pod 'NECommonUIKit', '9.5.0' - pod 'NECommonKit', '9.5.0' - pod 'NECoreIMKit', '9.5.0' - pod 'NECoreKit', '9.5.0' - pod 'NEMapKit', '9.5.0' - + pod 'NIMSDK_LITE','9.14.0' + pod 'NECommonUIKit', '9.6.5' + pod 'NECommonKit', '9.6.4' + pod 'NECoreIMKit', '9.6.5' + pod 'NECoreKit', '9.6.5' + + #扩展库 +# pod 'NEMapKit', '9.6.5' + + #呼叫组件,音视频通话能力,需要开通 音视频2.0,可选,聊天一面会根据依赖初始化自动显示音视频通话入口 + pod 'NIMSDK_LITE','9.14.0' + pod 'NERtcCallKit/NOS_Special', '2.2.0' + pod 'NERtcCallUIKit/NOS_Special', '2.2.0' + pod 'NERtcSDK', '5.5.2' + + + # # 如果需要查看UI部分源码请注释掉以上在线依赖,打开下面的本地依赖 + 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 'NEMapKit', :path => '../../NEMapKit/NEMapKit.podspec' +# pod 'NERtcCallUIKit', :path => '../../NERtcCallUIKit/NERtcCallUIKit.podspec' +end - #fix bug in Xcode 14 - post_install do |installer| - installer.pods_project.targets.each do |target| - if target.name == 'RSKPlaceholderTextView' - target.build_configurations.each do |config| - config.build_settings['BUILD_LIBRARY_FOR_DISTRIBUTION'] = 'YES' - end - 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'] = '11.0' end end - - # 如果需要查看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' - end diff --git a/Podfile b/Podfile index 15c129a7..a5edbe7a 100644 --- a/Podfile +++ b/Podfile @@ -10,10 +10,10 @@ target 'app' do pod 'YXLogin', '1.0.0' #可选UI库 - pod 'NEContactUIKit', '9.6.5' - pod 'NEConversationUIKit', '9.6.5' - pod 'NEChatUIKit', '9.6.5' - pod 'NETeamUIKit', '9.6.5' +# pod 'NEContactUIKit', '9.6.5' +# pod 'NEConversationUIKit', '9.6.5' +# pod 'NEChatUIKit', '9.6.5' +# pod 'NETeamUIKit', '9.6.5' #可选Kit库(和UIKit对应) pod 'NEChatKit', '9.6.5' @@ -26,21 +26,22 @@ target 'app' do pod 'NECoreKit', '9.6.5' #扩展库 - pod 'NEMapKit', '9.6.5' +# pod 'NEMapKit', '9.6.5' #呼叫组件,音视频通话能力,需要开通 音视频2.0,可选,聊天一面会根据依赖初始化自动显示音视频通话入口 + pod 'NIMSDK_LITE','9.14.0' pod 'NERtcCallKit/NOS_Special', '2.2.0' - pod 'NERtcCallUIKit/NOS_Special', '2.2.0' +# pod 'NERtcCallUIKit/NOS_Special', '2.2.0' pod 'NERtcSDK', '5.5.2' # # 如果需要查看UI部分源码请注释掉以上在线依赖,打开下面的本地依赖 - # 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 'NEMapKit', :path => 'NEMapKit/NEMapKit.podspec' - # pod 'NERtcCallUIKit', :path => 'NERtcCallUIKit/NERtcCallUIKit.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 'NEMapKit', :path => 'NEMapKit/NEMapKit.podspec' + pod 'NERtcCallUIKit', :path => 'NERtcCallUIKit/NERtcCallUIKit.podspec' end @@ -50,7 +51,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'] = '12.0' + config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0' end end end diff --git a/app/Main/AppDelegate.swift b/app/Main/AppDelegate.swift index c0389c49..0314f050 100644 --- a/app/Main/AppDelegate.swift +++ b/app/Main/AppDelegate.swift @@ -33,14 +33,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func setupInit(){ - // init + // 初始化NIMSDK let option = NIMSDKOption() option.appKey = AppKey.appKey option.apnsCername = AppKey.pushCerName IMKitClient.instance.setupCoreKitIM(option) - let account = "<#account#>" - let token = "<#token#>" + // 登录IM之前先初始化 @ 消息监听mananger + NEAtMessageManager.setupInstance() + + let account = "chenyu3" + let token = "123456" weak var weakSelf = self IMKitClient.instance.loginIM(account, token) { error in diff --git a/app/Main/AppKey.swift b/app/Main/AppKey.swift index 234281af..618e0c4a 100644 --- a/app/Main/AppKey.swift +++ b/app/Main/AppKey.swift @@ -6,7 +6,7 @@ public struct AppKey { #if DEBUG public static let pushCerName = "<#请输入推送证书#>" - public static let appKey = "<#请输入appkey#>" + public static let appKey = "3e215d27b6a6a9e27dad7ef36dd5b65c" public static let gaodeMapAppkey = "<#输入高德地图key#>" #else public static let pushCerName = "<#请输入推送证书#>" From b6b6131ab8974cff840f082cc71f25378cc943c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=AF=97=E6=96=87?= Date: Mon, 11 Dec 2023 19:48:29 +0800 Subject: [PATCH 2/4] rm account --- app/Main/AppDelegate.swift | 4 ++-- app/Main/AppKey.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Main/AppDelegate.swift b/app/Main/AppDelegate.swift index 0314f050..d87ceaa6 100644 --- a/app/Main/AppDelegate.swift +++ b/app/Main/AppDelegate.swift @@ -42,8 +42,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // 登录IM之前先初始化 @ 消息监听mananger NEAtMessageManager.setupInstance() - let account = "chenyu3" - let token = "123456" + let account = "<#account#>" + let token = "<#token#>" weak var weakSelf = self IMKitClient.instance.loginIM(account, token) { error in diff --git a/app/Main/AppKey.swift b/app/Main/AppKey.swift index 618e0c4a..234281af 100644 --- a/app/Main/AppKey.swift +++ b/app/Main/AppKey.swift @@ -6,7 +6,7 @@ public struct AppKey { #if DEBUG public static let pushCerName = "<#请输入推送证书#>" - public static let appKey = "3e215d27b6a6a9e27dad7ef36dd5b65c" + public static let appKey = "<#请输入appkey#>" public static let gaodeMapAppkey = "<#输入高德地图key#>" #else public static let pushCerName = "<#请输入推送证书#>" From 610c0b2404b3ca0a93bc77af462877c080e9a649 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=AF=97=E6=96=87?= Date: Thu, 25 Jan 2024 16:56:56 +0800 Subject: [PATCH 3/4] v9.7.0 --- .../project.pbxproj | 20 +- .../IMUIKitOCExample/AppKey.h | 2 +- .../IMUIKitOCExample/Info.plist | 8 +- .../IMUIKitOCExample/Main/AppDelegate.h | 1 - .../IMUIKitOCExample/Main/AppDelegate.m | 116 +- .../Main/InhertViewController.swift | 24 + .../Main/NETabbarController.m | 24 +- .../Main/TestViewController.swift | 23 + IMUIKitOC/IMUIKitOCExample/Podfile | 70 +- NEChatUIKit/NEChatUIKit.podspec | 2 +- .../fun_select_delete.imageset/Contents.json | 22 + .../fun_selece_delete@2x.png | Bin 0 -> 1692 bytes .../fun_selece_delete@3x.png | Bin 0 -> 2582 bytes .../Contents.json | 22 + .../fun_select_merge@2x.png | Bin 0 -> 1998 bytes .../fun_select_merge@3x.png | Bin 0 -> 2992 bytes .../Contents.json | 22 + .../fun_select_perItem@2x.png | Bin 0 -> 1956 bytes .../fun_select_perItem@3x.png | Bin 0 -> 3036 bytes .../Contents.json | 22 + .../fun_unselect_delete@2x.png | Bin 0 -> 1813 bytes .../fun_unselect_delete@3x.png | Bin 0 -> 2604 bytes .../Contents.json | 22 + .../fun_unselect_merge@2x.png | Bin 0 -> 2135 bytes .../fun_unselect_merge@3x.png | Bin 0 -> 3073 bytes .../Contents.json | 22 + .../fun_unselect_perItem@2x.png | Bin 0 -> 2115 bytes .../fun_unselect_perItem@3x.png | Bin 0 -> 3090 bytes .../Contents.json | 22 + .../merge_message_receive_fun@2x.png | Bin 0 -> 693 bytes .../merge_message_receive_fun@3x.png | Bin 0 -> 1062 bytes .../Contents.json | 22 + .../merge_message_send_fun@2x.png | Bin 0 -> 702 bytes .../merge_message_send_fun@3x.png | Bin 0 -> 1070 bytes .../fun_input_fold.imageset/Contents.json | 22 + .../fun_input_fold.imageset/Frame@2x.png | Bin 0 -> 511 bytes .../fun_input_fold.imageset/Frame@3x.png | Bin 0 -> 683 bytes .../fun_input_unfold.imageset/Contents.json | 22 + .../fun_input_unfold.imageset/Vector@2x.png | Bin 0 -> 507 bytes .../fun_input_unfold.imageset/Vector@3x.png | Bin 0 -> 675 bytes .../fun_select.imageset/Contents.json | 22 + .../fun_select.imageset/clicked_we@2x.png | Bin 0 -> 1304 bytes .../fun_select.imageset/clicked_we@3x.png | Bin 0 -> 1780 bytes .../Contents.json | 22 + .../multiple_send_image.imageset/Frame@2x.png | Bin 0 -> 1330 bytes .../multiple_send_image.imageset/Frame@3x.png | Bin 0 -> 1846 bytes .../Contents.json | 22 + .../merge_message_receive@2x.png | Bin 0 -> 1273 bytes .../merge_message_receive@3x.png | Bin 0 -> 1964 bytes .../Contents.json | 22 + .../merge_message_send@2x.png | Bin 0 -> 1295 bytes .../merge_message_send@3x.png | Bin 0 -> 1949 bytes .../normal_input_fold.imageset/Contents.json | 22 + .../normal_input_fold.imageset/Frame@2x.png | Bin 0 -> 568 bytes .../normal_input_fold.imageset/Frame@3x.png | Bin 0 -> 771 bytes .../Contents.json | 22 + .../Vector@2x.png | Bin 0 -> 551 bytes .../Vector@3x.png | Bin 0 -> 707 bytes .../Chat/select_delete.imageset/Contents.json | 22 + .../selece_delete@2x.png | Bin 0 -> 1794 bytes .../selece_delete@3x.png | Bin 0 -> 2720 bytes .../Contents.json | 22 + .../select_merge@2x.png | Bin 0 -> 2168 bytes .../select_merge@3x.png | Bin 0 -> 3294 bytes .../Contents.json | 22 + .../select_perItem@2x.png | Bin 0 -> 2165 bytes .../select_perItem@3x.png | Bin 0 -> 3299 bytes .../unselect_delete.imageset/Contents.json | 22 + .../unselect_delete@2x.png | Bin 0 -> 1995 bytes .../unselect_delete@3x.png | Bin 0 -> 2980 bytes .../Contents.json | 22 + .../unselect_merge@2x.png | Bin 0 -> 2357 bytes .../unselect_merge@3x.png | Bin 0 -> 3537 bytes .../Contents.json | 22 + .../unselect_perItem@2x.png | Bin 0 -> 2344 bytes .../unselect_perItem@3x.png | Bin 0 -> 3552 bytes .../Assets/en.lproj/Localizable.strings | 27 +- .../Assets/zh-Hans.lproj/Localizable.strings | 47 +- .../Base/BaseView/ChatCenterTextCell.swift | 4 +- .../Base/BaseView/ChatCornerCell.swift | 2 +- .../Base/BaseView/ChatHeaderView.swift | 6 +- .../Base/BaseView/ChatImageTextCell.swift | 6 +- .../Base/BaseView/ChatSectionView.swift | 4 +- .../Classes/Base/BaseView/ChatStateCell.swift | 15 +- .../Base/BaseView/ChatTextArrowCell.swift | 4 +- .../Classes/Base/BaseView/ChatTextCell.swift | 4 +- .../Base/BaseView/ChatUnfoldCell.swift | 15 +- .../Base/BaseView/ChatUserHeaderView.swift | 6 +- .../Base/BaseView/NEChatBaseCell.swift | 3 +- .../ChatTableViewController.swift | 8 +- .../Chat/Controller/ChatViewController.swift | 1240 ++++++++++++----- .../MultiForwardViewController.swift | 387 +++++ .../NEBaseForwardAlertViewController.swift | 129 +- .../NEBasePinMessageViewController.swift | 390 +++++- .../Controller/NEBaseReadViewController.swift | 14 +- .../NEBaseSelectUserViewController.swift | 68 +- .../NEBaseUserSettingViewController.swift | 6 +- .../Controller/NEDetailMapController.swift | 33 +- .../Chat/Controller/TextViewController.swift | 108 +- .../Classes/Chat/Emoji/EmojiPageView.swift | 14 +- .../Emoji/InputEmoticonContainerView.swift | 18 +- .../Chat/Emoji/InputEmoticonTabView.swift | 11 +- .../Chat/Emoji/NEEmotionAttachment.swift | 2 +- .../Classes/Chat/Emoji/NEEmotionTool.swift | 2 +- .../Chat/Emoji/NIMInputEmoticonButton.swift | 8 +- .../Chat/Emoji/NIMInputEmoticonManager.swift | 16 +- .../Chat/Helper/ChatDeduplicationHelper.swift | 79 ++ .../Chat/Helper/ChatMessageHelper.swift | 294 ++++ .../Classes/Chat/Helper/MessageUtils.swift | 43 +- .../Helper/NotificationMessageUtils.swift | 46 +- .../Chat/Helper/ReplyMessageUtil.swift | 35 +- .../Chat/Model/MessageAtCacheModel.swift | 2 +- .../Chat/Model/MessageAtInfoModel.swift | 2 +- .../Chat/Model/MessageAudioModel.swift | 7 +- .../Chat/Model/MessageCallRecordModel.swift | 6 +- .../Chat/Model/MessageContentModel.swift | 54 +- .../Chat/Model/MessageCustomModel.swift | 8 +- .../Classes/Chat/Model/MessageFileModel.swift | 6 +- .../Chat/Model/MessageImageModel.swift | 7 +- .../Chat/Model/MessageLocationModel.swift | 8 +- .../Classes/Chat/Model/MessageModel.swift | 13 +- .../Chat/Model/MessageRichTextModel.swift | 41 + .../Classes/Chat/Model/MessageTextModel.swift | 6 +- .../Classes/Chat/Model/MessageTipsModel.swift | 68 +- .../Chat/Model/MessageVideoModel.swift | 8 +- .../Model/NECustomAttachmentProtocol.swift | 11 - .../Classes/Chat/Model/NEMoreItemModel.swift | 5 +- .../Classes/Chat/Model/OperationItem.swift | 18 +- .../Chat/Model/PinMessageFileModel.swift | 15 + .../Classes/Chat/Model/PinMessageModel.swift | 124 +- .../Chat/Model/UserSettingCellModel.swift | 4 +- .../View/Cell/NEBaseChatMessageCell.swift | 170 ++- .../View/Cell/NEBaseChatMessageTipCell.swift | 44 +- .../View/Cell/NEBaseChatTeamMemberCell.swift | 4 +- .../View/Cell/NEBaseUserSettingCell.swift | 11 - .../Cell/NEBaseUserSettingSwitchCell.swift | 11 - .../Chat/View/Cell/OperationCell.swift | 2 + .../PinCell/NEBasePinMessageAudioCell.swift | 39 +- .../Cell/PinCell/NEBasePinMessageCell.swift | 23 +- .../PinCell/NEBasePinMessageDefaultCell.swift | 2 +- .../PinCell/NEBasePinMessageFileCell.swift | 42 +- .../PinCell/NEBasePinMessageImageCell.swift | 12 +- .../NEBasePinMessageLocationCell.swift | 6 +- .../NEBasePinMessageMultiForwardCell.swift | 191 +++ .../NEBasePinMessageRichTextCell.swift | 57 + .../PinCell/NEBasePinMessageTextCell.swift | 12 +- .../PinCell/NEBasePinMessageVideoCell.swift | 6 +- .../View/Cell/UserBaseTableViewCell.swift | 9 +- .../ChatView/ChatActivityIndicatorView.swift | 4 +- .../View/ChatView/ChatBrokenNetworkView.swift | 6 +- .../Chat/View/ChatView/ChatRecordView.swift | 6 +- .../View/ChatView/CirleProgressView.swift | 5 +- .../View/ChatView/MessageOperationView.swift | 16 +- .../View/ChatView/NEBaseChatInputView.swift | 348 ++++- .../View/ChatView/NEChatMoreActionView.swift | 14 +- .../Chat/View/ChatView/NEInputMoreCell.swift | 5 +- .../ChatView/NEMutilSelectBottomView.swift | 164 +++ .../Chat/View/ChatView/ReplyView.swift | 6 +- .../Chat/View/MapView/NEMapAddressCell.swift | 4 +- .../View/MapView/NEMapGuideBottomView.swift | 4 +- .../Chat/ViewModel/ChatViewModel.swift | 1102 +++++++++------ .../ViewModel/MultiForwardViewModel.swift | 94 ++ .../Chat/ViewModel/PinMessageViewModel.swift | 67 +- .../Chat/ViewModel/TeamChatViewModel.swift | 40 +- .../Chat/ViewModel/TeamMemberSelectVM.swift | 6 +- .../Chat/ViewModel/UserSettingViewModel.swift | 44 +- .../Classes/ChatConfig/ChatUIConfig.swift | 8 + .../Classes/ChatRouter/NEBaseChatRouter.swift | 2 +- .../Common/ChatCellConstantValue.swift | 23 +- .../Classes/Common/ChatConstant.swift | 12 +- .../Classes/Common/NEChatUIKitClient.swift | 8 +- .../Extension/ChatStringExtension.swift | 15 +- .../FunUI/Cell/FunChatMessageAudioCell.swift | 7 +- .../FunUI/Cell/FunChatMessageBaseCell.swift | 11 +- .../FunUI/Cell/FunChatMessageCallCell.swift | 7 +- .../FunUI/Cell/FunChatMessageFileCell.swift | 23 +- .../FunUI/Cell/FunChatMessageImageCell.swift | 10 +- .../Cell/FunChatMessageLocationCell.swift | 12 +- .../Cell/FunChatMessageMultiForwardCell.swift | 384 +++++ .../FunUI/Cell/FunChatMessageReplyCell.swift | 36 +- .../FunUI/Cell/FunChatMessageRevokeCell.swift | 37 +- .../Cell/FunChatMessageRichTextCell.swift | 117 ++ .../FunUI/Cell/FunChatMessageTextCell.swift | 9 +- .../FunUI/Cell/FunChatMessageTipCell.swift | 4 +- .../FunUI/Cell/FunChatMessageVideoCell.swift | 7 +- .../Cell/PinCell/FunPinMessageAudioCell.swift | 2 +- .../Cell/PinCell/FunPinMessageImageCell.swift | 1 + .../FunPinMessageMultiForwardCell.swift | 76 + .../PinCell/FunPinMessageRichTextCell.swift | 16 + .../Controller/FunChatViewController.swift | 288 ++-- .../FunForwardAlertViewController.swift | 8 +- .../FunGroupChatViewController.swift | 131 +- .../FunMultiForwardViewController.swift | 69 + .../Controller/FunP2PChatViewController.swift | 2 +- .../FunPinMessageViewController.swift | 30 +- .../Controller/FunReadViewController.swift | 6 +- .../FunSelectUserViewController.swift | 4 +- .../FunUserSettingViewController.swift | 8 +- .../Classes/FunUI/FunChatRouter.swift | 8 +- .../Classes/FunUI/FunChatUIColor.swift | 2 + .../Classes/FunUI/View/FunChatInputView.swift | 177 ++- .../FunUI/View/FunRecordAudioView.swift | 6 +- .../NormalUI/Cell/ChatMessageAudioCell.swift | 7 +- .../NormalUI/Cell/ChatMessageCallCell.swift | 11 +- .../NormalUI/Cell/ChatMessageFileCell.swift | 21 +- .../NormalUI/Cell/ChatMessageImageCell.swift | 10 +- .../Cell/ChatMessageLocationCell.swift | 12 +- .../Cell/ChatMessageMultiForwardCell.swift | 382 +++++ .../NormalUI/Cell/ChatMessageReplyCell.swift | 13 +- .../NormalUI/Cell/ChatMessageRevokeCell.swift | 23 +- .../Cell/ChatMessageRichTextCell.swift | 134 ++ .../NormalUI/Cell/ChatMessageTextCell.swift | 10 +- .../NormalUI/Cell/ChatMessageVideoCell.swift | 7 +- .../PinCell/PinMessageMultiForwardCell.swift | 68 + .../Cell/PinCell/PinMessageRichTextCell.swift | 8 + .../ForwardAlertViewController.swift | 8 +- .../Controller/GroupChatViewController.swift | 133 +- .../Controller/NormalChatViewController.swift | 133 +- .../NormalMultiForwardViewController.swift | 28 + .../Controller/P2PChatViewController.swift | 2 +- .../Controller/PinMessageViewController.swift | 21 +- .../Controller/ReadViewController.swift | 2 +- .../Controller/SelectUserViewController.swift | 4 +- .../Classes/NormalUI/NormalChatRouter.swift | 8 +- .../Classes/NormalUI/View/ChatInpuView.swift | 115 +- .../Protocol/ChatInputViewDelegate.swift | 7 +- NEContactUIKit/NEContactUIKit.podspec | 2 +- .../Classes/Base/NEBaseContactViewCell.swift | 14 +- .../BlackList/Cell/NEBaseBlackListCell.swift | 8 +- .../NEBaseBlackListViewController.swift | 20 +- .../ViewModel/BlackListViewModel.swift | 10 +- .../Classes/Common/ContactConst.swift | 6 - .../Classes/Common/NEBaseContactRouter.swift | 2 +- .../FunUI/Cell/FunContactTableViewCell.swift | 2 + .../Classes/FunUI/FunContactRouter.swift | 10 +- .../FunUI/View/FunUserInfoHeaderView.swift | 4 +- .../FunBlackListViewController.swift | 4 +- .../FunContactUserViewController.swift | 6 +- .../FunContactsSelectedViewController.swift | 4 +- .../FunTeamListViewController.swift | 4 +- .../Classes/Model/ContactInfo.swift | 7 +- .../Classes/Model/ContactSection.swift | 5 + .../NormalUI/NromalContactRouter.swift | 10 +- .../NormalUI/View/UserInfoHeaderView.swift | 4 +- .../BlackListViewController.swift | 2 +- .../ContactUserViewController.swift | 4 +- .../ContactsSelectedViewController.swift | 4 +- .../TeamListViewController.swift | 4 +- .../Team/Cell/NEBaseTeamTableViewCell.swift | 6 +- .../NEBaseTeamListViewController.swift | 10 +- .../Team/ViewModel/TeamListViewModel.swift | 10 +- ...EBaseValidationMessageViewController.swift | 14 +- .../ValidationMessageViewModel.swift | 26 +- .../Views/NEBaseSystemNotificationCell.swift | 4 +- .../Views/NEBaseValidationCell.swift | 21 +- .../Classes/ViewModel/ContactGroup.swift | 2 +- .../ViewModel/ContactUserViewModel.swift | 20 +- .../Classes/ViewModel/ContactViewModel.swift | 23 +- .../ViewModel/FindFriendViewModel.swift | 4 +- .../Cell/NEBaseContactSelectedCell.swift | 13 +- .../Cell/NEBaseContactTableViewCell.swift | 10 +- ...NEBaseContactRemakNameViewController.swift | 17 +- .../NEBaseContactUserViewController.swift | 33 +- ...NEBaseContactsSelectedViewController.swift | 24 +- .../Views/NEBaseContactsViewController.swift | 60 +- .../NEBaseFindFriendViewController.swift | 2 +- .../Views/NEBaseUserInfoHeaderView.swift | 2 +- .../NEConversationUIKit.podspec | 2 +- .../Assets/en.lproj/Localizable.strings | 7 + .../Assets/zh-Hans.lproj/Localizable.strings | 8 +- .../ConversationDeduplicationHelper.swift | 50 + .../Cell/NEBaseConversationListCell.swift | 19 +- .../Cell/NEBaseConversationSearchCell.swift | 17 +- .../NEBaseConversationController.swift | 61 +- ...BaseConversationNavigationController.swift | 2 +- .../NEBaseConversationSearchController.swift | 47 +- ...ntroller.swift => NEBasePopListView.swift} | 25 +- .../ConversationSearchViewModel.swift | 14 +- .../ViewModel/ConversationViewModel.swift | 198 ++- .../FunConversationController.swift | 2 +- ...wController.swift => FunPopListView.swift} | 10 +- .../Classes/Manager/NEAtMessageManager.swift | 22 +- .../Controller/ConversationController.swift | 2 +- ...ViewController.swift => PopListView.swift} | 6 +- .../Classes/Util/NEMessageUtil.swift | 30 +- NEMapKit/NEMapKit.podspec | 2 +- NERtcCallUIKit/NERtcCallUIKit.podspec | 46 +- NETeamUIKit/NETeamUIKit.podspec | 2 +- .../fun_select.imageset/Contents.json | 22 + .../fun_select.imageset/clicked_we@2x.png | Bin 0 -> 1304 bytes .../fun_select.imageset/clicked_we@3x.png | Bin 0 -> 1780 bytes .../fun_user_empty.imageset/Contents.json | 22 + .../fun_user_empty@2x.png | Bin 0 -> 9965 bytes .../fun_user_empty@3x.png | Bin 0 -> 15670 bytes .../common/search.imageset/Contents.json | 22 + .../common/search.imageset/Frame@2x.png | Bin 0 -> 814 bytes .../common/search.imageset/Frame@3x.png | Bin 0 -> 1132 bytes .../common/unselect.imageset/Contents.json | 22 + .../common/unselect.imageset/unselect@2x.png | Bin 0 -> 956 bytes .../common/unselect.imageset/unslect@3x.png | Bin 0 -> 1369 bytes .../select.imageset/Contents.json | 22 + .../select.imageset/select@2x.png | Bin 0 -> 940 bytes .../select.imageset/select@3x.png | Bin 0 -> 1312 bytes .../Assets/en.lproj/Localizable.strings | 39 +- .../Assets/zh-Hans.lproj/Localizable.strings | 33 + .../FunUI/Cell/FunTeamArrowSettingCell.swift | 2 +- .../FunUI/Cell/FunTeamManagerMemberCell.swift | 32 + .../FunUI/Cell/FunTeamMemberCell.swift | 36 +- .../FunUI/Cell/FunTeamMemberSelectCell.swift | 47 + .../Cell/FunTeamSettingLabelArrowCell.swift | 32 + .../Controller/FunTeamManageController.swift | 67 + .../FunTeamManagerListController.swift | 94 ++ .../FunTeamMemberSelectController.swift | 45 + .../Controller/FunTeamMembersController.swift | 28 +- .../FunTeamSettingViewController.swift | 27 +- .../Classes/FunUI/FunTeamUIColor.swift | 5 + .../Classes/NEBaseTeamRouter.swift | 22 +- .../NormalUI/Cell/TeamManagerMemberCell.swift | 13 + .../NormalUI/Cell/TeamMemberCell.swift | 26 +- .../NormalUI/Cell/TeamMemberSelectCell.swift | 33 + .../Cell/TeamSettingLabelArrowCell.swift | 33 + .../Controller/TeamManageController.swift | 55 + .../TeamManagerListController.swift | 95 ++ .../TeamMemberSelectController.swift | 34 + .../Controller/TeamMembersController.swift | 23 +- .../TeamSettingViewController.swift | 30 +- .../Classes/NormalUI/NormalTeamUIColor.swift | 12 + .../Setting/Model/NESelectTeamMember.swift | 12 + .../Model/SettingCellLabelArrowModel.swift | 9 + .../Setting/Model/SettingCellModel.swift | 2 +- .../Setting/Model/SettingSectionModel.swift | 2 +- .../NEBaseTeamHistoryMessageController.swift | 21 +- .../Setting/NEBaseTeamManageController.swift | 386 +++++ .../NEBaseTeamManagerListController.swift | 173 +++ .../NEBaseTeamMemberSelectController.swift | 266 ++++ .../Setting/NEBaseTeamMembersController.swift | 106 +- .../View/NEBaseHistoryMessageCell.swift | 32 +- .../View/NEBaseTeamArrowSettingCell.swift | 2 +- .../View/NEBaseTeamAvatarViewController.swift | 12 +- .../View/NEBaseTeamInfoViewController.swift | 2 +- .../NEBaseTeamIntroduceViewController.swift | 18 +- .../Setting/View/NEBaseTeamMemberCell.swift | 79 +- .../View/NEBaseTeamMemberSelectCell.swift | 66 + .../View/NEBaseTeamNameViewController.swift | 15 +- .../Setting/View/NEBaseTeamSettingCell.swift | 13 +- .../View/NEBaseTeamSettingHeaderCell.swift | 5 +- .../View/NEBaseTeamSettingSelectCell.swift | 2 +- .../View/NEBaseTeamSettingSwitchCell.swift | 4 +- .../NEBaseTeamSettingViewController.swift | 132 +- .../Setting/View/NEBaseTeamUserCell.swift | 6 +- .../View/TeamSettingRightCustomCell.swift | 15 +- .../View/TeamSettingSubtitleCell.swift | 13 +- .../ViewModel/TeamAvatarViewModel.swift | 20 + .../Setting/ViewModel/TeamInfoViewModel.swift | 2 +- .../ViewModel/TeamIntroduceViewModel.swift | 21 + .../ViewModel/TeamManageViewModel.swift | 230 +++ .../ViewModel/TeamManagerListViewModel.swift | 90 ++ .../ViewModel/TeamMemberSelectViewModel.swift | 135 ++ .../ViewModel/TeamMembersViewModel.swift | 82 ++ .../Setting/ViewModel/TeamNameViewModel.swift | 21 + .../ViewModel/TeamSettingViewModel.swift | 187 +-- .../NETeamUIKit/Classes/TeamConstant.swift | 1 + Podfile | 37 +- app/Custom/CustomAttachment.swift | 60 +- app/Custom/CustomChatCell.swift | 2 +- app/Custom/CustomContactTableViewCell.swift | 1 + app/Custom/CustomContactsViewController.swift | 1 + app/Custom/CustomConversationController.swift | 14 +- app/Custom/CustomConversationListCell.swift | 6 +- app/Custom/CustomP2PChatViewController.swift | 14 +- app/Main/AppDelegate.swift | 16 +- app/Mine/Controller/MeViewController.swift | 20 +- .../Controller/NELoginViewController.swift | 12 +- .../Controller/PersonInfoViewController.swift | 4 +- app/Mine/ViewModel/PersonInfoViewModel.swift | 2 +- 375 files changed, 11011 insertions(+), 2932 deletions(-) create mode 100644 IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/InhertViewController.swift create mode 100644 IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/TestViewController.swift create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_delete.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_delete.imageset/fun_selece_delete@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_delete.imageset/fun_selece_delete@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_multiForward.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_multiForward.imageset/fun_select_merge@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_multiForward.imageset/fun_select_merge@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_singleForward.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_singleForward.imageset/fun_select_perItem@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_singleForward.imageset/fun_select_perItem@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_delete.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_delete.imageset/fun_unselect_delete@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_delete.imageset/fun_unselect_delete@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_multiForward.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_multiForward.imageset/fun_unselect_merge@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_multiForward.imageset/fun_unselect_merge@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_singleForward.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_singleForward.imageset/fun_unselect_perItem@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_singleForward.imageset/fun_unselect_perItem@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_receive_fun.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_receive_fun.imageset/merge_message_receive_fun@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_receive_fun.imageset/merge_message_receive_fun@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_send_fun.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_send_fun.imageset/merge_message_send_fun@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_send_fun.imageset/merge_message_send_fun@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_fold.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_fold.imageset/Frame@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_fold.imageset/Frame@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_unfold.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_unfold.imageset/Vector@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_unfold.imageset/Vector@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_select.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_select.imageset/clicked_we@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_select.imageset/clicked_we@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Commom/multiple_send_image.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Commom/multiple_send_image.imageset/Frame@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Commom/multiple_send_image.imageset/Frame@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_receive.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_receive.imageset/merge_message_receive@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_receive.imageset/merge_message_receive@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_fold.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_fold.imageset/Frame@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_fold.imageset/Frame@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_unfold.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_unfold.imageset/Vector@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_unfold.imageset/Vector@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_delete.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_delete.imageset/selece_delete@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_delete.imageset/selece_delete@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_multiForward.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_multiForward.imageset/select_merge@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_multiForward.imageset/select_merge@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_singleForward.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_singleForward.imageset/select_perItem@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_singleForward.imageset/select_perItem@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/unselect_delete.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/unselect_delete.imageset/unselect_delete@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/unselect_delete.imageset/unselect_delete@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/unselect_multiForward.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/unselect_multiForward.imageset/unselect_merge@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/unselect_multiForward.imageset/unselect_merge@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/unselect_singleForward.imageset/Contents.json create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/unselect_singleForward.imageset/unselect_perItem@2x.png create mode 100644 NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/unselect_singleForward.imageset/unselect_perItem@3x.png create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift delete mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NECustomAttachmentProtocol.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageMultiForwardCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageRichTextCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/PinCell/PinMessageMultiForwardCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/PinCell/PinMessageRichTextCell.swift create mode 100644 NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift create mode 100644 NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift rename NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/{NEBasePopListViewController.swift => NEBasePopListView.swift} (87%) rename NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/{FunPopListViewController.swift => FunPopListView.swift} (70%) rename NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/{PopListViewController.swift => PopListView.swift} (61%) create mode 100644 NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_select.imageset/Contents.json create mode 100644 NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_select.imageset/clicked_we@2x.png create mode 100644 NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_select.imageset/clicked_we@3x.png create mode 100644 NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_user_empty.imageset/Contents.json create mode 100644 NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_user_empty.imageset/fun_user_empty@2x.png create mode 100644 NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_user_empty.imageset/fun_user_empty@3x.png create mode 100644 NETeamUIKit/NETeamUIKit/Assets/NEBaseTeamUIKit.xcassets/common/search.imageset/Contents.json create mode 100644 NETeamUIKit/NETeamUIKit/Assets/NEBaseTeamUIKit.xcassets/common/search.imageset/Frame@2x.png create mode 100644 NETeamUIKit/NETeamUIKit/Assets/NEBaseTeamUIKit.xcassets/common/search.imageset/Frame@3x.png create mode 100644 NETeamUIKit/NETeamUIKit/Assets/NEBaseTeamUIKit.xcassets/common/unselect.imageset/Contents.json create mode 100644 NETeamUIKit/NETeamUIKit/Assets/NEBaseTeamUIKit.xcassets/common/unselect.imageset/unselect@2x.png create mode 100644 NETeamUIKit/NETeamUIKit/Assets/NEBaseTeamUIKit.xcassets/common/unselect.imageset/unslect@3x.png create mode 100644 NETeamUIKit/NETeamUIKit/Assets/NormalTeamUIKit.xcassets/select.imageset/Contents.json create mode 100644 NETeamUIKit/NETeamUIKit/Assets/NormalTeamUIKit.xcassets/select.imageset/select@2x.png create mode 100644 NETeamUIKit/NETeamUIKit/Assets/NormalTeamUIKit.xcassets/select.imageset/select@3x.png create mode 100644 NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamManagerMemberCell.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamMemberSelectCell.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingLabelArrowCell.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManageController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamManagerMemberCell.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamMemberSelectCell.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManageController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamUIColor.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingCellLabelArrowModel.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManageController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManageViewModel.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift create mode 100644 NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.swift diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample.xcodeproj/project.pbxproj b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample.xcodeproj/project.pbxproj index 28c16230..0a9d44c6 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample.xcodeproj/project.pbxproj +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample.xcodeproj/project.pbxproj @@ -22,6 +22,8 @@ 39ED7B1E290FA9C700CAE608 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 39ED7B20290FA9C700CAE608 /* Localizable.strings */; }; DD774A7729485F6600347A9E /* CustomRouterViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DD774A7629485F6600347A9E /* CustomRouterViewController.m */; }; DDC1AC8929651B6C008C085A /* ConversationController+Test.m in Sources */ = {isa = PBXBuildFile; fileRef = DDC1AC8829651B6C008C085A /* ConversationController+Test.m */; }; + DDE8123A29E950440040E401 /* InhertViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE8123929E950440040E401 /* InhertViewController.swift */; }; + DDE8123C29E9510F0040E401 /* TestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DDE8123B29E9510F0040E401 /* TestViewController.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -72,6 +74,8 @@ DD774A7629485F6600347A9E /* CustomRouterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CustomRouterViewController.m; sourceTree = ""; }; DDC1AC8729651B6C008C085A /* ConversationController+Test.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ConversationController+Test.h"; sourceTree = ""; }; DDC1AC8829651B6C008C085A /* ConversationController+Test.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "ConversationController+Test.m"; sourceTree = ""; }; + DDE8123929E950440040E401 /* InhertViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InhertViewController.swift; sourceTree = ""; }; + DDE8123B29E9510F0040E401 /* TestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestViewController.swift; sourceTree = ""; }; DDF443AC294851D90077184F /* IMUIKitOCExample-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "IMUIKitOCExample-Bridging-Header.h"; sourceTree = ""; }; F984299B79B347145EA4AF84 /* Pods-IMUIKitOCExample.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-IMUIKitOCExample.debug.xcconfig"; path = "Target Support Files/Pods-IMUIKitOCExample/Pods-IMUIKitOCExample.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -179,6 +183,8 @@ DD774A7629485F6600347A9E /* CustomRouterViewController.m */, DDC1AC8729651B6C008C085A /* ConversationController+Test.h */, DDC1AC8829651B6C008C085A /* ConversationController+Test.m */, + DDE8123B29E9510F0040E401 /* TestViewController.swift */, + DDE8123929E950440040E401 /* InhertViewController.swift */, 39ED7B20290FA9C700CAE608 /* Localizable.strings */, DDF443AC294851D90077184F /* IMUIKitOCExample-Bridging-Header.h */, ); @@ -205,7 +211,7 @@ 39A2F20628D9B2C400DDCAF2 /* Frameworks */, 39A2F20728D9B2C400DDCAF2 /* Resources */, DB787379DE2DF44860C11CF1 /* [CP] Embed Pods Frameworks */, - 8F14E37FD20AEF03EA4142FE /* [CP] Copy Pods Resources */, + 2A3377106BCBB5C08F3A27B1 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -325,7 +331,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 8F14E37FD20AEF03EA4142FE /* [CP] Copy Pods Resources */ = { + 2A3377106BCBB5C08F3A27B1 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -394,6 +400,8 @@ 39A2F24B28DABC6700DDCAF2 /* NENavigationController.m in Sources */, 39A2F24728DABC1700DDCAF2 /* NETabbarController.m in Sources */, 39A2F21F28D9B2C500DDCAF2 /* main.m in Sources */, + DDE8123A29E950440040E401 /* InhertViewController.swift in Sources */, + DDE8123C29E9510F0040E401 /* TestViewController.swift in Sources */, DD774A7729485F6600347A9E /* CustomRouterViewController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -593,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 = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -629,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 = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -647,6 +655,7 @@ 39A2F23C28D9B2C600DDCAF2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -664,6 +673,7 @@ 39A2F23D28D9B2C600DDCAF2 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; @@ -681,6 +691,7 @@ 39A2F23F28D9B2C600DDCAF2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; @@ -696,6 +707,7 @@ 39A2F24028D9B2C600DDCAF2 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/AppKey.h b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/AppKey.h index 5e105323..93201753 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/AppKey.h +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/AppKey.h @@ -7,6 +7,6 @@ #define AppKey_h /// IM key -static NSString *const AppKey = @""; +static NSString *const AppKey = @"3e215d27b6a6a9e27dad7ef36dd5b65c"; #endif /* AppKey_h */ diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Info.plist b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Info.plist index 0c67376e..97ff79dd 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Info.plist +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Info.plist @@ -1,5 +1,11 @@ - + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.h b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.h index 17470e70..fa0a737e 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.h +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.h @@ -8,5 +8,4 @@ @interface AppDelegate : UIResponder @property(nonatomic, strong) UIWindow *window; - @end diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m index 2915938b..83c37813 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/AppDelegate.m @@ -8,14 +8,14 @@ #import "AppKey.h" #import "NETabbarController.h" -#import -#import #import #import #import -#import -//#import +#import +#import +// #import #import "CustomRouterViewController.h" +#import "IMUIKitOCExample-Swift.h" @import NIMSDK; @@ -28,45 +28,115 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { self.window = [[UIWindow alloc] init]; - self.window.backgroundColor = [UIColor whiteColor]; - self.window.frame = UIScreen.mainScreen.bounds; + self.window.frame = [[UIScreen mainScreen] bounds]; [self.window makeKeyAndVisible]; - UIViewController *controller = [[UIViewController alloc] init]; - self.window.rootViewController = controller; + [self.window makeKeyWindow]; + UIViewController *root = [[UIViewController alloc] init]; + root.view.backgroundColor = [UIColor whiteColor]; + self.window.backgroundColor = [UIColor clearColor]; + self.window.rootViewController = root; [self setupInit]; return YES; } - (void)setupInit { - // 初始化NIMSDK - NIMSDKOption *option = [NIMSDKOption optionWithAppKey:AppKey]; - option.apnsCername = @""; - option.pkCername = @""; - [[IMKitClient instance] setupCoreKitIM:option]; - - // 登录IM之前先初始化 @ 消息监听mananger - [NEAtMessageManager setupInstance]; - - [[IMKitClient instance] loginIM:@"imaccid" :@"imToken" :^(NSError * _Nullable error) { - if (error != nil) { - NSLog(@"NEKitCore login error : %@", [error description]); + // 初始化NIMSDK + NIMSDKOption *option = [NIMSDKOption optionWithAppKey:AppKey]; + [[IMKitClient instance] setupCoreKitIM:option]; + + // 统一登录组件 + YXConfig *config = [[YXConfig alloc] init]; + config.appKey = AppKey; + config.parentScope = @2; + config.scope = @7; + config.supportInternationalize = false; + config.type = YXLoginPhone; + +#ifdef DEBUG + config.isOnline = NO; +#else + config.isOnline = YES; +#endif + [[AuthorManager shareInstance] initAuthorWithConfig:config]; + if ([AuthorManager shareInstance].canAutologin) { + [[AuthorManager shareInstance] + autoLoginWithCompletion:^(YXUserInfo *_Nullable userinfo, NSError *_Nullable error) { + if (error) { + NSLog(@"auto login failed,error = %@", error); + } else { + [self setupXKit:userinfo]; + } + }]; + } else { + [self loginWithUI]; + } +} + +- (void)loginWithUI { + [[AuthorManager shareInstance] + startLoginWithCompletion:^(YXUserInfo *_Nullable userinfo, NSError *_Nullable error) { + if (!error) { + [self setupXKit:userinfo]; } else { - [self setupTabbar]; + NSLog(@"login failed,error = %@", error); } - }]; + }]; +} + +- (void)setupXKit:(YXUserInfo *)user { + // 登录云信IM + if (user.imToken && user.imAccid) { + [[IMKitClient instance] loginIM:user.imAccid :user.imToken :^(NSError * _Nullable error) { + if (!error) { + [ChatRouter setupInit]; + [self setupTabbar]; + //登录圈组模块,如不需要圈组功能则不用登录 +// QChatLoginParam *parama = [[QChatLoginParam alloc]init:user.imAccid :user.imToken]; +// [[IMKitClient instance] loginQchat:parama completion:^(NSError * _Nullable error, QChatLoginResult * _Nullable result) { +// if (!error) { +// [self setupTabbar]; +// }else { +// NSLog(@"qchat login failed,error = %@",error); +// } +// }]; + + }else { + NSLog(@"loginIM failed,error = %@",error); + } + }]; + } else { + NSLog(@"parameter is nil"); + } } - (void)setupTabbar { + self.window.backgroundColor = [UIColor whiteColor]; self.window.rootViewController = [[NETabbarController alloc] init]; [self registerRouter]; } +- (void)customClick { + NSLog(@"custom more action click"); +} + // 注册路由 - (void)registerRouter { [ChatRouter register]; [ConversationRouter register]; [ContactRouter register]; - [TeamRouter register]; + + // 聊天页面自定义面板示例 + /* + NEMoreItemModel *item = [[NEMoreItemModel alloc] init]; + item.title = @"自定义"; + item.customDelegate = self; + item.action = @selector(customClick); + item.image = [UIImage imageNamed:@"chatSelect"]; + NSMutableArray *muta = [[NSMutableArray alloc] initWithArray: + NEChatUIKitClient.instance.moreAction]; + [muta addObject:item]; + NEChatUIKitClient.instance.moreAction = muta; + */ [[Router shared] register:@"imkit://chat/p2pChat.page" closure:^(NSDictionary *_Nonnull param) { diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/InhertViewController.swift b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/InhertViewController.swift new file mode 100644 index 00000000..d28217c4 --- /dev/null +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/InhertViewController.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 NEConversationUIKit +import UIKit + +public class InhertViewController: ConversationController { + override public func viewDidLoad() { + super.viewDidLoad() + let test = TestViewController(sessionId: "") + // Do any additional setup after loading the view. + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ +} diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/NETabbarController.m b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/NETabbarController.m index 7c0fef9e..b87216eb 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/NETabbarController.m +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/NETabbarController.m @@ -6,12 +6,14 @@ #import "NETabbarController.h" #import "NENavigationController.h" +#import #import +#import #import #import #import #import -//#import +// #import @interface NETabbarController () @@ -26,8 +28,14 @@ - (void)viewDidLoad { } - (void)setUpControllers { + // ChatViewController *chat = [[ChatViewController alloc] init]; + // NENavigationView *nenav = [[NENavigationView alloc] init]; + + [ChatRouter registerFun]; + // 会话列表页 ConversationController *sessionCtrl = [[ConversationController alloc] init]; + sessionCtrl.view.backgroundColor = [UIColor whiteColor]; sessionCtrl.tabBarItem = [[UITabBarItem alloc] initWithTitle:NSLocalizedString(@"message", @"") image:[UIImage imageNamed:@"chat"] @@ -46,13 +54,13 @@ - (void)setUpControllers { // 圈组 -// QChatHomeViewController *qchatCtrl = [[QChatHomeViewController alloc] init]; -// qchatCtrl.tabBarItem = -// [[UITabBarItem alloc] initWithTitle:NSLocalizedString(@"qchat", @"") -// image:[UIImage imageNamed:@"qchat_tabbar_icon"] -// selectedImage:[UIImage imageNamed:@"qchat_tabbar_icon"]]; -// NENavigationController *qchatNav = -// [[NENavigationController alloc] initWithRootViewController:qchatCtrl]; + // QChatHomeViewController *qchatCtrl = [[QChatHomeViewController alloc] init]; + // qchatCtrl.tabBarItem = + // [[UITabBarItem alloc] initWithTitle:NSLocalizedString(@"qchat", @"") + // image:[UIImage imageNamed:@"qchat_tabbar_icon"] + // selectedImage:[UIImage imageNamed:@"qchat_tabbar_icon"]]; + // NENavigationController *qchatNav = + // [[NENavigationController alloc] initWithRootViewController:qchatCtrl]; self.tabBar.backgroundColor = [UIColor whiteColor]; self.viewControllers = @[ sessionNav, contactNav ]; diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/TestViewController.swift b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/TestViewController.swift new file mode 100644 index 00000000..9d0b33f3 --- /dev/null +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/Main/TestViewController.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 NEChatUIKit +import UIKit +public class TestViewController: P2PChatViewController { + override public func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ +} diff --git a/IMUIKitOC/IMUIKitOCExample/Podfile b/IMUIKitOC/IMUIKitOCExample/Podfile index 61b957b6..ae9e436f 100644 --- a/IMUIKitOC/IMUIKitOCExample/Podfile +++ b/IMUIKitOC/IMUIKitOCExample/Podfile @@ -1,57 +1,35 @@ # Uncomment the next line to define a global platform for your project - platform :ios, '11.0' + +platform :ios, '11.0' source 'https://github.com/CocoaPods/Specs.git' target 'IMUIKitOCExample' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! - #登录组件 - pod 'YXLogin', '1.0.0' - - #可选UI库 -# pod 'NEContactUIKit', '9.6.5' -# pod 'NEConversationUIKit', '9.6.5' -# pod 'NEChatUIKit', '9.6.5' -# pod 'NETeamUIKit', '9.6.5' - - #可选Kit库(和UIKit对应) - pod 'NEChatKit', '9.6.5' - - #基础kit库 - pod 'NIMSDK_LITE','9.14.0' - pod 'NECommonUIKit', '9.6.5' - pod 'NECommonKit', '9.6.4' - pod 'NECoreIMKit', '9.6.5' - pod 'NECoreKit', '9.6.5' - - #扩展库 -# pod 'NEMapKit', '9.6.5' - - #呼叫组件,音视频通话能力,需要开通 音视频2.0,可选,聊天一面会根据依赖初始化自动显示音视频通话入口 - pod 'NIMSDK_LITE','9.14.0' - pod 'NERtcCallKit/NOS_Special', '2.2.0' - pod 'NERtcCallUIKit/NOS_Special', '2.2.0' - pod 'NERtcSDK', '5.5.2' - - - # # 如果需要查看UI部分源码请注释掉以上在线依赖,打开下面的本地依赖 - 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 'NEMapKit', :path => '../../NEMapKit/NEMapKit.podspec' -# pod 'NERtcCallUIKit', :path => '../../NERtcCallUIKit/NERtcCallUIKit.podspec' + 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' + + # 如果需要查看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' 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'] = '11.0' - end - end -end + diff --git a/NEChatUIKit/NEChatUIKit.podspec b/NEChatUIKit/NEChatUIKit.podspec index e83b1a6d..34d6e4d9 100644 --- a/NEChatUIKit/NEChatUIKit.podspec +++ b/NEChatUIKit/NEChatUIKit.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = 'NEChatUIKit' - s.version = '9.6.5' + s.version = '9.7.0' s.summary = 'Chat Module of IM.' # This description is used to generate tags and improve search results. diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_delete.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_delete.imageset/Contents.json new file mode 100644 index 00000000..56de524b --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_delete.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "fun_selece_delete@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "fun_selece_delete@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_delete.imageset/fun_selece_delete@2x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_delete.imageset/fun_selece_delete@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..be3612db3ec1927c64e63f06dc422219fd04cec5 GIT binary patch literal 1692 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 literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_multiForward.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_multiForward.imageset/Contents.json new file mode 100644 index 00000000..5780241e --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_multiForward.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "fun_select_merge@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "fun_select_merge@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_multiForward.imageset/fun_select_merge@2x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_select_multiForward.imageset/fun_select_merge@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..48e3b003adbcdec54c5d388f732e69aac1ade6a3 GIT binary patch literal 1998 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 emdIzZ==dKWJPRW4p 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 N002ovPDHLkV1l`X-y#42 literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_multiForward.imageset/fun_unselect_merge@3x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_multiForward.imageset/fun_unselect_merge@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..9d70a7efe9f05f887bc86b630bc3fb3bf5c4c17c GIT binary patch literal 3073 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 P00000NkvXXu0mjf3~+lu literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_singleForward.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_singleForward.imageset/Contents.json new file mode 100644 index 00000000..aa0750d5 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_singleForward.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "fun_unselect_perItem@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "fun_unselect_perItem@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_singleForward.imageset/fun_unselect_perItem@2x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_unselect_singleForward.imageset/fun_unselect_perItem@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d0434735922032d196f3be216b063466240e0a05 GIT binary patch literal 2115 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 literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_receive_fun.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_receive_fun.imageset/Contents.json new file mode 100644 index 00000000..487d440c --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_receive_fun.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "merge_message_receive_fun@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "merge_message_receive_fun@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_receive_fun.imageset/merge_message_receive_fun@2x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_receive_fun.imageset/merge_message_receive_fun@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4bf7d9c7caf96415fb5b16df1d7024edbb7c90f5 GIT binary patch literal 693 zcmeAS@N?(olHy`uVBq!ia0vp^?|?XjgAGWw?&mfHQk(@Ik;M!QVyYm_=ozH)0Vv2= z9OUlAucjQ;Dg z(CekK-AsF?eXXmNYD+Bs(Yw$mqY~;yS`QDz|Ji% zcV~Uvr+s;gr*@RZ`fD-S@4s|LFMqoz&z{R*`qe3&)xiyxvHqOby;EjfzdB|5sbt19 z)2|Oj43r-_DNukGOgOG=AzRy$1M|&_@X~` z7F*Zcu8kFEeR1#M{^ldxivQ}E+bR?fBCE=1wa>PEjsKntE2r~a>K3;u zIdj$YQR_LG>c}}23ze70oY#tbapdAdv)>i_#S_BRTW?$VvV8u0(^0@<2bn0tYsYM+ zGnuxkchze7Q<%U)U{omF7?<7j>yZUw=L-CH)ym@}}cKFWS^)}h#)|FVX z2Qk-5*Jkaw?dd=DHvhKx0G~LE>DO*$-H)4TQpkVxp0kelsuteL#H8TIF{ao#1&v#(`qjhJN8@)fh=k>0<-&gG4Bsr=ctF`>szuu7T!Rqa) zX1^c(Q4$Mkipf7_u6Tl>|8MZKWhI~fe2mvA3fB97(bZCqx#4-tH0jT2-`k%B%H^L3 zXFL${f2zU#^39o3j>{IsM$Tbn2)-K6vTu#`vuoB*cWo0$X*hf(?^?Ukt-Di>M}Nv? zC|Gkp>fV%ic820P+j~B(W=QC>k6x#5*IsLMK<4(eKsly{NU`58@(k_5sSPeJk_v)> zY#uIC7=;As#my9Ynkb>i)Npgxj7*+SsSF0U*JyAXIYhg=M^0e8Wvz9piDyGk^J1M7 z45uwMK%6xv1I1Drp89reQed8QY%_>+`Df-7R>=oj=JkNY?%e|#aKNgr1IStPd^d=* zYo9C7v}@(^sg7(X77C{{*knz~KYf#(VfAWI5Qx08{Uo?%e`zWIIdJD`Z) ztDWZkUqB+pPi1RCnmm1KXP3$!;@PmnGjLstWJ2ZS#W9@52R89_6{(&9`p4qbR0C$6 z$160#5Aj^McM+s!!P-bLHTP!Vx?=|S*%&sPbQV30t!MbWmxJMw0KH-b6#Y?dmC_7n z3e`_BRMuokKJ8~bAoG5AMv6q+=As%thQ6=Wn`akYFD-w%1IYQft)%4kn`JRmZx_Y) z%{EF0rqi!Mzc;z;WFoR5z@wd%?i%&gek8d?#_Sq`AyK?=@Uyl}UI%T1M z+%+PV;Z1P3*nQTWQ97rt?nvIJ^SPX1Lvif0vw5i>FDweYC;9l#t~0BR>RWCbf5^Vs k7i@pN(9~}2w(H^*3w{?Wd_6H0n5`K+UHx3vIVCg!07+=BH2?qr literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_send_fun.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_send_fun.imageset/Contents.json new file mode 100644 index 00000000..d3b7d64c --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_send_fun.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "merge_message_send_fun@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "merge_message_send_fun@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_send_fun.imageset/merge_message_send_fun@2x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/multiForward_message_send_fun.imageset/merge_message_send_fun@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5e86f750cc35823a853665a56d9c4cace053b3fb GIT binary patch literal 702 zcmeAS@N?(olHy`uVBq!ia0vp^?|?XjgAGWw?&mfHQk(@Ik;M!QVyYm_=ozH)0Vv2= z9OUlAuxO0EoksF>{t9#)8x5YLrdvvTL+IM$;+qU*HlZ@B= zeb09vsorw)_YzTEnXvqKtKKZwmRWu2+kMs<-t%{r2j{&IE?@qraqrGK6@RT_+*4*; zKYHZ#b@!*CjA>WdxAs9K)eP5t2BNwRP5)B&LUlB-SlIq8%e~{Mtt@@nf4p z_Z({dSR=8%AkpXjF`)4E*&yL>Ss>xP9pO7BJd6Y~W&A%@Dce5zsTOl$b4f^>jkmD) zCY{?>=7oyKt!_oV-dmt}IC#rn`G<#o%yYfX3DmE2%WsFly70wsTey>}j6s}vC zceeYq)J0#npPDUwzWXG1`=#jHnz!B^&E~)L#WwSHY4rSVPM+UeH2CA${@p0ke2&06-k?C0vU`MG+}7=J|G+4}RwGw0d+y){k4!nWPbG2E9iz5Ju)F@~CN`_87F zwU}?a=VH?x|6|(~PcZQRz4&Zf=_lVcPi9Xp`t3OVZILL0&C^wLXP6t_sGag$_){sv zhSl43&p7Y?sd?%%-_G?q=a?H}zs9rdlh~R3Z&UEQY;H%k8~*Q~?Uq<^X8DtP<_71# zm;6qb8#>r)?&o8eCh^V8t^d<%hJ?=eC6j6#B`4K6N{3W9XxN+ir$c3Q$=3gfXIQi5rLY!c~(Gh8JT*0?5$r8a!z{A<-6#`FydivHMo2F0>vd^>*BC!0Jvg11!$p;o$ zHi94(*TR$AK`J(^ld@*pDV*By^~w$96LWz+S$u3BNM*tvDRvf+#KtTc36PFsD>o{D z6!ZipCr+^b|3ogKC*8XlWGjR2301ZY`xxjOKQ9B<0naz<*!7 z?)*96%dnyU>4RHz>YmK$r#GBpxUp=#P8erV__n)q z%Fl%7*6}m!+mZSC`HQJXub)c3Kj-nEU1wLDoj?3i^2g3AmA~#6$DGe!z4lf8vTgU5 VJeW6+8JN8pJYD@<);T3K0RT~duY&*p literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_fold.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_fold.imageset/Contents.json new file mode 100644 index 00000000..5c4d3b18 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_fold.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_fold.imageset/Frame@2x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_fold.imageset/Frame@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2046334427dc1777b148feab1e2d6099075eba2f GIT binary patch literal 511 zcmVP{CRTUXWt0RC?>5MXEpLK|cF z5<+P@>6iBP%`4G@8ILR=rMwhENYC>=k|cT1jFGeCTVV)Iaz8YQi zWK9{!Zt%!mm%^AsZU~y6mFc@jW-C-RYzS%5$Ye!PtjV>INhSm2QfR1*6Cs?U!7|JN zSq35(oi42m<20!07a7L-3UyGN48t(2JFulJc~eQc7K$OT*NH^>jn~z-O@9EySx~iS z7viPoZ5Z(rmFM{rEiC+3S_K*xSF~TxG*LK2id+aqQ556&J=1VF!=B77Smz770#lBj_aB7)&st zIjNLbELbDPQkZV*qIL++#j+J*RZ^ISGu)ROvCeog6{-DxpKI@u!Bj{kn0~pDx?!xC zf)u|Nc18V-oQjW1F~*G;gaYiQ13l)%|{DE&5RCe5OG zY2b-|#}OmHuKUz-1SQGqGU`8Ap1RHmms2uuyXW48j-{x<>8{X z*gSfiB~pn6X+nw>yWOt$M^Zd`&!myYq%Bz_vD4|qfn-7rKw26=gq+|un_)WgUpc*^ zPH6Yb+D#z0=8-NLZ;Co#?SY?|0U1uMdn}b&kOe4<*(Aj%i780MWEhoV3Q|jKTBD46 zl!~cH>5MMQC=|0Y|8DdPg??1+Wg z6Zkuds*%DHGk<*+3sniqZ={iI2Hk_mDvj5%IHU;9z|+HoGX-)u9N#eJ6W8{A RNl5?z002ovPDHLkV1k)LE-e56 literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_unfold.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_unfold.imageset/Contents.json new file mode 100644 index 00000000..6a4d508b --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_unfold.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Vector@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Vector@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_unfold.imageset/Vector@2x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_unfold.imageset/Vector@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..b9af77884a502bebb3ff81f1860d3ec8fe97b9e2 GIT binary patch literal 507 zcmVVK~#7F?Uyl5 z!!Q(vpOfw}H?XSgbj%1N6L5l-BQUbT2}qB?2{;0bj2SzHFfzqlAmM)!NQvttjd&@l z)K9XKHi^C$+wa9zB#1F)6$HT}PW6tDqyY&MhT(FAoLPwmP*s(sap)NdQrGnZ`FlVs zr53(t6ip1~g@OD9&CtFB7?Lxhb6^K~fnj>3sRMv};ge&Mp5yp2X#gxoE%3?<;NCA9 zz^|Oznb0X04FGaZwdbZB@{z8g{OTcqaql)@}y-zX*pL x5@Ph45ItZ>5bl9B-t^MF(6O7bxOYv0d;?cs($c9MrZ4~i002ovPDHLkV1iZ_({um; literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_unfold.imageset/Vector@3x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_input_unfold.imageset/Vector@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e8892f6cccbcebc614a7fdfd912a6af895193efd GIT binary patch literal 675 zcmV;U0$lxxP)0sHlFWP`Y5HDX5#)eww|j%r1V>CiM;?#Iu^`JNNfHM^up{XQde#MW;irqk(c@#`gIfQTG`d-oCsZgJVF=$7J0NP&|XP|@r4hA=tP>1%xr zsNhGeu6V{P0}y|Y(_@`6-WWKEpOiFbn3Y9QgBJ!=;t_Fq(Z3coIyayrUP`$t8}HPB zLcCOoXPm?VMpL{b1x{wblz7Iv0c4{iG9;d{ZXgW9VM$YM@r-o?5LuLsD#fFHSu`#r z%fxZ~j%WJl+gDn@e3oUh4G_>S_#=)LuE$Q2Ijn;OyRs23cMv~^c9VOAhK7cQhJVDb z0$IP`PZ15d&Pj*E;jV5TV*-JoI6#8+03iu|h3Lg*vze*}7L`6uDZ!E~5+c2L!I|ro zTq))}BW<->&&WOA$_7=`ZQ2<3L>383N_?edZ{Cbl)NQINZD*lYI<44A5fZc+Sw`D{ zN<5gZ*Xuc1M$3SXcr5&?_Oqch;Inw9(tz*cnNkCPK|E7f*utZg#iteHyb#Y6&HxOg z6=S>+&(tlecqN{xW57G{jKE^eHTp>$r9ld}+zLT#K@QM0poHI`Y`5EMvW&6PB4*)= z9N^ts%DS)tQ)N9#4v0SgvLXa;3lmjm^y7U((O!YyWm&)fnGeRH1j_iJi_-uA002ov JPDHLkV1m@jAtC?( literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_select.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_select.imageset/Contents.json new file mode 100644 index 00000000..ea2e4827 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_select.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "clicked_we@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "clicked_we@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_select.imageset/clicked_we@2x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/fun_select.imageset/clicked_we@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f69ac91380fc592530790707e299c2d0b2b96d11 GIT binary patch literal 1304 zcmV+z1?T#SP)zGN4NiatPb}&D&wHg!_oJiy9=wXTyBzAJ6h-H)(xpaQ@xfQ{Q8{J6`FR<~5 z9ZvVQ8DIXNVZ;I0pa`i}*Fu{=I&!TJiQIx+8KvK+CkMWgQoHqBFrCp2)a;0 zps>J?8c)#iIrY8e#b!8p`!qbvFIo?vWQ#sCn4}c(v0VBK0;E{1!PwkIxb@&0%P|}= z7BNyI{#nk)AV8i8Z?P21JhGczC`63SlzR)39)o}=C-(l|zrvmQAIgr`b%+UxvK|8B zNbTFOYwJrT=YN0vCw%hbImhvcx+XB7g`%_e5VX9|Tymbpo|^fLj(@oLFIdlEO>@x` zwv(gm+PV|`z+sESS25niD#we4-7Lk&UVeXNcBUVE4HxfD*oPkrvG3FzEIH2(#%ADF zEW=tI-#GSduQNSLZl7*C=@jS{JgZh>nX$Pq$kBcQudof5edV?i#$I0Vc6-rp(v7j* zT5*=d-kJX%yuw&7$8LETC;^GdWD~Q-Go(hFCPL5}wLecCwnD8)DyyR9wEVD>;IWbO!IdZ(Lmw{L4@wySsVtHYx z4Du=$5esn`amed%7R`a*F2geNgNOM?PzeylxH=jq5;uG9fseNitK8P>lIwz8>cD6X zH&#|UGU$Hr0gGaBKRpR+e5|loew|;j0CQd9W=|R*xct}+5-@xDz(}L<-JDC{@=>=D zBM4XvVz_-Il~;nV-CI?j;* O0000sGdY}gd^k6-ZU_xu7 zhpvs$jVXa%+L(w-jRzAoRh!sDO*+JCn)r`H3nJ`1f4{f8Fx}nRc{4jhXW(-v3p)$@ zzWe(>@0GzS(3$Sn0RC$g5NqUSMl%`H7(g{Xr4?Li2!L5Ggkka+J{A|_U>cw3Cx3u| zVGt0V={{UI+rx#~BeILG1OwmcvVuK){2UZgXNxuZCNQm}yfAuC<925fj;ZEX%AfAPI;fucZ7#F7lbOE;* zAjEL$*^^1|fNLQ_V+A%a2BY(69h3U#tgF|#xtL;s5U~k-Q4@q445$g+jjJdN@nX8S z$rNk?igSNhaV3V55F~;SEFGeFpad>%mNB8L6l9JNPA;7-^X;(}g0cgN0MUsF<-j1nI3d?$gRKyhB4KTaAfZf`Ta(@M#dJ>f_FzIj z4QmkWeD{`W+=_(A#|2nJ)?kH#=R8Z)6&742TqCxoT%r&Po~2ymHjBtv!!=kBpjB9l ztpbaP4Z^zNcB@#nh;?NRc`Q~DU-@bDtf8U09@-w(;en0!!yo_t4dd6&gK_h^`Y$k- zdhp*NdblElfL4eQPj7ny4mZ5C{C(}++o7dqCyZSF0KWV4C#BzkrT0=Yqt}!#M94m=d2r--ERni@tM3_S>c>B_OD8^sl7QD6C#7z#h5Mo~4aS#LK zLuIca5#I7B0>PN6alGNGK}b1-De@6al}@~m?l9w~kSc%ytL0%G!*ytFup5geC(c|Q z#{%j{s07l0DoYJKP;q%%1`=UkU7O=|aUP; z{ezXSZhRx-g5Qu32KWrQ$veA_DcAf?{X?Fao4?5n5da3KVWyi`P_Cvj)(NtjY&E#W z;X1YcfdVoWOhG^za2#(N;1Vih>5Vx-VjSN!AT6Zy^XjEDiEztBGvx-XEN5an_dot5 zO5&WbEl?##!;gmh>Y;kWJ!P*=@B}=y^)V^?6^kXg6~SxOsDcpu^Vp-^E!(ihY`oaq%mp`nybXLSjJ#(TiH=$x5 zL4{C}Gw|(kr6B5rAN_+ea?fUdX34>q-K^9BW&@jdF?{SFl)%pOT=r`YKine^Wdxcd zP&{yh-~EFE(k)@-%!)M-MLTA? zrus%j_MV{cP!OdGvj)p*sxVcWw?L5AaN>aVM@18=yLfzPK`eF$G0UVNdhtf^m7)Vi zp*0XigPA1lEn@5dtW}sOd7Po-gz8g$Y?AqyPQwC~2r0evF$UXF9k~bwkiZX>ts=$( zuF396TOr6r1k3D&07iLt0J&4Y+!+qXP3ckHnV*NoaDpY8osKIwPO)iMJMC_FJKGEc z`JMSHXEv0^`6LFqNi#&tO$^nu8iewF4OxP-j1CbHLr{>%nBe9Fv8x+AB*sScw=kzx zN|6gL&vOx;k2`jzdwX!Q6`Fn96b~|32zos-?djepAq$hNtN7lrx`nKOcHCETZrrm) zSjlZ)9>Ry+u$qMBNOU0Ewq literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Commom/multiple_send_image.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Commom/multiple_send_image.imageset/Contents.json new file mode 100644 index 00000000..5c4d3b18 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Commom/multiple_send_image.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Commom/multiple_send_image.imageset/Frame@2x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Commom/multiple_send_image.imageset/Frame@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2a398288fa4be181910405dbe51603e6d019456c GIT binary patch literal 1330 zcmV-213qtL)&jivTWYLd0({XAhE56YC^yWuwo=0oj6PRkQgLEBxW3!}7$j_#qpA zQU{PiL!*!D6tn~}$DcDHoMY_!2dbI24RhJ}#vNdypa~%!(4e!$V(}60HACb5vTMRH zeA*OPFKE!Dpj}4MViHBLx3jZ*gQ0&=b^!^X@p$|oJJ+fM9u->b!(SO%x1c5AJ{Sz< zcwY}!o{aZ&u9OKNrWFU|6{7p`y!OEukKxWE)9E!j7cQVWQUS!&iU5NJZH;WWGuPK& zKg4l7fm(=L)md!^$Tt{_D(SUwXE5x~(fCuSNs$0zN*$1{HiqA_*BV>xNAlsGyza85 zoTCXR(10QV#7G^WyTQq09$!Y00Ag|uNQ#Y^py&5md{~r*J5jaA zt569b#?4hyRODgneuQXn4V!^JsX?13Rv;m#zrM$8Oq2&DjG z?CijBIQ%{w_V$H{-r^g-!Z5mrW>C2AtBn8r`O63k?Tty}36%h1w30jxP27j0t!B#y zE!5%8H0$w7lmdvUT;+n`tLHeek12AD5#r101wNj>SFJchJs#)Q?(u2_ehAV(Dx1}~ z%ulCNUkK|2Z*`c~jeGR`zUmrNJzfPy)!Aq?n3T;k*5}9(SW<52I1GQkDr!GuyzX;+ zGvcGN^doFAMEYX)bnG}gC$c$dkN?@e@#O-DG3J1T_=vskX(G@^UODRC)(!k5k zVB=Y|pqiY7p}k-C5xj~3Vp?;+Y8oWZ3F~ByXDE82$UdUI2&hOE0mQWCfYmg>w>Dcp zE9NreSFaro`zPpx_q3Y?np6=$Op7T%#A!vgR`h_lqUUs-P(>$PM3M7aHzleFAg0yt z2qhn#?cDLE2^mi-3pKzr!VMPse4-Go9iS|&MgXlctsov=;?HSa;|VN~rx*#iVGDQi z8KQFj6EE(&!s(XWH!OIsSI4PdP|83>TCM@+NeX1|zf>~_SlSME7-_YH)QgLNRVW%< oH}pvSRPFyA9UUDV9UUEi0J%S2jtn(BZU6uP07*qoM6N<$f(IODVgLXD literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Commom/multiple_send_image.imageset/Frame@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Commom/multiple_send_image.imageset/Frame@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..c78c895464c78886be80a53d46d7de603c99bacb GIT binary patch literal 1846 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 vITfE08o?7XDGG<(6JDWi(Cec5NCpcPzYyxuKSc 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 X(=Ba&?};Qse*ySFVnDP1*}Q)Nkd;wh literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..7b4b6a0c99b12eefb9e9bb6f2ec970066824c461 GIT binary patch literal 1949 zcmd^A`#;nBAOGz5+Uc;9q|?Hd?>YKrI43fv?b}Ezj1H642u&n}Y;HqjbXJn<*&)k4 zlN{s{u^kyw?)SvpX0DseDAzD{_We7)KfGV>_w)IDKVFyT`-k`A%Xr-PN*YQ40DNz4 zWljJ9kf;0{e*i3xK~pZ*dxm@M=_JaFJaEayi5bF`r$Mx;J1eF6hi(G1d)b#;fIMtc-Cy( zG$(#T*uhUKxqEIy4Ux|C))}8WV%VKtBQE`DQjOUly-JrTBLB@ z?|~}mgGGdFQf5-LbUVa^+n=QCRM+Ud|4{DM%+)OsS~jmz#2U0_xWupLNvb{V!6S6< z-tPAT85;FC#OpWBU`@&$uG}!1NkMVGQ+LPJeao}Pa=-|Qf|f;`RhMAVl04FKkV@_1sQ{_Ni@$mK)!U_=~3;fQ$}Na?3eKv91E_c){TSo8k_ z*{T-dNQ47A185J~Sq2NLp6F)}#=jUqxX2<#9XkE!dH|l)b4P!^PT+Ht!_%g}wMx66 z>SM}d85f@E9Fnw8Ez2KHXOJH`+s-Q4ANN`Ri9XHg-D*^9QoC8{WV^$(7@WnEdR|(d z+)J_tvQG#qL}FKsB5_X7v~vag6b@|mGQ|U3rL2HfADgV-Vi|Q>(5o z`9%#0ATg;^<64gxw(&!o)BEY1o!RzV+>D9|CuA#-%*L1{SC#*H}P1*{YOb9oaPv4Lj4ryzfyY8I!Dhdp9Nhwk1@Q zo3~P9$L0?(DGvsG*>SIB0q)@BgvfWhBu$ZIYOFBsjf>1BGb@KXR9|yMor=-T zjr2X4OUnK(Mu&*GNX8w#g1%Q4wqK#F?_xl6(q(HW3LUvx88roo6xuO-sOPm`73?xQ zI5Tgs_ts?7SMl#&mhLQW@_cp^e9F?Cw64BH+nR?$4g8~5@|ExoUBSUTR`NXH=1fEWHo4jl$bM@Cuj%5Z#5sv_k3q2_`3~rLl)AvI)PF^c7 zT^utG^8m78R5R*XXzfaEcz^lshcPW1qUZ*wX*$V!a^jcvvNrNjvZ%|xZ5-6}_E)&6 z?85{uWp^ZZc?vCSZjO}D>Hs&v7_Ao0@ABTFvRji3h6TQ~nINDNB~iMYf+b6KkJf4C zm2r>rzc#g)#qf=x8<~DVsj8pP!4?{X9p#C2`|nB7048F=CV_jlzxcewKm!7XPPl#Z zIp#&eTGkr$;NiL9d^;dB5$W~8v(#(qfw)kQ`?nylgSYURq75=vbS@eQ>S~)r?Sg{0ihtC~lvqg)$Rxu7!ymvo0IpP+S6Uh;YfL zgQM`3v=^Z#+<0NfF3{xGl{i3S6SzK)!r$Zp7TBAL{n;P2<-aLlZGks0zu@-!Kh{&0 Ac>n+a literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_fold.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_fold.imageset/Contents.json new file mode 100644 index 00000000..5c4d3b18 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_fold.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Frame@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Frame@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_fold.imageset/Frame@2x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_fold.imageset/Frame@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..a1e475c757c1114fac6e25d16249aacababb945f GIT binary patch literal 568 zcmV-80>}M{P)<{fkb_O&R`CG{51^@uMgm6g35-v`(TnltrYC##R&OMP z9d}8Jl(eNZ-94Co7usRde3{+M&H&`ZgP;Jb(W(<8lOg-+U7z2Ft!XrDbBZ_s!~-Na z?cH2BGR`1l>b6}LUmYM|n}~)d&6XW5!$wj@z7s;*|1WbPV+`d$0I3`<1Li{5QzjHg zgp6C@yau|3;$*sA#|522NoDj15hRw86hSeFc)S#fl#ysX)UZNEqC7)E(3@gmU^UMc zLL?E&At&d*5~vEcXs*V~ z{mcS}0iNSH9_SFVEPDq^>=lyZPBwUDn6coz4p9k@U_DGDCYx#bD~yCnw<0O?yX;nU z{bE}1t-)Cv15=tS5#snu_$zF#4WuQ*Gi;;rIk?yODZETK@M~Yx`m=- zaKdZE@eOne#mV&hj&hlRP{f&Wi=0<<$pnO;$R3tuRyE{mg@QA9#1Myp_pt$)7t@0O zP_6EN3oFrtqm(%e%goF3lUv!{dl%K~6F7D5uAC3Z(abNGXwthtWrl750000leK~#7F?V3$) z(?Aq}-y0`VcH{^-LXcRn!`uK)kW@;LRW?pIK+6FDv!JaUp_CJl8_*qHAjpS3qT8sceGt;R%3@tH4#7&pN_0f?Rlx+uH zTuOx@608J8vv^wp8ksutF?7WbOTbbfeGY{erR!gf-My5b8;-ciWF0eb*!osW4zc+q!{BD=*#d)2Flajd3GHF&p&*tL>{$@6|<1h zy?Z??XoMIevw}gqtW}I4`UUrd<{raJ6G9d|!B|mrqTCrYbD2d8l9@0*fEz)7;GB-D z&YCSPEiElACX-m8dHWbUVvhi?HYMfua{wiX(FDMoMRNkw>Ojou!ymvARn!39%+fF^X7Iq!>j^ zezh29RjiW~qYlN4l%-IL(73=^6(c-JR*80sda6UwN^YPBSGVDhbpY={BT5E6cHMsG zA9{#5+CE$~%laQh8k~XiZ?6X%&+{6Q{K~#7F?Uy}H z0znvt-*0yPNi=c20XGn1jFeJZsV4vvNr;Urot<7lmr5dyH}D8hT98Odi8sJbgUshl zq6z!M0`XfC6P^mPyLsMBX5N`02vVum%B*ltfH%wg#0V55Au7!(w=khB8i0uhm=Di| zAT-ps>^kugAb14MCw_yBsaE~mh*Zm%a#(bLX>yC~B%ChqGgBK`2dGs1e3IPJx(MjE zOaoXsuO!PGji4WWUo^n4JkkXd<)Q(~zVE?R&tW-r)8wK7)<29mH0;#fSRKXWrv1jt zYT!VKPUmH?yIao_>ShD2b;G1=B`yIm2dDTC3 zlqzZP`*4+So)VVM_{L(7QI8LIR}eyxQY~$Y%w)NAKUFzRZvUG#>`RVBxCx29H2Dwx zv}*lZfGKh|)rp}#g&>Im?5_5u)N533&V0pYGw34A07+YFR`PfNrkzE~EumvM5l%vR zGL~B=PQpdVEmXOv@q|ZR1rEt=Svi^m$z_;wGvd4{sK{-GFjBN-ew;yw`Hy5wISAsK pVXZ5s)rqW literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_unfold.imageset/Vector@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/normal_input_unfold.imageset/Vector@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5bcfcba85b0c2cfe99616eee6f1d184dcdd82f61 GIT binary patch literal 707 zcmV;!0zCbRP)9>@{)2suJ>1A2m{QWu5rkZm{s=>ghoA84Zlr1k{l2suJI0egey5mDk{ zrtpO9*b9rjRrix3tztX+|BU0Y$3cidHhwoDpcw*m;qNI)>m!7~4D-DE?7O;yIw0IA z2u$*=(}TJ|>IFA|YPW^@=mwucLiT3*6%bOWfdnGZAwAe3<{Xcgwmt~dC%_aOM2Ucl z*Kbb_iSXK|Z}8uMi-#rFQ@qe91IOd(dtMuhDq|XBK*aNWo|)PRU`qsRQ(GepxWwbV z9`Ks~#nc|%fRFgqs<^d{k7{6_c&2oT7oy|=!6m+<^2+Xw1Inl z5M?tr8s+H^g+l~&_u~2PrYMR!&j>9oEiElih<{3C$D_QjP)+1nkv^%qkEsL9vb+Zg z&iOIrjnEZ>efyW?TFqTyiHCKr_H&|$jc)=V2d>rD1N_n7Z;stL5O%yL+HlC74zmjj( ztx1YV-F~_{TSEkV4Y)suZ%~$3=LZm&+A4E|@CqW(xLeAqumP&8tXDz=5>0f(HOdLE pGO4RGPm`|cFEYRHvVzZl;U|6_2kJ>mliC0P002ovPDHLkV1n{+H>v;t literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_delete.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_delete.imageset/Contents.json new file mode 100644 index 00000000..f30c214d --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_delete.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "selece_delete@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "selece_delete@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_delete.imageset/selece_delete@2x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_delete.imageset/selece_delete@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..dfca74b6f7c47dd9f6934d95f344fa70913f4611 GIT binary patch literal 1794 zcmV+d2mSboP)U zLO%o%EfQ6!0|(TK1MbHqf#9V{dLkjc#N4Lyc4wW9z4m%{$DUbxe$vbOD`W3_Gw;pJ zzM0KHQy61eemE^;go`C4>k~gaLTumhDVH)yCX?BLhGd`#Lg5o!&T`4aRklpZLhf=| z<_59R?~u~DOmiu*?o;AY;9{ZQ;3u7DYLwoL>s%(#%doxLUSaxD;Zi`a!B1+6%X5|< zDy$a_!CY5UVdXngS?@#Vn1v*y z48)k*JWG&2c+wS{%eaie+yNwnE1#wc9-@DgPgq<^;@lIF$N>BYoQJD?X-zUkSX>?o z9mFQK0r(eB@k0@YBG<(ZiZBFG;70g5%0EOhfG^-0 znlr|YP;U3bZN~u7)w~Wv7$p{A)ZA7J5Ce~X>uM&!hT9TCufnz40zQFJeiSUYItwMV z1u%F1V*!RZga+l72I#{n2HB7c8f!IW5$1x7>q!R1&e>KUXxajd>yZjg+${&a(aHc7 znA$H3n;YO*&>Mj+NYs7{23c6m=8FPbfT{f*fdcZe`msSn3oy06V^BaY)W6u!0!;1i zC=IdTCI1uVW`|kOgz_JMVdc>iRClY0!@%GGjt(Eenb%&0CQvNM`+G$DS^$5OCfk&! z=xuFpc(%0-8RT+@aB=#596odi8bFD`Zw_?1 zF>#hx6n439{_x|^up6_FFN;C$qCWrhBj?yywA5i}^vW@uyv z_o`Rdq5k=b;}+CGBjuyRLr}yx#Q-(6AMObCOiqi@sVYuWFUW#H6&4v_1R8Mjr`srA zUBt8P;3)_iM9qJ-fIGk5Yg)#g(mi}JH{Y@hnv(DaW*XoaKzV98O+Vbc^)v4O_MoYP z7r5fefBubYKP&~#6YYu-FLgD){VrI+zwol}|Ng)+)W*gpG+;zK7gXJSfjIc5tjQRm zpNaWBlF4e(0{R$Z0L5ezuMsmO!x%s@#sG>T18BLERG+Y844@Wc0L9p302>BX{6_|O z3WF+43s8(>fVS7P8l+P;22e}+fXG+3Vdyr_Zob|~_%00H#_>F!0hVFtHcqAY3~&RQ z<0aKn!tP)|$|9vP9_H^63bP!duuOlP%sBEzj4^s44^5#g=~E|9V0h?AU=kI*c5&(g zs?`@59zFuiscjza&yJ=Gsd;D&WjnVbm!1@t+V%zh>F6R9;m=_HXLGaZHQgjonX8YV zKogd|2elTU(hZT%CjU)q12#7Pb#g`r1`a|dK1A&`S*^fUn7=>qBKQH zQM^1ik8be6^o3A&)v= zw4{C(hE6ca4N&yI5M%&KQ=wL1(pBUVxM4@bMR_qdbO{F81nSgmSJNG$ZC4|hgh3Wf zJxWspi2V$-pCd{E#rnYOr=3mv`gM178Q2K9&p^w}IPs4&FvR6j*i9ZnQxiq=xdKB} zuC#JJ9%k#}&R|34*2SVA+T+G#(S>8f_4{q<%4}aRpU!MwPl$re_@FtHQIthtSK zYyiUC$}m1s7U_u}GKT&bG1EDY$xwbrpt!)&eG}(q&@FlkNCV8hp>vV-4s>&B3u%L; zzco~to2rv;So#b>?q(Xj2GidV6)w|E;vKWKgrDF9bN#9=lvppgny4^0SQmr>R483} zZI?0-lL6b?znbI~$luM;hMSd4D^qT?J73>vv@t+EY;QwZ5)1C;f?4r6i^fEO9iP~6 k7ogLTrV7fsD7>Ek0Uz(`jQjUe5C8xG07*qoM6N<$g0n?H_5c6? literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_delete.imageset/selece_delete@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_delete.imageset/selece_delete@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..bedf9ea532d1746da959dea8e73d741ed09b07bf GIT binary patch literal 2720 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 literal 0 HcmV?d00001 diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_singleForward.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_singleForward.imageset/Contents.json new file mode 100644 index 00000000..ec424469 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_singleForward.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "select_perItem@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "select_perItem@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_singleForward.imageset/select_perItem@2x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/select_singleForward.imageset/select_perItem@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3edad741fa246ef1d1f1afe5d44346af3ea3b18d GIT binary patch literal 2165 zcmV-*2#WWKP)c6R-EZ zc|Y@ZcD4>Ag)!EEn>OCoV2kk9AT*JA-ZWwR3|op?U0vNgNJt$>0vC7}wyoHrU_VzTBmo&B*R2z6w zJ=o@0cu2EqPy{nvO`3(5Gz+65@JC0O;a97LAr_WkjX(4t3zxeomNb*u8kiAePq6BT z&anYJ!jpj*Gt4sy-VdH+MZ?zN#bE9gC=(Pul?uk8c7#tvuqFAqmql3$z#niB4B>lb zk|833ZJf)XWJ*>59=HcL2~Z?Z7QIj+CB*BE@dS|+X)z)daz3MXQygfnjb z!Ic7pfUC$&J1QYn>>n2$19+-=5fp_;G8d!fqAY+9JZde~EC&q?ODK31u2>8B29)sK z5CKDG1u19&%&311fua-&2Iag8sKFA0*N_Wp&sVZ2uvSPs!afcbR!y*te#_vCZGryjN`uch}+I)fGAWE`*!bw&JT82`?qf2h3`)e$QGRScxx(PD})dVxHoVA1^-Sz z&-$!$=XTh;$Fg*ontCo~9@M308-*0Magzhimu&QMx1dKi?tK+_C4kU*B_voM3VRY<{tA0-&Rgmq|~6WSHdzyWmOao8R69ZEtMIDePR3LeHHaK^Jz{dT(3D7T71m3ZLmC z1<-W59*_hQ66Nlr$r~B(lh8lJ9=oEc{5p?)e9(dx)cs{Vh#FEqDS#$hNl}8B!3<^K zR00F%wDlhK_}GE{o(NvVt^^9s0Tl8^NCD*kqYyA8d_7H^G15dD(RS{9A6>+|P$^Om z9zo*g#X%Y=;n=rdgO8-B936pxu?xScX*nFmY{u(eZ-8O}+>YPE zr$Bt}!iW{*&Ext^@Cx|t{IKQo{SyAA%U96Sip!ty-xMoL1p$3am7! z5*bZi$M;99eZMj+muRE_4Sc*7K!P>{Aqk}g1*zA5@>D$6x4sJNaof_o(lJ|ER=+f3 z-=1dDcTmp{D$~isf24qC;3HxdLKCTu9y^{990?LwJEn*4pmn$v+Sv=k86{^KGMzkF z7T_C?(Z8FBf+9%pA7Oo-wxyC;@0TOf$%C$dqVH+-g2}1u>$hGA-cIbmC$eNZ`AGS$ zfLY<9xRDZe>6wsM(05X>l_=B6M`{VpSI3gH20m3q&FivWmH^t?UWc1^?#Y_zYQ3^`Imx8R9PzEJomXnVJDj;quAO`A&Jim#f0urVIq@S8nh0yIvnm#j* z-?uad3cs)x4;N=gkAT82tmO-=E27E``O4ZX3s7#zH%7#@BkcEx9RWq9Nr*e@HdTMg zx=qz-se*n`REjJQ<75F8jL8aWaWU3P;<#W(mId%&tYU%pILcHlSK|-vmaE~!LE#lS zJxWdm@bwIaK~XML^-SdYu$V>rYISvVFVLXCa|ZG*+QZsKl)Pq==D z3=b?SGs>o~n4oLNl?xJ)^$z=hDCzkzM9B&u)N~e|cqu*Kq$~vxJTTMU_7SKp^vuw$ z&$m;y0tode!_q1HQHBKnA?;8bBW60sb}xiq5hyOOa6iO37xakU7=!_4JkUA8st0;F zbp*l&3%@m_nX#f+xMAT_1i71%AJ8c+OnybAvGpHEhGh*6S;cGzzf8duAJN5o~L~m>dw{ rTYJx)7n%mf4d}F`Qb9@@=`P)-RYWxRl-XvW3Z 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�q5>Zu?8Ep3_o5N`W%uHDe(2 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 a-1rgUoM+NL_P|a60000 2 ? String(name[name.index(name.endIndex, offsetBy: -2)...]) : name } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift index 271d01b7..2ce65364 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift @@ -6,7 +6,7 @@ import UIKit @objcMembers -public class ChatImageTextCell: ChatStateCell { +open class ChatImageTextCell: ChatStateCell { var circleView = UIImageView() override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -50,7 +50,7 @@ public class ChatImageTextCell: ChatStateCell { ]) } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -80,7 +80,7 @@ public class ChatImageTextCell: ChatStateCell { return label }() - public func setup(accid: String?, nickName: String?) { + open func setup(accid: String?, nickName: String?) { let name = nickName?.count ?? 0 > 0 ? nickName : accid nameLabel.text = name guard let n = name else { return } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift index 1a495bce..e0998732 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift @@ -6,14 +6,14 @@ import UIKit @objcMembers -public class ChatSectionView: UITableViewHeaderFooterView { +open class ChatSectionView: UITableViewHeaderFooterView { public var titleLabel = UILabel() override init(reuseIdentifier: String?) { super.init(reuseIdentifier: reuseIdentifier) commonUI() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift index 5cfd2959..84f90b78 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift @@ -13,7 +13,7 @@ public enum RightStyle: Int { } @objcMembers -public class ChatStateCell: ChatCornerCell { +open class ChatStateCell: ChatCornerCell { private var style: RightStyle = .none public var rightImage = UIImageView() var rightImageMargin: NSLayoutConstraint? @@ -51,18 +51,7 @@ public class ChatStateCell: ChatCornerCell { ]) } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift index 338df7af..1acc67a0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift @@ -6,13 +6,13 @@ import UIKit @objcMembers -public class ChatTextArrowCell: ChatTextCell { +open class ChatTextArrowCell: ChatTextCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) rightStyle = .indicate } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift index 2e26829f..d8fa170d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift @@ -6,7 +6,7 @@ import UIKit @objcMembers -public class ChatTextCell: ChatStateCell { +open class ChatTextCell: ChatStateCell { public var titleLabel: UILabel = .init() public var detailLabel: UILabel = .init() public var line = UIView() @@ -63,7 +63,7 @@ public class ChatTextCell: ChatStateCell { ]) } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift index 049697b3..e9b21bc2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift @@ -6,7 +6,7 @@ import UIKit @objcMembers -public class ChatUnfoldCell: ChatCornerCell { +open class ChatUnfoldCell: ChatCornerCell { lazy var arrowImage: UIImageView = { let arrow = UIImageView() arrow.translatesAutoresizingMaskIntoConstraints = false @@ -22,23 +22,12 @@ public class ChatUnfoldCell: ChatCornerCell { return label }() - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupUI() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { super.init(coder: coder) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUserHeaderView.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUserHeaderView.swift index bc25c960..34ba3b5b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUserHeaderView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUserHeaderView.swift @@ -6,7 +6,7 @@ import UIKit @objcMembers -public class ChatUserHeaderView: UIImageView { +open class ChatUserHeaderView: UIImageView { public lazy var titleLabel: UILabel = { let label = UILabel() label.font = DefaultTextFont(12) @@ -20,7 +20,7 @@ public class ChatUserHeaderView: UIImageView { setupUI() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { super.init(coder: coder) } @@ -36,7 +36,7 @@ public class ChatUserHeaderView: UIImageView { backgroundColor = .clear } - public func setTitle(_ name: String) { + open func setTitle(_ name: String) { titleLabel.text = name .count > 2 ? String(name[name.index(name.endIndex, offsetBy: -2)...]) : name } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift index 517da2ce..42f8e49d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift @@ -17,9 +17,10 @@ open class NEChatBaseCell: UITableViewCell { fatalError("init(coder:) has not been implemented") } - public func uploadProgress(byRight: Bool, _ progress: Float) { + open func uploadProgress(byRight: Bool, _ progress: Float) { fatalError("override in sub class") } open func setModel(_ model: MessageContentModel) {} + open func setModel(_ model: MessageContentModel, _ isSend: Bool = false) {} } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/ChatTableViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/ChatTableViewController.swift index c7af6d83..4bfcb77a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/ChatTableViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/ChatTableViewController.swift @@ -13,7 +13,7 @@ open class ChatTableViewController: NEBaseViewController, UITableViewDelegate, public var topConstraint: NSLayoutConstraint? public var bottomConstraint: NSLayoutConstraint? - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() tableView.separatorStyle = .none tableView.delegate = self @@ -47,12 +47,12 @@ open class ChatTableViewController: NEBaseViewController, UITableViewDelegate, tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) } - public func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { tableView.dequeueReusableCell(withIdentifier: "UITableViewCell", for: indexPath) } - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 0 } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift index b70f658b..bc51c77a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift @@ -19,7 +19,7 @@ import WebKit open class ChatViewController: ChatBaseViewController, UINavigationControllerDelegate, ChatInputViewDelegate, ChatViewModelDelegate, NIMMediaManagerDelegate, MessageOperationViewDelegate, UITableViewDataSource, - UITableViewDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate, CLLocationManagerDelegate, UITextViewDelegate { + UITableViewDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate, CLLocationManagerDelegate, UITextViewDelegate, ChatInputMultilineDelegate { private let tag = "ChatViewController" private let kCallKitDismissNoti = "kCallKitDismissNoti" private let kCallKitShowNoti = "kCallKitShowNoti" @@ -32,16 +32,16 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel private var playingModel: MessageAudioModel? private var timer: Timer? private var isFile: Bool? // 是否以文件形式发送 - private var isCurrentPage = true + public var isCurrentPage = true + public var isMute = false // 是否禁言 + private var isMutilSelect = false // 是否多选模式 public var operationCellFilter: [OperationType]? // 消息长按菜单全局过滤列表 public var cellRegisterDic = [String: UITableViewCell.Type]() private var needMarkReadMsgs = [NIMMessage]() private var atUsers = [NSRange]() - let group = DispatchGroup() var replyView = ReplyView() -// public var menuView: NEBaseChatInputView! public var operationView: MessageOperationView? public var normalOffset: CGFloat = 0 @@ -62,12 +62,14 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - public lazy var bottomViewHeight: CGFloat = 304 { + public lazy var bottomViewHeight: CGFloat = 404 { didSet { bottomViewHeightAnchor?.constant = bottomViewHeight } } + public var currentKeyboardHeight: CGFloat = 0 + public var bodyTopViewHeightAnchor: NSLayoutConstraint? public var bodyBottomViewHeightAnchor: NSLayoutConstraint? public var contentViewTopAnchor: NSLayoutConstraint? @@ -77,12 +79,10 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel public init(session: NIMSession) { viewmodel = ChatViewModel(session: session, anchor: nil) super.init(nibName: nil, bundle: nil) -// menuView = getMenuView() NEKeyboardManager.shared.enable = false NEKeyboardManager.shared.enableAutoToolbar = false NIMSDK.shared().mediaManager.add(self) - NIMSDK.shared().mediaManager.switch(viewmodel.getHandSetEnable() ? .receiver : .speaker) } public required init?(coder: NSCoder) { @@ -129,7 +129,10 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel if NIMSDK.shared().mediaManager.isPlaying() { NIMSDK.shared().mediaManager.stopPlay() } + clearAtRemind() + chatInputView.textView.resignFirstResponder() + chatInputView.titleField.resignFirstResponder() } override open func viewDidDisappear(_ animated: Bool) { @@ -137,9 +140,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel stopPlay() } - open func commonUI() { - title = viewmodel.session.sessionId - navigationView.titleBarBottomLine.isHidden = false + open func setMoreButton() { if NEKitChatConfig.shared.ui.messageProperties.showTitleBarRightIcon { let image = NEKitChatConfig.shared.ui.messageProperties.titleBarRightRes ?? UIImage.ne_imageNamed(name: "three_point") addRightAction(image, #selector(toSetting), self) @@ -147,6 +148,13 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } else { navigationView.moreButton.isHidden = true } + } + + open func commonUI() { + title = viewmodel.session.sessionId + navigationView.titleBarBottomLine.isHidden = false + setMoreButton() + setMutilSelectBottomView() view.addSubview(bodyTopView) view.addSubview(bodyView) @@ -168,28 +176,28 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel bodyTopView.rightAnchor.constraint(equalTo: view.rightAnchor), ]) + bottomViewTopAnchor = bottomView.topAnchor.constraint(equalTo: view.bottomAnchor, constant: -normalInputHeight) + bottomViewTopAnchor?.isActive = true + bottomViewHeightAnchor = bottomView.heightAnchor.constraint(equalToConstant: bottomViewHeight) + bottomViewHeightAnchor?.isActive = true 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), + bottomView.leftAnchor.constraint(equalTo: view.leftAnchor), + bottomView.rightAnchor.constraint(equalTo: view.rightAnchor), ]) bodyBottomViewHeightAnchor = bodyBottomView.heightAnchor.constraint(equalToConstant: bodyBottomViewHeight) bodyBottomViewHeightAnchor?.isActive = true NSLayoutConstraint.activate([ - bodyBottomView.bottomAnchor.constraint(equalTo: chatInputView.topAnchor), + bodyBottomView.bottomAnchor.constraint(equalTo: bottomView.topAnchor), bodyBottomView.leftAnchor.constraint(equalTo: view.leftAnchor), bodyBottomView.rightAnchor.constraint(equalTo: view.rightAnchor), ]) - bottomViewTopAnchor = bottomView.topAnchor.constraint(equalTo: view.bottomAnchor, constant: -normalInputHeight) - bottomViewTopAnchor?.isActive = true - bottomViewHeightAnchor = bottomView.heightAnchor.constraint(equalToConstant: bottomViewHeight) - bottomViewHeightAnchor?.isActive = true NSLayoutConstraint.activate([ - bottomView.leftAnchor.constraint(equalTo: view.leftAnchor), - bottomView.rightAnchor.constraint(equalTo: view.rightAnchor), + 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( @@ -214,6 +222,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // MARK: 子类可重写方法 + public func onTeamMemberChange(team: NIMTeam) {} + override open func backEvent() { super.backEvent() cleanDelegate() @@ -253,9 +263,17 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return } + // 多选模式下屏蔽消息长按事件 + if isMutilSelect { + return + } + // 底部收起 - chatInputView.textView.resignFirstResponder() - layoutInputView(offset: 0) + if chatInputView.textView.isFirstResponder || chatInputView.titleField.isFirstResponder { + chatInputView.textView.resignFirstResponder() + chatInputView.titleField.resignFirstResponder() + layoutInputView(offset: 0) + } operationView?.removeFromSuperview() @@ -283,42 +301,39 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel setOperationItems(items: &filterItems, model: model) viewmodel.operationModel = model - group.notify(queue: .main) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, 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 - - if let index = tableView.indexPath(for: cell) { - 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.isOutgoingMsg { - 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 - } + 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.isOutgoingMsg { + 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 + } - operationView = MessageOperationView(frame: frame) - operationView!.delegate = self - operationView!.items = filterItems - view.addSubview(operationView!) - } - })) - } + operationView = MessageOperationView(frame: frame) + operationView!.delegate = self + operationView!.items = filterItems + view.addSubview(operationView!) + })) } // MARK: lazy Method @@ -405,24 +420,40 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = UIColor.clear - view.addSubview(chatInputView) + view.addSubview(chatInputView) NSLayoutConstraint.activate([ chatInputView.leftAnchor.constraint(equalTo: view.leftAnchor), chatInputView.rightAnchor.constraint(equalTo: view.rightAnchor), - chatInputView.heightAnchor.constraint(equalToConstant: 304), - chatInputView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + chatInputView.heightAnchor.constraint(equalToConstant: 404), + chatInputView.topAnchor.constraint(equalTo: view.topAnchor), + ]) + + view.addSubview(mutilSelectBottomView) + NSLayoutConstraint.activate([ + mutilSelectBottomView.leftAnchor.constraint(equalTo: view.leftAnchor), + mutilSelectBottomView.rightAnchor.constraint(equalTo: view.rightAnchor), + mutilSelectBottomView.heightAnchor.constraint(equalToConstant: 304), + mutilSelectBottomView.topAnchor.constraint(equalTo: view.topAnchor), ]) return view }() public lazy var chatInputView: NEBaseChatInputView = { - let menu = getMenuView() - menu.translatesAutoresizingMaskIntoConstraints = false - menu.backgroundColor = UIColor(hexString: "#EFF1F3") - menu.delegate = self - return menu + let inputView = getMenuView() + inputView.translatesAutoresizingMaskIntoConstraints = false + inputView.backgroundColor = .ne_backgroundColor + inputView.delegate = self + return inputView + }() + + public lazy var mutilSelectBottomView: NEMutilSelectBottomView = { + let view = NEMutilSelectBottomView() + view.translatesAutoresizingMaskIntoConstraints = false + view.delegate = self + view.isHidden = true + return view }() // MARK: UIGestureRecognizerDelegate @@ -433,18 +464,25 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return true } - // 消息操作按钮 - if view.bounds.size.width == 60 { - return false - } // 点击重发按钮 // 点击撤回重新编辑按钮 if view.isKind(of: UIButton.self) { return false } - if view.isKind(of: UIImageView.self) { + + // 回复消息view + // 已读未读按钮 + // 消息操作按钮(撤回、删除) + // 地图view + // 文件消息图标 + if view.accessibilityIdentifier == "id.replyTextView" || + view.accessibilityIdentifier == "id.readView" || + view.accessibilityIdentifier == "id.menuCell" || + view.accessibilityIdentifier == "id.mapView" || + view.accessibilityIdentifier == "id.fileStatus" { return false } + return true } @@ -476,7 +514,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } deinit { - print("will deinit") + NELog.infoLog(className(), desc: "deinit") cleanDelegate() } @@ -487,7 +525,54 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // MARK: objc 方法 - override open func toSetting() {} + func getUserSettingViewController() -> NEBaseUserSettingViewController { + UserSettingViewController(userId: viewmodel.session.sessionId) + } + + /// 设置按钮点击事件 + override open func toSetting() { + // 自定义设置按钮点击事件 + if let block = NEKitChatConfig.shared.ui.messageProperties.titleBarRightClick { + block() + return + } + + // 多选模式下屏蔽设置按钮点击事件 + if isMutilSelect { + return + } + + if viewmodel.session.sessionType == .team { + Router.shared.use( + TeamSettingViewRouter, + parameters: ["nav": navigationController as Any, + "teamid": viewmodel.session.sessionId], + closure: nil + ) + } else if viewmodel.session.sessionType == .P2P { + let userSetting = getUserSettingViewController() + navigationController?.pushViewController(userSetting, animated: true) + } + } + + open func viewTap(tap: UITapGestureRecognizer) { + if isMutilSelect { + return + } + + if let opeView = operationView, + view.subviews.contains(opeView) { + opeView.removeFromSuperview() + + } else { + if chatInputView.textView.isFirstResponder || chatInputView.titleField.isFirstResponder { + chatInputView.textView.resignFirstResponder() + chatInputView.titleField.resignFirstResponder() + } else { + layoutInputView(offset: 0) + } + } + } // MARK: private 方法 @@ -501,10 +586,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ) if let ms = weakSelf?.viewmodel.messages, ms.count > 0 { - weakSelf?.tableView.reloadData() + weakSelf?.didRefreshTable() if weakSelf?.viewmodel.isHistoryChat == true { let indexPath = IndexPath(row: index, section: 0) - print("queryRoamMsgHasMoreTime_v2 index : ", index) weakSelf?.tableView.scrollToRow(at: indexPath, at: .middle, animated: false) if newEnd > 0 { weakSelf?.addBottomLoadMore() @@ -520,7 +604,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } else if let err = error { - weakSelf?.showToast(err.localizedDescription) + weakSelf?.showErrorToast(err) } weakSelf?.loadDataFinish() } @@ -533,7 +617,6 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ModuleName + " " + self.tag, desc: #function + "CALLBACK dropDownRemoteRefresh " + (error?.localizedDescription ?? "no error") ) - print("dropDownRemoteRefresh messages count ", messages?.count as Any) weakSelf?.tableView.reloadData() if count > 0 { @@ -566,10 +649,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } func addObseve() { - NotificationCenter.default.addObserver(self, - selector: #selector(markNeedReadMsg), - name: UIApplication.willEnterForegroundNotification, - object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didRefreshTable), name: NENotificationName.updateFriendInfo, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, @@ -627,11 +707,15 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel func appEnterForegournd() { isCurrentPage = true + markNeedReadMsg() } // MARK: 键盘通知相关操作 open func keyBoardWillShow(_ notification: Notification) { + if !isCurrentPage { + return + } operationView?.removeFromSuperview() if chatInputView.currentType != .text { return @@ -644,9 +728,14 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel let keyboardRect = (notification .userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue + var animationDuration: TimeInterval = 0.1 + + if let userInfo = notification.userInfo, + let keyboardAnimationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval { + animationDuration = keyboardAnimationDuration + } - print("chat view key board size : ", keyboardRect) - layoutInputView(offset: keyboardRect.size.height) + layoutInputViewWithAnimation(offset: keyboardRect.size.height, animationDuration) weak var weakSelf = self UIView.animate(withDuration: 0.25, animations: { weakSelf?.view.layoutIfNeeded() @@ -665,16 +754,18 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return } chatInputView.currentButton?.isSelected = false - // 解决点击operation点击无效问题 -// if operationView?.superview != nil { -// operationView?.removeFromSuperview() -// } - layoutInputView(offset: 0) + var animationDuration: TimeInterval = 0.1 + + if let userInfo = notification.userInfo, + let keyboardAnimationDuration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? TimeInterval { + animationDuration = keyboardAnimationDuration + } + layoutInputViewWithAnimation(offset: 0, animationDuration) } private func scrollTableViewToBottom() { - print("self.viewmodel.messages.count\(viewmodel.messages.count)") - print("self.tableView.numberOfRows(inSection: 0)\(tableView.numberOfRows(inSection: 0))") + NELog.infoLog(className(), desc: "self.viewmodel.messages.count\(viewmodel.messages.count)") + NELog.infoLog(className(), desc: "self.tableView.numberOfRows(inSection: 0)\(tableView.numberOfRows(inSection: 0))") if viewmodel.messages.count > 0 { weak var weakSelf = self DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: DispatchWorkItem(block: { @@ -687,16 +778,21 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func layoutInputView(offset: CGFloat) { - print("layoutInputView offset : ", offset) - print("normalOffset : ", normalOffset) - print("normalInputHeight : ", normalInputHeight) + layoutInputViewWithAnimation(offset: offset) + } + + open func layoutInputViewWithAnimation(offset: CGFloat, _ animation: CGFloat = 0.1) { + NELog.infoLog(className(), desc: "normal height : \(normalInputHeight) normal offset: \(normalOffset) offset : \(offset)") weak var weakSelf = self - let topValue = normalInputHeight - normalOffset + var topValue = normalInputHeight + if chatInputView.chatInpuMode != .multipleReturn { + topValue -= normalOffset + } if offset == 0 { chatInputView.contentSubView?.isHidden = true chatInputView.currentButton?.isSelected = false } - UIView.animate(withDuration: 0.1, animations: { + UIView.animate(withDuration: animation, animations: { weakSelf?.bottomViewTopAnchor?.constant = -topValue - offset }) } @@ -704,6 +800,64 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // MARK: ChatInputViewDelegate open func sendText(text: String?, attribute: NSAttributedString?) { + if let title = chatInputView.titleField.text, title.trimmingCharacters(in: .whitespaces).isEmpty == false { + // 换行消息 + NELog.infoLog(className(), desc: "换行消息: \(title)") + var dataDic = [String: Any]() + dataDic["title"] = title + if let t = text?.trimmingCharacters(in: .whitespacesAndNewlines), !t.isEmpty { + dataDic["body"] = text + } + + var attachDic = [String: Any]() + attachDic["type"] = customRichTextType + attachDic["data"] = dataDic + + let attachment = NECustomAttachment(customType: customRichTextType, data: attachDic) + let remoteExt = chatInputView.getRemoteExtension(attribute) + + weak var weakSelf = self + if viewmodel.isReplying, let msg = viewmodel.operationModel?.message { + viewmodel.replyMessageWithoutThread(message: + MessageUtils.customMessage(attachment: attachment, + remoteExt: remoteExt, + apnsContent: title), + target: msg) { [weak self] error in + NELog.infoLog( + ModuleName + " " + (self?.tag ?? "ChatViewController"), + desc: #function + "CALLBACK replyMessage " + (error?.localizedDescription ?? "no error") + ) + if error != nil { + weakSelf?.showErrorToast(error) + } else { + weakSelf?.closeReply(button: nil) + } + self?.chatInputView.titleField.text = nil + self?.chatInputView.textView.text = nil + self?.didSendFinishAndCheckoutInput() + } + } else { + viewmodel.sendCustomMessage(attachment: attachment, + remoteExt: remoteExt, + apnsConstent: title) { [weak self] error in + self?.showErrorToast(error) + self?.chatInputView.titleField.text = nil + self?.chatInputView.textView.text = nil + self?.didSendFinishAndCheckoutInput() + } + } + } else { + // 文本消息 + sendContentText(text: text, attribute: attribute) + } + } + + func sendContentText(text: String?, attribute: NSAttributedString?) { + guard let removeSpace = text?.trimmingCharacters(in: .whitespaces), removeSpace.count > 0 else { + chatInputView.titleField.text = nil + showToast(chatLocalizable("null_message_not_support")) + return + } guard let content = text, content.count > 0 else { return } @@ -717,10 +871,11 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel desc: #function + "CALLBACK replyMessage " + (error?.localizedDescription ?? "no error") ) if error != nil { - weakSelf?.view.makeToast(error?.localizedDescription) + weakSelf?.showErrorToast(error) } else { weakSelf?.closeReply(button: nil) } + weakSelf?.didSendFinishAndCheckoutInput() } } else { @@ -729,9 +884,10 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ModuleName + " " + (self?.tag ?? "ChatViewController"), desc: #function + "CALLBACK sendTextMessage " + (error?.localizedDescription ?? "no error") ) - if error != nil { - weakSelf?.view.makeToast(error?.localizedDescription) - } + weakSelf?.showErrorToast(error) + self?.chatInputView.titleField.text = nil + self?.chatInputView.textView.text = nil + self?.didSendFinishAndCheckoutInput() } } } @@ -817,9 +973,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ctrl.completion = { model in NELog.infoLog(self.className(), desc: "position : \(model.yx_modelToJSONString() ?? "")") weakSelf?.viewmodel.sendLocationMessage(model) { error in - if let err = error { - weakSelf?.showToast(err.localizedDescription) - } + weakSelf?.showErrorToast(error) } } } @@ -893,23 +1047,41 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return true } - open func textFieldDidChange(_ textField: UITextView) { - if let text = textField.text { - if text.count > 0 { + open func textFieldDidEndEditing(_ text: String?) { + viewmodel.sendInputTypingEndState() + } + + open func textFieldDidBeginEditing(_ text: String?) { + checkAndSendTypingState() + } + + open func textFieldDidChange(_ text: String?) { + checkAndSendTypingState() + } + + func checkAndSendTypingState() { + if chatInputView.chatInpuMode == .normal { + if let content = chatInputView.textView.text, content.count > 0 { viewmodel.sendInputTypingState() } else { viewmodel.sendInputTypingEndState() } - } - } + } else { + var title = "" + var content = "" - open func textFieldDidEndEditing(_ textField: UITextView) { - viewmodel.sendInputTypingEndState() - } + if let titleText = chatInputView.titleField.text { + title = titleText + } - open func textFieldDidBeginEditing(_ textField: UITextView) { - if let count = textField.text?.count, count > 0 { - viewmodel.sendInputTypingState() + if let contentText = chatInputView.textView.text { + content = contentText + } + if title.count <= 0, content.count <= 0 { + viewmodel.sendInputTypingEndState() + } else { + viewmodel.sendInputTypingState() + } } } @@ -920,7 +1092,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func willSelectItem(button: UIButton?, index: Int) { operationView?.removeFromSuperview() - if button?.isSelected == true { + if index == 2 || button?.isSelected == true { if index == 0 { // 语音 layoutInputView(offset: bottomExanpndHeight) @@ -1023,9 +1195,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ModuleName + " " + (weakSelf?.tag ?? "ChatViewController"), desc: #function + "CALLBACK sendVideoMessage " + (error?.localizedDescription ?? "no error") ) - if let err = error { - weakSelf?.showToast(err.localizedDescription) - } + weakSelf?.showErrorToast(error) } } return @@ -1054,6 +1224,49 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } } else { + if let url = info[.referenceURL] as? URL { + if url.absoluteString.hasSuffix("ext=GIF") == true { + // GIF 需要特殊处理 + let imageAsset: PHAsset? + if #available(iOS 11.0, *) { + imageAsset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset + } else { + imageAsset = PHAsset.fetchAssets(withALAssetURLs: [url], options: nil).firstObject + } + let options = PHImageRequestOptions() + options.version = .current + guard let asset = imageAsset else { + return + } + weak var weakSelf = self + PHImageManager.default().requestImageData(for: asset, options: options) { imageData, dataUTI, orientation, info in + if let data = imageData { + let tempDirectoryURL = FileManager.default.temporaryDirectory + let uniqueString = UUID().uuidString + let temUrl = tempDirectoryURL.appendingPathComponent(uniqueString + ".gif") + print("tem url path : ", temUrl.path) + do { + try data.write(to: temUrl) + DispatchQueue.main.async { + weakSelf?.viewmodel.sendImageMessage(path: temUrl.path) { error in + NELog.infoLog( + ModuleName + " " + (weakSelf?.tag ?? "ChatViewController"), + desc: #function + "CALLBACK sendImageMessage " + (error?.localizedDescription ?? "no error") + ) + if error != nil { + weakSelf?.view.makeToast(error?.localizedDescription) + } + } + } + } catch { + NELog.infoLog(ModuleName, desc: #function + "write tem gif data error : \(error.localizedDescription)") + } + } + } + return + } + } + viewmodel.sendImageMessage(image: image) { [weak self] error in NELog.infoLog( ModuleName + " " + (self?.tag ?? "ChatViewController"), @@ -1071,8 +1284,6 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController .InfoKey: Any]) { -// picker.dismiss(animated: true, completion: nil) -// sendMediaMessage(didFinishPickingMediaWithInfo: info) weak var weakSelf = self picker.dismiss(animated: true, completion: { weakSelf?.sendMediaMessage(didFinishPickingMediaWithInfo: info) @@ -1211,6 +1422,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel if atIndexs.isEmpty { return } + viewmodel.selectedMessages.removeAll(where: { $0.messageId == message.messageId }) operationView?.removeFromSuperview() tableViewDeleteIndexs(atIndexs) DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in @@ -1226,6 +1438,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel if atIndexs.isEmpty { return } + viewmodel.selectedMessages.removeAll(where: { $0.messageId == message.messageId }) operationView?.removeFromSuperview() NELog.infoLog(className(), desc: "on revoke message at indexs \(atIndexs)") tableViewReloadIndexs(atIndexs) @@ -1256,6 +1469,12 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel tableView.reloadRows(at: indexs, with: .none) tableView.endUpdates() } + + indexs.forEach { index in + if index.row == tableView.numberOfRows(inSection: 0) - 1 { + tableView.scrollToRow(at: index, at: .bottom, animated: true) + } + } } open func didReadedMessageIndexs() { @@ -1275,11 +1494,12 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func didRefreshTable() { - group.enter() - DispatchQueue.main.async { [weak self] in - self?.tableView.reloadData() - self?.group.leave() - } + getSessionInfo(session: viewmodel.session) + tableView.reloadData() + } + + open func selectedMessagesChanged(_ count: Int) { + mutilSelectBottomView.setEnable(count > 0) } // record audio @@ -1314,31 +1534,26 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - open func viewTap(tap: UITapGestureRecognizer) { - if let opeView = operationView, - view.subviews.contains(opeView) { - opeView.removeFromSuperview() - - } else { - if chatInputView.textView.isFirstResponder { - chatInputView.textView.resignFirstResponder() - } else { - layoutInputView(offset: 0) - } - } - } - // MARK: audio play - func startPlaying(audio: NIMAudioObject, isSend: Bool) { + func startPlaying(audioMessage: NIMMessage?, isSend: Bool) { + guard let message = audioMessage, let audio = message.messageObject as? NIMAudioObject else { + return + } playingCell?.startAnimation(byRight: isSend) - if let url = audio.path { + if let path = audio.path, FileManager.default.fileExists(atPath: path) { + NELog.infoLog(className(), desc: #function + " play path : " + path) + if viewmodel.getHandSetEnable() == true { NIMSDK.shared().mediaManager.switch(.receiver) } else { NIMSDK.shared().mediaManager.switch(.speaker) } - NIMSDK.shared().mediaManager.play(url) + NIMSDK.shared().mediaManager.play(path) + } else { + NELog.infoLog(className(), desc: #function + " audio path is empty, play url : " + (audio.url ?? "")) + playingCell?.stopAnimation(byRight: isSend) + ChatMessageHelper.downloadAudioFile(message: message) } } @@ -1351,17 +1566,17 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel if NIMSDK.shared().mediaManager.isPlaying() { stopPlay() } else { - startPlaying(audio: audio, isSend: isSend) + startPlaying(audioMessage: model?.message, isSend: isSend) } } else { stopPlay() playingCell = cell playingModel = model - startPlaying(audio: audio, isSend: isSend) + startPlaying(audioMessage: model?.message, isSend: isSend) } } - public func stopPlay() { + open func stopPlay() { if NIMSDK.shared().mediaManager.isPlaying() { playingCell?.startAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) NIMSDK.shared().mediaManager.stopPlay() @@ -1379,9 +1594,10 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // play open func playAudio(_ filePath: String, didBeganWithError error: Error?) { - print(#function + "\(error)") + print(#function + "\(error?.localizedDescription ?? "")") + NIMSDK.shared().mediaManager.switch(viewmodel.getHandSetEnable() ? .receiver : .speaker) if let e = error { - showToast(e.localizedDescription) + showErrorToast(error) // stop playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) playingModel?.isPlaying = false @@ -1389,20 +1605,16 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func playAudio(_ filePath: String, didCompletedWithError error: Error?) { - print(#function + "\(error)") - if let e = error { - showToast(e.localizedDescription) - } + print(#function + "\(error?.localizedDescription ?? "")") + showErrorToast(error) // stop playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) playingModel?.isPlaying = false } open func stopPlayAudio(_ filePath: String, didCompletedWithError error: Error?) { - print(#function + "\(error)") - if let e = error { - showToast(e.localizedDescription) - } + print(#function + "\(error?.localizedDescription ?? "")") + showErrorToast(error) playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) playingModel?.isPlaying = false } @@ -1422,16 +1634,16 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel playingModel?.isPlaying = false } - // record + // record open func recordAudio(_ filePath: String?, didBeganWithError error: Error?) { - print("[record] sdk Began error:\(error)") + print("[record] sdk Began error:\(error?.localizedDescription ?? "")") } open func recordAudio(_ filePath: String?, didCompletedWithError error: Error?) { - print("[record] sdk Completed error:\(error)") + print("[record] sdk Completed error:\(error?.localizedDescription ?? "")") chatInputView.stopRecordAnimation() guard let fp = filePath else { - showToast(error?.localizedDescription ?? "") + showErrorToast(error) return } let dur = recordDuration(filePath: fp) @@ -1443,9 +1655,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ModuleName + " " + self.tag, desc: #function + "CALLBACK sendAudioMessage " + (error?.localizedDescription ?? "no error") ) - if let e = error { - self.showToast(e.localizedDescription) - } else {} + self.showErrorToast(error) } } else { showToast(chatLocalizable("record_too_short")) @@ -1563,7 +1773,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel var addText = "" var accid = "" - if index == 0 { + if model == nil { addText += chatLocalizable("user_select_all") } else { if let m = model { @@ -1579,6 +1789,17 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel present(selectVC, animated: true, completion: nil) } + private func showErrorToast(_ error: Error?) { + if let err = error as? NSError { + switch err.code { + case noNetworkCode: + showToast(commonLocalizable("network_error")) + default: + showToast(err.localizedDescription) + } + } + } + // MARK: MessageOperationViewDelegate open func didSelectedItem(item: OperationItem) { @@ -1593,7 +1814,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel case .delete: deleteMessage() case .reply: - showReplyMessageView() + if !isMute { + showReplyMessageView() + } case .recall: recallMessage() case .collection: @@ -1604,6 +1827,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel pinMessage() case .removePin: removePinMessage() + case .multiSelect: + selectMessage() default: customOperation() } @@ -1612,37 +1837,44 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func customOperation() {} open func copyMessage() { - if let model = viewmodel.operationModel as? MessageTextModel, - let text = model.message?.text { - let pasteboard = UIPasteboard.general - pasteboard.string = text - view.makeToast(chatLocalizable("copy_success"), duration: 2, position: .center) + if let model = viewmodel.operationModel as? MessageTextModel { + if let text = model.message?.text, !text.isEmpty { + UIPasteboard.general.string = text + showToast(chatLocalizable("copy_success")) + } else if let body = model.attributeStr?.string, !body.isEmpty { + UIPasteboard.general.string = body + showToast(chatLocalizable("copy_success")) + } else if let model = viewmodel.operationModel as? MessageRichTextModel { + if let title = model.titleAttributeStr?.string, !title.isEmpty { + UIPasteboard.general.string = title + showToast(chatLocalizable("copy_success")) + } + } } } open func deleteMessage() { showAlert(message: chatLocalizable("message_delete_confirm")) { - if let message = self.viewmodel.operationModel?.message { - self.viewmodel.deleteMessage(message: message) { error in - if error != nil { - self.showToast(chatLocalizable("delete_failed")) - } - } + self.viewmodel.deleteMessage { error in + self.showErrorToast(error) } } } open func showReplyMessageView(isReEdit: Bool = false) { viewmodel.isReplying = true - 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 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 let message = viewmodel.operationModel?.message { if isReEdit { replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: viewmodel.operationModel?.replyText ?? "", @@ -1654,38 +1886,18 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } else { var text = chatLocalizable("msg_reply") if let uid = message.from { - var showName = viewmodel.getShowName(userId: uid, teamId: viewmodel.session.sessionId, false) + var showName = ChatUserCache.getShowName(userId: uid, teamId: viewmodel.session.sessionId, false) if viewmodel.session.sessionType != .P2P, !IMKitClient.instance.isMySelf(uid) { addToAtUsers(addText: "@" + showName + "", isReply: true, accid: uid) } let user = viewmodel.getUserInfo(userId: uid) - if let alias = user?.alias { + if let alias = user?.alias, !alias.isEmpty { showName = alias } text += " " + showName } - text += ": " - switch message.messageType { - case .text: - if let t = message.text { - text += t - } - case .image: - text += "[\(chatLocalizable("msg_image"))]" - case .audio: - text += "[\(chatLocalizable("msg_audio"))]" - case .video: - text += "[\(chatLocalizable("msg_video"))]" - case .file: - text += "[\(chatLocalizable("msg_file"))]" - case .location: - text += "[\(chatLocalizable("msg_location"))]" - case .custom: - text += "[\(chatLocalizable("msg_custom"))]" - default: - text += "[\(chatLocalizable("msg_unknown"))]" - } + text += ": \(ChatMessageHelper.contentOfMessage(message))" replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, font: replyView.textLabel.font, color: replyView.textLabel.textColor) @@ -1703,12 +1915,15 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel weak var weakSelf = self showAlert(message: chatLocalizable("message_revoke_confirm")) { if let message = weakSelf?.viewmodel.operationModel?.message { - if let messageType = weakSelf?.viewmodel.operationModel?.message?.messageType, messageType == .text { + if message.messageType == .text { weakSelf?.viewmodel.operationModel?.isRevokedText = true } -// DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + .seconds(120)) { -// weakSelf?.tableView.reloadData() -// } + + if message.messageType == .custom, + let attach = NECustomAttachment.attachmentOfCustomMessage(message: message), attach.customType == customRichTextType { + weakSelf?.viewmodel.operationModel?.isRevokedText = true + } + let isPin = weakSelf?.viewmodel.operationModel?.isPined ?? false weakSelf?.viewmodel.revokeMessage(message: message) { error in NELog.infoLog( @@ -1716,12 +1931,10 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel desc: #function + "CALLBACK revokeMessage " + (error?.localizedDescription ?? "no error") ) if let err = error as? NSError { - if err.code == 408 { - weakSelf?.showToast(chatLocalizable("ravoked_failed")) - } else if err.code == 508 { + if err.code == 508 { weakSelf?.showToast(chatLocalizable("ravokable_time_expired")) } else { - weakSelf?.showToast(err.localizedDescription) + weakSelf?.showToast(chatLocalizable("ravoked_failed")) } } else { // 自己撤回成功 & 收到对方撤回 都会走回调方法 onRevokeMessage @@ -1750,7 +1963,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel desc: #function + "CALLBACK addColletion " + (error?.localizedDescription ?? "no error") ) if error != nil { - self.showToast(error!.localizedDescription) + self.showErrorToast(error) } else { self.showToast(chatLocalizable("collection_success")) } @@ -1762,10 +1975,28 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel NEBaseForwardAlertViewController() } - func forwardMessageToUser(message: NIMMessage) { + func addForwardAlertController(items: [ForwardItem], + type: String, + _ sureBlock: ((String?) -> Void)? = nil) { + let forwardAlert = getForwardAlertController() + forwardAlert.setItems(items) + forwardAlert.type = type + forwardAlert.context = ChatMessageHelper.getSessionName(session: viewmodel.session) + forwardAlert.sureBlock = sureBlock + + addChild(forwardAlert) + view.addSubview(forwardAlert.view) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: DispatchWorkItem(block: { + UIApplication.shared.keyWindow?.endEditing(true) + })) + } + + func forwardMessageToUser(isMultiForward: Bool = false, + depth: Int = 0, + _ sureBlock: (() -> Void)? = nil) { weak var weakSelf = self Router.shared.register(ContactSelectedUsersRouter) { param in - print("user setting accids : ", param) var items = [ForwardItem]() if let users = param["im_user"] as? [NIMUser] { @@ -1773,40 +2004,40 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel let item = ForwardItem() item.uid = user.userId item.avatar = user.userInfo?.avatarUrl - item.name = user.userInfo?.nickName + item.name = user.getShowName() items.append(item) } - let forwardAlert = weakSelf?.getForwardAlertController() ?? NEBaseForwardAlertViewController() - forwardAlert.setItems(items) - if let senderName = message.senderName { - forwardAlert.context = senderName - } - weakSelf?.addChild(forwardAlert) - weakSelf?.view.addSubview(forwardAlert.view) - - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: DispatchWorkItem(block: { - UIApplication.shared.keyWindow?.endEditing(true) - })) + let type = isMultiForward ? chatLocalizable("select_multi") : + (weakSelf?.isMutilSelect == true ? chatLocalizable("select_per_item") : chatLocalizable("operation_forward")) + weakSelf?.addForwardAlertController(items: items, type: type) { comment in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } - forwardAlert.sureBlock = { - print("sure click ") - weakSelf?.viewmodel.forwardUserMessage(message, users) + weakSelf?.viewmodel.forwardUserMessage(users, isMultiForward, depth, comment) { error in + if let err = error as? NSError { + if err.code != 0 { + weakSelf?.showErrorToast(err) + } + } else { + sureBlock?() + } + } } } } + var param = [String: Any]() param["nav"] = weakSelf?.navigationController as Any param["limit"] = 6 - if let session = weakSelf?.viewmodel.session, session.sessionType == .P2P { - var filters = Set() - filters.insert(session.sessionId) - param["filters"] = filters - } Router.shared.use(ContactUserSelectRouter, parameters: param, closure: nil) } - func forwardMessageToTeam(message: NIMMessage) { + func forwardMessageToTeam(isMultiForward: Bool = false, + depth: Int = 0, + _ sureBlock: (() -> Void)? = nil) { weak var weakSelf = self Router.shared.register(ContactTeamDataRouter) { param in if let team = param["team"] as? NIMTeam { @@ -1815,19 +2046,24 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel item.name = team.getShowName() item.uid = team.teamId - let forwardAlert = weakSelf!.getForwardAlertController() - forwardAlert.setItems([item]) - if let senderName = message.senderName { - forwardAlert.context = senderName - } - forwardAlert.sureBlock = { - weakSelf?.viewmodel.forwardTeamMessage(message, team) + let type = isMultiForward ? chatLocalizable("select_multi") : + (weakSelf?.isMutilSelect == true ? chatLocalizable("select_per_item") : chatLocalizable("operation_forward")) + weakSelf?.addForwardAlertController(items: [item], type: type) { comment in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + + weakSelf?.viewmodel.forwardTeamMessage(team, isMultiForward, depth, comment) { error in + if let err = error as? NSError { + if err.code != 0 { + weakSelf?.showErrorToast(err) + } + } else { + sureBlock?() + } + } } - weakSelf?.addChild(forwardAlert) - weakSelf?.view.addSubview(forwardAlert.view) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: DispatchWorkItem(block: { - UIApplication.shared.keyWindow?.endEditing(true) - })) } } @@ -1841,25 +2077,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func forwardMessage() { if let message = viewmodel.operationModel?.message { - if IMKitClient.instance.getConfigCenter().teamEnable { - weak var weakSelf = self - let userAction = UIAlertAction(title: chatLocalizable("contact_user"), - style: .default) { action in - weakSelf?.forwardMessageToUser(message: message) - } - - let teamAction = UIAlertAction(title: chatLocalizable("team"), style: .default) { action in - weakSelf?.forwardMessageToTeam(message: message) - } - - let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), - style: .cancel) { action in - } - - showActionSheet([teamAction, userAction, cancelAction]) - } else { - forwardMessageToUser(message: message) - } + viewmodel.selectedMessages = [message] + didClickSingleForwardButton() } } @@ -1877,7 +2096,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel desc: #function + "CALLBACK pinMessage " + (error?.localizedDescription ?? "no error") ) if let err = error as? NSError { - if err.code == 408 { + if err.code == noNetworkCode { self?.view.makeToast(commonLocalizable("network_error"), position: .center) } else { self?.view.makeToast(error?.localizedDescription, position: .center) @@ -1906,7 +2125,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel if let err = error as? NSError { if err.code == 404 { return - } else if err.code == 408 { + } else if err.code == noNetworkCode { self?.view.makeToast(commonLocalizable("network_error"), position: .center) } else { self?.view.makeToast(error?.localizedDescription, position: .center) @@ -1921,6 +2140,38 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } + open func selectMessage() { + isMutilSelect = true + if let model = viewmodel.operationModel, let msg = model.message { + model.isSelected = true + viewmodel.selectedMessages = [msg] + } + + navigationView.setMoreButtonTitle(chatLocalizable("cancel")) + navigationView.moreButton.setTitleColor(.ne_darkText, for: .normal) + navigationView.addMoreButtonTarget(target: self, selector: #selector(cancelMutilSelect)) + setInputView(edit: false) + tableView.reloadData() + } + + func cancelMutilSelect() { + isMutilSelect = false + viewmodel.selectedMessages.removeAll() + viewmodel.messages.forEach { model in + model.isSelected = false + } + setMoreButton() + setInputView(edit: true) + tableView.reloadData() + } + + // edit: 是否显示输入框 + func setInputView(edit: Bool) { + bottomViewTopAnchor?.constant = edit ? -normalInputHeight : -100 + chatInputView.isHidden = !edit + mutilSelectBottomView.isHidden = edit + } + // MARK: UITableViewDataSource, UITableViewDelegate open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -1931,16 +2182,34 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard indexPath.row < viewmodel.messages.count else { return NEBaseChatMessageCell() } let model = viewmodel.messages[indexPath.row] var reuseId = "" if model.replyedModel?.isReplay == true, model.isRevoked == false { - reuseId = "\(MessageType.reply.rawValue)" + if model.replyedModel?.message?.serverID == nil || + model.replyedModel?.message?.serverID.isEmpty == true { + if let message = model.message { + model.replyedModel = viewmodel.getReplyMessageWithoutThread(message: message) + } + } + + if let attch = NECustomAttachment.attachmentOfCustomMessage(message: model.message), + attch.customType == customRichTextType { + reuseId = "\(MessageType.richText.rawValue)" + } else { + reuseId = "\(MessageType.reply.rawValue)" + } } else { let key = "\(model.type.rawValue)" - if model.type == .custom, let object = model.message?.messageObject as? NIMCustomObject, let custom = object.attachment as? NECustomAttachmentProtocol { - if cellRegisterDic["\(custom.customType)"] != nil { - reuseId = "\(custom.customType)" + if model.type == .custom, + let attch = NECustomAttachment.attachmentOfCustomMessage(message: model.message) { + if attch.customType == customMultiForwardType { + reuseId = "\(MessageType.multiForward.rawValue)" + } else if attch.customType == customRichTextType { + reuseId = "\(MessageType.richText.rawValue)" + } else if NEChatUIKitClient.instance.getRegisterCustomCell()["\(attch.customType)"] != nil { + reuseId = "\(attch.customType)" } else { reuseId = "\(NEBaseChatMessageCell.self)" } @@ -1956,7 +2225,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel let cell = tableView.dequeueReusableCell(withIdentifier: reuseId, for: indexPath) if let c = cell as? NEBaseChatMessageTipCell { if let m = model as? MessageTipsModel { - m.resetNotiText() + m.setText() c.setModel(m) } return c @@ -1965,16 +2234,17 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel if let m = model as? MessageContentModel { // 更新好友昵称、头像 if let from = model.message?.from, - let user = viewmodel.newUserInfoDic[from] { + let user = ChatUserCache.getUserInfo(from) { if let uid = user.userId, viewmodel.session.sessionType == .team || viewmodel.session.sessionType == .superTeam { - m.fullName = viewmodel.getShowName(userId: uid, teamId: viewmodel.session.sessionId) - m.shortName = viewmodel.getShortName(name: user.showName(false) ?? "", length: 2) + m.fullName = ChatUserCache.getShowName(userId: uid, teamId: viewmodel.session.sessionId) + m.shortName = ChatUserCache.getShortName(name: user.showName(false) ?? "", length: 2) } m.avatar = user.userInfo?.avatarUrl } - c.setModel(m) + c.setModel(m, m.message?.isOutgoingMsg ?? false) + c.setSelect(m, isMutilSelect) } if let audioCell = cell as? ChatAudioCellProtocol, @@ -1988,7 +2258,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return c } else if let c = cell as? NEChatBaseCell, let m = model as? MessageContentModel { - c.setModel(m) + c.setModel(m, m.message?.isOutgoingMsg ?? false) return cell } else { return NEBaseChatMessageCell() @@ -1996,6 +2266,23 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard indexPath.row < viewmodel.messages.count else { return } + if isMutilSelect { + if indexPath.row < viewmodel.messages.count { + let model = viewmodel.messages[indexPath.row] + if !model.isRevoked, + let cell = tableView.cellForRow(at: indexPath) as? NEBaseChatMessageCell { + model.isSelected = !model.isSelected + cell.seletedBtn.isSelected = model.isSelected + viewmodel.selectedMessages.removeAll(where: { $0.messageId == model.message?.messageId }) + if model.isSelected, let msg = model.message { + viewmodel.selectedMessages.append(msg) + } + } + } + return + } + operationView?.removeFromSuperview() if chatInputView.textView.isFirstResponder { chatInputView.textView.resignFirstResponder() @@ -2006,13 +2293,13 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let m = viewmodel.messages[indexPath.row] - if m.type == .custom { - if let object = m.message?.messageObject as? NIMCustomObject, let custom = object.attachment as? NECustomAttachmentProtocol { - return custom.cellHeight - } + guard indexPath.row < viewmodel.messages.count else { return 0 } + let model = viewmodel.messages[indexPath.row] + if let m = model as? MessageTipsModel { + m.commonInit() } - return m.cellHeight() + + return model.cellHeight() + chat_content_margin } open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { @@ -2025,7 +2312,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // MARK: UIScrollViewDelegate - public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { operationView?.removeFromSuperview() } @@ -2066,12 +2353,42 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // print(error) // } - func getTextViewController(text: String) -> TextViewController { - let textViewController = TextViewController(content: text) + func getTextViewController(title: String?, body: String?) -> TextViewController { + let textViewController = TextViewController(title: title, body: body) textViewController.view.backgroundColor = .white return textViewController } + // MARK: OVERRIDE + + open func getMenuView() -> NEBaseChatInputView { + NEBaseChatInputView() + } + + open func setMutilSelectBottomView() { + mutilSelectBottomView.backgroundColor = .ne_backgroundColor + } + + open func getMultiForwardViewController(_ messageAttachmentUrl: String?, + _ messageAttachmentFilePath: String, + _ messageAttachmentMD5: String?) -> MultiForwardViewController { + MultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) + } + + open func expandMoreAction() { + chatInputView.chatAddMoreView.configData(data: NEChatUIKitClient.instance.getMoreActionData(sessionType: viewmodel.session.sessionType)) + } + + open func showTextViewController(_ model: MessageContentModel?) { + let title = NECustomAttachment.titleOfRichText(message: model?.message) + let body = NECustomAttachment.bodyOfRichText(message: model?.message) ?? model?.message?.text + let textView = getTextViewController(title: title, body: body) + textView.modalPresentationStyle = .fullScreen + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in + self?.navigationController?.present(textView, animated: false) + })) + } + open func didTapMessage(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ replyIndex: Int? = nil) { if model?.type == .audio { startPlay(cell: cell as? ChatAudioCellProtocol, model: model as? MessageAudioModel) @@ -2088,14 +2405,13 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } if imageUrl.count > 0 { let showController = PhotoBrowserController( - urls: viewmodel.getUrls(), + urls: ChatMessageHelper.getUrls(messages: viewmodel.messages), url: imageUrl ) showController.modalPresentationStyle = .overFullScreen present(showController, animated: false, completion: nil) } } - } else if model?.type == .video, let object = model?.message?.messageObject as? NIMVideoObject { stopPlay() @@ -2118,7 +2434,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } videoModel.state = .Downalod if let videoCell = cell as? NEBaseChatMessageCell { - videoCell.setModel(videoModel) + videoCell.setModel(videoModel, videoModel.message?.isOutgoingMsg ?? false) } viewmodel.downLoad(urlString, path) { progress in @@ -2130,20 +2446,11 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } videoModel.cell?.uploadProgress(byRight: videoModel.message?.isOutgoingMsg ?? true, progress) } _: { error in - if let err = error as NSError? { - weakSelf?.showToast(err.localizedDescription) - } + weakSelf?.showErrorToast(error) } } } else if replyIndex != nil, model?.type == .text || model?.type == .reply { - print("message did tap: text") - if let text = model?.message?.text { - let textView = getTextViewController(text: text) - textView.modalPresentationStyle = .fullScreen - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in - self?.navigationController?.present(textView, animated: false) - })) - } + showTextViewController(model) } else if model?.type == .location { if let locationModel = model as? MessageLocationModel, let lat = locationModel.lat, let lng = locationModel.lng { let mapDetail = NEDetailMapController(type: .detail) @@ -2165,7 +2472,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } fileModel.state = .Downalod if let fileCell = cell as? NEBaseChatMessageCell { - fileCell.setModel(fileModel) + fileCell.setModel(fileModel, fileModel.message?.isOutgoingMsg ?? false) } viewmodel.downLoad(urlString, path) { [weak self] progress in @@ -2181,9 +2488,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel fileModel.cell?.uploadProgress(byRight: fileModel.message?.isOutgoingMsg ?? true, newProgress) } _: { [weak self] error in - if let err = error as NSError? { - self?.showToast(err.localizedDescription) - } + self?.showErrorToast(error) } } } else { @@ -2209,19 +2514,204 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel param["type"] = NSNumber(integerLiteral: 2) as AnyObject } Router.shared.use(CallViewRouter, parameters: param) + } else if model?.type == .custom, let attach = NECustomAttachment.attachmentOfCustomMessage(message: model?.message) { + if attach.customType == customRichTextType { + if replyIndex != nil { + showTextViewController(model) + } + } else if attach.customType == customMultiForwardType, + let data = NECustomAttachment.dataOfCustomMessage(message: model?.message) { + let url = data["url"] as? String + let md5 = data["md5"] as? String + guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } + let fileName = multiForwardFileName + (model?.message?.messageId ?? "") + let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(fileName)").relativePath + let multiForwardVC = getMultiForwardViewController(url, filePath, md5) + navigationController?.pushViewController(multiForwardVC, animated: true) + } } else { - print(#function + "message did tap, type:\(model?.type.rawValue)") + print(#function + "message did tap, type:\(String(describing: model?.type.rawValue))") } } +} - // MARK: OVERRIDE +// MARK: NEMutilSelectBottomViewDelegate + +extension ChatViewController: NEMutilSelectBottomViewDelegate { + /// 移除不可转发的消息 + /// - Parameters cancelSelect: 是否取消勾选 + func filterSelectedMessage(invalidMessages: [NIMMessage]) { + // 取消勾选 + for msg in viewmodel.selectedMessages { + if invalidMessages.contains(msg) { + for (row, model) in viewmodel.messages.enumerated() { + if msg.messageId == model.message?.messageId { + let indexPath = IndexPath(row: row, section: 0) + let model = viewmodel.messages[indexPath.row] + if !model.isRevoked, + let cell = tableView.cellForRow(at: indexPath) as? NEBaseChatMessageCell { + model.isSelected = !model.isSelected + cell.seletedBtn.isSelected = model.isSelected + } + } + } + } + } - open func getMenuView() -> NEBaseChatInputView { - NEBaseChatInputView() + // 无论UI上是否取消勾选,都需要移除该消息 + viewmodel.selectedMessages.removeAll(where: { invalidMessages.contains($0) }) } - open func expandMoreAction() { - chatInputView.chatAddMoreView.configData(data: NEChatUIKitClient.instance.getMoreActionData(sessionType: viewmodel.session.sessionType)) + // 合并转发 + open func didClickMultiForwardButton() { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + if viewmodel.selectedMessages.count > customMultiForwardLimitCount { + showToast(String(format: chatLocalizable("multiForward_forward_limit"), customMultiForwardLimitCount)) + return + } + + var depth = 0 + var invalidMessages = [NIMMessage]() + for msg in viewmodel.selectedMessages { + if msg.deliveryState == .failed || msg.isBlackListed { + invalidMessages.append(msg) + continue + } + + // 解析消息中的depth + if let data = NECustomAttachment.dataOfCustomMessage(message: msg) { + if let dep = data["depth"] as? Int { + if dep >= customMultiForwardMaxDepth { + invalidMessages.append(msg) + } else if dep >= depth { + depth = dep + } + } + } + } + + depth += 1 + + // 存在不可转发的消息:提示+取消勾选 + if invalidMessages.count > 0 { + showAlert(title: chatLocalizable("exception_description"), + message: chatLocalizable("exist_invalid")) { [self] in + filterSelectedMessage(invalidMessages: invalidMessages) + if !viewmodel.selectedMessages.isEmpty { + multiForwardForward(depth) + } + } + } else { + if !viewmodel.selectedMessages.isEmpty { + multiForwardForward(depth) + } + } + } + + open func multiForwardForward(_ depth: Int) { + weak var weakSelf = self + if IMKitClient.instance.getConfigCenter().teamEnable { + let userAction = UIAlertAction(title: chatLocalizable("contact_user"), style: .default) { action in + weakSelf?.forwardMessageToUser(isMultiForward: true, depth: depth) { + weakSelf?.cancelMutilSelect() + } + } + + let teamAction = UIAlertAction(title: chatLocalizable("team"), style: .default) { action in + weakSelf?.forwardMessageToTeam(isMultiForward: true, depth: depth) { + weakSelf?.cancelMutilSelect() + } + } + + let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), style: .cancel) + showActionSheet([teamAction, userAction, cancelAction]) + } else { + forwardMessageToUser(isMultiForward: true, depth: depth) { + weakSelf?.cancelMutilSelect() + } + } + } + + // 逐条转发 + open func didClickSingleForwardButton() { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + if viewmodel.selectedMessages.count > customSingleForwardLimitCount { + showToast(String(format: chatLocalizable("per_item_forward_limit"), customSingleForwardLimitCount)) + return + } + + var invalidMessages = [NIMMessage]() + for msg in viewmodel.selectedMessages { + if msg.messageType == .audio || + msg.messageType == .rtcCallRecord || + msg.deliveryState == .failed + || msg.isBlackListed { + invalidMessages.append(msg) + } + } + + if invalidMessages.count > 0 { + showAlert(title: chatLocalizable("exception_description"), + message: chatLocalizable("exist_invalid")) { [self] in + filterSelectedMessage(invalidMessages: invalidMessages) + if !viewmodel.selectedMessages.isEmpty { + singleForward() + } + } + } else { + if !viewmodel.selectedMessages.isEmpty { + singleForward() + } + } + } + + open func singleForward() { + weak var weakSelf = self + if IMKitClient.instance.getConfigCenter().teamEnable { + let userAction = UIAlertAction(title: chatLocalizable("contact_user"), style: .default) { action in + weakSelf?.forwardMessageToUser { + weakSelf?.cancelMutilSelect() + } + } + + let teamAction = UIAlertAction(title: chatLocalizable("team"), style: .default) { action in + weakSelf?.forwardMessageToTeam { + weakSelf?.cancelMutilSelect() + } + } + + let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), style: .cancel) + showActionSheet([teamAction, userAction, cancelAction]) + } else { + forwardMessageToUser { + weakSelf?.cancelMutilSelect() + } + } + } + + // 多选删除 + open func didClickDeleteButton() { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + showAlert(message: chatLocalizable("message_delete_confirm")) { [weak self] in + if let messages = self?.viewmodel.selectedMessages { + self?.viewmodel.deleteMessages(messages: messages) { error in + self?.showErrorToast(error) + } + } + self?.cancelMutilSelect() + } } } @@ -2229,16 +2719,21 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel extension ChatViewController: ChatBaseCellDelegate { open func didLongPressAvatar(_ cell: UITableViewCell, _ model: MessageContentModel?) { - print("didLongPressAvatar") - if viewmodel.session.sessionType == .P2P { + // 非群聊 + // 禁言 + // 多选 + guard viewmodel.session.sessionType == .team, + !isMute, + !isMutilSelect else { return } + var addText = "" var accid = "" if let m = model, let from = m.message?.from { accid = from - addText += viewmodel.getShowName(userId: from, teamId: viewmodel.session.sessionId, false) + addText += ChatUserCache.getShowName(userId: from, teamId: viewmodel.session.sessionId, false) } addText = "@" + addText + "" @@ -2247,6 +2742,10 @@ extension ChatViewController: ChatBaseCellDelegate { } open func didTapAvatarView(_ cell: UITableViewCell, _ model: MessageContentModel?) { + // 多选模式下屏蔽头像点击事件 + if isMutilSelect { + return + } didTapHeadPortrait(model: model) } @@ -2256,7 +2755,10 @@ extension ChatViewController: ChatBaseCellDelegate { return } - if model?.isRevoked == true { + // 已撤回消息不可点击 + // 多选模式下屏蔽消息点击事件 + guard model?.isRevoked == false, + !isMutilSelect else { return } @@ -2290,6 +2792,10 @@ extension ChatViewController: ChatBaseCellDelegate { } open func didTapResendView(_ cell: UITableViewCell, _ model: MessageContentModel?) { + if isMutilSelect { + return + } + if playingCell?.messageId == model?.message?.messageId { if playingCell?.isPlaying == true { stopPlay() @@ -2315,7 +2821,23 @@ extension ChatViewController: ChatBaseCellDelegate { } open func didTapReeditButton(_ cell: UITableViewCell, _ model: MessageContentModel?) { - if model?.type == .revoke, model?.message?.messageType == .text, let message = model?.message { + // 禁言下不可重新编辑 + // 多选下不可重新编辑 + guard !isMute, + !isMutilSelect else { + return + } + + if model?.type == .revoke, let message = model?.message, + message.messageType == .text || message.messageType == .custom { + if message.messageType == .custom { + guard let attach = NECustomAttachment.attachmentOfCustomMessage(message: message), + attach.customType == customRichTextType else { + return + } + } + let data = NECustomAttachment.dataOfCustomMessage(message: model?.message) + let time = message.timestamp let date = Date() let currentTime = date.timeIntervalSince1970 @@ -2324,17 +2846,24 @@ extension ChatViewController: ChatBaseCellDelegate { didRefreshTable() return } - if model?.message?.remoteExt?[keyReplyMsgKey] != nil { + if message.remoteExt?[keyReplyMsgKey] != nil { viewmodel.operationModel = model showReplyMessageView(isReEdit: true) } else { closeReply(button: nil) } - guard let text = message.text else { - return + + var attributeStr: NSMutableAttributedString? + var text = "" + if message.messageType == .text, let txt = message.text { + text = txt + } else if message.messageType == .custom, let body = data?["body"] as? String { + text = body } - let attributeStr = NSMutableAttributedString(string: text) - attributeStr.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)], range: NSMakeRange(0, text.utf16.count)) + + attributeStr = NSMutableAttributedString(string: text) + attributeStr?.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)], range: NSMakeRange(0, text.utf16.count)) + if let remoteExt = message.remoteExt, let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { dic.forEach { (key: String, value: AnyObject) in if let contentDic = value as? [String: AnyObject] { @@ -2348,8 +2877,8 @@ extension ChatViewController: ChatBaseCellDelegate { chatInputView.nickAccidDic[text] = key } - if attributeStr.length > model.end { - attributeStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_blueText, range: NSMakeRange(model.start, model.end - model.start)) + if (attributeStr?.length ?? 0) > model.end { + attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_blueText, range: NSMakeRange(model.start, model.end - model.start)) } } } @@ -2357,12 +2886,40 @@ extension ChatViewController: ChatBaseCellDelegate { } } } + + if let title = data?["title"] as? String { + // 切换换行输入框 + // 标题填入 + chatInputView.titleField.text = title + expandButtonDidClick() + } + chatInputView.textView.attributedText = attributeStr chatInputView.textView.becomeFirstResponder() } } - open func didTapReadView(_ cell: UITableViewCell, _ model: MessageContentModel?) {} + open func didTapReadView(_ cell: UITableViewCell, _ model: MessageContentModel?) { + if isMutilSelect { + return + } + + if let msg = model?.message, msg.session?.sessionType == .team { + let readVC = getReadView(msg) + navigationController?.pushViewController(readVC, animated: true) + } + } + + open func didTapSelectButton(_ cell: UITableViewCell, _ model: MessageContentModel?) { + viewmodel.selectedMessages.removeAll(where: { $0.messageId == model?.message?.messageId }) + if model?.isSelected == true, let msg = model?.message { + viewmodel.selectedMessages.append(msg) + } + } + + open func getReadView(_ message: NIMMessage) -> NEBaseReadViewController { + ReadViewController(message: message) + } open func loadDataFinish() {} @@ -2373,4 +2930,33 @@ extension ChatViewController: ChatBaseCellDelegate { } open func didDismissCallView() {} + + // MARK: mutile line delegate + + open func expandButtonDidClick() { + chatInputView.textView.resignFirstResponder() + scrollTableViewToBottom() + operationView?.removeFromSuperview() + } + + open func didHideMultipleButtonClick() { + chatInputView.restoreNormalInputStyle() + chatInputView.textView.resignFirstResponder() + chatInputView.titleField.resignFirstResponder() + } + + open func titleTextDidClearEmpty() {} + + open func didSendFinishAndCheckoutInput() { + if chatInputView.chatInpuMode != .normal { + chatInputView.chatInpuMode = .normal + chatInputView.restoreNormalInputStyle() + if chatInputView.textView.isFirstResponder || chatInputView.titleField.isFirstResponder { + chatInputView.textView.becomeFirstResponder() + } + didHideMultiple() + } + } + + open func didHideMultiple() {} } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift new file mode 100644 index 00000000..bf5be3c6 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift @@ -0,0 +1,387 @@ + +// 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 MultiForwardViewController: ChatBaseViewController, UINavigationControllerDelegate, UITableViewDataSource, UITableViewDelegate, ChatBaseCellDelegate, UIDocumentInteractionControllerDelegate, MultiForwardViewModelDelegate { + public var viewmodel = MultiForwardViewModel() + private var messageAttachmentUrl: String? + private var messageAttachmentFilePath: String = "" + private var messageAttachmentMD5: String? + public var cellRegisterDic = [String: UITableViewCell.Type]() + public var brokenNetworkViewHeight: CGFloat = 36 + let interactionController = UIDocumentInteractionController() + + public init(_ attachmentUrl: String?, + _ attachmentFilePath: String, + _ attachmentMD5: String?) { + super.init(nibName: nil, bundle: nil) + messageAttachmentFilePath = attachmentFilePath + messageAttachmentUrl = attachmentUrl + messageAttachmentMD5 = attachmentMD5 + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + NEChatDetectNetworkTool.shareInstance.netWorkReachability { [weak self] status in + if status == .notReachable { + self?.brokenNetworkView.isHidden = false + } else { + self?.brokenNetworkView.isHidden = true + } + } + } + + override func backEvent() { + super.backEvent() + } + + override open func viewDidLoad() { + super.viewDidLoad() + viewmodel.delegate = self + commonUI() + loadData() + } + + open func commonUI() { + title = chatLocalizable("chat_history") + navigationView.backgroundColor = .white + navigationView.titleBarBottomLine.isHidden = false + navigationView.moreButton.isHidden = true + navigationView.addBackButtonTarget(target: self, selector: #selector(backEvent)) + + tableView.register( + NEBaseChatMessageCell.self, + forCellReuseIdentifier: "\(NEBaseChatMessageCell.self)" + ) + + NEChatUIKitClient.instance.getRegisterCustomCell().forEach { (key: String, value: UITableViewCell.Type) in + cellRegisterDic[key] = value + } + + cellRegisterDic.forEach { (key: String, value: UITableViewCell.Type) in + tableView.register(value, forCellReuseIdentifier: key) + } + + view.addSubview(tableView) + if #available(iOS 11.0, *) { + 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.safeAreaLayoutGuide.bottomAnchor), + ]) + } else { + 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, constant: -34), + ]) + } + + view.addSubview(brokenNetworkView) + NSLayoutConstraint.activate([ + brokenNetworkView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + brokenNetworkView.leftAnchor.constraint(equalTo: view.leftAnchor), + brokenNetworkView.rightAnchor.constraint(equalTo: view.rightAnchor), + brokenNetworkView.heightAnchor.constraint(equalToConstant: brokenNetworkViewHeight), + ]) + } + + func loadData() { + view.makeToastActivity(.center) + viewmodel.loadData(messageAttachmentUrl, + messageAttachmentFilePath, + messageAttachmentMD5) { [weak self] error in + self?.view.hideToastActivity() + if let err = error as? NSError { + if err.code == 0 { + self?.showToast(err.domain) + } else { + self?.showToast(chatLocalizable("multiForward_open_failed")) + } + self?.navigationController?.popViewController(animated: true) + } else { + self?.tableView.reloadData() + } + } + } + + private func showErrorToast(_ error: Error?) { + if let err = error as? NSError { + switch err.code { + case noNetworkCode, -1009: + showToast(commonLocalizable("network_error")) + default: + showToast(err.localizedDescription) + } + } + } + + // MARK: lazy Method + + public lazy var brokenNetworkView: NEBrokenNetworkView = { + let view = NEBrokenNetworkView() + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + return view + }() + + public lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.showsVerticalScrollIndicator = false + tableView.delegate = self + tableView.dataSource = self + tableView.backgroundColor = .clear + tableView.keyboardDismissMode = .onDrag + return tableView + }() + + // MARK: UIDocumentInteractionControllerDelegate + + open func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController { + self + } + + // MARK: UITableViewDataSource, UITableViewDelegate + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + let count = viewmodel.messages.count + print("numberOfRowsInSection count : ", count) + return count + } + + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let model = viewmodel.messages[indexPath.row] + model.inMultiForward = true + model.isPined = false + var reuseId = "" + if model.replyedModel?.isReplay == true, + model.isRevoked == false { + reuseId = "\(MessageType.reply.rawValue)" + } else { + let key = "\(model.type.rawValue)" + if model.type == .custom { + if let attch = NECustomAttachment.attachmentOfCustomMessage(message: model.message) { + if attch.customType == customMultiForwardType { + reuseId = "\(MessageType.multiForward.rawValue)" + } else if attch.customType == customRichTextType { + reuseId = "\(MessageType.richText.rawValue)" + } else if NEChatUIKitClient.instance.getRegisterCustomCell()["\(attch.customType)"] != nil { + reuseId = "\(attch.customType)" + } else { + reuseId = "\(NEBaseChatMessageCell.self)" + } + } else { + reuseId = "\(NEBaseChatMessageCell.self)" + } + } else if model.type == .time || model.type == .notification || model.type == .tip { + reuseId = "\(MessageType.time.rawValue)" + } else if cellRegisterDic[key] != nil { + reuseId = key + } else { + reuseId = "\(NEBaseChatMessageCell.self)" + } + } + + let cell = tableView.dequeueReusableCell(withIdentifier: reuseId, for: indexPath) + if let c = cell as? NEBaseChatMessageTipCell { + if let m = model as? MessageTipsModel { + c.setModel(m) + } + return c + } else if let c = cell as? NEBaseChatMessageCell { + if let m = model as? MessageContentModel { + c.setModel(m, false) + c.setSelect(m, false) + c.delegate = self + } + + return c + } else if let c = cell as? NEChatBaseCell, let m = model as? MessageContentModel { + c.setModel(m, false) + return cell + } else { + return NEBaseChatMessageCell() + } + } + + open func tableView(_ tableView: UITableView, + heightForRowAt indexPath: IndexPath) -> CGFloat { + viewmodel.messages[indexPath.row].cellHeight() + chat_content_margin + } + + open func getMultiForwardViewController(_ messageAttachmentUrl: String?, + _ messageAttachmentFilePath: String, + _ messageAttachmentMD5: String?) -> MultiForwardViewController { + MultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) + } + + // MARK: ChatBaseCellDelegate + + open func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) { + if let tapClick = NEKitChatConfig.shared.ui.messageItemClick { + tapClick(cell, model) + return + } + + // 已撤回消息不可点击 + if model?.isRevoked == true { + return + } + + didTapMessage(cell, model) + } + + open func didTapMessage(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ replyIndex: Int? = nil) { + if model?.type == .image { + if let imageObject = model?.message?.messageObject as? NIMImageObject { + var imageUrl = "" + + if let url = imageObject.url { + imageUrl = url + } else { + if let path = imageObject.path, FileManager.default.fileExists(atPath: path) { + imageUrl = path + } + } + if imageUrl.count > 0 { + let showController = PhotoBrowserController( + urls: ChatMessageHelper.getUrls(messages: viewmodel.messages), + url: imageUrl + ) + showController.modalPresentationStyle = .overFullScreen + present(showController, animated: false, completion: nil) + } + } + + } else if model?.type == .video, + let object = model?.message?.messageObject as? NIMVideoObject { + weak var weakSelf = self + let videoPlayer = VideoPlayerViewController() + videoPlayer.modalPresentationStyle = .overFullScreen + if let path = object.path, FileManager.default.fileExists(atPath: path) == true { + let url = URL(fileURLWithPath: path) + videoPlayer.videoUrl = url + videoPlayer.totalTime = object.duration + print("video url : ", videoPlayer.videoUrl as Any) + present(videoPlayer, animated: true, completion: nil) + } else if let urlString = object.url, let path = object.path, + let videoModel = model as? MessageVideoModel { + print("fetch message attachment") + if let index = replyIndex, index >= 0 { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), + at: .middle, + animated: true) + } + videoModel.state = .Downalod + if let videoCell = cell as? NEBaseChatMessageCell { + videoCell.setModel(videoModel, false) + } + + viewmodel.downLoad(urlString, path) { progress in + NELog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: #function + "CALLBACK downLoad: \(progress)") + + videoModel.progress = progress + if progress >= 1.0 { + videoModel.state = .Success + } + videoModel.cell?.uploadProgress(byRight: false, progress) + } _: { error in + weakSelf?.showErrorToast(error) + } + } + } else if model?.type == .location { + if let locationModel = model as? MessageLocationModel, let lat = locationModel.lat, let lng = locationModel.lng { + let mapDetail = NEDetailMapController(type: .detail) + mapDetail.currentPoint = CGPoint(x: lat, y: lng) + mapDetail.locationTitle = locationModel.title + mapDetail.subTitle = locationModel.subTitle + navigationController?.pushViewController(mapDetail, animated: true) + } + } else if model?.type == .file, + let object = model?.message?.messageObject as? NIMFileObject, + let path = object.path { + if !FileManager.default.fileExists(atPath: path) { + if let urlString = object.url, let path = object.path, + let fileModel = model as? MessageFileModel { + if let index = replyIndex, index >= 0 { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), + at: .middle, + animated: true) + } + fileModel.state = .Downalod + if let fileCell = cell as? NEBaseChatMessageCell { + fileCell.setModel(fileModel, false) + } + + viewmodel.downLoad(urlString, path) { [weak self] progress in + NELog.infoLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + "downLoad file progress: \(progress)") + var newProgress = progress + if newProgress < 0 { + newProgress = abs(progress) / fileModel.size + } + fileModel.progress = newProgress + if newProgress >= 1.0 { + fileModel.state = .Success + } + fileModel.cell?.uploadProgress(byRight: false, newProgress) + + } _: { [weak self] error in + self?.showErrorToast(error) + } + } + } else { + let url = URL(fileURLWithPath: path) + interactionController.url = url + interactionController.delegate = self // UIDocumentInteractionControllerDelegate + if interactionController.presentPreview(animated: true) {} else { + interactionController.presentOptionsMenu(from: view.bounds, in: view, animated: true) + } + } + } else if model?.type == .custom, let attach = NECustomAttachment.attachmentOfCustomMessage(message: model?.message) { + if attach.customType == customMultiForwardType, + let data = NECustomAttachment.dataOfCustomMessage(message: model?.message) { + let url = data["url"] as? String + let md5 = data["md5"] as? String + guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } + let fileName = multiForwardFileName + (model?.message?.messageId ?? "") + let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(fileName)").relativePath + let multiForwardVC = getMultiForwardViewController(url, filePath, md5) + navigationController?.pushViewController(multiForwardVC, animated: true) + } + } else { + print(#function + "message did tap, type:\(String(describing: model?.type.rawValue))") + } + } + + // MARK: ChatBaseCellDelegate ignore protocol + + public func didTapAvatarView(_ cell: UITableViewCell, _ model: MessageContentModel?) {} + + public func didLongPressAvatar(_ cell: UITableViewCell, _ model: MessageContentModel?) {} + + public func didLongPressMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) {} + + public func didTapResendView(_ cell: UITableViewCell, _ model: MessageContentModel?) {} + + public func didTapReeditButton(_ cell: UITableViewCell, _ model: MessageContentModel?) {} + + public func didTapReadView(_ cell: UITableViewCell, _ model: MessageContentModel?) {} + + public func didTapSelectButton(_ cell: UITableViewCell, _ model: MessageContentModel?) {} +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift index 8d5d2f73..f19da8fd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift @@ -7,8 +7,8 @@ import NECommonKit import NECommonUIKit import UIKit -@objc -public class ForwardItem: NSObject { +@objcMembers +open class ForwardItem: NSObject { var name: String? var uid: String? var avatar: String? @@ -16,7 +16,7 @@ public class ForwardItem: NSObject { } @objcMembers -public class NEBaseForwardUserCell: UICollectionViewCell { +open class NEBaseForwardUserCell: UICollectionViewCell { lazy var userHeader: NEUserHeaderView = { let header = NEUserHeaderView(frame: .zero) header.translatesAutoresizingMaskIntoConstraints = false @@ -31,7 +31,7 @@ public class NEBaseForwardUserCell: UICollectionViewCell { setupUI() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { super.init(coder: coder) } @@ -47,17 +47,19 @@ public class NEBaseForwardUserCell: UICollectionViewCell { } @objcMembers -public class NEBaseForwardAlertViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, +open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { var datas = [ForwardItem]() - typealias ForwardCallBack = () -> Void + typealias ForwardCallBack = (String?) -> Void var cancelBlock: ForwardCallBack? var sureBlock: ForwardCallBack? + var type = chatLocalizable("operation_forward") // 合并转发/逐条转发/转发 var context = "" public let sureBtn = UIButton() public let tip = UILabel() + public var contentViewCenterYAnchor: NSLayoutConstraint? lazy var userCollection: UICollectionView = { let flow = UICollectionViewFlowLayout() @@ -103,25 +105,61 @@ public class NEBaseForwardAlertViewController: UIViewController, UICollectionVie label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(14.0) label.textColor = .ne_darkText - label.numberOfLines = 0 + label.numberOfLines = 1 + label.lineBreakMode = .byTruncatingMiddle label.accessibilityIdentifier = "id.forwardContentText" return label }() - override public func viewDidLoad() { - super.viewDidLoad() + // 留言 + lazy var commentTextFeild: UITextField = { + let textFeild = UITextField() + textFeild.translatesAutoresizingMaskIntoConstraints = false + textFeild.placeholder = chatLocalizable("leave_message") + textFeild.layer.cornerRadius = 4 + textFeild.layer.borderWidth = 1 + textFeild.layer.borderColor = forwardLineColor.cgColor + textFeild.leftView = UIView(frame: CGRect(x: 0, y: 0, width: 12, height: 0)) + textFeild.leftViewMode = .always + textFeild.font = .systemFont(ofSize: 14) + return textFeild + }() + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let parent = parent as? ChatViewController { + parent.isCurrentPage = false + } + } - // Do any additional setup after loading the view. + override open func viewDidLoad() { + super.viewDidLoad() setupUI() + NotificationCenter.default.addObserver(self, + selector: #selector(keyBoardWillShow(_:)), + name: UIResponder.keyboardWillShowNotification, + object: nil) + + NotificationCenter.default.addObserver(self, + selector: #selector(keyBoardWillHide(_:)), + name: UIResponder.keyboardWillHideNotification, + object: nil) + } + + override open func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + navigationController?.viewControllers.last?.view.endEditing(false) } - public func setupUI() { + open func setupUI() { view.backgroundColor = NEConstant.hexRGB(0x000000).withAlphaComponent(0.4) view.addSubview(contentView) + contentViewCenterYAnchor = contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor) NSLayoutConstraint.activate([ contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + contentViewCenterYAnchor!, contentView.widthAnchor.constraint(equalToConstant: 276), + contentView.heightAnchor.constraint(equalToConstant: 250), ]) tip.translatesAutoresizingMaskIntoConstraints = false @@ -181,24 +219,32 @@ public class NEBaseForwardAlertViewController: UIViewController, UICollectionVie contentText.topAnchor.constraint(equalTo: textBack.topAnchor, constant: 7), contentText.bottomAnchor.constraint(equalTo: textBack.bottomAnchor, constant: -7), ]) - contentText.text = "[转发]\(context)的会话记录" + contentText.text = "[\(type)]\(context)的会话记录" + + // 留言 + contentView.addSubview(commentTextFeild) + NSLayoutConstraint.activate([ + commentTextFeild.leftAnchor.constraint(equalTo: textBack.leftAnchor), + commentTextFeild.rightAnchor.constraint(equalTo: textBack.rightAnchor), + commentTextFeild.topAnchor.constraint(equalTo: textBack.bottomAnchor, constant: 16), + commentTextFeild.heightAnchor.constraint(equalToConstant: 32), + ]) let verticalLine = UIView() verticalLine.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(verticalLine) - verticalLine.backgroundColor = NEConstant.hexRGB(0xE1E6E8) + verticalLine.backgroundColor = forwardLineColor NSLayoutConstraint.activate([ verticalLine.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - verticalLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), verticalLine.widthAnchor.constraint(equalToConstant: 1.0), verticalLine.heightAnchor.constraint(equalToConstant: 51), - verticalLine.topAnchor.constraint(equalTo: textBack.bottomAnchor, constant: 16.0), + verticalLine.topAnchor.constraint(equalTo: commentTextFeild.bottomAnchor, constant: 24.0), ]) let horizontalLine = UIView() horizontalLine.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(horizontalLine) - horizontalLine.backgroundColor = NEConstant.hexRGB(0xE1E6E8) + horizontalLine.backgroundColor = forwardLineColor NSLayoutConstraint.activate([ horizontalLine.leftAnchor.constraint(equalTo: contentView.leftAnchor), horizontalLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), @@ -236,7 +282,25 @@ public class NEBaseForwardAlertViewController: UIViewController, UICollectionVie ]) } - public func setItems(_ items: [ForwardItem]) { + // MARK: 键盘通知相关操作 + + open func keyBoardWillShow(_ notification: Notification) { + contentViewCenterYAnchor?.constant = -60 + + UIView.animate(withDuration: 0.5) { + self.view.layoutIfNeeded() + } + } + + open func keyBoardWillHide(_ notification: Notification) { + contentViewCenterYAnchor?.constant = 0 + + UIView.animate(withDuration: 0.5) { + self.view.layoutIfNeeded() + } + } + + open func setItems(_ items: [ForwardItem]) { datas.append(contentsOf: items) if datas.count == 1 { let item = datas[0] @@ -247,7 +311,7 @@ public class NEBaseForwardAlertViewController: UIViewController, UICollectionVie oneUserHead.setTitle(uid) oneUserName.text = uid } - if let url = item.avatar { + if let url = item.avatar, !url.isEmpty { oneUserHead.sd_setImage(with: URL(string: url), completed: nil) oneUserHead.titleLabel.text = "" oneUserHead.backgroundColor = .clear @@ -264,35 +328,38 @@ public class NEBaseForwardAlertViewController: UIViewController, UICollectionVie func sureClick() { if let block = sureBlock { - block() + block(commentTextFeild.text) } removeSelf() } func cancelClick() { if let block = cancelBlock { - block() + block(commentTextFeild.text) } removeSelf() } - override public func touchesBegan(_ touches: Set, with event: UIEvent?) { - removeSelf() + override open func touchesBegan(_ touches: Set, with event: UIEvent?) { + view.endEditing(true) } func removeSelf() { + if let parent = parent as? ChatViewController { + parent.isCurrentPage = true + } view.removeFromSuperview() removeFromParent() } - public func collectionView(_ collectionView: UICollectionView, - numberOfItemsInSection section: Int) -> Int { + open func collectionView(_ collectionView: UICollectionView, + numberOfItemsInSection section: Int) -> Int { datas.count } open func setCellModel(cell: NEBaseForwardUserCell, indexPath: IndexPath) -> UICollectionViewCell { let item = datas[indexPath.row] - if let url = item.avatar { + if let url = item.avatar, !url.isEmpty { cell.userHeader.sd_setImage(with: URL(string: url), completed: nil) cell.userHeader.titleLabel.text = "" cell.userHeader.backgroundColor = .clear @@ -308,14 +375,14 @@ public class NEBaseForwardAlertViewController: UIViewController, UICollectionVie return cell } - public func collectionView(_ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + open func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { UICollectionViewCell() } - public func collectionView(_ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeForItemAt indexPath: IndexPath) -> CGSize { + open func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { CGSize(width: 32.0, height: 32) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift index a0e29241..c14e7c20 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift @@ -2,26 +2,40 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit +import NECommonUIKit +import NECoreIMKit import NIMSDK import UIKit let PinMessageDefaultType = 1000 @objcMembers -open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDataSource, UITableViewDelegate, PinMessageViewModelDelegate, PinMessageCellDelegate { +open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDataSource, UITableViewDelegate, PinMessageViewModelDelegate, PinMessageCellDelegate, UIDocumentInteractionControllerDelegate, NIMMediaManagerDelegate { let viewmodel = PinMessageViewModel() var session: NIMSession + var cellClassDic: [String: NEBasePinMessageCell.Type] = [:] + // pin 列表内容最大宽度 + public var pin_content_maxW = (kScreenWidth - 72) + + var playingCell: NEBasePinMessageAudioCell? + var playingModel: MessageAudioModel? public init(session: NIMSession) { self.session = session super.init(nibName: nil, bundle: nil) + NIMSDK.shared().mediaManager.add(self) } public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + NIMSDK.shared().mediaManager.remove(self) + } + private lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) tableView.translatesAutoresizingMaskIntoConstraints = false @@ -45,7 +59,9 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa return view }() - override public func viewDidLoad() { + let interactionController = UIDocumentInteractionController() + + override open func viewDidLoad() { super.viewDidLoad() viewmodel.delegate = self @@ -53,6 +69,11 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa loadData() } + override open func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + stopPlay() + } + func loadData() { weak var weakSelf = self viewmodel.getPinitems(session: session) { error in @@ -63,6 +84,9 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa weakSelf?.showToast(err.localizedDescription) } } else { + weakSelf?.viewmodel.items.forEach { model in + ChatMessageHelper.downloadAudioFile(message: model.message) + } weakSelf?.emptyView.isHidden = (weakSelf?.viewmodel.items.count ?? 0) > 0 weakSelf?.tableView.reloadData() } @@ -88,8 +112,9 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), ]) - let cellClassDic = getRegisterCellDic() - cellClassDic.forEach { (key: Int, value: NEBasePinMessageCell.Type) in + cellClassDic = getRegisterCellDic() + tableView.register(NEBasePinMessageTextCell.self, forCellReuseIdentifier: "\(NEBasePinMessageTextCell.self)") + cellClassDic.forEach { (key: String, value: NEBasePinMessageCell.Type) in tableView.register(value, forCellReuseIdentifier: "\(key)") } } @@ -104,7 +129,7 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa } */ - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let item = viewmodel.items[indexPath.row] if item.message.session?.sessionType == .P2P { let session = item.message.session @@ -125,21 +150,39 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa } } - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let model = viewmodel.items[indexPath.row] - let cell = tableView.dequeueReusableCell(withIdentifier: "\(model.getMessageType())", for: indexPath) as! NEBasePinMessageCell + var reuseId = "\(model.chatmodel.type.rawValue)" + if model.chatmodel.type == .custom, + let attach = NECustomAttachment.attachmentOfCustomMessage(message: model.chatmodel.message) { + if attach.customType == customMultiForwardType { + reuseId = "\(MessageType.multiForward.rawValue)" + } else if attach.customType == customRichTextType { + reuseId = "\(MessageType.richText.rawValue)" + } else { + reuseId = "\(NEBasePinMessageTextCell.self)" + } + } else if cellClassDic[reuseId] == nil { + reuseId = "\(NEBasePinMessageTextCell.self)" + } + let cell = tableView.dequeueReusableCell(withIdentifier: reuseId, for: indexPath) as! NEBasePinMessageCell cell.delegate = self cell.configure(model) return cell } - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { viewmodel.items.count } - public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let model = viewmodel.items[indexPath.row] - return model.cellHeight() + if let attach = NECustomAttachment.attachmentOfCustomMessage(message: model.message) { + if attach.customType == customMultiForwardType { + return model.cellHeight(pinContentMaxW: pin_content_maxW) - 30 + } + } + return model.cellHeight(pinContentMaxW: pin_content_maxW) } func cancelPinActionClicked(item: PinMessageModel) { @@ -183,28 +226,6 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa open func showAction(item: PinMessageModel) { var actions = [UIAlertAction]() weak var weakSelf = self - /* - let jumpAction = UIAlertAction(title: chatLocalizable("pin_jump_detail"), style: .default) { _ in - if item.message.session?.sessionType == .P2P { - let session = item.message.session - Router.shared.use( - PushP2pChatVCRouter, - parameters: ["nav": weakSelf?.navigationController as Any, "session": session as Any, - "anchor": item.message], - closure: nil - ) - } else if item.message.session?.sessionType == .team { - let session = item.message.session - Router.shared.use( - PushTeamChatVCRouter, - parameters: ["nav": weakSelf?.navigationController as Any, "session": session as Any, - "anchor": item.message], - closure: nil - ) - } - } - actions.append(jumpAction) - */ let cancelPinAction = UIAlertAction(title: chatLocalizable("operation_cancel_pin"), style: .default) { _ in weakSelf?.cancelPinActionClicked(item: item) } @@ -245,32 +266,38 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa let item = ForwardItem() item.uid = user.userId item.avatar = user.userInfo?.avatarUrl - item.name = user.userInfo?.nickName + item.name = user.getShowName() items.append(item) } let forwardAlert = weakSelf!.getForwardAlertController() forwardAlert.setItems(items) - if let senderName = message.senderName { - forwardAlert.context = senderName + if let session = self.viewmodel.session { + forwardAlert.context = ChatMessageHelper.getSessionName(session: session) } weakSelf?.addChild(forwardAlert) weakSelf?.view.addSubview(forwardAlert.view) - forwardAlert.sureBlock = { - print("sure click ") - weakSelf?.viewmodel.forwardUserMessage(message, users) + forwardAlert.sureBlock = { comment in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + weakSelf?.viewmodel.forwardUserMessage(message, users, comment) { error in + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else { + weakSelf?.showToast(err.localizedDescription) + } + } + } } } } var param = [String: Any]() param["nav"] = weakSelf?.navigationController as Any param["limit"] = 6 - if let session = weakSelf?.session, session.sessionType == .P2P { - var filters = Set() - filters.insert(session.sessionId) - param["filters"] = filters - } Router.shared.use(ContactUserSelectRouter, parameters: param, closure: nil) } @@ -285,11 +312,23 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa let forwardAlert = weakSelf!.getForwardAlertController() forwardAlert.setItems([item]) - if let senderName = message.senderName { - forwardAlert.context = senderName + if let session = self.viewmodel.session { + forwardAlert.context = ChatMessageHelper.getSessionName(session: session) } - forwardAlert.sureBlock = { - weakSelf?.viewmodel.forwardTeamMessage(message, team) + forwardAlert.sureBlock = { comment in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + weakSelf?.viewmodel.forwardTeamMessage(message, team, comment) { error in + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else { + weakSelf?.showToast(err.localizedDescription) + } + } + } } weakSelf?.addChild(forwardAlert) weakSelf?.view.addSubview(forwardAlert.view) @@ -328,29 +367,258 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa // MARK: PinMessageViewModelDelegate - public func didNeedRefreshUI() { + open func didNeedRefreshUI() { loadData() } // MARK: PinMessageCellDelegate - public func didClickMore(_ model: PinMessageModel?) { - print("did click more") + open func didClickMore(_ model: PinMessageModel?) { if let item = model { showAction(item: item) } } - open func getRegisterCellDic() -> [Int: NEBasePinMessageCell.Type] { - let cellClassDic = [ - NIMMessageType.text.rawValue: NEBasePinMessageTextCell.self, - NIMMessageType.image.rawValue: NEBasePinMessageImageCell.self, - NIMMessageType.audio.rawValue: NEBasePinMessageAudioCell.self, - NIMMessageType.video.rawValue: NEBasePinMessageVideoCell.self, - NIMMessageType.location.rawValue: NEBasePinMessageLocationCell.self, - NIMMessageType.file.rawValue: NEBasePinMessageFileCell.self, - PinMessageDefaultType: NEBasePinMessageDefaultCell.self, - ] - return cellClassDic + open func didClickContent(_ model: PinMessageModel?, _ cell: NEBasePinMessageCell) { + NELog.infoLog(className(), desc: #function + "didClickContent") + + if model?.message.messageType == .audio { + startPlay(cell: cell, model: model) + } else if model?.message.messageType == .image { + if let imageObject = model?.message.messageObject as? NIMImageObject { + var imageUrl = "" + + if let url = imageObject.url { + imageUrl = url + } else { + if let path = imageObject.path, FileManager.default.fileExists(atPath: path) { + imageUrl = path + } + } + if imageUrl.count > 0 { + let showController = PhotoBrowserController( + urls: [imageUrl], + url: imageUrl + ) + showController.modalPresentationStyle = .overFullScreen + present(showController, animated: false, completion: nil) + } + } + } else if model?.message.messageType == .video, + let object = model?.message.messageObject as? NIMVideoObject { + stopPlay() + let videoPlayer = VideoPlayerViewController() + videoPlayer.modalPresentationStyle = .overFullScreen + videoPlayer.totalTime = object.duration + + if let path = object.path, FileManager.default.fileExists(atPath: path) == true { + let url = URL(fileURLWithPath: path) + videoPlayer.videoUrl = url + present(videoPlayer, animated: true, completion: nil) + + } else if let url = object.url, let remoteUrl = URL(string: url) { + videoPlayer.videoUrl = remoteUrl + present(videoPlayer, animated: true, completion: nil) + } + + } else if model?.message.messageType == .text { + showTextViewController(model) + } else if model?.message.messageType == .location, let title = model?.message.text, + let locationObject = model?.message.messageObject as? NIMLocationObject { + let lat = locationObject.latitude + let lng = locationObject.longitude + let subTitle = locationObject.title + + let mapDetail = NEDetailMapController(type: .detail) + mapDetail.currentPoint = CGPoint(x: lat, y: lng) + mapDetail.locationTitle = title + mapDetail.subTitle = subTitle + navigationController?.pushViewController(mapDetail, animated: true) + } else if model?.message.messageType == .file, + let object = model?.message.messageObject as? NIMFileObject, + let path = object.path { + guard let fileModel = model?.pinFileModel as? PinMessageFileModel else { + NELog.infoLog(ModuleName + " " + className(), desc: #function + "PinMessageFileModel not exit") + return + } + + if fileModel.state == .Downalod { + NELog.infoLog(ModuleName + " " + className(), desc: #function + "downLoad state, click ingore") + return + } + if !FileManager.default.fileExists(atPath: path) { + if let urlString = object.url, let path = object.path { + fileModel.state = .Downalod + + viewmodel.downLoad(urlString, path) { [weak self] progress in + NELog.infoLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + "downLoad file progress: \(progress)") + var newProgress = progress + if newProgress < 0 { + newProgress = abs(progress) / fileModel.size + } + fileModel.progress = newProgress + if newProgress >= 1.0 { + fileModel.state = .Success + } + fileModel.cell?.uploadProgress(progress: newProgress) + + } _: { error in + } + } + } else { + let url = URL(fileURLWithPath: path) + interactionController.url = url + interactionController.delegate = self + if interactionController.presentPreview(animated: true) {} + else { + interactionController.presentOptionsMenu(from: view.bounds, in: view, animated: true) + } + } + } else if model?.message.messageType == .custom, let attach = NECustomAttachment.attachmentOfCustomMessage(message: model?.message) { + if attach.customType == customRichTextType { + showTextViewController(model) + } else if attach.customType == customMultiForwardType, + let data = NECustomAttachment.dataOfCustomMessage(message: model?.message) { + let url = data["url"] as? String + let md5 = data["md5"] as? String + guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } + let fileName = multiForwardFileName + (model?.message.messageId ?? "") + let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(fileName)").relativePath + let multiForwardVC = getMultiForwardViewController(url, filePath, md5) + navigationController?.pushViewController(multiForwardVC, animated: true) + } + } + } + + private func startPlay(cell: NEBasePinMessageCell?, model: PinMessageModel?) { + guard let audioModel = model?.chatmodel as? MessageAudioModel else { + return + } + + if playingModel == audioModel { + if NIMSDK.shared().mediaManager.isPlaying() { + stopPlay() + } else { + startPlaying(audioMessage: model?.message) + } + } else { + stopPlay() + if let pCell = cell as? NEBasePinMessageAudioCell { + playingCell = pCell + } + if let audioModel = model?.chatmodel as? MessageAudioModel { + playingModel = audioModel + } + startPlaying(audioMessage: model?.message) + } + } + + func startPlaying(audioMessage: NIMMessage?) { + guard let message = audioMessage, let audio = message.messageObject as? NIMAudioObject else { + return + } + playingCell?.startAnimation() + if let path = audio.path, FileManager.default.fileExists(atPath: path) == true { + NELog.infoLog(className(), desc: #function + " play path : " + path) + if viewmodel.getHandSetEnable() == true { + NIMSDK.shared().mediaManager.switch(.receiver) + } else { + NIMSDK.shared().mediaManager.switch(.speaker) + } + NIMSDK.shared().mediaManager.play(path) + } else { + NELog.infoLog(className(), desc: #function + " audio path is empty, play url : " + (audio.url ?? "")) + ChatMessageHelper.downloadAudioFile(message: message) + playingCell?.stopAnimation() + } + } + + func stopPlay() { + if NIMSDK.shared().mediaManager.isPlaying() { + playingCell?.stopAnimation() + playingModel?.isPlaying = false + NIMSDK.shared().mediaManager.stopPlay() + } + } + + // play + open func playAudio(_ filePath: String, didBeganWithError error: Error?) { + print(#function + "\(error?.localizedDescription ?? "")") + NIMSDK.shared().mediaManager.switch(viewmodel.getHandSetEnable() ? .receiver : .speaker) + if let e = error { + if e.localizedDescription.count > 0 { + view.makeToast(e.localizedDescription) + } + playingCell?.stopAnimation() + playingModel?.isPlaying = false + } + } + + open func playAudio(_ filePath: String, didCompletedWithError error: Error?) { + print(#function + "\(error?.localizedDescription ?? "")") + if error?.localizedDescription.count ?? 0 > 0 { + view.makeToast(error?.localizedDescription ?? "") + } + // stop + playingCell?.stopAnimation() + playingModel?.isPlaying = false + } + + open func stopPlayAudio(_ filePath: String, didCompletedWithError error: Error?) { + print(#function + "\(error?.localizedDescription ?? "")") + + playingCell?.stopAnimation() + playingModel?.isPlaying = false + } + + open func playAudio(_ filePath: String, progress value: Float) {} + + open func playAudioInterruptionEnd() { + print(#function) + playingCell?.stopAnimation() + playingModel?.isPlaying = false + } + + open func playAudioInterruptionBegin() { + print(#function) + // stop play + playingCell?.stopAnimation() + playingModel?.isPlaying = false + } + + open func getRegisterCellDic() -> [String: NEBasePinMessageCell.Type] { + cellClassDic + } + + open func showTextViewController(_ model: PinMessageModel?) { + let title = NECustomAttachment.titleOfRichText(message: model?.message) + let body = NECustomAttachment.bodyOfRichText(message: model?.message) ?? model?.message.text + let textView = getTextViewController(title: title, body: body) + textView.modalPresentationStyle = .fullScreen + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in + self?.navigationController?.present(textView, animated: false) + })) + } + + func getTextViewController(title: String?, body: String?) -> TextViewController { + let textViewController = TextViewController(title: title, body: body) + textViewController.view.backgroundColor = .white + return textViewController + } + + open func getMultiForwardViewController(_ messageAttachmentUrl: String?, + _ messageAttachmentFilePath: String, + _ messageAttachmentMD5: String?) -> MultiForwardViewController { + MultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) + } + + // MARK: UIDocumentInteractionControllerDelegate + + open func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController { + self + } + + open func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + controller.dismiss(animated: true) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift index ad90af47..ff999a40 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift @@ -15,8 +15,8 @@ open class NEBaseReadViewController: ChatBaseViewController, UIScrollViewDelegat public var line: UIView = .init() public var lineLeftCons: NSLayoutConstraint? public var readTableView = UITableView(frame: .zero, style: .plain) - public var readUsers = [User]() - public var unReadUsers = [User]() + public var readUsers = [NEKitUser]() + public var unReadUsers = [NEKitUser]() public let readButton = UIButton(type: .custom) public let unreadButton = UIButton(type: .custom) private var message: NIMMessage @@ -44,22 +44,24 @@ open class NEBaseReadViewController: ChatBaseViewController, UIScrollViewDelegat readButton.setTitleColor(UIColor.ne_darkText, for: .normal) readButton.translatesAutoresizingMaskIntoConstraints = false readButton.addTarget(self, action: #selector(readButtonEvent), for: .touchUpInside) - view.addSubview(readButton) + readButton.accessibilityIdentifier = "id.tabHasRead" unreadButton.titleLabel?.font = UIFont.systemFont(ofSize: 14) unreadButton.setTitle(chatLocalizable("unread"), for: .normal) unreadButton.setTitleColor(UIColor.ne_darkText, for: .normal) unreadButton.translatesAutoresizingMaskIntoConstraints = false unreadButton.addTarget(self, action: #selector(unreadButtonEvent), for: .touchUpInside) + readButton.accessibilityIdentifier = "id.tabUnRead" - view.addSubview(unreadButton) + view.addSubview(readButton) NSLayoutConstraint.activate([ readButton.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), readButton.leadingAnchor.constraint(equalTo: view.leadingAnchor), readButton.heightAnchor.constraint(equalToConstant: 48), - readButton.widthAnchor.constraint(equalTo: unreadButton.widthAnchor), + readButton.widthAnchor.constraint(equalToConstant: kScreenWidth / 2.0), ]) + view.addSubview(unreadButton) NSLayoutConstraint.activate([ unreadButton.topAnchor.constraint(equalTo: readButton.topAnchor), unreadButton.leadingAnchor.constraint(equalTo: readButton.trailingAnchor), @@ -166,7 +168,7 @@ open class NEBaseReadViewController: ChatBaseViewController, UIScrollViewDelegat NIMSDK.shared().chatManager.queryMessageReceiptDetail(message) { anError, receiptInfo in print("anError:\(anError) receiptInfo:\(receiptInfo)") if let error = anError as? NSError { - if error.code == 408 { + if error.code == noNetworkCode { self.showToast(commonLocalizable("network_error")) } else { self.showToast(error.localizedDescription) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift index e382e6f3..afbec002 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift @@ -6,6 +6,7 @@ import NEChatKit import NECoreIMKit import UIKit + public typealias DidSelectedAtRow = (_ index: Int, _ model: ChatTeamMemberInfoModel?) -> Void @objcMembers @@ -18,6 +19,7 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe var teamInfo: ChatTeamInfoModel? private var showSelf = true // 是否展示自己 var className = "SelectUserViewController" + var isShowAtAll = true init(sessionId: String, showSelf: Bool = true) { self.sessionId = sessionId @@ -29,7 +31,7 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe fatalError("init(coder:) has not been implemented") } - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() navigationController?.isNavigationBarHidden = true navigationView.isHidden = true @@ -40,6 +42,7 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe func commonUI() { let btn = UIButton(type: .custom) btn.translatesAutoresizingMaskIntoConstraints = false + btn.accessibilityIdentifier = "id.arrowDown" btn.setImage(UIImage.ne_imageNamed(name: "arrowDown"), for: .normal) btn.addTarget(self, action: #selector(btnEvent), for: .touchUpInside) view.addSubview(btn) @@ -115,33 +118,80 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe } // 人员选择页面移除自己 + var selfIndex = -1 if !(self?.showSelf ?? true), let users = team?.users { for (index, user) in users.enumerated() { + if let u = team?.users[index].nimUser { + ChatUserCache.updateUserInfo(u) + } if user.nimUser?.userId == IMKitLoginManager.instance.currentAccount() { - team?.users.remove(at: index) + if user.teamMember?.type != .manager, let custom = team?.team?.clientCustomInfo, custom.count > 0, let json = getDictionaryFromJSONString(custom), let atValue = json[keyAllowAtAll] as? String, atValue == allowAtManagerValue { + self?.isShowAtAll = false + } + selfIndex = index } } + team?.users.remove(at: selfIndex) + } + + // 根据身份+进群时间正序排序 + if let users = team?.users { + var owner: ChatTeamMemberInfoModel? // 群主 + var managers = [ChatTeamMemberInfoModel]() // 管理员 + var normals = [ChatTeamMemberInfoModel]() // 普通成员 + + for user in users { + if user.teamMember?.type == .owner { + owner = user + } else if user.teamMember?.type == .manager { + managers.append(user) + } else { + normals.append(user) + } + } + + managers.sort(by: { m1, m2 in + (m1.teamMember?.createTime ?? 0) < (m2.teamMember?.createTime ?? 0) + }) + + normals.sort(by: { m1, m2 in + (m1.teamMember?.createTime ?? 0) < (m2.teamMember?.createTime ?? 0) + }) + + if let owner = owner { + team?.users = [owner] + managers + normals + } else { + team?.users = managers + normals + } } + self?.teamInfo = team self?.tableView.reloadData() } } - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + open func numberOfSections(in tableView: UITableView) -> Int { + 2 + } + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0 { + return isShowAtAll ? 1 : 0 + } if let count = teamInfo?.users.count { - return count + 1 + return count } return 0 } - public func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { UITableViewCell() } - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if indexPath.row == 0 { + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if indexPath.section == 0 { if let block = selectedBlock { block(indexPath.row, nil) } @@ -149,7 +199,7 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe return } if let block = selectedBlock { - block(indexPath.row, teamInfo?.users[indexPath.row - 1]) + block(indexPath.row, teamInfo?.users[indexPath.row]) } dismiss(animated: true, completion: nil) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift index e702da51..b9fc7f57 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift @@ -70,7 +70,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV fatalError("init(coder:) has not been implemented") } - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() viewmodel.delegate = self if let uid = userId { @@ -124,7 +124,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV tap.numberOfTapsRequired = 1 tap.numberOfTouchesRequired = 1 - if let url = viewmodel.userInfo?.userInfo?.avatarUrl { + if let url = viewmodel.userInfo?.userInfo?.avatarUrl, !url.isEmpty { userHeader.sd_setImage(with: URL(string: url), completed: nil) userHeader.setTitle("") userHeader.backgroundColor = .clear @@ -265,7 +265,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV // MARK: UITableViewDataSource, UITableViewDelegate - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { viewmodel.cellDatas.count } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift index 4e05cb17..2c51c0c9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift @@ -7,7 +7,7 @@ import NECoreKit import UIKit @objcMembers -public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDelegate { +open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDelegate { // 地图展示类型 public var mapType: NEMapType? public var currentPoint = CGPoint(x: 0, y: 0) @@ -20,9 +20,6 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView // 记录键盘弹起状态 private var foldKeyBoard = true -// private let defaultFieldFrame = CGRect(x: 12, y: 12, width: kScreenWidth - 24, height: 32) -// private let focusFieldFrame = CGRect(x: 12, y: 12, width: kScreenWidth - 64, height: 32) - private let defaultTableHeight: CGFloat = 230 var completion: NEPositionSelectCompletion? @@ -40,11 +37,11 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView super.init(nibName: nil, bundle: nil) } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } - override public func viewWillAppear(_ animated: Bool) { + override open func viewWillAppear(_ animated: Bool) { navigationController?.navigationBar.isHidden = true weak var weakSelf = self NEChatDetectNetworkTool.shareInstance.netWorkReachability { status in @@ -58,11 +55,11 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView } } - override public func viewWillDisappear(_ animated: Bool) { + override open func viewWillDisappear(_ animated: Bool) { navigationController?.navigationBar.isHidden = false } - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() setupSubviews() } @@ -257,14 +254,10 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView func keyBoardWillHide(_ notification: Notification) { foldKeyBoard = true -// layoutInputView(offset: 0) } private func layoutInputView(offset: CGFloat) { tableViewBottomConstraint?.constant = defaultTableHeight + offset -// UIView.animate(withDuration: 0.25, animations: { -// self.view.layoutIfNeeded() -// }) } lazy var backBtn: UIButton = { @@ -364,7 +357,6 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView ) tableView.rowHeight = 72 tableView.backgroundColor = .white -// tableView.tableHeaderView = searchBgView tableView.keyboardDismissMode = .onDrag return tableView }() @@ -388,7 +380,6 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView }() lazy var searchBgView: UIView = { - // let bgView = UIView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 60)) let bgView = UIView() bgView.translatesAutoresizingMaskIntoConstraints = false return bgView @@ -520,7 +511,7 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView // MARK: NEMapGuideBottomViewDelegate - public func didClickGuide() { + open func didClickGuide() { showBottomSelectAlert(firstContent: chatLocalizable("gaode_map"), secondContent: chatLocalizable("tencent_map")) { value in if value == 0 { @@ -571,7 +562,7 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView } } - public func loadModels(models: [ChatLocaitonModel]) { + open func loadModels(models: [ChatLocaitonModel]) { currentIndex = 0 locations.removeAll() if let keyword = searchTextField.text, keyword.count > 0 { @@ -596,7 +587,7 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView refreshCurrentCache() } - public func refreshCurrentCache() { + open func refreshCurrentCache() { if locations.count > currentIndex { currentModel = locations[currentIndex] } @@ -604,12 +595,12 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView } extension NEDetailMapController: UITableViewDelegate, UITableViewDataSource { - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { locations.count } - public func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: reuseId, for: indexPath @@ -619,7 +610,7 @@ extension NEDetailMapController: UITableViewDelegate, UITableViewDataSource { return cell } - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if let preiousCell = tableView.cellForRow(at: IndexPath(row: currentIndex, section: 0)) as? NEMapAddressCell { preiousCell.selectImg.isHidden = true } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift index ec9fe42f..6b365f5d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift @@ -7,9 +7,10 @@ import NECommonKit @objcMembers open class TextViewController: ChatBaseViewController { - let leftRightMargin: CGFloat = 24 + let leftRightMargin: CGFloat = 20 var contentMaxWidth: CGFloat = 0 - let textFont = UIFont.systemFont(ofSize: 24, weight: .regular) + let titleFont = UIFont.systemFont(ofSize: 24, weight: .semibold) + let bodyFont = UIFont.systemFont(ofSize: 24) lazy var scrollView: UIScrollView = { let scrollView = UIScrollView() @@ -18,27 +19,66 @@ open class TextViewController: ChatBaseViewController { return scrollView }() + lazy var textView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + + view.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: view.topAnchor), + titleLabel.leftAnchor.constraint(equalTo: view.leftAnchor), + titleLabel.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) + + view.addSubview(bodyLabel) + NSLayoutConstraint.activate([ + bodyLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor), + bodyLabel.leftAnchor.constraint(equalTo: view.leftAnchor), + bodyLabel.rightAnchor.constraint(equalTo: view.rightAnchor), + bodyLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + return view + }() + + lazy var titleLabel: CopyableLabel = { + let label = CopyableLabel() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + label.font = titleFont + label.backgroundColor = .clear + label.textColor = .ne_darkText + return label + }() + + lazy var bodyLabel: CopyableLabel = { + let label = CopyableLabel() + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + label.font = bodyFont + label.backgroundColor = .clear + label.textColor = .ne_darkText + return label + }() + var contentLabelTopAnchor: NSLayoutConstraint? var contentLabelLeftAnchor: NSLayoutConstraint? - lazy var contentLabel: CopyableLabel = { - let contentLabel = CopyableLabel() - contentLabel.numberOfLines = 0 - contentLabel.translatesAutoresizingMaskIntoConstraints = false - contentLabel.font = textFont - contentLabel.backgroundColor = .clear - return contentLabel - }() - init(content: String) { + init(title: String?, body: String?) { super.init(nibName: nil, bundle: nil) contentMaxWidth = kScreenWidth - leftRightMargin * 2 - let att = NEEmotionTool.getAttWithStr(str: content, font: textFont, CGPoint(x: 0, y: -3)) - contentLabel.copyString = att.string - contentLabel.attributedText = att - let line = String.calculateMaxLines(width: kScreenWidth - 2 * leftRightMargin, - string: att.string, - font: textFont) - contentLabel.textAlignment = .justified + if let title = title { + let titleAtt = NEEmotionTool.getAttWithStr(str: title, font: titleFont, CGPoint(x: 0, y: -3)) + titleLabel.copyString = titleAtt.string + titleLabel.attributedText = titleAtt + } + + if let body = body { + let bodyAtt = NEEmotionTool.getAttWithStr(str: body, font: bodyFont, CGPoint(x: 0, y: -3)) + bodyLabel.copyString = bodyAtt.string + bodyLabel.attributedText = bodyAtt + } } public required init?(coder: NSCoder) { @@ -64,35 +104,45 @@ open class TextViewController: ChatBaseViewController { view.addSubview(scrollView) if #available(iOS 11.0, *) { NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), - scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + scrollView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20), + scrollView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -20), scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -0), ]) } else { NSLayoutConstraint.activate([ - scrollView.topAnchor.constraint(equalTo: view.topAnchor), - scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + scrollView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20), + scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -20), scrollView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), scrollView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -0), ]) } - scrollView.addSubview(contentLabel) - contentLabel.preferredMaxLayoutWidth = contentMaxWidth - contentLabelTopAnchor = contentLabel.topAnchor.constraint(equalTo: scrollView.topAnchor) - contentLabelLeftAnchor = contentLabel.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: leftRightMargin) + titleLabel.preferredMaxLayoutWidth = contentMaxWidth + bodyLabel.preferredMaxLayoutWidth = contentMaxWidth + scrollView.addSubview(textView) + contentLabelTopAnchor = textView.topAnchor.constraint(equalTo: scrollView.topAnchor) + contentLabelLeftAnchor = textView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: leftRightMargin) NSLayoutConstraint.activate([ contentLabelTopAnchor!, - contentLabel.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), + textView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), contentLabelLeftAnchor!, - contentLabel.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -leftRightMargin), + textView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -leftRightMargin), ]) } // textView 垂直居中 func contentSizeToFit() { - let textSize = contentLabel.attributedText?.finalSize(textFont, CGSize(width: contentMaxWidth, height: CGFloat.greatestFiniteMagnitude)) ?? .zero + var label = bodyLabel + if let titleLength = titleLabel.attributedText?.length { + if let bodyLength = bodyLabel.attributedText?.length { + label = titleLength > bodyLength ? titleLabel : bodyLabel + } else { + label = titleLabel + } + } + + let textSize = label.attributedText?.finalSize(bodyFont, CGSize(width: contentMaxWidth, height: CGFloat.greatestFiniteMagnitude)) ?? .zero let textViewHeight = kScreenHeight - kNavigationHeight - KStatusBarHeight if textSize.height <= textViewHeight { let offsetY = (textViewHeight - textSize.height) / 2 diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift index e32d44e7..cc6471f8 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift @@ -19,9 +19,9 @@ import UIKit @objc optional func needScrollAnimation() -> Bool } -public class EmojiPageView: UIView { - public weak var dataSource: EmojiPageViewDataSource? - public weak var pageViewDelegate: EmojiPageViewDelegate? +open class EmojiPageView: UIView { + open weak var dataSource: EmojiPageViewDataSource? + open weak var pageViewDelegate: EmojiPageViewDelegate? private var currentPage: NSInteger = 0 private var pages = [AnyObject]() private let className = "EmojiPageView" @@ -31,7 +31,7 @@ public class EmojiPageView: UIView { setupControls() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -52,14 +52,14 @@ public class EmojiPageView: UIView { addSubview(scrollView) } - public func scrollToPage(page: NSInteger) { + open func scrollToPage(page: NSInteger) { if currentPage != page || page == 0 { currentPage = page reloadData() } } - public func reloadData() { + open func reloadData() { calculatePageNumbers() setupInit() // reloadPage() @@ -187,7 +187,7 @@ public class EmojiPageView: UIView { } } - override public func layoutSubviews() { + override open func layoutSubviews() { super.layoutSubviews() let size = bounds.size scrollView.contentSize = CGSize( diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift index 40ab4145..1d6ec145 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift @@ -10,7 +10,7 @@ import UIKit func didPressSend(sender: UIButton) } -public class InputEmoticonContainerView: UIView { +open class InputEmoticonContainerView: UIView { private let classTag = "InputEmoticonContainerView" public weak var delegate: InputEmoticonContainerViewDelegate? @@ -45,7 +45,7 @@ public class InputEmoticonContainerView: UIView { loadEmojiData() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -287,11 +287,11 @@ extension InputEmoticonContainerView { // MARK: ====== EmojiPageViewDelegate,EmojiPageViewDataSource ============== extension InputEmoticonContainerView: EmojiPageViewDelegate, EmojiPageViewDataSource { - public func numberOfPages(pageView: EmojiPageView?) -> NSInteger { + open func numberOfPages(pageView: EmojiPageView?) -> NSInteger { sumPages() } - public func pageView(pageView: EmojiPageView?, index: NSInteger) -> UIView { + open func pageView(pageView: EmojiPageView?, index: NSInteger) -> UIView { var page = 0 var resultEmotion = NIMInputEmoticonCatalog() @@ -311,13 +311,13 @@ extension InputEmoticonContainerView: EmojiPageViewDelegate, EmojiPageViewDataSo return emojiPageView(pageView: targetView, emoticon: resultEmotion, page: index - page) } - public func needScrollAnimation() -> Bool { + open func needScrollAnimation() -> Bool { true } - public func pageViewDidScroll(_ pageView: EmojiPageView?) {} + open func pageViewDidScroll(_ pageView: EmojiPageView?) {} - public func pageViewScrollEnd(_ pageView: EmojiPageView?, currentIndex: Int, totolPages: Int) { + open func pageViewScrollEnd(_ pageView: EmojiPageView?, currentIndex: Int, totolPages: Int) { // let emticon = emoticonWithIndex(index: currentIndex) // 补充pageController逻辑 } @@ -326,13 +326,13 @@ extension InputEmoticonContainerView: EmojiPageViewDelegate, EmojiPageViewDataSo // MARK: =============== InputEmoticonTabViewDelegate =============== extension InputEmoticonContainerView: InputEmoticonTabViewDelegate { - public func tabView(_ tabView: InputEmoticonTabView?, didSelectTabIndex index: Int) {} + open func tabView(_ tabView: InputEmoticonTabView?, didSelectTabIndex index: Int) {} } // MARK: =============== InputEmoticonTabViewDelegate =============== extension InputEmoticonContainerView: NIMInputEmoticonButtonDelegate { - public func selectedEmoticon(emotion: NIMInputEmoticon, catalogID: String) { + open func selectedEmoticon(emotion: NIMInputEmoticon, catalogID: String) { guard let emotionId = emotion.emoticonID else { NELog.errorLog(classTag, desc: "❌emoticonID is nil") return diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift index 042dad9e..acc9fb49 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift @@ -9,8 +9,8 @@ import UIKit @objc optional func tabView(_ tabView: InputEmoticonTabView?, didSelectTabIndex index: Int) } -public class InputEmoticonTabView: UIControl { - public weak var delegate: InputEmoticonTabViewDelegate? +open class InputEmoticonTabView: UIControl { + open weak var delegate: InputEmoticonTabViewDelegate? private var tabs = [UIButton]() private var seps = [UIView]() private var className = "InputEmoticonTabView" @@ -20,7 +20,7 @@ public class InputEmoticonTabView: UIControl { setUpSubViews() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -35,14 +35,14 @@ public class InputEmoticonTabView: UIControl { ]) } - public func selectTabIndex(_ index: Int) { + open func selectTabIndex(_ index: Int) { for i in 0 ..< tabs.count { let btn = tabs[i] btn.isSelected = i == index } } - public func loadCatalogs(_ emoticonCatalogs: [NIMInputEmoticonCatalog]?) { + open func loadCatalogs(_ emoticonCatalogs: [NIMInputEmoticonCatalog]?) { tabs.forEach { btn in btn.removeFromSuperview() } @@ -86,6 +86,7 @@ public class InputEmoticonTabView: UIControl { button.titleLabel?.textColor = .white button.backgroundColor = UIColor.ne_blueText button.titleLabel?.font = DefaultTextFont(14) + button.accessibilityIdentifier = "id.emojiSend" return button }() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionAttachment.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionAttachment.swift index b765ec19..76afc50c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionAttachment.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionAttachment.swift @@ -5,7 +5,7 @@ import UIKit -public class NEEmotionAttachment: NSTextAttachment { +open class NEEmotionAttachment: NSTextAttachment { private var _emotion: NIMInputEmoticon? public var emotion: NIMInputEmoticon? { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift index dfb54c64..f5879a7e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift @@ -5,7 +5,7 @@ import UIKit -public class NEEmotionTool: NSObject { +open class NEEmotionTool: NSObject { class func getAttWithStr(str: String, font: UIFont, _ offset: CGPoint = CGPoint(x: 0, y: -3)) -> NSMutableAttributedString { let regular = "\\[[^\\[|^\\]]+\\]" diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonButton.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonButton.swift index be4553e0..35f25ac0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonButton.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonButton.swift @@ -9,14 +9,14 @@ public protocol NIMInputEmoticonButtonDelegate: NSObjectProtocol { func selectedEmoticon(emotion: NIMInputEmoticon, catalogID: String) } -public class NIMInputEmoticonButton: UIButton { +open class NIMInputEmoticonButton: UIButton { public var emotionData: NIMInputEmoticon? public var catalogID: String? public weak var delegate: NIMInputEmoticonButtonDelegate? private let classsTag = "NIMInputEmoticonButton" - public class func iconButtonWithData(data: NIMInputEmoticon, catalogID: String, - delegate: NIMInputEmoticonButtonDelegate) + open class func iconButtonWithData(data: NIMInputEmoticon, catalogID: String, + delegate: NIMInputEmoticonButtonDelegate) -> NIMInputEmoticonButton { let icon = NIMInputEmoticonButton() icon.addTarget(icon, action: #selector(onIconSelected), for: .touchUpInside) @@ -26,6 +26,8 @@ public class NIMInputEmoticonButton: UIButton { icon.isExclusiveTouch = true icon.contentMode = .scaleToFill icon.delegate = delegate + icon.accessibilityIdentifier = "id.emoji" + icon.accessibilityValue = data.tag switch data.type { case .unicode: icon.setTitle(data.unicode, for: .normal) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift index 1305c12b..60c43eee 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift @@ -10,7 +10,7 @@ public enum NIMEmoticonType: NSInteger { case unicode } -public class NIMInputEmoticon: NSObject { +open class NIMInputEmoticon: NSObject { public var type: NIMEmoticonType { if let ucode = unicode, ucode.count > 0 { return .unicode @@ -25,7 +25,7 @@ public class NIMInputEmoticon: NSObject { public var unicode: String? } -public class NIMInputEmoticonLayout: NSObject { +open class NIMInputEmoticonLayout: NSObject { public var rows: NSInteger = 0 // 行数 public var columes: NSInteger = 0 // 列数 public var itemCountInPage: NSInteger = 0 // 每页显示几项 @@ -50,7 +50,7 @@ public class NIMInputEmoticonLayout: NSObject { } } -public class NIMInputEmoticonCatalog: NSObject { +open class NIMInputEmoticonCatalog: NSObject { public var layout: NIMInputEmoticonLayout? public var catalogID: String? public var title: String? @@ -65,7 +65,7 @@ public class NIMInputEmoticonCatalog: NSObject { public var pagesCount: NSInteger = 0 } -public class NIMInputEmoticonManager: NSObject { +open class NIMInputEmoticonManager: NSObject { public static let shared = NIMInputEmoticonManager() private var catalogs: [NIMInputEmoticonCatalog]? private var classTag = "NIMInputEmoticonManager" @@ -136,7 +136,7 @@ public class NIMInputEmoticonManager: NSObject { func preloadEmoticonResource() {} - public func emoticonCatalog(catalogID: String) -> NIMInputEmoticonCatalog? { + open func emoticonCatalog(catalogID: String) -> NIMInputEmoticonCatalog? { guard let infos = catalogs else { return nil } for catalog in infos { @@ -147,7 +147,7 @@ public class NIMInputEmoticonManager: NSObject { return nil } - public func emoticonByTag(tag: String) -> NIMInputEmoticon? { + open func emoticonByTag(tag: String) -> NIMInputEmoticon? { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { @@ -168,7 +168,7 @@ public class NIMInputEmoticonManager: NSObject { return emotion } - public func emoticonByID(emoticonID: String) -> NIMInputEmoticon? { + open func emoticonByID(emoticonID: String) -> NIMInputEmoticon? { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { NELog.errorLog(classTag, desc: "❌catalogs is nil") @@ -188,7 +188,7 @@ public class NIMInputEmoticonManager: NSObject { return emotion } - public func emoticonByCatalogID(catalogID: String, emoticonID: String) -> NIMInputEmoticon? { + open func emoticonByCatalogID(catalogID: String, emoticonID: String) -> NIMInputEmoticon? { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { NELog.errorLog(classTag, desc: "❌catalogs is nil") diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.swift new file mode 100644 index 00000000..ae841bf6 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.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 Foundation +import NIMSDK +@objcMembers +public class ChatDeduplicationHelper: NSObject, NIMLoginManagerDelegate { + // 单例变量 + static let instance = ChatDeduplicationHelper() + // 最多缓存数量,可外部修改 + public var limit = 100 + // 黑名单消息id记录 + public var blackListMessageIds = Set() + // 音频消息记录 + public var recordAudioMessagePaths = Set() + // 撤回消息记录 + public var revokeMessageIds = Set() + + override private init() { + super.init() + NIMSDK.shared().loginManager.add(self) + } + + deinit { + NIMSDK.shared().loginManager.remove(self) + } + + public func onLogin(_ step: NIMLoginStep) { + if step == .logout { + clearCache() + } + } + + public func onKickout(_ result: NIMLoginKickoutResult) { + clearCache() + } + + public func clearCache() { + blackListMessageIds.removeAll() + recordAudioMessagePaths.removeAll() + revokeMessageIds.removeAll() + } + + // 是否已经发送过对应消息的提示 + public func isBlackTipSended(messageId: String) -> Bool { + if blackListMessageIds.contains(messageId) { + return true + } + if blackListMessageIds.count > limit { + blackListMessageIds.removeAll() + } + blackListMessageIds.insert(messageId) + return false + } + + // 是否已经发过对应路径的音频消息,防止重复发送 + public func isRecordAudioSended(path: String) -> Bool { + if recordAudioMessagePaths.contains(path) { + return true + } + if recordAudioMessagePaths.count > limit { + recordAudioMessagePaths.removeAll() + } + return false + } + + // 是否已经保存过此撤回消息,防止重复保存本地撤回记录 + public func isRevokeMessageSaved(messageId: String) -> Bool { + if revokeMessageIds.contains(messageId) { + return true + } + if revokeMessageIds.count > limit { + revokeMessageIds.removeAll() + } + revokeMessageIds.insert(messageId) + return false + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift index 697e8847..258da275 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift @@ -3,10 +3,17 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import CommonCrypto import Foundation +import NEChatKit +import NECommonKit +import NECoreIMKit +import NIMSDK @objcMembers public class ChatMessageHelper: NSObject { + public static let repo = ChatRepo.shared + // 获取图片合适尺寸 public class func getSizeWithMaxSize(_ maxSize: CGSize, size: CGSize, miniWH: CGFloat) -> CGSize { @@ -34,4 +41,291 @@ public class ChatMessageHelper: NSObject { return realSize } + + public static func getSessionName(session: NIMSession, showAlias: Bool = true) -> String { + session.sessionType == .P2P ? ChatUserCache.getShowName(userId: session.sessionId, teamId: nil, showAlias) : repo.getTeamInfo(teamId: session.sessionId)?.teamName ?? "" + } + + // MARK: message + + public static func getChatCellRegisterDic(isFun: Bool) -> [String: UITableViewCell.Type] { + [ + "\(MessageType.text.rawValue)": + isFun ? FunChatMessageTextCell.self : ChatMessageTextCell.self, + "\(MessageType.rtcCallRecord.rawValue)": + isFun ? FunChatMessageCallCell.self : ChatMessageCallCell.self, + "\(MessageType.audio.rawValue)": + isFun ? FunChatMessageAudioCell.self : ChatMessageAudioCell.self, + "\(MessageType.image.rawValue)": + isFun ? FunChatMessageImageCell.self : ChatMessageImageCell.self, + "\(MessageType.revoke.rawValue)": + isFun ? FunChatMessageRevokeCell.self : ChatMessageRevokeCell.self, + "\(MessageType.video.rawValue)": + isFun ? FunChatMessageVideoCell.self : ChatMessageVideoCell.self, + "\(MessageType.file.rawValue)": + isFun ? FunChatMessageFileCell.self : ChatMessageFileCell.self, + "\(MessageType.reply.rawValue)": + isFun ? FunChatMessageReplyCell.self : ChatMessageReplyCell.self, + "\(MessageType.location.rawValue)": + isFun ? FunChatMessageLocationCell.self : ChatMessageLocationCell.self, + "\(MessageType.time.rawValue)": + isFun ? FunChatMessageTipCell.self : ChatMessageTipCell.self, + "\(MessageType.multiForward.rawValue)": + isFun ? FunChatMessageMultiForwardCell.self : ChatMessageMultiForwardCell.self, + "\(MessageType.richText.rawValue)": + isFun ? FunChatMessageRichTextCell.self : ChatMessageRichTextCell.self, + ] + } + + public static func getPinCellRegisterDic(isFun: Bool) -> [String: NEBasePinMessageCell.Type] { + [ + "\(MessageType.text.rawValue)": + isFun ? FunPinMessageTextCell.self : PinMessageTextCell.self, + "\(MessageType.image.rawValue)": + isFun ? FunPinMessageImageCell.self : PinMessageImageCell.self, + "\(MessageType.audio.rawValue)": + isFun ? FunPinMessageAudioCell.self : PinMessageAudioCell.self, + "\(MessageType.video.rawValue)": + isFun ? FunPinMessageVideoCell.self : PinMessageVideoCell.self, + "\(MessageType.location.rawValue)": + isFun ? FunPinMessageLocationCell.self : PinMessageLocationCell.self, + "\(MessageType.file.rawValue)": + isFun ? FunPinMessageFileCell.self : PinMessageFileCell.self, + "\(MessageType.multiForward.rawValue)": + isFun ? FunPinMessageMultiForwardCell.self : PinMessageMultiForwardCell.self, + "\(MessageType.richText.rawValue)": + isFun ? FunPinMessageRichTextCell.self : PinMessageRichTextCell.self, + "\(NEBasePinMessageTextCell.self)": + isFun ? FunPinMessageDefaultCell.self : PinMessageDefaultCell.self, + ] + } + + public static func modelFromMessage(message: NIMMessage) -> MessageModel { + var model: MessageModel + switch message.messageType { + case .video: + model = MessageVideoModel(message: message) + case .text: + model = MessageTextModel(message: message) + case .image: + model = MessageImageModel(message: message) + case .audio: + model = MessageAudioModel(message: message) + case .notification, .tip: + model = MessageTipsModel(message: message) + case .file: + model = MessageFileModel(message: message) + case .location: + model = MessageLocationModel(message: message) + case .rtcCallRecord: + model = MessageCallRecordModel(message: message) + case .custom: + if let attach = NECustomAttachment.attachmentOfCustomMessage(message: message) { + if attach.customType == customRichTextType { + return MessageRichTextModel(message: message) + } + return MessageCustomModel(message: message) + } + fallthrough + default: + // 未识别的消息类型,默认为文本消息类型,text为未知消息体 + message.text = chatLocalizable("msg_unknown") + model = MessageTextModel(message: message) + } + return model + } + + /// 获取消息列表的中所以图片消息的 url + public static func getUrls(messages: [MessageModel]) -> [String] { + NELog.infoLog(ModuleName + " " + className(), desc: #function) + var urls = [String]() + messages.forEach { model in + if model.type == .image, let message = model.message?.messageObject as? NIMImageObject { + if let url = message.url { + urls.append(url) + } else { + if let path = message.path, FileManager.default.fileExists(atPath: path) { + urls.append(path) + } + } + } + } + return urls + } + + // history message insert message at first of messages, send message add last of messages + static func addTimeMessage(_ model: MessageModel, _ lastModel: MessageModel?) { + guard let message = model.message else { + NELog.errorLog(ModuleName + " " + className(), desc: #function + ", model.message is nil") + return + } + + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) + if NotificationMessageUtils.isDiscussSeniorTeamNoti(message: message) { + return + } + + let lastTs = lastModel?.message?.timestamp ?? 0.0 + let curTs = message.timestamp + let dur = curTs - lastTs + if (dur / 60) > 5 { + let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: curTs)) + model.timeContent = timeText + } + } + + public static func contentOfMessage(_ message: NIMMessage?) -> String { + switch message?.messageType { + case .text: + if let t = message?.text { + return t + } else { + return chatLocalizable("message_not_found") + } + case .image: + return chatLocalizable("msg_image") + case .audio: + return chatLocalizable("msg_audio") + case .video: + return chatLocalizable("msg_video") + case .file: + return chatLocalizable("msg_file") + case .location: + return chatLocalizable("msg_location") + case .rtcCallRecord: + if let record = message?.messageObject as? NIMRtcCallRecordObject { + return record.callType == .audio ? chatLocalizable("msg_rtc_audio") : + chatLocalizable("msg_rtc_video") + } + return chatLocalizable("msg_rtc_call") + case .custom: + if let content = NECustomAttachment.contentOfRichText(message: message) { + return content + } + + if let attach = NECustomAttachment.attachmentOfCustomMessage(message: message), + attach.customType == customMultiForwardType { + return "[\(chatLocalizable("chat_history"))]" + } + + return chatLocalizable("msg_custom") + default: + return chatLocalizable("msg_unknown") + } + } + + /// 移除消息扩展字段中的 回复、@ + public static func clearForwardAtMark(_ forwardMessage: NIMMessage) { + forwardMessage.remoteExt?.removeValue(forKey: yxAtMsg) + forwardMessage.remoteExt?.removeValue(forKey: keyReplyMsgKey) + if forwardMessage.remoteExt?.count ?? 0 <= 0 { + forwardMessage.remoteExt = nil + } + } + + public static func buildHeader(messageCount: Int) -> String { + var dic = [String: Any]() + dic["version"] = 0 // 功能版本 + dic["terminal"] = 2 // iOS + // dic["sdk_version"] = IMKitClient.instance.sdkVersion() + // dic["app_version"] = imkitVersion + dic["message_count"] = messageCount // 转发消息数量 + + return getJSONStringFromDictionary(dic) + } + + public static func buildBody(messages: [NIMMessage], + _ completion: @escaping (String, [[String: Any]]) -> Void) { + let enter = "\n" // 分隔符 + var body = "" // 序列化结果 + var abstracts = [[String: Any]]() // 摘要信息 + let group = DispatchGroup() + + for (i, msg) in messages.enumerated() { + // 移除扩展字段中的 回复、@ 信息 + let remoteExt = msg.remoteExt + clearForwardAtMark(msg) + + // 保存消息昵称和头像 + if let from = msg.from { + group.enter() + ChatUserCache.getUserInfo(from) { user, error in + if let user = user { + let senderNick = user.showName(false) + if msg.remoteExt != nil { + msg.remoteExt![mergedMessageNickKey] = senderNick + msg.remoteExt![mergedMessageAvatarKey] = user.userInfo?.avatarUrl ?? user.shortName(count: 2) + } else { + msg.remoteExt = [mergedMessageNickKey: senderNick as Any, + mergedMessageAvatarKey: user.userInfo?.avatarUrl as Any] + } + + // 摘要信息 + if i < 3 { + let content = ChatMessageHelper.contentOfMessage(msg) + abstracts.append(["senderNick": senderNick as Any, + "content": content, + "userAccId": from]) + } + } + body.append(enter) + let data = NIMSDK.shared().conversationManager.encodeMessage(toData: msg) + if let stringData = String(data: data, encoding: .utf8) { + body.append(stringData) + } + group.leave() + } + } + + // 恢复扩展字段中的 回复、@ 信息 + msg.remoteExt = remoteExt + } + + group.notify(queue: .main, work: DispatchWorkItem(block: { + completion(body, abstracts) + })) + } + + public static func getFileChecksum(fileURL: URL) -> String? { + // 打开文件,创建文件句柄 + let file = FileHandle(forReadingAtPath: fileURL.path) + guard file != nil else { return nil } + + // 创建 CC_MD5_CTX 上下文对象 + var context = CC_MD5_CTX() + CC_MD5_Init(&context) + + // 读取文件数据并更新上下文对象 + while autoreleasepool(invoking: { + let data = file?.readData(ofLength: 1024) + if data?.count == 0 { + return false + } + data?.withUnsafeBytes { buffer in + CC_MD5_Update(&context, buffer.baseAddress, CC_LONG(buffer.count)) + } + return true + }) {} + + // 计算 MD5 值并关闭文件 + var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) + CC_MD5_Final(&digest, &context) + file?.closeFile() + + // 将 MD5 值转换为字符串格式 + let md5String = digest.map { String(format: "%02hhx", $0) }.joined() + return md5String + } + + // 检测语音消息是否下载,非漫游的云端消息不会自动下载语音文件,需要手动下载 + public static func downloadAudioFile(message: NIMMessage) { + if message.messageType == .audio { + if let audio = message.messageObject as? NIMAudioObject { + if let path = audio.path, FileManager.default.fileExists(atPath: path) == false { + repo.downloadMessageAttachment(message) { error in + } + } + } + } + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift index 97542349..1856b5a4 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift @@ -8,15 +8,15 @@ import NECoreIMKit import NIMSDK @objcMembers -public class MessageUtils: NSObject { - public class func textMessage(text: String) -> NIMMessage { +open class MessageUtils: NSObject { + open class func textMessage(text: String) -> NIMMessage { let message = NIMMessage() message.setting = messageSetting() message.text = text return message } - public class func textMessage(text: String, remoteExt: [String: Any]?) -> NIMMessage { + open class func textMessage(text: String, remoteExt: [String: Any]?) -> NIMMessage { let message = NIMMessage() message.setting = messageSetting() message.text = text @@ -26,15 +26,19 @@ public class MessageUtils: NSObject { return message } - public class func imageMessage(image: UIImage) -> NIMMessage { + open class func imageMessage(image: UIImage) -> NIMMessage { imageMessage(imageObject: NIMImageObject(image: image)) } - public class func imageMessage(path: String) -> NIMMessage { + open class func imageMessage(path: String) -> NIMMessage { imageMessage(imageObject: NIMImageObject(filepath: path)) } - public class func imageMessage(imageObject: NIMImageObject) -> NIMMessage { + open class func imageMessage(data: Data, ext: String) -> NIMMessage { + imageMessage(imageObject: NIMImageObject(data: data, extension: ext)) + } + + open class func imageMessage(imageObject: NIMImageObject) -> NIMMessage { let message = NIMMessage() let option = NIMImageOption() option.compressQuality = 0.8 @@ -45,7 +49,7 @@ public class MessageUtils: NSObject { return message } - public class func audioMessage(filePath: String) -> NIMMessage { + open class func audioMessage(filePath: String) -> NIMMessage { let messageObject = NIMAudioObject(sourcePath: filePath) let message = NIMMessage() message.messageObject = messageObject @@ -54,7 +58,7 @@ public class MessageUtils: NSObject { return message } - public class func videoMessage(filePath: String) -> NIMMessage { + open class func videoMessage(filePath: String) -> NIMMessage { let messageObject = NIMVideoObject(sourcePath: filePath) let message = NIMMessage() message.messageObject = messageObject @@ -63,7 +67,7 @@ public class MessageUtils: NSObject { return message } - public class func locationMessage(_ lat: Double, _ lng: Double, _ title: String, _ address: String) -> NIMMessage { + open class func locationMessage(_ lat: Double, _ lng: Double, _ title: String, _ address: String) -> NIMMessage { let messageObject = NIMLocationObject(latitude: lat, longitude: lng, title: address) let message = NIMMessage() message.messageObject = messageObject @@ -73,7 +77,7 @@ public class MessageUtils: NSObject { return message } - public class func fileMessage(filePath: String, displayName: String?) -> NIMMessage { + open class func fileMessage(filePath: String, displayName: String?) -> NIMMessage { let messageObject = NIMFileObject(sourcePath: filePath) if let dpName = displayName { messageObject.displayName = dpName @@ -85,7 +89,7 @@ public class MessageUtils: NSObject { return message } - public class func fileMessage(data: Data, displayName: String?) -> NIMMessage { + open class func fileMessage(data: Data, displayName: String?) -> NIMMessage { let dpName = displayName ?? "" let pointIndex = dpName.lastIndex(of: ".") ?? dpName.startIndex let suffix = dpName[dpName.index(after: pointIndex) ..< dpName.endIndex] @@ -98,10 +102,21 @@ public class MessageUtils: NSObject { return message } - public class func messageSetting() -> NIMMessageSetting { + open class func customMessage(attachment: NIMCustomAttachment?, + remoteExt: [String: Any]?, + apnsContent: String?) -> NIMMessage { + let messageObject = NIMCustomObject() + messageObject.attachment = attachment + let message = NIMMessage() + message.messageObject = messageObject + message.apnsContent = apnsContent + message.remoteExt = remoteExt + message.setting = messageSetting() + return message + } + + open class func messageSetting() -> NIMMessageSetting { let setting = NIMMessageSetting() - print("getMessageRead: \(SettingProvider.shared.getMessageRead())") -// FIXME: setting.teamReceiptEnabled = SettingProvider.shared.getMessageRead() return setting } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift index e526ea8a..2f8e97d8 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift @@ -14,8 +14,8 @@ public enum TeamType { case discussTeam } -public class NotificationMessageUtils: NSObject { - public class func textForNotification(message: NIMMessage) -> String { +open class NotificationMessageUtils: NSObject { + open class func textForNotification(message: NIMMessage) -> String { if message.messageType != .notification { return "" } @@ -37,7 +37,7 @@ public class NotificationMessageUtils: NSObject { } /// 是否是群通知 - public class func isDiscussSeniorTeamNoti(message: NIMMessage) -> Bool { + open class func isDiscussSeniorTeamNoti(message: NIMMessage) -> Bool { if let object = message.messageObject as? NIMNotificationObject, let _ = object.content as? NIMTeamNotificationContent { return true @@ -45,7 +45,7 @@ public class NotificationMessageUtils: NSObject { return false } - public class func isDiscussSeniorTeamUpdateCustomNoti(message: NIMMessage) -> Bool { + open class func isDiscussSeniorTeamUpdateCustomNoti(message: NIMMessage) -> Bool { if let object = message.messageObject as? NIMNotificationObject { guard let content = object.content as? NIMTeamNotificationContent else { return false @@ -73,7 +73,7 @@ public class NotificationMessageUtils: NSObject { return false } - public class func isTeamLeaveOrDismiss(message: NIMMessage) -> (isLeave: Bool, isDismiss: Bool) { + open class func isTeamLeaveOrDismiss(message: NIMMessage) -> (isLeave: Bool, isDismiss: Bool) { var leave = false var dismiss = false if let object = message.messageObject as? NIMNotificationObject, object.notificationType == .team { @@ -93,7 +93,7 @@ public class NotificationMessageUtils: NSObject { return (leave, dismiss) } - public class func textForTeamNotificationMessage(message: NIMMessage) -> String { + open class func textForTeamNotificationMessage(message: NIMMessage) -> String { var text = chatLocalizable("unknown_system_message") if let object = message.messageObject as? NIMNotificationObject { if let content = object.content as? NIMTeamNotificationContent { @@ -130,7 +130,7 @@ public class NotificationMessageUtils: NSObject { case .transferOwner: text = fromName + chatLocalizable("transfer") + toFirstName case .addManager: - text = toFirstName + chatLocalizable("added_manager") + text = toNamestext + chatLocalizable("added_manager") case .removeManager: text = toFirstName + chatLocalizable("removed_manager") case .acceptInvitation: @@ -154,34 +154,14 @@ public class NotificationMessageUtils: NSObject { return text } - // 获取展示的用户名字,p2p: 备注 > 昵称 > ID team: 备注 > 群昵称 > 昵称 > ID - public class func getShowName(userId: String, nimSession: NIMSession?) -> String { - let user = UserInfoProvider.shared.getUserInfo(userId: userId) - var fullName = userId - if let nickName = user?.userInfo?.nickName { - fullName = nickName - } - if let session = nimSession, session.sessionType == .team { - // team - let teamMember = TeamProvider.shared.teamMember(userId, session.sessionId) - if let teamNickname = teamMember?.nickname { - fullName = teamNickname - } - } - if let alias = user?.alias { - fullName = alias - } - return fullName - } - - public class func fromName(message: NIMMessage) -> String { + open class func fromName(message: NIMMessage) -> String { if let object = message.messageObject as? NIMNotificationObject { if let content = object.content as? NIMTeamNotificationContent { if content.sourceID == NIMSDK.shared().loginManager.currentAccount() { return chatLocalizable("You") + " " } else { if let sourceId = content.sourceID { - return getShowName(userId: sourceId, nimSession: message.session) + return ChatUserCache.getShowName(userId: sourceId, teamId: message.session?.sessionId) } } } @@ -189,7 +169,7 @@ public class NotificationMessageUtils: NSObject { return "" } - public class func toName(message: NIMMessage) -> [String] { + open class func toName(message: NIMMessage) -> [String] { var toNames = [String]() guard let object = message.messageObject as? NIMNotificationObject, let content = object.content as? NIMTeamNotificationContent, @@ -201,13 +181,13 @@ public class NotificationMessageUtils: NSObject { toNames.append(chatLocalizable("You") + " ") } else { toNames - .append(getShowName(userId: targetID, nimSession: message.session)) + .append(ChatUserCache.getShowName(userId: targetID, teamId: message.session?.sessionId)) } } return toNames } - public class func teamName(message: NIMMessage) -> String { + open class func teamName(message: NIMMessage) -> String { let teamtype = teamType(message: message) switch teamtype { case .advanceTeam: @@ -217,7 +197,7 @@ public class NotificationMessageUtils: NSObject { } } - public class func teamType(message: NIMMessage) -> TeamType { + open class func teamType(message: NIMMessage) -> TeamType { let team = TeamProvider.shared.getTeam(teamId: message.session?.sessionId ?? "") if team?.isDisscuss() == true { return .discussTeam diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift index c1892062..83e54bf7 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift @@ -5,35 +5,22 @@ import Foundation -public class ReplyMessageUtil: NSObject { +open class ReplyMessageUtil: NSObject { public static func textForReplyModel(model: MessageContentModel) -> String { - var text = "|" + var text = "" if let name = model.fullName { - text += name + text += name + ": " } - text += ": " - switch model.type { - case .text, .reply: - if let t = model.message?.text { - text += t - } else { - text = chatLocalizable("message_not_found") + + if model.type == .reply { + if let content = NECustomAttachment.contentOfRichText(message: model.message) { + return text + content } - case .image: - text += "[\(chatLocalizable("msg_image"))]" - case .audio: - text += "[\(chatLocalizable("msg_audio"))]" - case .video: - text += "[\(chatLocalizable("msg_video"))]" - case .file: - text += "[\(chatLocalizable("msg_file"))]" - case .custom: - text += "[\(chatLocalizable("msg_custom"))]" - case .location: - text += "[\(chatLocalizable("msg_location"))]" - default: - text += "[\(chatLocalizable("msg_unknown"))]" + text += "\(model.message?.text ?? chatLocalizable("message_not_found"))" + } else { + text += "\(ChatMessageHelper.contentOfMessage(model.message))" } + return text } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtCacheModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtCacheModel.swift index 2a789cb3..10e3646e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtCacheModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtCacheModel.swift @@ -5,7 +5,7 @@ import UIKit @objcMembers -public class MessageAtCacheModel: NSObject { +open class MessageAtCacheModel: NSObject { public var atModel: MessageAtInfoModel public var accid: String public var text: String? diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtInfoModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtInfoModel.swift index 97ec3da6..ce7358f0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtInfoModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtInfoModel.swift @@ -5,7 +5,7 @@ import UIKit @objcMembers -public class MessageAtInfoModel: NSObject { +open class MessageAtInfoModel: NSObject { public var start = 0 public var end = 0 public var broken = false diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift index e51515cd..78b8ddeb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift @@ -7,10 +7,11 @@ import Foundation import NIMSDK @objcMembers -class MessageAudioModel: MessageContentModel { +open class MessageAudioModel: MessageContentModel { public var duration: Int = 0 public var isPlaying = false - required init(message: NIMMessage?) { + + public required init(message: NIMMessage?) { super.init(message: message) type = .audio var audioW = 96.0 @@ -23,6 +24,6 @@ class MessageAudioModel: MessageContentModel { } } contentSize = CGSize(width: audioW, height: chat_min_h) - height = Float(contentSize.height + chat_content_margin) + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift index 79800eab..56acfafe 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift @@ -8,10 +8,10 @@ import NIMSDK import UIKit @objcMembers -class MessageCallRecordModel: MessageContentModel { +open class MessageCallRecordModel: MessageContentModel { public var attributeStr: NSMutableAttributedString? - required init(message: NIMMessage?) { + public required init(message: NIMMessage?) { super.init(message: message) type = .rtcCallRecord var isAuiodRecord = false @@ -68,6 +68,6 @@ class MessageCallRecordModel: MessageContentModel { h = textSize.height + (isAuiodRecord ? 20 : 24) contentSize = CGSize(width: textSize.width + chat_cell_margin * 2, height: h) - height = Float(contentSize.height + chat_content_margin) + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift index effad06b..5616d902 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift @@ -10,25 +10,38 @@ import NIMSDK import simd @objcMembers -public class MessageContentModel: NSObject, MessageModel { +open class MessageContentModel: NSObject, MessageModel { public var offset: CGFloat = 0 - public func cellHeight() -> CGFloat { + open func cellHeight() -> CGFloat { CGFloat(height) + offset } public var isReplay: Bool = false + public var showSelect: Bool = false // 多选按钮是否展示 + public var isSelected: Bool = false // 多选是否选中 + public var inMultiForward: Bool = false { // 是否是合并消息中的子消息 + didSet { +// fullNameHeight = 0 // 合并消息中的子消息不显示昵称 + fullNameHeight = 20 // 合并消息中的子消息显示昵称 + if inMultiForward { + height += fullNameHeight + } else if oldValue { + height -= fullNameHeight + } + } + } public var pinAccount: String? public var pinShowName: String? public var type: MessageType = .custom public var message: NIMMessage? public var contentSize = CGSize(width: 32.0, height: chat_min_h) - public var height: Float = 48 + public var height: CGFloat = 48 public var shortName: String? // 昵称 > uid public var fullName: String? // 备注 >(群昵称)> 昵称 > uid public var avatar: String? public var replyText: String? - public var fullNameHeight: Float = 0 + public var fullNameHeight: CGFloat = 0 public var isRevokedText: Bool = false public var timeOut = false @@ -59,11 +72,21 @@ public class MessageContentModel: NSObject, MessageModel { } else { contentSize = CGSize(width: 130, height: chat_min_h) } - height = Float(contentSize.height + chat_content_margin) + fullNameHeight + height = contentSize.height + chat_content_margin + fullNameHeight + + // time + if let time = timeContent, !time.isEmpty { + height += chat_timeCellH + } } else { type = .custom contentSize = CGSize(width: 32.0, height: chat_min_h) - height = Float(chat_min_h + chat_content_margin) + fullNameHeight + height = chat_min_h + chat_content_margin + fullNameHeight + + // time + if let time = timeContent, !time.isEmpty { + height += chat_timeCellH + } } } } @@ -71,9 +94,18 @@ public class MessageContentModel: NSObject, MessageModel { public var isPined: Bool = false { didSet { if isPined { - height = Float(contentSize.height + chat_content_margin) + fullNameHeight + Float(chat_pin_height) - } else { - height = Float(contentSize.height + chat_content_margin) + fullNameHeight + height += chat_pin_height + } else if oldValue { + height -= chat_pin_height + } + } + } + + // 是否显示时间 + public var timeContent: String? { + didSet { + if let time = timeContent, !time.isEmpty, time != oldValue { + height += chat_timeCellH } } } @@ -82,8 +114,8 @@ public class MessageContentModel: NSObject, MessageModel { self.message = message if message?.session?.sessionType == .team, !IMKitClient.instance.isMySelf(message?.from) { - fullNameHeight = 20 + fullNameHeight = NEKitChatConfig.shared.ui.messageProperties.showTeamMessageNick ? 20 : 0 } - print("self.fullNameHeight\(fullNameHeight)") + height = contentSize.height + chat_content_margin * 2 + fullNameHeight } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift index c9d8e843..fcecc360 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift @@ -6,9 +6,13 @@ import NIMSDK import UIKit @objc -public class MessageCustomModel: MessageContentModel { - required init(message: NIMMessage?) { +open class MessageCustomModel: MessageContentModel { + public required init(message: NIMMessage?) { super.init(message: message) type = .custom + if let attachment = NECustomAttachment.attachmentOfCustomMessage(message: message) { + contentSize = CGSize(width: 0, height: Int(attachment.cellHeight)) + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift index 2f989053..4a64f250 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift @@ -7,7 +7,7 @@ import NIMSDK import UIKit @objcMembers -class MessageFileModel: MessageContentModel { +open class MessageFileModel: MessageContentModel { public var displayName: String? public var path: String? public var url: String? @@ -18,7 +18,7 @@ class MessageFileModel: MessageContentModel { public var state = DownloadState.Success public weak var cell: NEChatBaseCell? - required init(message: NIMMessage?) { + public required init(message: NIMMessage?) { super.init(message: message) type = .file if let fileObject = message?.messageObject as? NIMFileObject { @@ -28,6 +28,6 @@ class MessageFileModel: MessageContentModel { fileLength = fileObject.fileLength } contentSize = chat_file_size - height = Float(contentSize.height + chat_content_margin) + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift index bb018124..ccb7bd0f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift @@ -7,9 +7,10 @@ import Foundation import NIMSDK @objcMembers -class MessageImageModel: MessageContentModel { +open class MessageImageModel: MessageContentModel { public var imageUrl: String? - required init(message: NIMMessage?) { + + public required init(message: NIMMessage?) { super.init(message: message) type = .image if let imageObject = message?.messageObject as? NIMImageObject { @@ -26,6 +27,6 @@ class MessageImageModel: MessageContentModel { } else { contentSize = chat_pic_size } - height = Float(contentSize.height + chat_content_margin) + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift index 4ee29b40..d193d92e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift @@ -5,12 +5,14 @@ import NIMSDK import UIKit -class MessageLocationModel: MessageContentModel { +@objcMembers +open class MessageLocationModel: MessageContentModel { public var lat: Double? public var lng: Double? public var title: String? public var subTitle: String? - required init(message: NIMMessage?) { + + public required init(message: NIMMessage?) { super.init(message: message) type = .location if let locationObject = message?.messageObject as? NIMLocationObject { @@ -20,6 +22,6 @@ class MessageLocationModel: MessageContentModel { title = message?.text contentSize = CGSize(width: 242, height: 140) } - height = Float(contentSize.height + chat_content_margin) + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift index 23a2041d..db8fb8a9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift @@ -22,6 +22,12 @@ public enum MessageType: Int { case time case revoke case reply + + /// 合并转发消息 + case multiForward + + /// 带标题的文本消息 + case richText } @objc @@ -29,7 +35,7 @@ public protocol MessageModel: NSObjectProtocol { var message: NIMMessage? { get set } // 气泡区域的大小 不包含气泡上下到cell上下的边距 var contentSize: CGSize { get set } - var height: Float { get set } + var height: CGFloat { get set } // 名字后2位 var shortName: String? { get set } // 名字全长 @@ -46,6 +52,11 @@ public protocol MessageModel: NSObjectProtocol { var replyText: String? { get set } var isRevokedText: Bool { get set } var isReplay: Bool { get set } + var showSelect: Bool { get set } // 多选按钮是否展示 + var isSelected: Bool { get set } // 多选是否选中 + var inMultiForward: Bool { get set } // 是否是合并消息中的子消息 + + var timeContent: String? { get set } // 具体时间 init(message: NIMMessage?) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift new file mode 100644 index 00000000..ce115450 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift @@ -0,0 +1,41 @@ + +// 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 NECommonKit +import NIMSDK + +@objcMembers +open class MessageRichTextModel: MessageTextModel { + public var titleAttributeStr: NSMutableAttributedString? + public var titleTextHeight: CGFloat = 0 + + public required init(message: NIMMessage?) { + guard let data = NECustomAttachment.dataOfCustomMessage(message: message), + let title = data["title"] as? String else { + super.init(message: message) + return + } + + let body = (data["body"] as? String) ?? "" + message?.text = body + super.init(message: message) + type = .custom + + let font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) + titleAttributeStr = NEEmotionTool.getAttWithStr( + str: title, + font: font + ) + + let textSize = titleAttributeStr?.finalSize(font, CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) ?? .zero + + titleTextHeight = textSize.height + contentSize = CGSize(width: max(contentSize.width, textSize.width + chat_content_margin * 2), + height: contentSize.height + titleTextHeight + + (body.isEmpty ? 0 : chat_content_margin)) + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift index 14b6776d..0f92533a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift @@ -8,11 +8,11 @@ import NECommonKit import NIMSDK @objcMembers -class MessageTextModel: MessageContentModel { +open class MessageTextModel: MessageContentModel { public var attributeStr: NSMutableAttributedString? public var textHeight: CGFloat = 0 - required init(message: NIMMessage?) { + public required init(message: NIMMessage?) { super.init(message: message) type = .text @@ -41,6 +41,6 @@ class MessageTextModel: MessageContentModel { textHeight = textSize.height contentSize = CGSize(width: textSize.width + chat_content_margin * 2, height: textHeight + chat_content_margin * 2) - height = Float(contentSize.height + chat_content_margin) + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift index 641d7163..b2e1c071 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift @@ -7,37 +7,15 @@ import Foundation import NIMSDK @objcMembers -class MessageTipsModel: NSObject, MessageModel { - var offset: CGFloat = 0 +open class MessageTipsModel: MessageContentModel { + var text: String? - func cellHeight() -> CGFloat { - CGFloat(height) + offset + public required init(message: NIMMessage?) { + super.init(message: message) + commonInit() } - var tipTimeStamp: TimeInterval? - - var isReplay: Bool = false - - var pinToAccount: String? - var pinFromAccount: String? - var isPined: Bool = false - var pinAccount: String? - var pinShowName: String? - var replyText: String? - var type: MessageType = .tip - var message: NIMMessage? - var contentSize: CGSize = .zero - var height: Float = 28 - var shortName: String? - var fullName: String? - var avatar: String? - var text: String? - var isRevoked: Bool = false - var replyedModel: MessageModel? - var isRevokedText: Bool = false - weak var tipMessage: NIMMessage? - - func commonInit(message: NIMMessage?) { + func setText() { if let msg = message { if msg.messageType == .notification { text = NotificationMessageUtils.textForNotification(message: msg) @@ -46,37 +24,19 @@ class MessageTipsModel: NSObject, MessageModel { text = msg.text type = .tip } - - tipMessage = msg - tipTimeStamp = msg.timestamp } - - var font: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.timeTextSize) - - contentSize = String.getTextRectSize(text ?? "", - font: font, - size: CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) - height = Float(max(contentSize.height + chat_content_margin, 28)) } - required init(message: NIMMessage?) { - super.init() - commonInit(message: message) - } + func commonInit() { + setText() + let font: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.timeTextSize) - init(message: NIMMessage?, initType: MessageType = .tip, initText: String? = nil) { - super.init() - type = initType - text = initText - commonInit(message: message) - } + contentSize = text?.finalSize(font, CGSize(width: chat_content_maxW, height: CGFloat.greatestFiniteMagnitude)) ?? .zero + height = ceil(contentSize.height) - public func resetNotiText() { - if let msg = tipMessage { - if msg.messageType == .notification { - text = NotificationMessageUtils.textForNotification(message: msg) - type = .notification - } + // time + if let time = timeContent, !time.isEmpty { + height += chat_timeCellH } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift index 23d57343..50fd4e6e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift @@ -7,18 +7,18 @@ import NIMSDK import UIKit @objc -enum DownloadState: Int { +public enum DownloadState: Int { case Success = 1 case Downalod } @objcMembers -class MessageVideoModel: MessageContentModel { +open class MessageVideoModel: MessageContentModel { public var imageUrl: String? public var state = DownloadState.Success public var progress: Float = 0 public weak var cell: NEChatBaseCell? - required init(message: NIMMessage?) { + public required init(message: NIMMessage?) { super.init(message: message) type = .video if let videoObject = message?.messageObject as? NIMVideoObject { @@ -31,6 +31,6 @@ class MessageVideoModel: MessageContentModel { } else { contentSize = chat_pic_size } - height = Float(contentSize.height + chat_content_margin) + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NECustomAttachmentProtocol.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NECustomAttachmentProtocol.swift deleted file mode 100644 index f925429f..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NECustomAttachmentProtocol.swift +++ /dev/null @@ -1,11 +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 - -@objc -public protocol NECustomAttachmentProtocol: NSObjectProtocol { - var customType: Int { get set } - var cellHeight: CGFloat { get set } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEMoreItemModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEMoreItemModel.swift index 3d1debcf..b3c666b3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEMoreItemModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEMoreItemModel.swift @@ -5,7 +5,8 @@ import UIKit -public enum NEMoreActionType: Int { +@objc +public enum NEMoreActionType: NSInteger { case takePicture = 1 case location case rtc @@ -18,7 +19,7 @@ public enum NEMoreActionType: Int { @objc @objcMembers -public class NEMoreItemModel: NSObject { +open class NEMoreItemModel: NSObject { // 单元图标 public var image: UIImage? diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItem.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItem.swift index 9aa565a4..fad8d57b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItem.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItem.swift @@ -19,7 +19,7 @@ public enum OperationType: Int { } @objcMembers -public class OperationItem: NSObject { +open class OperationItem: NSObject { public var text: String = "" public var imageName: String = "" public var type: OperationType? @@ -64,15 +64,15 @@ public class OperationItem: NSObject { return item } -// static public func selectItem() -> OperationItem { -// OperationItem( -// text: chatLocalizable("operation_select"), -// imageName: "op_select", -// type: .multiSelect -// ) -// } + public static func selectItem() -> OperationItem { + let item = OperationItem() + item.text = chatLocalizable("operation_select") + item.imageName = "op_select" + item.type = .multiSelect + return item + } -// static public func collectionItem() -> OperationItem { +// static open func collectionItem() -> OperationItem { // OperationItem( // text: chatLocalizable("operation_collection"), // imageName: "op_collection", diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift new file mode 100644 index 00000000..c8b768ae --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift @@ -0,0 +1,15 @@ +//// 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 PinMessageFileModel: NSObject { + public var progress: Float = 0 + public var size: Float = 0 + + public var state = DownloadState.Success + public weak var cell: NEBasePinMessageFileCell? +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift index d7e38c4b..e1fe22af 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift @@ -7,12 +7,14 @@ import NECoreIMKit import NIMSDK import UIKit -public class PinMessageModel: NSObject { - var chatmodel: MessageModel? +@objcMembers +open class PinMessageModel: NSObject { + var chatmodel: MessageModel = MessageTextModel(message: nil) var message: NIMMessage var item: NIMMessagePinItem var session: NIMSession var repo = ChatRepo.shared + var pinFileModel: PinMessageFileModel? init(message: NIMMessage, item: NIMMessagePinItem) { self.message = message @@ -20,56 +22,23 @@ public class PinMessageModel: NSObject { self.item = item super.init() chatmodel = modelFromMessage(message: message) + if chatmodel.type == .file { + pinFileModel = PinMessageFileModel() + if let filemodel = chatmodel as? MessageFileModel { + pinFileModel?.size = filemodel.size + } + } } private func modelFromMessage(message: NIMMessage) -> MessageModel { - var model: MessageModel - switch message.messageType { - case .video: - model = MessageVideoModel(message: message) - case .text: - model = MessageTextModel(message: message) - case .image: - model = MessageImageModel(message: message) - case .audio: - model = MessageAudioModel(message: message) - case .notification: - model = MessageTipsModel(message: message) - case .file: - model = MessageFileModel(message: message) - case .tip: - model = MessageTipsModel(message: message) - case .location: - model = MessageLocationModel(message: message) - case .rtcCallRecord: - model = MessageCallRecordModel(message: message) - default: - // 未识别的消息类型,默认为文本消息类型,text为未知消息 - message.text = "未知消息" - model = MessageContentModel(message: message) - } + let model = ChatMessageHelper.modelFromMessage(message: message) if let uid = message.from { - let user = UserInfoProvider.shared.getUserInfo(userId: uid) - var fullName = uid - if let nickName = user?.userInfo?.nickName { - fullName = nickName - } + let user = ChatUserCache.getUserInfo(uid) + let fullName = ChatUserCache.getShowName(userId: uid, teamId: session.sessionId) model.avatar = user?.userInfo?.avatarUrl - if session.sessionType == .team { - // team - let teamMember = TeamProvider.shared.teamMember(uid, session.sessionId) - if let teamNickname = teamMember?.nickname { - fullName = teamNickname - } - } - if let alias = user?.alias { - fullName = alias - } model.fullName = fullName - model.shortName = fullName - .count > 2 ? String(fullName[fullName.index(fullName.endIndex, offsetBy: -2)...]) : - fullName + model.shortName = ChatUserCache.getShortName(name: user?.showName(false) ?? "", length: 2) } // model.replyedModel = getReplyMessageWithoutThread(message: message) @@ -84,37 +53,34 @@ public class PinMessageModel: NSObject { return model } - public func getMessageType() -> Int { - if message.messageType == .file || - message.messageType == .audio || - message.messageType == .text || - message.messageType == .image || - message.messageType == .video || - message.messageType == .location { - return message.messageType.rawValue - } - return PinMessageDefaultType - } - - public func cellHeight() -> CGFloat { - var height = chatmodel?.contentSize.height ?? 0 + open func cellHeight(pinContentMaxW: CGFloat) -> CGFloat { + var height = chatmodel.contentSize.height if let textModel = chatmodel as? MessageTextModel { - height = textModel.textHeight + // 文本消息最多显示 3 行 + let textSize = textModel.attributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize), CGSize(width: pinContentMaxW, height: CGFloat.greatestFiniteMagnitude), 3) ?? .zero + height = textSize.height } - height += 100 - if chatmodel?.type == .text, height > 162 { - height = 162 + if let textModel = chatmodel as? MessageRichTextModel { + // 换行消息中的标题最多显示 1 行 + let titleSize = textModel.titleAttributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize, weight: .semibold), CGSize(width: pinContentMaxW, height: CGFloat.greatestFiniteMagnitude), 1) ?? .zero + height = titleSize.height + + // 换行消息中的内容最多显示 2 行 + let textSize = textModel.attributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize), CGSize(width: pinContentMaxW, height: CGFloat.greatestFiniteMagnitude), 2) ?? .zero + height += textSize.height } - if chatmodel?.replyedModel?.isReplay == true { + height += 100 + + if chatmodel.replyedModel?.isReplay == true { height += 12 } return height } - public func getReplyMessageWithoutThread(message: NIMMessage) -> MessageModel? { + open func getReplyMessageWithoutThread(message: NIMMessage) -> MessageModel? { var replyId: String? = message.repliedMessageId if let yxReplyMsg = message.remoteExt?[keyReplyMsgKey] as? [String: Any] { replyId = yxReplyMsg["idClient"] as? String @@ -135,32 +101,4 @@ public class PinMessageModel: NSObject { model.isReplay = true return model } - - // 获取展示的用户名字,p2p: 备注 > 昵称 > ID team: 备注 > 群昵称 > 昵称 > ID - private func getShowName(userId: String, teamId: String?) -> String { - let user = getUserInfo(userId: userId) - var fullName = userId - if let nickName = user?.userInfo?.nickName { - fullName = nickName - } - if let tID = teamId, session.sessionType == .team { - // team - let teamMember = getTeamMember(userId: userId, teamId: tID) - if let teamNickname = teamMember?.nickname { - fullName = teamNickname - } - } - if let alias = user?.alias { - fullName = alias - } - return fullName - } - - public func getUserInfo(userId: String) -> User? { - repo.getUserInfo(userId: userId) - } - - public func getTeamMember(userId: String, teamId: String) -> NIMTeamMember? { - repo.getTeamMemberList(userId: userId, teamId: teamId) - } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/UserSettingCellModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/UserSettingCellModel.swift index 7c2be1eb..e276bf9c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/UserSettingCellModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/UserSettingCellModel.swift @@ -5,13 +5,13 @@ import Foundation -enum UserSettingType: Int { +public enum UserSettingType: Int { case SwitchType = 1 case SelectType = 2 } @objcMembers -public class UserSettingCellModel: NSObject { +open class UserSettingCellModel: NSObject { typealias SwitchChangeCompletion = (Bool) -> Void typealias CellClick = () -> Void var cellName: String? diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift index 2fb2393f..e0c1ece7 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift @@ -30,6 +30,9 @@ public protocol ChatBaseCellDelegate: NSObjectProtocol { // 单击已读未读按钮 func didTapReadView(_ cell: UITableViewCell, _ model: MessageContentModel?) + + // 单击多选按钮 + func didTapSelectButton(_ cell: UITableViewCell, _ model: MessageContentModel?) } @objc @@ -42,13 +45,14 @@ protocol ChatAudioCellProtocol { @objcMembers open class NEBaseChatMessageCell: NEChatBaseCell { - public var seletedBtn = UIButton(type: .custom) // 多选按钮 private let bubbleWidth: CGFloat = 218 // 气泡默认宽度 + private let pinLabelMaxWidth: CGFloat = 280 // pin 文案最大宽度 public weak var delegate: ChatBaseCellDelegate? public var contentModel: MessageContentModel? // 消息模型 /// Left public var avatarImageLeft = UIImageView() // 左侧头像 + public var avatarImageLeftAnchor: NSLayoutConstraint? // 左侧头像左侧布局依赖 public var nameLabelLeft = UILabel() // 左侧头像文字(无头像预设) public var bubbleImageLeft = UIImageView() // 左侧气泡 public var bubbleTopAnchorLeft: NSLayoutConstraint? // 左侧气泡顶部布局约束 @@ -56,6 +60,7 @@ open class NEBaseChatMessageCell: NEChatBaseCell { public var bubbleHLeft: NSLayoutConstraint? // 左侧气泡高度布局约束 public var pinImageLeft = UIImageView() // 左侧标记图片 public var pinLabelLeft = UILabel() // 左侧标记文案 + public var pinLabelLeftTopAnchor: NSLayoutConstraint? // 左侧标记文案顶部布局约束 private var pinLabelHLeft: NSLayoutConstraint? // 左侧标记文案宽度布局约束 private var pinLabelWLeft: NSLayoutConstraint? // 左侧标记文案高度布局约束 public var fullNameLabel = UILabel() // 群昵称(只在群聊中有效) @@ -69,13 +74,19 @@ open class NEBaseChatMessageCell: NEChatBaseCell { public var bubbleHRight: NSLayoutConstraint? // 右侧气泡高度布局约束 public var pinImageRight = UIImageView() // 右侧标记图片 public var pinLabelRight = UILabel() // 右侧标记文案 + public var pinLabelRightTopAnchor: NSLayoutConstraint? // 右侧标记文案顶部布局约束 private var pinLabelHRight: NSLayoutConstraint? // 右侧标记文案宽度布局约束 private var pinLabelWRight: NSLayoutConstraint? // 右侧标记文案高度布局约束 + // 已读未读视图 public var readView = CirleProgressView(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) + public var activityView = ChatActivityIndicatorView() // 消息状态视图 + public var seletedBtn = UIButton(type: .custom) // 多选按钮 + public var timeLabel = UILabel() // 消息时间 + public var timeLabelHeightAnchor: NSLayoutConstraint? // 消息时间高度约束 + // 已读未读点击手势 private var tapGesture: UITapGestureRecognizer? - public var activityView = ChatActivityIndicatorView() // 消息状态视图 override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -89,7 +100,20 @@ open class NEBaseChatMessageCell: NEChatBaseCell { fatalError("init(coder:) has not been implemented") } + deinit { + gestureRecognizers?.forEach { gestrue in + removeGestureRecognizer(gestrue) + } + } + open func initProperty() { + timeLabel.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.timeTextSize) + timeLabel.textColor = NEKitChatConfig.shared.ui.messageProperties.timeTextColor + timeLabel.textAlignment = .center + timeLabel.translatesAutoresizingMaskIntoConstraints = false + timeLabel.accessibilityIdentifier = "id.messageTipText" + timeLabel.backgroundColor = .white + // avatar avatarImageLeft.backgroundColor = UIColor(hexString: "#537FF4") avatarImageLeft.translatesAutoresizingMaskIntoConstraints = false @@ -126,14 +150,14 @@ open class NEBaseChatMessageCell: NEChatBaseCell { bubbleImageLeft.backgroundColor = NEKitChatConfig.shared.ui.messageProperties.receiveMessageBg var image = NEKitChatConfig.shared.ui.messageProperties.leftBubbleBg ?? UIImage.ne_imageNamed(name: "chat_message_receive") bubbleImageLeft.image = image? - .resizableImage(withCapInsets: UIEdgeInsets(top: 35, left: 25, bottom: 10, right: 25)) + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) bubbleImageLeft.translatesAutoresizingMaskIntoConstraints = false bubbleImageLeft.isUserInteractionEnabled = true bubbleImageRight.backgroundColor = NEKitChatConfig.shared.ui.messageProperties.selfMessageBg image = NEKitChatConfig.shared.ui.messageProperties.rightBubbleBg ?? UIImage.ne_imageNamed(name: "chat_message_send") bubbleImageRight.image = image? - .resizableImage(withCapInsets: UIEdgeInsets(top: 35, left: 25, bottom: 10, right: 25)) + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) bubbleImageRight.translatesAutoresizingMaskIntoConstraints = false bubbleImageRight.isUserInteractionEnabled = true @@ -142,30 +166,56 @@ open class NEBaseChatMessageCell: NEChatBaseCell { pinLabelLeft.font = UIFont.systemFont(ofSize: 12) pinLabelLeft.textAlignment = .left pinLabelLeft.lineBreakMode = .byTruncatingMiddle + pinLabelLeft.accessibilityIdentifier = "id.signal" pinLabelRight.translatesAutoresizingMaskIntoConstraints = false pinLabelRight.textColor = UIColor.ne_greenText pinLabelRight.font = UIFont.systemFont(ofSize: 12) pinLabelRight.textAlignment = .right pinLabelRight.lineBreakMode = .byTruncatingMiddle + pinLabelRight.accessibilityIdentifier = "id.signal" pinImageLeft.translatesAutoresizingMaskIntoConstraints = false pinImageLeft.contentMode = .scaleAspectFit pinImageRight.translatesAutoresizingMaskIntoConstraints = false pinImageRight.contentMode = .scaleAspectFit + + readView.translatesAutoresizingMaskIntoConstraints = false + readView.accessibilityIdentifier = "id.readView" + + activityView.accessibilityIdentifier = "id.status" + + seletedBtn.translatesAutoresizingMaskIntoConstraints = false + seletedBtn.setImage(.ne_imageNamed(name: "unselect"), for: .normal) + seletedBtn.setImage(.ne_imageNamed(name: "select"), for: .selected) + seletedBtn.addTarget(self, action: #selector(selectButtonClicked), for: .touchUpInside) } open func baseCommonUI() { - baseCommonUIRight() + selectionStyle = .none + backgroundColor = .clear + + // time + contentView.addSubview(timeLabel) + timeLabelHeightAnchor = timeLabel.heightAnchor.constraint(equalToConstant: chat_timeCellH) + NSLayoutConstraint.activate([ + timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), + timeLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0), + timeLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -0), + timeLabelHeightAnchor!, + ]) + baseCommonUILeft() + baseCommonUIRight() } open func baseCommonUILeft() { contentView.addSubview(avatarImageLeft) + avatarImageLeftAnchor = avatarImageLeft.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16) NSLayoutConstraint.activate([ - avatarImageLeft.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4), - avatarImageLeft.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + avatarImageLeft.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: chat_content_margin), + avatarImageLeftAnchor!, avatarImageLeft.widthAnchor.constraint(equalToConstant: 32), avatarImageLeft.heightAnchor.constraint(equalToConstant: 32), ]) @@ -200,10 +250,11 @@ open class NEBaseChatMessageCell: NEChatBaseCell { ]) contentView.addSubview(pinLabelLeft) + pinLabelLeftTopAnchor = pinLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.bottomAnchor, constant: 4) pinLabelHLeft = pinLabelLeft.heightAnchor.constraint(equalToConstant: 0) - pinLabelWLeft = pinLabelLeft.widthAnchor.constraint(equalToConstant: 210) + pinLabelWLeft = pinLabelLeft.widthAnchor.constraint(equalToConstant: pinLabelMaxWidth) NSLayoutConstraint.activate([ - pinLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.bottomAnchor, constant: 4), + pinLabelLeftTopAnchor!, pinLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: 14), pinLabelWLeft!, pinLabelHLeft!, @@ -218,15 +269,12 @@ open class NEBaseChatMessageCell: NEChatBaseCell { } open func baseCommonUIRight() { - selectionStyle = .none - backgroundColor = .clear - contentView.addSubview(avatarImageRight) NSLayoutConstraint.activate([ avatarImageRight.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), avatarImageRight.widthAnchor.constraint(equalToConstant: 32), avatarImageRight.heightAnchor.constraint(equalToConstant: 32), - avatarImageRight.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4), + avatarImageRight.topAnchor.constraint(equalTo: avatarImageLeft.topAnchor, constant: 0), ]) contentView.addSubview(nameLabelRight) @@ -241,7 +289,7 @@ open class NEBaseChatMessageCell: NEChatBaseCell { bubbleWRight = bubbleImageRight.widthAnchor.constraint(equalToConstant: bubbleWidth) bubbleHRight = bubbleImageRight.heightAnchor.constraint(equalToConstant: bubbleWidth) NSLayoutConstraint.activate([ - bubbleImageRight.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 4), + bubbleImageRight.topAnchor.constraint(equalTo: avatarImageRight.topAnchor, constant: 0), bubbleImageRight.rightAnchor.constraint(equalTo: avatarImageRight.leftAnchor, constant: -chat_content_margin), bubbleWRight!, bubbleHRight!, @@ -260,7 +308,6 @@ open class NEBaseChatMessageCell: NEChatBaseCell { // readView contentView.addSubview(readView) - readView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ readView.rightAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: -chat_content_margin), readView.bottomAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: 0), @@ -270,7 +317,6 @@ open class NEBaseChatMessageCell: NEChatBaseCell { // seletedBtn contentView.addSubview(seletedBtn) - seletedBtn.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ seletedBtn.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), seletedBtn.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), @@ -279,10 +325,11 @@ open class NEBaseChatMessageCell: NEChatBaseCell { ]) contentView.addSubview(pinLabelRight) + pinLabelRightTopAnchor = pinLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: 4) pinLabelHRight = pinLabelRight.heightAnchor.constraint(equalToConstant: 0) pinLabelWRight = pinLabelRight.widthAnchor.constraint(equalToConstant: 210) NSLayoutConstraint.activate([ - pinLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: 4), + pinLabelRightTopAnchor!, pinLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: 0), pinLabelWRight!, pinLabelHRight!, @@ -298,14 +345,15 @@ open class NEBaseChatMessageCell: NEChatBaseCell { open func addGesture() { // avatar - let tapRight = UITapGestureRecognizer(target: self, action: #selector(tapAvatar)) - tapRight.cancelsTouchesInView = false - avatarImageRight.addGestureRecognizer(tapRight) - let tapLeft = UITapGestureRecognizer(target: self, action: #selector(tapAvatar)) - tapLeft.cancelsTouchesInView = false - avatarImageLeft.addGestureRecognizer(tapLeft) + let avatarTapRight = UITapGestureRecognizer(target: self, action: #selector(tapAvatar)) + avatarTapRight.cancelsTouchesInView = false + avatarImageRight.addGestureRecognizer(avatarTapRight) + let avatarTapLeft = UITapGestureRecognizer(target: self, action: #selector(tapAvatar)) + avatarTapLeft.cancelsTouchesInView = false + avatarImageLeft.addGestureRecognizer(avatarTapLeft) let avatarLongGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressAvatar)) + avatarLongGesture.cancelsTouchesInView = false avatarImageLeft.addGestureRecognizer(avatarLongGesture) let messageTapRight = UITapGestureRecognizer(target: self, action: #selector(tapMessage)) @@ -315,18 +363,13 @@ open class NEBaseChatMessageCell: NEChatBaseCell { messageTapLeft.cancelsTouchesInView = false bubbleImageLeft.addGestureRecognizer(messageTapLeft) - let messageLongPressRight = UILongPressGestureRecognizer( - target: self, - action: #selector(longPress) - ) + let messageLongPressRight = UILongPressGestureRecognizer(target: self, action: #selector(longPress)) bubbleImageRight.addGestureRecognizer(messageLongPressRight) - let messageLongPressLeft = UILongPressGestureRecognizer( - target: self, - action: #selector(longPress) - ) + let messageLongPressLeft = UILongPressGestureRecognizer(target: self, action: #selector(longPress)) bubbleImageLeft.addGestureRecognizer(messageLongPressLeft) let tapReadView = UITapGestureRecognizer(target: self, action: #selector(tapReadView)) + tapReadView.cancelsTouchesInView = false readView.addGestureRecognizer(tapReadView) tapGesture = tapReadView } @@ -392,12 +435,28 @@ open class NEBaseChatMessageCell: NEChatBaseCell { delegate?.didTapReadView(self, contentModel) } + open func selectButtonClicked() { + seletedBtn.isSelected = !seletedBtn.isSelected + if let model = contentModel { + model.isSelected = !model.isSelected + } + delegate?.didTapSelectButton(self, contentModel) + } + // MARK: set data + open func setSelect(_ model: MessageContentModel, _ enableSelect: Bool = false) { + // 多选框 + seletedBtn.isHidden = model.isRevoked || !enableSelect + seletedBtn.isSelected = model.isSelected + avatarImageLeftAnchor?.constant = enableSelect ? 42 : 16 + } + override open func setModel(_ model: MessageContentModel) { - guard let isSend = model.message?.isOutgoingMsg else { - return - } + setModel(model, model.message?.isOutgoingMsg ?? false) + } + + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { let bubbleW = isSend ? bubbleWRight : bubbleWLeft let bubbleH = isSend ? bubbleHRight : bubbleHLeft let nameLabel = isSend ? nameLabelRight : nameLabelLeft @@ -408,12 +467,23 @@ open class NEBaseChatMessageCell: NEChatBaseCell { showLeftOrRight(showRight: isSend) updatePinStatus(model) + // time + if let time = model.timeContent, !time.isEmpty { + timeLabelHeightAnchor?.constant = chat_timeCellH + timeLabel.text = time + timeLabel.isHidden = false + } else { + timeLabelHeightAnchor?.constant = 0 + timeLabel.text = "" + timeLabel.isHidden = true + } + bubbleW?.constant = model.contentSize.width bubbleH?.constant = model.contentSize.height // avatar nameLabel.text = model.shortName - if let avatarURL = model.avatar { + if let avatarURL = model.avatar, !avatarURL.isEmpty { avatarImage .sd_setImage(with: URL(string: avatarURL)) { image, error, type, url in if image != nil { @@ -445,19 +515,21 @@ open class NEBaseChatMessageCell: NEChatBaseCell { } fullNameH?.constant = CGFloat(model.fullNameHeight) - switch model.message?.deliveryState { - case .delivering: - activityView.messageStatus = .sending - case .deliveried: - // 同一个账号,在多端登录,被对方拉黑,需要根据isBlackListed判断,进而更新信息状态 - if let isBlackMsg = model.message?.isBlackListed, isBlackMsg { + if isSend { + switch model.message?.deliveryState { + case .delivering: + activityView.messageStatus = .sending + case .deliveried: + // 同一个账号,在多端登录,被对方拉黑,需要根据isBlackListed判断,进而更新信息状态 + if let isBlackMsg = model.message?.isBlackListed, isBlackMsg { + activityView.messageStatus = .failed + } else { + activityView.messageStatus = .successed + } + case .failed: activityView.messageStatus = .failed - } else { - activityView.messageStatus = .successed + default: break } - case .failed: - activityView.messageStatus = .failed - default: break } if isSend, model.message?.deliveryState == .deliveried { @@ -524,12 +596,12 @@ open class NEBaseChatMessageCell: NEChatBaseCell { pinLabelLeft.isHidden = showRight fullNameLabel.isHidden = showRight - activityView.isHidden = !showRight avatarImageRight.isHidden = !showRight nameLabelRight.isHidden = !showRight bubbleImageRight.isHidden = !showRight pinImageRight.isHidden = !showRight pinLabelRight.isHidden = !showRight + activityView.isHidden = !showRight readView.isHidden = !showRight } @@ -561,10 +633,10 @@ open class NEBaseChatMessageCell: NEChatBaseCell { let size = String.getTextRectSize( pinLabel.text ?? pinText, font: UIFont.systemFont(ofSize: 12.0), - size: CGSize(width: kScreenWidth - 56 - 22, height: CGFloat.greatestFiniteMagnitude) + size: CGSize(width: pinLabelMaxWidth, height: CGFloat.greatestFiniteMagnitude) ) pinLabelH?.constant = CGFloat(chat_pin_height) - pinLabelW?.constant = size.width + 1 + pinLabelW?.constant = min(size.width + 1, pinLabelMaxWidth) } else { pinImage.image = nil pinLabelH?.constant = 0 diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift index 9ce2e8a9..fa9858c8 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift @@ -7,7 +7,7 @@ import UIKit @objcMembers open class NEBaseChatMessageTipCell: UITableViewCell { - var timeLabelWidthAnchor: NSLayoutConstraint? + var timeLabelHeightAnchor: NSLayoutConstraint? // 消息时间高度约束 override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -21,19 +21,36 @@ open class NEBaseChatMessageTipCell: UITableViewCell { } open func commonUI() { - timeLabel.numberOfLines = 0 contentView.addSubview(timeLabel) + timeLabelHeightAnchor = timeLabel.heightAnchor.constraint(equalToConstant: 22) NSLayoutConstraint.activate([ - timeLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - timeLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), + timeLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + timeLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), + timeLabelHeightAnchor!, + ]) + + contentView.addSubview(contentLabel) + NSLayoutConstraint.activate([ + contentLabel.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 4), + contentLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + contentLabel.widthAnchor.constraint(equalToConstant: chat_content_maxW), ]) - timeLabelWidthAnchor = timeLabel.widthAnchor.constraint(equalToConstant: chat_content_maxW) - timeLabelWidthAnchor?.isActive = true } func setModel(_ model: MessageTipsModel) { - timeLabel.text = model.text - timeLabelWidthAnchor?.constant = model.contentSize.width + // time + if let time = model.timeContent, !time.isEmpty { + timeLabelHeightAnchor?.constant = chat_timeCellH + timeLabel.text = time + timeLabel.isHidden = false + } else { + timeLabelHeightAnchor?.constant = 0 + timeLabel.text = "" + timeLabel.isHidden = true + } + + contentLabel.text = model.text } public lazy var timeLabel: UILabel = { @@ -45,4 +62,15 @@ open class NEBaseChatMessageTipCell: UITableViewCell { label.accessibilityIdentifier = "id.messageTipText" return label }() + + public lazy var contentLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.timeTextSize) + label.textColor = NEKitChatConfig.shared.ui.messageProperties.timeTextColor + label.textAlignment = .center + label.numberOfLines = 0 + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.messageTipText" + return label + }() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift index ec5223c2..334d7de9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift @@ -27,7 +27,7 @@ open class NEBaseChatTeamMemberCell: UITableViewCell { return label }() - override public func setSelected(_ selected: Bool, animated: Bool) { + override open func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) // Configure the view for the selected state @@ -61,7 +61,7 @@ open class NEBaseChatTeamMemberCell: UITableViewCell { } open func configure(_ model: ChatTeamMemberInfoModel) { - if let url = model.nimUser?.userInfo?.avatarUrl { + if let url = model.nimUser?.userInfo?.avatarUrl, !url.isEmpty { headerView.sd_setImage(with: URL(string: url), completed: nil) headerView.setTitle("") } else { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift index 6021a110..8d059d2e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift @@ -31,17 +31,6 @@ open class NEBaseUserSettingCell: CornerCell { super.init(coder: coder) } - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } - open func configure(_ anyModel: Any) { if let m = anyModel as? UserSettingCellModel { model = m diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSwitchCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSwitchCell.swift index 3058f94a..8dc4b4c0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSwitchCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSwitchCell.swift @@ -15,17 +15,6 @@ open class NEBaseUserSettingSwitchCell: NEBaseUserSettingCell { return q }() - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } - override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift index ad08fbb3..86a43d98 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift @@ -23,6 +23,8 @@ open class OperationCell: UICollectionViewCell { override public init(frame: CGRect) { super.init(frame: frame) + contentView.accessibilityIdentifier = "id.menuCell" + imageView.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(imageView) imageView.contentMode = .center diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift index b33504d0..a5ce8d4d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift @@ -10,6 +10,8 @@ open class NEBasePinMessageAudioCell: NEBasePinMessageCell { var audioTimeLabel = UILabel() public var bubbleImage = UIImageView() + public var isPlaying = false + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } @@ -23,7 +25,7 @@ open class NEBasePinMessageAudioCell: NEBasePinMessageCell { let image = NEKitChatConfig.shared.ui.messageProperties.leftBubbleBg ?? UIImage.ne_imageNamed(name: "chat_message_receive") bubbleImage.image = image? - .resizableImage(withCapInsets: UIEdgeInsets(top: 35, left: 25, bottom: 10, right: 25)) + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) bubbleImage.translatesAutoresizingMaskIntoConstraints = false bubbleImage.isUserInteractionEnabled = true backView.addSubview(bubbleImage) @@ -45,8 +47,14 @@ open class NEBasePinMessageAudioCell: NEBasePinMessageCell { audioImageView.widthAnchor.constraint(equalToConstant: 28), audioImageView.heightAnchor.constraint(equalToConstant: 28), ]) + audioImageView.animationDuration = 1 + if let leftImage1 = UIImage.ne_imageNamed(name: "left_play_1"), + let leftmage2 = UIImage.ne_imageNamed(name: "left_play_2"), + let leftmage3 = UIImage.ne_imageNamed(name: "left_play_3") { + audioImageView.animationImages = [leftImage1, leftmage2, leftmage3] + } - audioTimeLabel.font = UIFont.systemFont(ofSize: 14) + audioTimeLabel.font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) audioTimeLabel.textColor = UIColor.ne_darkText audioTimeLabel.textAlignment = .left audioTimeLabel.translatesAutoresizingMaskIntoConstraints = false @@ -57,12 +65,37 @@ open class NEBasePinMessageAudioCell: NEBasePinMessageCell { audioTimeLabel.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: -12), audioTimeLabel.heightAnchor.constraint(equalToConstant: 28), ]) + + if let gesture = contentGesture { + bubbleImage.addGestureRecognizer(gesture) + } } - override public func configure(_ item: PinMessageModel) { + override open func configure(_ item: PinMessageModel) { super.configure(item) if let m = item.chatmodel as? MessageAudioModel { audioTimeLabel.text = "\(m.duration)" + "s" + m.isPlaying == true ? startAnimation() : stopAnimation() + } + } + + open func startAnimation() { + if !audioImageView.isAnimating { + audioImageView.startAnimating() + } + if let m = pinModel?.chatmodel as? MessageAudioModel { + m.isPlaying = true + isPlaying = true + } + } + + open func stopAnimation() { + if audioImageView.isAnimating { + audioImageView.stopAnimating() + } + if let m = pinModel?.chatmodel as? MessageAudioModel { + m.isPlaying = false + isPlaying = false } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift index e7eab21f..1bdb169c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift @@ -10,6 +10,7 @@ import UIKit @objc public protocol PinMessageCellDelegate { func didClickMore(_ model: PinMessageModel?) + func didClickContent(_ model: PinMessageModel?, _ cell: NEBasePinMessageCell) } @objcMembers @@ -22,6 +23,8 @@ open class NEBasePinMessageCell: UITableViewCell { public var delegate: PinMessageCellDelegate? + public var contentGesture: UITapGestureRecognizer? + lazy var headerView: NEUserHeaderView = { let header = NEUserHeaderView(frame: .zero) header.titleLabel.font = NEConstant.defaultTextFont(12) @@ -29,7 +32,6 @@ open class NEBasePinMessageCell: UITableViewCell { header.layer.cornerRadius = 16 header.clipsToBounds = true header.translatesAutoresizingMaskIntoConstraints = false - header.accessibilityIdentifier = "id.avatar" return header }() @@ -66,6 +68,7 @@ open class NEBasePinMessageCell: UITableViewCell { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none backgroundColor = .clear + contentGesture = UITapGestureRecognizer(target: self, action: #selector(contentClick)) setupUI() } @@ -153,20 +156,24 @@ open class NEBasePinMessageCell: UITableViewCell { line.backgroundColor = .ne_greyLine } - public func configure(_ item: PinMessageModel) { + open func configure(_ item: PinMessageModel) { pinModel = item - headerView.configHeadData(headUrl: item.chatmodel?.avatar, - name: item.chatmodel?.fullName ?? "", - uid: item.chatmodel?.message?.from ?? "") - nameLabel.text = item.chatmodel?.fullName + headerView.configHeadData(headUrl: item.chatmodel.avatar, + name: item.chatmodel.fullName ?? "", + uid: item.chatmodel.message?.from ?? "") + nameLabel.text = item.chatmodel.fullName print("config time : ", item.message.timestamp) timeLabel.text = String.stringFromDate(date: Date(timeIntervalSince1970: item.message.timestamp)) - contentWidth?.constant = item.chatmodel?.contentSize.width ?? 0 - contentHeight?.constant = item.chatmodel?.contentSize.height ?? 0 + contentWidth?.constant = item.chatmodel.contentSize.width + contentHeight?.constant = item.chatmodel.contentSize.height } func moreClick() { delegate?.didClickMore(pinModel) } + + open func contentClick() { + delegate?.didClickContent(pinModel, self) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift index 8740a3ca..524b562a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift @@ -29,7 +29,7 @@ open class NEBasePinMessageDefaultCell: NEBasePinMessageTextCell { super.setupUI() } - override public func configure(_ item: PinMessageModel) { + override open func configure(_ item: PinMessageModel) { super.configure(item) contentLabel.text = chatLocalizable("unkonw_pin_message") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift index cad66392..edab5229 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift @@ -7,6 +7,13 @@ import UIKit @objcMembers open class NEBasePinMessageFileCell: NEBasePinMessageCell { + public lazy var stateView: FileStateView = { + let state = FileStateView() + state.translatesAutoresizingMaskIntoConstraints = false + state.backgroundColor = .clear + return state + }() + public var bubbleImage = UIImageView() lazy var imgView: UIImageView = { @@ -95,7 +102,7 @@ open class NEBasePinMessageFileCell: NEBasePinMessageCell { bubbleImage.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), ]) - backView.addSubview(imgView) + bubbleImage.addSubview(imgView) NSLayoutConstraint.activate([ imgView.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: 10), imgView.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: 10), @@ -103,18 +110,43 @@ open class NEBasePinMessageFileCell: NEBasePinMessageCell { imgView.heightAnchor.constraint(equalToConstant: 32), ]) - backView.addSubview(labelView) + bubbleImage.addSubview(labelView) NSLayoutConstraint.activate([ labelView.leftAnchor.constraint(equalTo: imgView.rightAnchor, constant: 15), labelView.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: 10), labelView.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: -10), labelView.bottomAnchor.constraint(equalTo: bubbleImage.bottomAnchor, constant: 0), ]) + + bubbleImage.addSubview(stateView) + NSLayoutConstraint.activate([ + stateView.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: 10), + stateView.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: 10), + stateView.widthAnchor.constraint(equalToConstant: 32), + stateView.heightAnchor.constraint(equalToConstant: 32), + ]) + + if let gesture = contentGesture { + bubbleImage.addGestureRecognizer(gesture) + } } - override public func configure(_ item: PinMessageModel) { + override open func configure(_ item: PinMessageModel) { super.configure(item) if let fileObject = item.message.messageObject as? NIMFileObject { + if let fileModel = item.pinFileModel { + fileModel.cell = self + if fileModel.state == .Success { + stateView.state = .FileOpen + } else { + stateView.state = .FileDownload + stateView.setProgress(fileModel.progress) + if fileModel.progress >= 1 { + fileModel.state = .Success + } + } + } + var imageName = "file_unknown" var displayName = "未知文件" if let filePath = fileObject.path as? NSString { @@ -165,4 +197,8 @@ open class NEBasePinMessageFileCell: NEBasePinMessageCell { sizeLabel.text = size_str } } + + open func uploadProgress(progress: Float) { + stateView.setProgress(progress) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift index 29ca04ce..23198d9d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift @@ -15,8 +15,6 @@ open class NEBasePinMessageImageCell: NEBasePinMessageCell { override open func setSelected(_ selected: Bool, animated: Bool) { super.setSelected(selected, animated: animated) - - // Configure the view for the selected state } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -32,6 +30,7 @@ open class NEBasePinMessageImageCell: NEBasePinMessageCell { contentImageView.translatesAutoresizingMaskIntoConstraints = false contentImageView.contentMode = .scaleAspectFill contentImageView.clipsToBounds = true + contentImageView.isUserInteractionEnabled = true contentImageView.addCustomCorner( conrners: [.bottomLeft, .bottomRight, .topRight, .topLeft], radius: 8, @@ -46,9 +45,13 @@ open class NEBasePinMessageImageCell: NEBasePinMessageCell { contentWidth?.isActive = true contentHeight = contentImageView.heightAnchor.constraint(equalToConstant: 0) contentHeight?.isActive = true + + if let gesture = contentGesture { + contentImageView.addGestureRecognizer(gesture) + } } - override public func configure(_ item: PinMessageModel) { + override open func configure(_ item: PinMessageModel) { super.configure(item) if let m = item.chatmodel as? MessageImageModel, let imageUrl = m.imageUrl { @@ -61,7 +64,8 @@ open class NEBasePinMessageImageCell: NEBasePinMessageCell { completed: nil ) } else { - contentImageView.image = UIImage(contentsOfFile: imageUrl) + let url = URL(fileURLWithPath: imageUrl) + contentImageView.sd_setImage(with: url) } } else { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift index 824913af..517a4488 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift @@ -118,9 +118,13 @@ open class NEBasePinMessageLocationCell: NEBasePinMessageCell { emptyLabel.bottomAnchor.constraint(equalTo: back.bottomAnchor, constant: -40), ]) } + mapView?.isUserInteractionEnabled = false + if let gesture = contentGesture { + back.addGestureRecognizer(gesture) + } } - override public func configure(_ item: PinMessageModel) { + override open func configure(_ item: PinMessageModel) { super.configure(item) if let m = item.chatmodel as? MessageLocationModel { locationTitleLabel.text = m.title diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift new file mode 100644 index 00000000..4498509b --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift @@ -0,0 +1,191 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NIMSDK +import UIKit + +@objcMembers +open class NEBasePinMessageMultiForwardCell: NEBasePinMessageCell { + public let funMargin: CGFloat = 5.2 + let contentW: CGFloat = 248 + var titleLabelFontSize: CGFloat = 14 + var contentLabelFontSize: CGFloat = 14 + var contentLabelColor: UIColor = .ne_lightText + + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func setupUI() { + super.setupUI() +// let image = UIImage.ne_imageNamed(name: "chat_message_receive") +// backViewLeft.image = image? +// .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) + backViewLeft.layer.cornerRadius = 8 + backViewLeft.layer.borderColor = multiForwardborderColor.cgColor + backViewLeft.layer.borderWidth = 1 + + backView.addSubview(backViewLeft) + NSLayoutConstraint.activate([ + backViewLeft.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + backViewLeft.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), + backViewLeft.widthAnchor.constraint(equalToConstant: 276), +// backViewLeft.heightAnchor.constraint(equalToConstant: 130), + backViewLeft.heightAnchor.constraint(equalToConstant: 100), + ]) + + if let gesture = contentGesture { + backViewLeft.addGestureRecognizer(gesture) + } + } + + override open func configure(_ item: PinMessageModel) { + super.configure(item) + guard let data = NECustomAttachment.dataOfCustomMessage(message: item.chatmodel.message) else { + return + } + + let font = UIFont.systemFont(ofSize: contentLabelFontSize) + let titleLabel = titleLabelLeft1 + let titleLabel2 = titleLabelLeft2 + let contentLabel1 = contentLabelLeft1 + let contentLabel2 = contentLabelLeft2 + let contentLabel3 = contentLabelLeft3 + + if let sessionName = data["sessionName"] as? String { + titleLabel.attributedText = + NEEmotionTool.getAttWithStr(str: sessionName, + font: .systemFont(ofSize: titleLabelFontSize), + color: .ne_darkText) + } else { + titleLabel2.text = chatLocalizable("chat_history") + } + + guard let abstracts = data["abstracts"] as? [[String: Any]] else { return } + + contentLabel2.attributedText = nil + contentLabel3.attributedText = nil + for i in 0 ..< abstracts.count { + var contentLabel = contentLabel1 + if i == 1 { + contentLabel = contentLabel2 + } else if i == 2 { + contentLabel = contentLabel3 + } + + var contentText = "" + if var senderNick = abstracts[i]["senderNick"] as? String { + if senderNick.count > 5 { + // 截取字符串 abcdefg -> ab...fg + let leftEndIndex = senderNick.index(senderNick.startIndex, offsetBy: 2) + let rightStartIndex = senderNick.index(senderNick.endIndex, offsetBy: -2) + senderNick = senderNick[senderNick.startIndex ..< leftEndIndex] + "..." + senderNick[rightStartIndex ..< senderNick.endIndex] + } + contentText = senderNick + if let content = abstracts[i]["content"] as? String { + contentText += ":" + content + } + } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 1 // 设置行间距 + paragraphStyle.lineBreakMode = .byTruncatingTail + let attributedText = NEEmotionTool.getAttWithStr(str: contentText, + font: font, + color: contentLabelColor) + attributedText.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedText.length)) + contentLabel.attributedText = attributedText + } + + let numCount1 = String.calculateMaxLines(width: contentW, + attributeString: contentLabel1.attributedText, + font: font) + if numCount1 == 1 { + contentLabel2.numberOfLines = 2 + contentLabel2.isHidden = contentLabel2.attributedText == nil + let numCount2 = String.calculateMaxLines(width: contentW, + attributeString: contentLabel2.attributedText, + font: font) + contentLabel3.isHidden = !(contentLabel3.attributedText != nil && numCount2 == 1) + } else if numCount1 == 2 { + contentLabel2.numberOfLines = 1 + contentLabel2.isHidden = contentLabel3.attributedText != nil + contentLabel3.isHidden = true + } else { + contentLabel2.isHidden = true + contentLabel3.isHidden = true + } + } + + // MARK: - lazy load + + public lazy var backViewLeft: UIImageView = { + let view = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = true + return view + }() + + public lazy var titleLabelLeft1: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.name1" + return label + }() + + public lazy var titleLabelLeft2: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = chatLocalizable("chat_history_by") + label.textColor = .ne_darkText + label.font = .systemFont(ofSize: titleLabelFontSize) + label.accessibilityIdentifier = "id.name2" + return label + }() + + public lazy var contentLabelLeft1: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 3 + label.accessibilityIdentifier = "id.content1" + return label + }() + + public lazy var contentLabelLeft2: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 2 + label.accessibilityIdentifier = "id.content2" + return label + }() + + public lazy var contentLabelLeft3: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.content3" + return label + }() + +// public lazy var dividerLineLeft: UIView = { +// let line = UIView() +// line.translatesAutoresizingMaskIntoConstraints = false +// line.backgroundColor = multiForwardLineColor +// return line +// }() +// +// public lazy var contentHistoryLeft: UILabel = { +// let label = UILabel() +// label.translatesAutoresizingMaskIntoConstraints = false +// label.font = .systemFont(ofSize: 12) +// label.textColor = .ne_lightText +// label.text = chatLocalizable("chat_history") +// label.accessibilityIdentifier = "id.contentHistoryLeft" +// return label +// }() +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift new file mode 100644 index 00000000..539049e7 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift @@ -0,0 +1,57 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open class NEBasePinMessageRichTextCell: NEBasePinMessageTextCell { + lazy var titleLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) + label.textColor = .ne_darkText + label.translatesAutoresizingMaskIntoConstraints = false + label.isUserInteractionEnabled = true + label.numberOfLines = 1 + return label + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func commonUI() { + backView.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: line.leftAnchor), + titleLabel.rightAnchor.constraint(equalTo: line.rightAnchor), + titleLabel.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), + ]) + + contentLabel.numberOfLines = 2 + backView.addSubview(contentLabel) + NSLayoutConstraint.activate([ + contentLabel.leftAnchor.constraint(equalTo: line.leftAnchor), + contentLabel.rightAnchor.constraint(equalTo: line.rightAnchor), + contentLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 1), + ]) + + if let gesture = contentGesture { + contentLabel.addGestureRecognizer(gesture) + } + + let titleGesture = UITapGestureRecognizer(target: self, action: #selector(contentClick)) + titleLabel.addGestureRecognizer(titleGesture) + } + + override open func configure(_ item: PinMessageModel) { + super.configure(item) + if let model = item.chatmodel as? MessageRichTextModel { + titleLabel.attributedText = model.titleAttributeStr + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift index 161c0324..74fcbc82 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift @@ -8,9 +8,10 @@ import UIKit open class NEBasePinMessageTextCell: NEBasePinMessageCell { lazy var contentLabel: UILabel = { let label = UILabel() - label.font = UIFont.systemFont(ofSize: 14.0) + label.font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) label.textColor = .ne_darkText label.translatesAutoresizingMaskIntoConstraints = false + label.isUserInteractionEnabled = true label.numberOfLines = 3 return label }() @@ -38,6 +39,10 @@ open class NEBasePinMessageTextCell: NEBasePinMessageCell { override open func setupUI() { super.setupUI() + commonUI() + } + + open func commonUI() { replyLabel.font = UIFont.systemFont(ofSize: 12) replyLabel.textColor = UIColor(hexString: "#929299") replyLabel.translatesAutoresizingMaskIntoConstraints = false @@ -54,9 +59,12 @@ open class NEBasePinMessageTextCell: NEBasePinMessageCell { contentLabel.rightAnchor.constraint(equalTo: line.rightAnchor), contentLabel.topAnchor.constraint(equalTo: replyLabel.bottomAnchor, constant: 1), ]) + if let gesture = contentGesture { + contentLabel.addGestureRecognizer(gesture) + } } - override public func configure(_ item: PinMessageModel) { + override open func configure(_ item: PinMessageModel) { super.configure(item) if let model = item.chatmodel as? MessageTextModel { contentLabel.attributedText = model.attributeStr diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift index 402b8c74..26b70bc1 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift @@ -47,7 +47,7 @@ open class NEBasePinMessageVideoCell: NEBasePinMessageImageCell { super.init(coder: coder) } - override public func setupUI() { + override open func setupUI() { super.setupUI() contentImageView.addSubview(stateView) contentImageView.addCustomCorner(conrners: [.topLeft], radius: 8, backcolor: .white) @@ -67,10 +67,10 @@ open class NEBasePinMessageVideoCell: NEBasePinMessageImageCell { stateView.isUserInteractionEnabled = false } - override public func configure(_ item: PinMessageModel) { + override open func configure(_ item: PinMessageModel) { super.configure(item) - if let videoObject = item.chatmodel?.message?.messageObject as? NIMVideoObject { + if let videoObject = item.chatmodel.message?.messageObject as? NIMVideoObject { if let path = videoObject.coverUrl { contentImageView.sd_setImage( with: URL(string: path), diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift index 46d6991a..93483e69 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift @@ -15,6 +15,7 @@ open class UserBaseTableViewCell: UITableViewCell { avatarImage.clipsToBounds = true avatarImage.isUserInteractionEnabled = true avatarImage.contentMode = .scaleAspectFill + avatarImage.accessibilityIdentifier = "id.avatar" return avatarImage }() @@ -25,6 +26,7 @@ open class UserBaseTableViewCell: UITableViewCell { nameLabel.font = UIFont.systemFont(ofSize: 12) nameLabel.textColor = .white nameLabel.text = "placeholder" + nameLabel.accessibilityIdentifier = "id.avatar" return nameLabel }() @@ -32,10 +34,11 @@ open class UserBaseTableViewCell: UITableViewCell { let titleLabel = UILabel() titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.text = "placeholder" + titleLabel.accessibilityIdentifier = "id.nickname" return titleLabel }() - public var userModel: User? + public var userModel: NEKitUser? override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -67,12 +70,12 @@ open class UserBaseTableViewCell: UITableViewCell { titleLabel.text = "placeholder" } - open func setModel(_ model: User) { + open func setModel(_ model: NEKitUser) { userModel = model nameLabel.text = model.shortName(showAlias: false, count: 2) titleLabel.text = model.showName() - if let avatarURL = model.userInfo?.avatarUrl { + if let avatarURL = model.userInfo?.avatarUrl, !avatarURL.isEmpty { avatarImage .sd_setImage(with: URL(string: avatarURL)) { [weak self] image, error, type, url in if image != nil { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift index 54bb3d74..d61d5336 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift @@ -13,7 +13,7 @@ public enum ChatSendMessageStatus: Int { } @objcMembers -public class ChatActivityIndicatorView: UIView { +open class ChatActivityIndicatorView: UIView { public var messageStatus: ChatSendMessageStatus? { didSet { failBtn.isHidden = true @@ -44,7 +44,7 @@ public class ChatActivityIndicatorView: UIView { commonUI() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift index 3e2a623a..a60b7cf4 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift @@ -6,13 +6,13 @@ import UIKit @objcMembers -public class ChatBrokenNetworkView: UIView { +open class ChatBrokenNetworkView: UIView { override init(frame: CGRect) { super.init(frame: frame) commonUI() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -32,7 +32,7 @@ public class ChatBrokenNetworkView: UIView { label.font = DefaultTextFont(14) label.textColor = HexRGB(0xFC596A) label.textAlignment = .center - label.text = chatLocalizable("network_unavailable") + label.text = commonLocalizable("network_error") return label }() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatRecordView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatRecordView.swift index 7a4c1c6b..87118f70 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatRecordView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatRecordView.swift @@ -14,7 +14,7 @@ public protocol ChatRecordViewDelegate: NSObjectProtocol { } @objcMembers -public class ChatRecordView: UIView, UIGestureRecognizerDelegate { +open class ChatRecordView: UIView, UIGestureRecognizerDelegate { var recordImageView = UIImageView() var topTipLabel = UILabel() var tipLabel = UILabel() @@ -25,7 +25,7 @@ public class ChatRecordView: UIView, UIGestureRecognizerDelegate { commonUI() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -99,7 +99,7 @@ public class ChatRecordView: UIView, UIGestureRecognizerDelegate { } } - public func stopRecordAnimation() { + open func stopRecordAnimation() { topTipLabel.isHidden = true recordImageView.stopAnimating() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift index a711a9df..df1b9fb5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift @@ -6,7 +6,7 @@ import UIKit @objcMembers -public class CirleProgressView: UIView { +open class CirleProgressView: UIView { // 0~1 public var progress: Float = 0 { didSet { @@ -40,7 +40,6 @@ public class CirleProgressView: UIView { override init(frame: CGRect) { super.init(frame: frame) backgroundColor = .clear - accessibilityIdentifier = "id.status" imageView.frame = bounds imageView.contentMode = .center addSubview(imageView) @@ -64,7 +63,7 @@ public class CirleProgressView: UIView { layer.addSublayer(sectorLayer) } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift index 03f74cdf..19b1b58c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift @@ -11,7 +11,7 @@ public protocol MessageOperationViewDelegate: AnyObject { } @objcMembers -public class MessageOperationView: UIView, UICollectionViewDataSource, UICollectionViewDelegate { +open class MessageOperationView: UIView, UICollectionViewDataSource, UICollectionViewDelegate { var collcetionView: UICollectionView public weak var delegate: MessageOperationViewDelegate? public var items = [OperationItem]() { @@ -56,19 +56,19 @@ public class MessageOperationView: UIView, UICollectionViewDataSource, UICollect addSubview(collcetionView) } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } // MARK: UICollectionViewDataSource - public func collectionView(_ collectionView: UICollectionView, - numberOfItemsInSection section: Int) -> Int { + open func collectionView(_ collectionView: UICollectionView, + numberOfItemsInSection section: Int) -> Int { items.count } - public func collectionView(_ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + open func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell( withReuseIdentifier: "\(OperationCell.self)", for: indexPath @@ -80,8 +80,8 @@ public class MessageOperationView: UIView, UICollectionViewDataSource, UICollect // MARK: UICollectionViewDelegate - public func collectionView(_ collectionView: UICollectionView, - didSelectItemAt indexPath: IndexPath) { + open func collectionView(_ collectionView: UICollectionView, + didSelectItemAt indexPath: IndexPath) { removeFromSuperview() delegate?.didSelectedItem(item: items[indexPath.row]) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift index bd6ce8e3..4d054363 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import NECommonKit +import NECommonUIKit import NECoreKit import UIKit @@ -16,14 +17,28 @@ public enum ChatMenuType: Int { case addMore } +@objc +public enum ChatInputMode: Int { + case normal + case multipleSend + case multipleReturn +} + public let yxAtMsg = "yxAitMsg" public let atRangeOffset = 1 public let atSegmentsKey = "segments" public let atTextKey = "text" +public protocol ChatInputMultilineDelegate: NSObject { + func expandButtonDidClick() + func didHideMultipleButtonClick() +} + @objcMembers open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, - InputEmoticonContainerViewDelegate, UITextViewDelegate, NEMoreViewDelegate { + InputEmoticonContainerViewDelegate, UITextViewDelegate, NEMoreViewDelegate, UITextFieldDelegate { + public weak var multipleLineDelegate: ChatInputMultilineDelegate? + public weak var delegate: ChatInputViewDelegate? public var currentType: ChatMenuType = .text public var currentButton: UIButton? @@ -35,6 +50,13 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, public var nickAccidDic = [String: String]() + public var isMultipleLineMode = false // 是否是多行模式 + + public var chatInpuMode = ChatInputMode.normal + + // 换行输入框 标题限制字数 + public var textLimit = 20 + public var textView: NETextView = { let textView = NETextView() textView.placeholderLabel.numberOfLines = 1 @@ -52,6 +74,23 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, return textView }() + lazy var backView: UIView = { + let back = UIView() + back.translatesAutoresizingMaskIntoConstraints = false + back.backgroundColor = .white + back.clipsToBounds = true + back.layer.cornerRadius = 8 + return back + }() + + // 展开按钮 + public var expandButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = .clear + return button + }() + public var stackView = UIStackView() var contentView = UIView() public var contentSubView: UIView? @@ -77,30 +116,44 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, open func commonUI() {} - public func addRecordView() { + open func addRecordView() { currentType = .audio textView.resignFirstResponder() + titleField.resignFirstResponder() contentSubView?.isHidden = true contentSubView = recordView contentSubView?.isHidden = false } - public func addEmojiView() { + open func addEmojiView() { currentType = .emoji textView.resignFirstResponder() + titleField.resignFirstResponder() + contentSubView?.isHidden = true contentSubView = emojiView contentSubView?.isHidden = false } - public func addMoreActionView() { + open func addMoreActionView() { currentType = .addMore textView.resignFirstResponder() + titleField.resignFirstResponder() + contentSubView?.isHidden = true contentSubView = chatAddMoreView contentSubView?.isHidden = false } + open func sendText(textView: NETextView) { + guard let text = getRealSendText(textView.attributedText) else { + return + } + delegate?.sendText(text: text, attribute: textView.attributedText) + textView.text = "" + atCache?.clean() + } + // MARK: ===================== lazy method ===================== public lazy var emojiView: UIView = { @@ -126,27 +179,26 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, return view }() - public func textViewDidChange(_ textView: UITextView) { - delegate?.textFieldDidChange(textView) + open func textViewDidChange(_ textView: UITextView) { + delegate?.textFieldDidChange(textView.text) } - public func textViewDidEndEditing(_ textView: UITextView) { - delegate?.textFieldDidEndEditing(textView) + open func textViewDidEndEditing(_ textView: UITextView) { + delegate?.textFieldDidEndEditing(textView.text) } - public func textViewDidBeginEditing(_ textView: UITextView) { - delegate?.textFieldDidBeginEditing(textView) + open func textViewDidBeginEditing(_ textView: UITextView) { + delegate?.textFieldDidBeginEditing(textView.text) } - public func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { + open func textViewShouldBeginEditing(_ textView: UITextView) -> Bool { currentType = .text return true } - public func checkRemoveAtMessage(range: NSRange, attribute: NSAttributedString) -> NSRange? { + open func checkRemoveAtMessage(range: NSRange, attribute: NSAttributedString) -> NSRange? { var temRange: NSRange? let start = range.location -// let end = range.location + range.length attribute.enumerateAttribute( NSAttributedString.Key.foregroundColor, in: NSMakeRange(0, attribute.length) @@ -161,23 +213,43 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, temRange = NSMakeRange(findRange.location, findRange.length + atRangeOffset) stop.pointee = true } - -// if (findRange.location <= start && start < findRange.location + findRange.length + atRangeOffset) || -// (findRange.location < end && end <= findRange.location + findRange.length + atRangeOffset) { -// temRange = NSMakeRange(findRange.location, findRange.length + atRangeOffset) -// stop.pointee = true -// } } return temRange } - public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, - replacementText text: String) -> Bool { - print("text view range : ", range) - print("select range : ", textView.selectedRange) + // 查找at消息位置并且根据光标位置距离高亮前段或者后端更近判断光标最终显示在前还是在后 + open func findShowPosition(range: NSRange, attribute: NSAttributedString) -> NSRange? { + var showRange: NSRange? + let start = range.location + attribute.enumerateAttribute( + NSAttributedString.Key.foregroundColor, + in: NSMakeRange(0, attribute.length) + ) { value, findRange, stop in + guard let findColor = value as? UIColor else { + return + } + if isEqualToColor(findColor, UIColor.ne_blueText) == false { + return + } + let findStart = findRange.location + let findEnd = findRange.location + findRange.length + atRangeOffset + if findStart <= start, start < findEnd { + if findEnd - start > start - findStart { + showRange = NSMakeRange(findStart, 0) + } else { + showRange = NSMakeRange(findRange.location, findRange.length + atRangeOffset) + } + stop.pointee = true + } + } + return showRange + } + + 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)] - if text == "\n" { + if chatInpuMode == .normal || chatInpuMode == .multipleSend, text == "\n" { guard var realText = getRealSendText(textView.attributedText) else { return true } @@ -189,20 +261,20 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, return false } - if textView.attributedText.length == 0, let pasteString = UIPasteboard.general.string, text.count > 0 { - if pasteString == text { - let muta = NSMutableAttributedString(string: text) - muta.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0)], range: NSMakeRange(0, text.count)) - textView.attributedText = muta - textView.selectedRange = NSMakeRange(text.count - 1, 0) - return false + // 处理粘贴,表情解析(存在表情则字符数量>=3) + if text.count >= 3 { + let mutaString = NSMutableAttributedString(attributedString: textView.attributedText) + let addString = NEEmotionTool.getAttWithStr(str: text, font: .systemFont(ofSize: 16)) + mutaString.replaceCharacters(in: range, with: addString) + textView.attributedText = mutaString + DispatchQueue.main.async { + textView.selectedRange = NSMakeRange(range.location + addString.length, 0) } + return false } if text.count == 0 { -// let selectRange = textView.selectedRange let temRange = checkRemoveAtMessage(range: range, attribute: textView.attributedText) - if let findRange = temRange { let mutableAttri = NSMutableAttributedString(attributedString: textView.attributedText) if mutableAttri.length >= findRange.location + findRange.length { @@ -228,28 +300,27 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, return true } - public func textViewDidChangeSelection(_ textView: UITextView) { - print("textViewDidChangeSelection") + open func textViewDidChangeSelection(_ textView: UITextView) { let range = textView.selectedRange - if let findRange = checkRemoveAtMessage(range: range, attribute: textView.attributedText) { + if let findRange = findShowPosition(range: range, attribute: textView.attributedText) { textView.selectedRange = NSMakeRange(findRange.location + findRange.length, 0) } } @available(iOS 10.0, *) - public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + open func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { print("action : ", interaction) return true } // @available(iOS 10.0, *) -// public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { +// open func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { // // return true // } - public func buttonEvent(button: UIButton) { + open func buttonEvent(button: UIButton) { button.isSelected = !button.isSelected if button.tag - 5 != 2, button != currentButton { currentButton?.isSelected = false @@ -273,7 +344,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, // MARK: NIMInputEmoticonContainerViewDelegate - public func selectedEmoticon(emoticonID: String, emotCatalogID: String, description: String) { + open func selectedEmoticon(emoticonID: String, emotCatalogID: String, description: String) { if emoticonID.isEmpty { // 删除键 textView.deleteBackward() print("delete ward") @@ -309,42 +380,37 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, } } - public func didPressSend(sender: UIButton) { - guard let text = getRealSendText(textView.attributedText) else { - return - } - delegate?.sendText(text: text, attribute: textView.attributedText) - textView.text = "" - atCache?.clean() + open func didPressSend(sender: UIButton) { + sendText(textView: textView) } - public func stopRecordAnimation() { + open func stopRecordAnimation() { greyView.isHidden = true recordView.stopRecordAnimation() } // MARK: NEMoreViewDelagate - public func moreViewDidSelectMoreCell(moreView: NEChatMoreActionView, cell: NEInputMoreCell) { + open func moreViewDidSelectMoreCell(moreView: NEChatMoreActionView, cell: NEInputMoreCell) { delegate?.didSelectMoreCell(cell: cell) } // MARK: ChatRecordViewDelegate - public func startRecord() { + open func startRecord() { greyView.isHidden = false delegate?.startRecord() } - public func moveOutView() { + open func moveOutView() { delegate?.moveOutView() } - public func moveInView() { + open func moveInView() { delegate?.moveInView() } - public func endRecord(insideView: Bool) { + open func endRecord(insideView: Bool) { greyView.isHidden = true delegate?.endRecord(insideView: insideView) } @@ -372,7 +438,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, return muta as String } - public func getRemoteExtension(_ attri: NSAttributedString?) -> [String: Any]? { + open func getRemoteExtension(_ attri: NSAttributedString?) -> [String: Any]? { guard let attribute = attri else { return nil } @@ -425,7 +491,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, return nil } - public func getAtRemoteExtension() -> [String: Any]? { + open func getAtRemoteExtension() -> [String: Any]? { var atDic = [String: Any]() NELog.infoLog(className(), desc: "at range cache : \(atRangeCache)") atRangeCache.forEach { (key: String, value: MessageAtCacheModel) in @@ -451,7 +517,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, return nil } - public func cleartAtCache() { + open func cleartAtCache() { nickAccidDic.removeAll() } @@ -483,4 +549,174 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, func missClickEmoj() { print("click one px space") } + + open func textFieldDidEndEditing(_ textField: UITextField) { + delegate?.textFieldDidChange(textField.text) + } + + open func textFieldDidBeginEditing(_ textField: UITextField) { + currentType = .text + delegate?.textFieldDidBeginEditing(textField.text) + } + + open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if textField == titleField { + if string == "\n" { + var realText = "" + if let text = getRealSendText(textView.attributedText) { + realText = text + } + delegate?.sendText(text: realText, attribute: textView.attributedText) + return false + } + } + + return true + } + + open func textFieldChange() { + delegate?.textFieldDidChange(titleField.text) + if titleField.text?.count ?? 0 <= 0 { + delegate?.titleTextDidClearEmpty() + } + guard let _ = titleField.markedTextRange else { + if let text = titleField.text, + text.utf16.count > textLimit { + titleField.text = String(text.prefix(textLimit)) + } + return + } + } + + public var multipleLineView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.white + view.clipsToBounds = true + view.layer.cornerRadius = 6.0 + view.isHidden = true + + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOpacity = 0.5 + view.layer.shadowOffset = CGSize(width: 3, height: 3) + view.layer.shadowRadius = 5 + view.layer.masksToBounds = false + return view + }() + + public var titleField: UITextField = { + let text = UITextField() + text.translatesAutoresizingMaskIntoConstraints = false + text.font = UIFont.systemFont(ofSize: 18.0) + text.textColor = .ne_darkText + text.returnKeyType = .send + text.attributedPlaceholder = NSAttributedString(string: coreLoader.localizable("multiple_line_placleholder"), attributes: [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText]) + text.addTarget(self, action: #selector(textFieldChange), for: .editingChanged) + return text + }() + + public var multipleLineExpandButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = .clear + return button + }() + + public var multipleSendButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = .clear + button.setImage(coreLoader.loadImage("multiple_send_image"), for: .normal) + return button + }() + + public var multipleLineViewHeight: NSLayoutConstraint? + + func setupMultipleLineView() { + addSubview(multipleLineView) + multipleLineViewHeight = multipleLineView.heightAnchor.constraint(equalToConstant: 400) + NSLayoutConstraint.activate([ + multipleLineViewHeight!, + multipleLineView.leftAnchor.constraint(equalTo: leftAnchor), + multipleLineView.rightAnchor.constraint(equalTo: rightAnchor), + multipleLineView.topAnchor.constraint(equalTo: topAnchor, constant: -4), + ]) + + multipleLineView.addSubview(titleField) + titleField.delegate = self + NSLayoutConstraint.activate([ + titleField.leftAnchor.constraint(equalTo: multipleLineView.leftAnchor, constant: 16), + titleField.rightAnchor.constraint(equalTo: multipleLineView.rightAnchor, constant: -56), + titleField.topAnchor.constraint(equalTo: multipleLineView.topAnchor, constant: 5), + titleField.heightAnchor.constraint(equalToConstant: 40), + ]) + + multipleLineView.addSubview(multipleLineExpandButton) + NSLayoutConstraint.activate([ + multipleLineExpandButton.rightAnchor.constraint(equalTo: multipleLineView.rightAnchor, constant: 0), + multipleLineExpandButton.topAnchor.constraint(equalTo: multipleLineView.topAnchor, constant: 5), + multipleLineExpandButton.widthAnchor.constraint(equalToConstant: 44), + multipleLineExpandButton.heightAnchor.constraint(equalToConstant: 40), + ]) + multipleLineExpandButton.addTarget(self, action: #selector(didClickHideMultipleButton), for: .touchUpInside) + + let dividerLine = UIView() + dividerLine.translatesAutoresizingMaskIntoConstraints = false + dividerLine.backgroundColor = UIColor(hexString: "#ECECEC") + multipleLineView.addSubview(dividerLine) + NSLayoutConstraint.activate([ + dividerLine.leftAnchor.constraint(equalTo: multipleLineView.leftAnchor), + dividerLine.rightAnchor.constraint(equalTo: multipleLineView.rightAnchor), + dividerLine.topAnchor.constraint(equalTo: multipleLineView.topAnchor, constant: 236), + dividerLine.heightAnchor.constraint(equalToConstant: 1), + ]) + + multipleLineView.addSubview(multipleSendButton) + NSLayoutConstraint.activate([ + multipleSendButton.rightAnchor.constraint(equalTo: multipleLineView.rightAnchor, constant: -16), + multipleSendButton.topAnchor.constraint(equalTo: dividerLine.bottomAnchor, constant: 8), + multipleSendButton.widthAnchor.constraint(equalToConstant: 44), + multipleSendButton.heightAnchor.constraint(equalToConstant: 40), + ]) + multipleSendButton.addTarget(self, action: #selector(didClickSendButton), for: .touchUpInside) + } + + open func didClickExpandButton() { + multipleLineDelegate?.expandButtonDidClick() + } + + open func didClickSendButton() { + sendText(textView: textView) + } + + open func didClickHideMultipleButton() { + multipleLineDelegate?.didHideMultipleButtonClick() + } + + open func restoreNormalInputStyle() { + multipleLineView.isHidden = true + if titleField.text?.count ?? 0 > 0 { + chatInpuMode = .multipleSend + } else { + chatInpuMode = .normal + } + isMultipleLineMode = false + } + + open func changeToMultipleLineStyle() { + chatInpuMode = .multipleReturn + isMultipleLineMode = true + multipleLineView.isHidden = false + } + + open func setMuteInputStyle() { + cleartAtCache() + expandButton.isEnabled = false + textView.attributedText = nil + textView.text = nil + } + + open func setUnMuteInputStyle() { + expandButton.isEnabled = true + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift index 803642f0..2498f77e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift @@ -11,7 +11,7 @@ public protocol NEMoreViewDelegate: NSObjectProtocol { } @objcMembers -public class NEChatMoreActionView: UIView { +open class NEChatMoreActionView: UIView { private var sectionCount: Int = 1 private var itemsInSection: Int = 4 // 默认行数 @@ -31,7 +31,7 @@ public class NEChatMoreActionView: UIView { setupConstraints() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -103,15 +103,15 @@ public class NEChatMoreActionView: UIView { } extension NEChatMoreActionView: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { - public func numberOfSections(in collectionView: UICollectionView) -> Int { + open func numberOfSections(in collectionView: UICollectionView) -> Int { sectionCount } - public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { itemsInSection } - public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { let cell = collectionView.dequeueReusableCell( withReuseIdentifier: NEMoreCell_ReuseId, for: indexPath @@ -136,13 +136,13 @@ extension NEChatMoreActionView: UICollectionViewDataSource, UICollectionViewDele return cell ?? UICollectionViewCell() } - public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { + open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if let cell = (collectionView.cellForItem(at: indexPath) as? NEInputMoreCell) { delegate?.moreViewDidSelectMoreCell(moreView: self, cell: cell) } } - public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { + open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize { NEInputMoreCell.getSize() } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift index 564d963a..7f151a83 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift @@ -6,7 +6,7 @@ import UIKit @objcMembers -public class NEInputMoreCell: UICollectionViewCell { +open class NEInputMoreCell: UICollectionViewCell { public var cellData: NEMoreItemModel? override init(frame: CGRect) { @@ -14,7 +14,7 @@ public class NEInputMoreCell: UICollectionViewCell { setupViews() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { super.init(coder: coder) } @@ -51,6 +51,7 @@ public class NEInputMoreCell: UICollectionViewCell { title.font = UIFont.systemFont(ofSize: 10) title.textAlignment = .center title.translatesAutoresizingMaskIntoConstraints = false + title.accessibilityIdentifier = "id.menuIcon" return title }() diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift new file mode 100644 index 00000000..ce90656f --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift @@ -0,0 +1,164 @@ +//// 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 UIKit + +public protocol NEMutilSelectBottomViewDelegate: NSObjectProtocol { + func didClickSingleForwardButton() + func didClickMultiForwardButton() + func didClickDeleteButton() +} + +open class NEMutilSelectBottomView: UIView { + public weak var delegate: NEMutilSelectBottomViewDelegate? + public var buttonTopAnchor: NSLayoutConstraint? + + override public init(frame: CGRect) { + super.init(frame: frame) + setupSubview() + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open func setupSubview() { + // 逐条转发 + addSubview(singleForwardButton) + buttonTopAnchor = singleForwardButton.topAnchor.constraint(equalTo: topAnchor, constant: 12) + NSLayoutConstraint.activate([ + buttonTopAnchor!, + singleForwardButton.centerXAnchor.constraint(equalTo: centerXAnchor), + singleForwardButton.widthAnchor.constraint(equalToConstant: 48), + singleForwardButton.heightAnchor.constraint(equalToConstant: 48), + ]) + + addSubview(singleForwardLabel) + NSLayoutConstraint.activate([ + singleForwardLabel.topAnchor.constraint(equalTo: singleForwardButton.bottomAnchor, constant: 3), + singleForwardLabel.centerXAnchor.constraint(equalTo: singleForwardButton.centerXAnchor), + singleForwardLabel.widthAnchor.constraint(equalToConstant: 48), + singleForwardLabel.heightAnchor.constraint(equalToConstant: 12), + ]) + + // 合并转发 + addSubview(multiForwardButton) + NSLayoutConstraint.activate([ + multiForwardButton.centerYAnchor.constraint(equalTo: singleForwardButton.centerYAnchor), + multiForwardButton.rightAnchor.constraint(equalTo: singleForwardButton.leftAnchor, constant: -52), + multiForwardButton.widthAnchor.constraint(equalToConstant: 48), + multiForwardButton.heightAnchor.constraint(equalToConstant: 48), + ]) + + addSubview(multiForwardLabel) + NSLayoutConstraint.activate([ + multiForwardLabel.centerYAnchor.constraint(equalTo: singleForwardLabel.centerYAnchor), + multiForwardLabel.centerXAnchor.constraint(equalTo: multiForwardButton.centerXAnchor), + multiForwardLabel.widthAnchor.constraint(equalToConstant: 48), + multiForwardLabel.heightAnchor.constraint(equalToConstant: 12), + ]) + + // 删除 + addSubview(deleteButton) + NSLayoutConstraint.activate([ + deleteButton.centerYAnchor.constraint(equalTo: singleForwardButton.centerYAnchor), + deleteButton.leftAnchor.constraint(equalTo: singleForwardButton.rightAnchor, constant: 52), + deleteButton.widthAnchor.constraint(equalToConstant: 48), + deleteButton.heightAnchor.constraint(equalToConstant: 48), + ]) + + addSubview(deleteLabel) + NSLayoutConstraint.activate([ + deleteLabel.centerYAnchor.constraint(equalTo: singleForwardLabel.centerYAnchor), + deleteLabel.centerXAnchor.constraint(equalTo: deleteButton.centerXAnchor), + deleteLabel.widthAnchor.constraint(equalToConstant: 48), + deleteLabel.heightAnchor.constraint(equalToConstant: 12), + ]) + } + + public lazy var singleForwardButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(.ne_imageNamed(name: "select_singleForward"), for: .normal) + button.setImage(.ne_imageNamed(name: "unselect_singleForward"), for: .disabled) + button.addTarget(self, action: #selector(singleForwardButtonAction), for: .touchUpInside) + return button + }() + + public lazy var singleForwardLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = chatLocalizable("select_per_item") + label.textColor = .ne_greyText + label.font = .systemFont(ofSize: 11) + label.textAlignment = .center + return label + }() + + public lazy var multiForwardButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(.ne_imageNamed(name: "select_multiForward"), for: .normal) + button.setImage(.ne_imageNamed(name: "unselect_multiForward"), for: .disabled) + button.addTarget(self, action: #selector(multiForwardButtonAction), for: .touchUpInside) + return button + }() + + public lazy var multiForwardLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = chatLocalizable("select_multi") + label.textColor = .ne_greyText + label.font = .systemFont(ofSize: 11) + label.textAlignment = .center + return label + }() + + public lazy var deleteButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(.ne_imageNamed(name: "select_delete"), for: .normal) + button.setImage(.ne_imageNamed(name: "unselect_delete"), for: .disabled) + button.addTarget(self, action: #selector(deleteButtonAction), for: .touchUpInside) + return button + }() + + public lazy var deleteLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = chatLocalizable("operation_delete") + label.textColor = .ne_greyText + label.font = .systemFont(ofSize: 11) + label.textAlignment = .center + return label + }() + + func setEnable(_ enable: Bool) { + multiForwardButton.isEnabled = enable + singleForwardButton.isEnabled = enable + deleteButton.isEnabled = enable + multiForwardLabel.isEnabled = enable + singleForwardLabel.isEnabled = enable + deleteLabel.isEnabled = enable + } + + func setLabelColor(color: UIColor) { + multiForwardLabel.textColor = color + singleForwardLabel.textColor = color + deleteLabel.textColor = color + } + + @objc func multiForwardButtonAction() { + delegate?.didClickMultiForwardButton() + } + + @objc func singleForwardButtonAction() { + delegate?.didClickSingleForwardButton() + } + + @objc func deleteButtonAction() { + delegate?.didClickDeleteButton() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift index 83d2e18f..380faa5a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift @@ -6,7 +6,7 @@ import UIKit @objcMembers -public class ReplyView: UIView { +open class ReplyView: UIView { var closeButton = UIButton(type: .custom) var line = UIView() var textLabel = UILabel() @@ -16,6 +16,7 @@ public class ReplyView: UIView { backgroundColor = UIColor(hexString: "#EFF1F2") closeButton.setImage(UIImage.ne_imageNamed(name: "close"), for: .normal) closeButton.translatesAutoresizingMaskIntoConstraints = false + closeButton.accessibilityIdentifier = "id.replyClose" // closeButton.addTarget(self, action: #selector(closeButtonEvent), for: .touchUpInside) addSubview(closeButton) NSLayoutConstraint.activate([ @@ -38,6 +39,7 @@ public class ReplyView: UIView { textLabel.font = UIFont.systemFont(ofSize: 12) textLabel.textColor = UIColor(hexString: "#929299") textLabel.translatesAutoresizingMaskIntoConstraints = false + textLabel.accessibilityIdentifier = "id.replyContent" addSubview(textLabel) NSLayoutConstraint.activate([ textLabel.leadingAnchor.constraint(equalTo: closeButton.trailingAnchor, constant: 8), @@ -47,7 +49,7 @@ public class ReplyView: UIView { ]) } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapAddressCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapAddressCell.swift index fd44700b..b60e2c77 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapAddressCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapAddressCell.swift @@ -6,14 +6,14 @@ import NEChatKit import UIKit @objcMembers -public class NEMapAddressCell: UITableViewCell { +open class NEMapAddressCell: UITableViewCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none setupSubviews() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapGuideBottomView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapGuideBottomView.swift index a381cd9a..97f79ba7 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapGuideBottomView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapGuideBottomView.swift @@ -10,7 +10,7 @@ public protocol NEMapGuideBottomViewDelegate: NSObjectProtocol { } @objcMembers -public class NEMapGuideBottomView: UIView { +open class NEMapGuideBottomView: UIView { public weak var delegate: NEMapGuideBottomViewDelegate? override public init(frame: CGRect) { @@ -18,7 +18,7 @@ public class NEMapGuideBottomView: UIView { setupSubviews() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift index d6616f66..8637638b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift @@ -8,7 +8,6 @@ import NECommonKit import NECoreIMKit import NECoreKit import NIMSDK -// import NEKitContact @objc public enum LoadMessageDirection: Int { @@ -33,9 +32,11 @@ public protocol ChatViewModelDelegate: NSObjectProtocol { func didLeaveTeam() func didDismissTeam() func didRefreshTable() + func onTeamMemberChange(team: NIMTeam) - @objc optional - func getMessageModel(model: MessageModel) + @objc optional func showErrorToast(error: Error?) + @objc optional func getMessageModel(model: MessageModel) + @objc optional func selectedMessagesChanged(_ count: Int) } let revokeLocalMessage = "revoke_message_local" @@ -43,13 +44,22 @@ let revokeLocalMessageContent = "revoke_message_local_content" let removePinMessageNoti = "remove_pin_message_noti" @objcMembers -public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDelegate, - NIMConversationManagerDelegate, NIMSystemNotificationManagerDelegate, ChatExtendProviderDelegate, FriendProviderDelegate { +open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDelegate, + NIMConversationManagerDelegate, NIMSystemNotificationManagerDelegate, ChatExtendProviderDelegate, FriendProviderDelegate, NIMTeamManagerDelegate { public var team: NIMTeam? + /// 当前成员的群成员对象类 + public var teamMember: NIMTeamMember? public var session: NIMSession public var messages = [MessageModel]() public weak var delegate: ChatViewModelDelegate? - public var newUserInfoDic = [String: User]() + + // 多选选中的消息 + public var selectedMessages = [NIMMessage]() { + didSet { + delegate?.selectedMessagesChanged?(selectedMessages.count) + } + } + // 上拉时间戳 private var newMsg: NIMMessage? // 下拉时间戳 @@ -57,10 +67,9 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel public var repo = ChatRepo.shared public var operationModel: MessageContentModel? - private var userInfo = [String: User]() public var isReplying = false public let messagPageNum: UInt = 100 - private let className = "ChatViewModel" + // 可信时间戳 public var credibleTimestamp: TimeInterval = 0 public var anchor: NIMMessage? @@ -72,7 +81,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel public var deletingMsgDic = Set() init(session: NIMSession) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + session.sessionId) + NELog.infoLog(ModuleName + " ChatViewModel", desc: #function + ", sessionId:" + session.sessionId) self.session = session anchor = nil super.init() @@ -81,11 +90,12 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel repo.addSessionDelegate(delegate: self) repo.addSystemNotificationDelegate(delegate: self) repo.addChatExtendDelegate(delegate: self) + repo.addTeamDelegate(delegate: self) addObserver() } init(session: NIMSession, anchor: NIMMessage?) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + session.sessionId) + NELog.infoLog(ModuleName + " ChatViewModel", desc: #function + ", sessionId:" + session.sessionId) self.session = session self.anchor = anchor super.init() @@ -97,11 +107,11 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel repo.addSessionDelegate(delegate: self) repo.addSystemNotificationDelegate(delegate: self) repo.addChatExtendDelegate(delegate: self) + repo.addTeamDelegate(delegate: self) addObserver() } func addObserver() { - NotificationCenter.default.addObserver(self, selector: #selector(updateFriendInfo), name: NotificationName.updateFriendInfo, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(removePinNoti), name: Notification.Name(removePinMessageNoti), object: nil) } @@ -112,8 +122,9 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } - public func sendTextMessage(text: String, remoteExt: [String: Any]?, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", text.count: \(text.count)") + /// 发送文本消息(当前会话) + open func sendTextMessage(text: String, remoteExt: [String: Any]?, _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") if text.count <= 0 { return } @@ -124,15 +135,22 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel ) } - func updateFriendInfo(notification: Notification) { - if let user = notification.object as? User, - let uid = user.userId { - newUserInfoDic[uid] = user + /// 发送文本消息(当前会话) + open func sendTextMessage(text: String, _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") + if text.count <= 0 { + return } + repo.sendMessage( + message: MessageUtils.textMessage(text: text), + session: session, + completion + ) } - public func sendTextMessage(text: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", text.count: \(text.count)") + /// 发送文本消息(非当前会话) + open func sendTextMessage(text: String, session: NIMSession, _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") if text.count <= 0 { return } @@ -143,8 +161,12 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel ) } - public func sendAudioMessage(filePath: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", filePath:" + filePath) + open func sendAudioMessage(filePath: String, _ completion: @escaping (Error?) -> Void) { + if ChatDeduplicationHelper.instance.isRecordAudioSended(path: filePath) == true { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ",duplicate send audio at filePath:" + filePath) + return + } + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", filePath:" + filePath) repo.sendMessage( message: MessageUtils.audioMessage(filePath: filePath), session: session, @@ -152,9 +174,8 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel ) } - public func sendImageMessage(image: UIImage, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", image.size: \(image.size)") - // TODO: + open func sendImageMessage(image: UIImage, _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", image.size: \(image.size)") repo.sendMessage( message: MessageUtils.imageMessage(image: image), session: session, @@ -162,30 +183,70 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel ) } - public func sendVideoMessage(url: URL, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", url.path:" + url.path) + open func sendImageMessage(data: Data, ext: String, _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", image data count: \(data.count)") + repo.sendMessage( + message: MessageUtils.imageMessage(data: data, ext: ext), + session: session, + completion + ) + } + + open func sendImageMessage(path: String, _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", image path: \(path)") + repo.sendMessage( + message: MessageUtils.imageMessage(path: path), + session: session, + completion + ) + } + + open func sendVideoMessage(url: URL, _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ",video url.path:" + url.path) weak var weakSelf = self - VideoFormatConvert - .convertToMP4(with: url, avQuality: AVAssetExportPresetHighestQuality) { path, image in - if let p = path, let s = weakSelf?.session { - weakSelf?.repo.sendMessage( - message: MessageUtils.videoMessage(filePath: p), - session: s, - completion - ) + + convertVideoToMP4(videoURL: url) { url, error in + if let p = url?.path, let s = weakSelf?.session { + weakSelf?.repo.sendMessage( + message: MessageUtils.videoMessage(filePath: p), + session: s, + completion + ) + } else { + NELog.errorLog("chat veiw model", desc: "convert mov to mp4 failed") + } + } + } + + func convertVideoToMP4(videoURL: URL, completion: @escaping (URL?, Error?) -> Void) { + let outputFileName = NIMKitFileLocationHelper.genFilename(withExt: "mp4") + guard let outputPath = NIMKitFileLocationHelper.filepath(forVideo: outputFileName) else { + return + } + let asset = AVURLAsset(url: videoURL, options: nil) + let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) + let outputUrl = URL(fileURLWithPath: outputPath) + session?.outputURL = outputUrl + session?.outputFileType = AVFileType.mp4 + session?.shouldOptimizeForNetworkUse = true + session?.exportAsynchronously { + DispatchQueue.main.async { + if session?.status == AVAssetExportSession.Status.completed { + completion(outputUrl, nil) } else { - NELog.errorLog("chat veiw model", desc: "convert mov to mp4 failed") + completion(nil, nil) } } + } } - public func sendLocationMessage(_ model: ChatLocaitonModel, _ completion: @escaping (Error?) -> Void) { + open func sendLocationMessage(_ model: ChatLocaitonModel, _ completion: @escaping (Error?) -> Void) { let message = MessageUtils.locationMessage(model.lat, model.lng, model.title, model.address) repo.sendMessage(message: message, session: session, completion) } - public func sendFileMessage(filePath: String, displayName: String?, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", filePath:") + open func sendFileMessage(filePath: String, displayName: String?, _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", filePath:\(filePath)") repo.sendMessage( message: MessageUtils.fileMessage(filePath: filePath, displayName: displayName), session: session, @@ -193,8 +254,8 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel ) } - public func sendFileMessage(data: Data, displayName: String?, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", filePath:") + open func sendFileMessage(data: Data, displayName: String?, _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", data.count:\(data.count)") repo.sendMessage( message: MessageUtils.fileMessage(data: data, displayName: displayName), session: session, @@ -202,10 +263,69 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel ) } + /// 发送自定义消息(当前会话) + open func sendCustomMessage(attachment: NIMCustomAttachment, + remoteExt: [String: Any]?, + apnsConstent: String?, + _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", apnsConstent:\(String(describing: apnsConstent))") + repo.sendMessage( + message: MessageUtils.customMessage(attachment: attachment, + remoteExt: remoteExt, + apnsContent: apnsConstent), + session: session, + completion + ) + } + + /// 发送自定义消息 + open func sendCustomMessage(attachment: NIMCustomAttachment, + remoteExt: [String: Any]?, + apnsConstent: String?, + session: NIMSession, + _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", apnsConstent:\(String(describing: apnsConstent))") + repo.sendMessage( + message: MessageUtils.customMessage(attachment: attachment, + remoteExt: remoteExt, + apnsContent: apnsConstent), + session: session, + completion + ) + } + + open func sendBlackListTip(_ errorSession: NIMSession?, _ message: NIMMessage) { +// if DeduplicationHelper.instance.isBlackTipSended(messageId: message.messageId) == true { +// NELog.infoLog(ModuleName + " " + className(), desc: #function + "sendBlackListTip") +// return +// } + guard let eSession = errorSession else { + return + } + NELog.infoLog(ModuleName + " " + className(), desc: #function + "sendBlackListTip") + let content = chatLocalizable("black_list_tip") + let tip = NIMMessage() + let object = NIMTipObject(attach: nil, callbackExt: nil) + tip.messageObject = object + tip.text = content + let setting = NIMMessageSetting() + setting.shouldBeCounted = false + tip.setting = setting + repo.saveMessageToDB(tip, eSession) { [weak self] error in + NELog.infoLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + "save black tip list tip result \(error?.localizedDescription ?? "")") + if let model = self?.modelFromMessage(message: tip) { + self?.messages.append(model) + if let currentSid = self?.session.sessionId, let errorSid = errorSession?.sessionId, currentSid == errorSid { + self?.delegate?.willSend(tip) + } + } + } + } + // 动态查询历史消息解决方案 - public func getMessagesModelDynamically(_ order: NIMMessageSearchOrder, message: NIMMessage?, - _ completion: @escaping (Error?, NSInteger, [MessageModel]?) - -> Void) { + open func getMessagesModelDynamically(_ order: NIMMessageSearchOrder, message: NIMMessage?, + _ completion: @escaping (Error?, NSInteger, [MessageModel]?) + -> Void) { let param = NIMGetMessagesDynamicallyParam() param.limit = messagPageNum param.session = session @@ -223,7 +343,6 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel repo.getMessagesDynamically(param) { error, isReliable, messages in if let messageArray = messages, messageArray.count > 0 { var count = 0 - var datas = messageArray var readMsg: NIMMessage? if order == .desc { weakSelf?.oldMsg = messageArray.last @@ -232,50 +351,54 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel readMsg = messageArray.last weakSelf?.newMsg = messageArray.last } - for (i, msg) in datas.enumerated() { - if let object = msg.messageObject as? NIMNotificationObject { - if let content = object.content as? NIMTeamNotificationContent, content.operationType == .invite { - if weakSelf?.filterInviteSet.contains(msg.messageId) == true { - continue - } else { - weakSelf?.filterInviteSet.insert(msg.messageId) - } + for msg in messageArray { + // 是否需要进行重复消息过滤 + var needFilter = msg.serverID.isEmpty + if let object = msg.messageObject as? NIMNotificationObject, + let content = object.content as? NIMTeamNotificationContent, + content.operationType == .invite { + needFilter = true + } + + if needFilter { + if weakSelf?.filterInviteSet.contains(msg.messageId) == true { + continue + } else { + weakSelf?.filterInviteSet.insert(msg.messageId) } } + print("message text : ", msg.text as Any) if let model = weakSelf?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { weakSelf?.filterRevokeMessage([model]) if order == .desc { - if weakSelf?.addTimeForHistoryMessage(msg) == true { - count += 1 - } - count += 1 + weakSelf?.addTimeForHistoryMessage(model) weakSelf?.messages.insert(model, at: 0) - - // 第一条消息默认显示时间 - if i == datas.count - 1 { - let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: msg.timestamp)) - let model = MessageTipsModel(message: nil, - initType: .time, - initText: timeText) - weakSelf?.messages.insert(model, at: 0) - } + count += 1 } else { - if weakSelf?.addTimeMessage(msg) == true { - count += 1 + if let last = weakSelf?.messages.last { + ChatMessageHelper.addTimeMessage(model, last) } - count += 1 weakSelf?.messages.append(model) + count += 1 } } } + + // 第一条消息默认显示时间 + if let firstModel = weakSelf?.messages.first, + let msg = firstModel.message { + let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: msg.timestamp)) + firstModel.timeContent = timeText + } + weakSelf?.checkAudioFile(messages: weakSelf?.messages) completion(error, count, weakSelf?.messages) if weakSelf?.session.sessionType == .P2P { if let nearMsg = readMsg { weakSelf?.markRead(messages: [nearMsg]) { error in NELog.infoLog( - ModuleName + " " + (weakSelf?.className ?? "ChatViewModel"), + ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") ) } @@ -283,26 +406,48 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } else if weakSelf?.session.sessionType == .team { weakSelf?.markRead(messages: messageArray) { error in NELog.infoLog( - ModuleName + " " + (weakSelf?.className ?? "ChatViewModel"), + ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") ) } weakSelf?.refreshReceipts(messages: messageArray) } + + let group = DispatchGroup() + for msg in messageArray { + if let object = msg.messageObject as? NIMNotificationObject, + let content = object.content as? NIMTeamNotificationContent { + let targetIDs = content.targetIDs ?? [] + targetIDs.forEach { uid in + if ChatUserCache.getUserInfo(uid) == nil { + group.enter() + ChatUserCache.getUserInfo(uid) { _, _ in + group.leave() + } + } + } + } + } + + group.notify(queue: .main) { + weakSelf?.delegate?.didRefreshTable() + } + } else { + weakSelf?.checkAudioFile(messages: weakSelf?.messages) completion(error, 0, weakSelf?.messages) } } } - public func queryRoamMsgHasMoreTime_v2(_ completion: @escaping (Error?, NSInteger, NSInteger, Int) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) + open func queryRoamMsgHasMoreTime_v2(_ completion: @escaping (Error?, NSInteger, NSInteger, Int) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function) weak var weakSelf = self // 记录可信时间戳 if anchor == nil { weakSelf?.getMessagesModelDynamically(.desc, message: nil) { error, count, models in NELog.infoLog( - ModuleName + " " + self.className, + ModuleName + " " + self.className(), desc: "CALLBACK getMessageHistory " + (error?.localizedDescription ?? "no error") ) completion(error, count, 0, 0) @@ -340,7 +485,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } weakSelf?.getMessagesModelDynamically(.asc, message: weakSelf?.anchor) { error, value, models in NELog.infoLog( - ModuleName + " " + self.className, + ModuleName + " " + self.className(), desc: "CALLBACK pullRemoteRefresh " + (error?.localizedDescription ?? "no error") ) newEnd = value @@ -362,10 +507,10 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } // 查询本地历史消息 - public func getMessageHistory(_ message: NIMMessage?, - _ completion: @escaping (Error?, NSInteger, [MessageModel]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId:" + (message?.messageId ?? "nil")) + open func getMessageHistory(_ message: NIMMessage?, + _ completion: @escaping (Error?, NSInteger, [MessageModel]?) + -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + (message?.messageId ?? "nil")) ChatProvider.shared.getMessageHistory( session: session, message: message, @@ -375,19 +520,20 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel self?.oldMsg = messageArray.first for msg in messageArray { if let model = self?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - self?.addTimeMessage(msg) + if let last = self?.messages.last { + ChatMessageHelper.addTimeMessage(model, last) + } self?.filterRevokeMessage([model]) self?.messages.append(model) } } completion(error, messageArray.count, self?.messages) -// mark read + // mark read self?.markRead(messages: messageArray) { error in NELog.infoLog( - ModuleName + " " + (self?.className ?? "ChatViewModel"), + ModuleName + " " + (self?.className() ?? "ChatViewModel"), desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") ) -// print("mark read \(error?.localizedDescription)") } } else { @@ -397,9 +543,9 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } // 查询更多本地历史消息 - public func getMoreMessageHistory(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) + open func getMoreMessageHistory(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NELog.infoLog(ModuleName + " " + className(), desc: #function) weak var weakSelf = self let messageParam = oldMsg ?? newMsg @@ -418,7 +564,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel if let isTrust = isCredible, isTrust { for msg in messageArray.reversed() { if let model = self?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - self?.addTimeForHistoryMessage(msg) + self?.addTimeForHistoryMessage(model) self?.messages.insert(model, at: 0) } } @@ -439,12 +585,10 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel weakSelf?.markRead(messages: messageArray) { error in NELog.infoLog( - ModuleName + " " + (weakSelf?.className ?? "ChatViewModel"), + ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") ) -// print("mark read \(error?.localizedDescription)") } - } else { if let messageArray = messages, messageArray.isEmpty, weakSelf?.credibleTimestamp ?? 0 > 0 { @@ -467,11 +611,11 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } // 查询远端历史消息 - public func getRemoteHistoryMessage(direction: LoadMessageDirection, updateCredible: Bool, - option: NIMHistoryMessageSearchOption, - _ completion: @escaping (Error?, NSInteger, - [MessageModel]?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", direction: \(direction.rawValue)") + open func getRemoteHistoryMessage(direction: LoadMessageDirection, updateCredible: Bool, + option: NIMHistoryMessageSearchOption, + _ completion: @escaping (Error?, NSInteger, + [MessageModel]?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", direction: \(direction.rawValue)") weak var weakSelf = self repo.getHistoryMessage(session: session, option: option) { error, messages in if error == nil { @@ -483,7 +627,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } for msg in messageArray { if let model = weakSelf?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - weakSelf?.addTimeForHistoryMessage(msg) + weakSelf?.addTimeForHistoryMessage(model) weakSelf?.messages.insert(model, at: 0) } } @@ -495,7 +639,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel .updateIncompleteSessions(messages: [updateMessage]) { error, recentSessions in if error != nil { NELog.errorLog( - ModuleName + " " + (weakSelf?.className ?? "ChatViewModel"), + ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), desc: "❌updateIncompleteSessions failed,error = \(error!)" ) } @@ -512,10 +656,10 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } // 下拉获取历史消息 - public func dropDownRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) + open func dropDownRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) -> Void) { - // 首次会话下拉,没有锚点消息,需要手动设置锚点消息 - if oldMsg == nil { + // 首次会话下拉,没有锚点消息 || 锚点消息被删除,需要手动设置锚点消息 + if oldMsg == nil || !messages.contains(where: { $0.message?.messageId == oldMsg?.messageId }) { for msg in messages { if let mmsg = msg.message { oldMsg = mmsg @@ -523,23 +667,28 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } } + + if messages.isEmpty { + oldMsg = nil + } + getMessagesModelDynamically(.desc, message: oldMsg, completion) - NELog.infoLog(ModuleName + " " + className, desc: #function) + NELog.infoLog(ModuleName + " " + className(), desc: #function) } // 上拉获取最新消息 - public func pullRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) + open func pullRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) -> Void) { getMessagesModelDynamically(.asc, message: newMsg, completion) - NELog.infoLog(ModuleName + " " + className, desc: #function) + NELog.infoLog(ModuleName + " " + className(), desc: #function) } // 搜索历史记录查询的本地消息 - public func searchMessageHistory(direction: LoadMessageDirection, startTime: TimeInterval, - endTime: TimeInterval, - _ completion: @escaping (Error?, NSInteger, [MessageModel]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", direction: \(direction.rawValue)") + open func searchMessageHistory(direction: LoadMessageDirection, startTime: TimeInterval, + endTime: TimeInterval, + _ completion: @escaping (Error?, NSInteger, [MessageModel]?) + -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", direction: \(direction.rawValue)") let option = NIMMessageSearchOption() option.startTime = startTime option.endTime = endTime @@ -557,7 +706,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } for msg in messageArray { if let model = weakSelf?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - weakSelf?.addTimeMessage(msg) + ChatMessageHelper.addTimeMessage(model, weakSelf?.messages.last) weakSelf?.messages.append(model) } } @@ -572,39 +721,33 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } // 判断消息是否可信 - public func isMessageCredible(message: NIMMessage) -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId:" + message.messageId) + open func isMessageCredible(message: NIMMessage) -> Bool { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) return credibleTimestamp <= 0 || message.timestamp >= credibleTimestamp } - public func markRead(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messages.count: \(messages.count)") -// if self.getMessageRead() { -// if self.session.sessionType == .P2P { -// self.markReadInP2P(messages: messages, completion) -// }else if self.session.sessionType == .team { -// self.markReadInTeam(messages: messages, completion) -// } -// } + open func markRead(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") if session.sessionType == .P2P { markReadInP2P(messages: messages, completion) } else if session.sessionType == .team { markReadInTeam(messages: messages, completion) } -// mark session read + // mark session read weak var weakself = self repo.markMessageRead(session) { error in if error != nil { NELog.errorLog( - ModuleName + " " + (weakself?.className ?? "ChatViewModel"), + ModuleName + " " + (weakself?.className() ?? "ChatViewModel"), desc: "❌markReadInSession failed,error = \(error!)" ) } } } + // 单人会话消息已读标记 private func markReadInP2P(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messages.count: \(messages.count)") + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") for message in messages.reversed() { if message.isReceivedMsg { let param = NIMMessageReceipt(message: message) @@ -615,8 +758,9 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel completion(nil) } + // 群消息已读标记 private func markReadInTeam(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messages.count: \(messages.count)") + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") var receipts = [NIMMessageReceipt]() for message in messages { let receiptEnable = message.setting?.teamReceiptEnabled ?? false @@ -634,8 +778,19 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } - public func deleteMessage(message: NIMMessage, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId:" + message.messageId) + // 删除消息 + open func deleteMessage(_ completion: @escaping (Error?) -> Void) { + guard let message = operationModel?.message else { + NELog.errorLog(ModuleName + " " + className(), desc: #function + ", message is nil") + return + } + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) + + // 已撤回的消息不能删除 + if operationModel?.isRevoked == true { + return + } + if deletingMsgDic.contains(message.messageId) { return } @@ -648,6 +803,15 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel return } weak var weakSelf = self + + if message.serverID == "0" { + repo.deleteMessage(message: message) + deleteMessageUpdateUI(message) + weakSelf?.deletingMsgDic.remove(message.messageId) + completion(nil) + return + } + repo.deleteServerMessage(message: message, ext: nil) { error in if error == nil { weakSelf?.deleteMessageUpdateUI(message) @@ -658,25 +822,62 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } - public func replyMessage(_ message: NIMMessage, _ target: NIMMessage, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId:" + message.messageId) + open func deleteMessages(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", message count:\(messages.count)") + var localMsgs = [NIMMessage]() + var remoteMsgs = [NIMMessage]() + for msg in messages { + if deletingMsgDic.contains(msg.messageId) { + continue + } + deletingMsgDic.insert(msg.messageId) + if msg.serverID.count <= 0 { + localMsgs.append(msg) + } else { + remoteMsgs.append(msg) + } + } + + localMsgs.forEach { msg in + repo.deleteMessage(message: msg) + deleteMessageUpdateUI(msg) + deletingMsgDic.remove(msg.messageId) + } + + weak var weakSelf = self + repo.deleteRemoteMessages(messages: remoteMsgs, exts: nil) { error in + if error == nil { + remoteMsgs.forEach { msg in + weakSelf?.deleteMessageUpdateUI(msg) + weakSelf?.deletingMsgDic.remove(msg.messageId) + } + } else { + completion(error) + } + } + } + + // 回复消息 + open func replyMessage(_ message: NIMMessage, _ target: NIMMessage, + _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) repo.replyMessage(message, target) { error in completion(error) } } - public func replyMessageWithoutThread(message: NIMMessage, - target: NIMMessage, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId:" + message.messageId) + open func replyMessageWithoutThread(message: NIMMessage, + target: NIMMessage, + _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) repo.replyMessageWithoutThread(message: message, session: session, target: target) { error in completion(error) } } - public func revokeMessage(message: NIMMessage, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId:" + message.messageId) + // 撤回消息 + open func revokeMessage(message: NIMMessage, _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) repo.revokeMessage(message: message) { error in if error == nil { self.revokeMessageUpdateUI(message) @@ -685,24 +886,31 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } - public func resendMessage(message: NIMMessage) -> NSError? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId:" + message.messageId) + // 消息重发 + @discardableResult + open func resendMessage(message: NIMMessage) -> NSError? { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) + ChatDeduplicationHelper.instance.clearCache() return repo.resendMessage(message: message) } - public func getUserInfo(userId: String) -> User? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:" + userId) + // 从本地获取用户信息 + open func getUserInfo(userId: String) -> NEKitUser? { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", userId:" + userId) return repo.getUserInfo(userId: userId) } - public func getTeamMember(userId: String, teamId: String) -> NIMTeamMember? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:" + userId) + // 获取指定的群成员 + open func getTeamMember(userId: String, teamId: String) -> NIMTeamMember? { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", userId:" + userId) return repo.getTeamMemberList(userId: userId, teamId: teamId) } - public func onReceive(_ notification: NIMCustomSystemNotification) { + // 系统通知回调 + // 自定义系统通知回调 + open func onReceive(_ notification: NIMCustomSystemNotification) { NELog.infoLog( - ModuleName + " " + className, + ModuleName + " " + className(), desc: #function + ", notification.description:" + notification.description ) print("on receive custom noti : ", notification) @@ -725,27 +933,21 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel // MARK: FriendProviderDelegate - public func onFriendChanged(user: User) { - if let uid = user.userId { - newUserInfoDic[uid] = user - delegate?.didRefreshTable() - } + open func onFriendChanged(user: NEKitUser) { + ChatUserCache.updateUserInfo(user) } - public func onUserInfoChanged(user: User) { - if let uid = user.userId { - newUserInfoDic[uid] = user - delegate?.didRefreshTable() - } + open func onUserInfoChanged(user: NEKitUser) { + ChatUserCache.updateUserInfo(user) } - public func onBlackListChanged() {} + open func onBlackListChanged() {} // MARK: NIMChatManagerDelegate // 收到消息 - public func onRecvMessages(_ messages: [NIMMessage]) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messages.count: \(messages.count)") + open func onRecvMessages(_ messages: [NIMMessage]) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count), first.messageID: \(messages.first?.messageId ?? "")") var count = 0 for msg in messages { if msg.session?.sessionId == session.sessionId { @@ -783,16 +985,16 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel count += 1 // 自定义消息处理 newMsg = msg - addTimeMessage(msg) let model = modelFromMessage(message: msg) + ChatMessageHelper.addTimeMessage(model, self.messages.last) self.messages.append(model) } } if count > 0 { delegate?.onRecvMessages(messages) } } - public func willSend(_ message: NIMMessage) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId:" + message.messageId) + open func willSend(_ message: NIMMessage) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) print("\(#function)") if message.session?.sessionId != session.sessionId { @@ -813,8 +1015,8 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } if !isResend { - addTimeMessage(message) let model = modelFromMessage(message: message) + ChatMessageHelper.addTimeMessage(model, messages.last) filterRevokeMessage([model]) messages.append(model) } @@ -822,14 +1024,16 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel delegate?.willSend(message) } - public func send(_ message: NIMMessage, progress: Float) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + // 发送消息进度回调 + open func send(_ message: NIMMessage, progress: Float) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) print("\(#function) progress\(progress)") delegate?.send(message, progress: progress) } + // 发送消息完成回调 open func send(_ message: NIMMessage, didCompleteWithError error: Error?) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) print("\(#function) message deliveryState:\(message.deliveryState) error:\(error)") for (i, msg) in messages.enumerated() { if message.messageId == msg.message?.messageId { @@ -837,20 +1041,33 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel break } } + // 判断发送失败原因是否是因为在黑名单中 + if error != nil { + if let err = error as NSError? { + if err.code == inBlackListCode { + weak var weakSelf = self + DispatchQueue.main.async { + weakSelf?.sendBlackListTip(message.session, message) + } + } + } + } + delegate?.send(message, didCompleteWithError: error) } // MARK: ChatExtendProviderDelegate - public func onNotifyAddMessagePin(pinItem: NIMMessagePinItem) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + pinItem.messageId) + // 添加标记消息回调 + open func onNotifyAddMessagePin(pinItem: NIMMessagePinItem) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + pinItem.messageId) var index = -1 for (i, model) in messages.enumerated() { if pinItem.messageServerID == model.message?.serverID { messages[i].isPined = true let pinID = pinItem.accountID ?? NIMSDK.shared().loginManager.currentAccount() messages[i].pinAccount = pinID - messages[i].pinShowName = getShowName(userId: pinID, teamId: session.sessionId) + messages[i].pinShowName = ChatUserCache.getShowName(userId: pinID, teamId: session.sessionId) index = i break } @@ -860,8 +1077,9 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } - public func onNotifyRemoveMessagePin(pinItem: NIMMessagePinItem) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + pinItem.messageId) + // 移除标记消息回调 + open func onNotifyRemoveMessagePin(pinItem: NIMMessagePinItem) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + pinItem.messageId) var index = -1 for (i, model) in messages.enumerated() { if pinItem.messageServerID == model.message?.serverID { @@ -880,17 +1098,17 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } - public func onNotifySyncStickTopSessions(_ response: NIMSyncStickTopSessionResponse) {} + open func onNotifySyncStickTopSessions(_ response: NIMSyncStickTopSessionResponse) {} - public func onNotifyAddStickTopSession(_ newInfo: NIMStickTopSessionInfo) {} + open func onNotifyAddStickTopSession(_ newInfo: NIMStickTopSessionInfo) {} - public func onNotifyRemoveStickTopSession(_ removedInfo: NIMStickTopSessionInfo) {} + open func onNotifyRemoveStickTopSession(_ removedInfo: NIMStickTopSessionInfo) {} // MARK: collection func addColletion(_ message: NIMMessage, completion: @escaping (NSError?, NIMCollectInfo?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) let param = NIMAddCollectParams() var string: String? if message.messageType == .text { @@ -924,29 +1142,32 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel // MARK: revoke - public func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { - NELog.infoLog(ModuleName + " " + className, desc: #function) + // 撤回消息回调 + open func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { + NELog.infoLog(ModuleName + " " + className(), desc: #function) guard let msg = notification.message else { return } + NELog.infoLog(ModuleName + className(), desc: #function + "messageId:\(msg.messageId), serverID:\(msg.serverID)") + revokeMessageUpdateUI(msg) } - public func onRecvMessageReceipts(_ receipts: [NIMMessageReceipt]) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", receipts.count: \(receipts.count)") + open func onRecvMessageReceipts(_ receipts: [NIMMessageReceipt]) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", receipts.count: \(receipts.count)") print( "chatViewModel: :\(receipts.count) messageId:\(receipts.first?.messageId) messageId:\(receipts.first?.timestamp)" ) delegate?.didReadedMessageIndexs() } - public func avalibleOperationsForMessage(_ model: MessageContentModel?) -> [OperationItem]? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", pinAccount: " + (model?.pinAccount ?? "nil")) - var pinItem = OperationItem.pinItem() + open func avalibleOperationsForMessage(_ model: MessageContentModel?) -> [OperationItem]? { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", pinAccount: " + (model?.pinAccount ?? "nil")) var items = [OperationItem]() - if model?.message?.deliveryState == .failed || model?.message?.deliveryState == .delivering, model?.message?.messageType != .rtcCallRecord { + /// 消息发送中的消息只能删除(文本可复制) + if model?.message?.deliveryState == .delivering { switch model?.message?.messageType { case .text: items.append(contentsOf: [ @@ -954,81 +1175,110 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel OperationItem.deleteItem(), ]) return items - case .audio, .video, .image, .location, .file: + default: + return [ + OperationItem.deleteItem(), + ] + } + } + + /// 发送失败 || 黑名单中的消息 || 话单消息 只能多选和删除(文本可复制) + if model?.message?.deliveryState == .failed || + model?.message?.messageType == .rtcCallRecord || + model?.message?.isBlackListed == true { + switch model?.message?.messageType { + case .text: items.append(contentsOf: [ + OperationItem.copyItem(), OperationItem.deleteItem(), + OperationItem.selectItem(), ]) return items default: - break + return [ + OperationItem.deleteItem(), + OperationItem.selectItem(), + ] } - - return nil } + + /// 消息发送成功 + let pinItem = model?.isPined == false ? OperationItem.pinItem() : OperationItem.removePinItem() switch model?.message?.messageType { case .location: - if let isPin = model?.isPined, isPin { - pinItem = OperationItem.removePinItem() - } items.append(contentsOf: [ OperationItem.replayItem(), OperationItem.forwardItem(), pinItem, OperationItem.deleteItem(), + OperationItem.selectItem(), ]) case .text: - if let isPin = model?.isPined, isPin { - pinItem = OperationItem.removePinItem() - } items = [ OperationItem.copyItem(), OperationItem.replayItem(), OperationItem.forwardItem(), pinItem, OperationItem.deleteItem(), + OperationItem.selectItem(), ] - case .image, .video, .file: - if let isPin = model?.isPined, isPin { - pinItem = OperationItem.removePinItem() - } items = [ OperationItem.replayItem(), OperationItem.forwardItem(), pinItem, OperationItem.deleteItem(), + OperationItem.selectItem(), ] case .audio: - if let isPin = model?.isPined, isPin { - pinItem = OperationItem.removePinItem() - } items = [ OperationItem.replayItem(), pinItem, OperationItem.deleteItem(), + OperationItem.selectItem(), ] - case .rtcCallRecord: - items = [OperationItem.deleteItem()] - - default: - if let isPin = model?.isPined, isPin { - pinItem = OperationItem.removePinItem() + case .custom: + if let attach = NECustomAttachment.attachmentOfCustomMessage(message: model?.message) { + if attach.customType == customRichTextType { + items = [ + OperationItem.copyItem(), + ] + } + items.append(contentsOf: [ + OperationItem.replayItem(), + OperationItem.forwardItem(), + pinItem, + OperationItem.deleteItem(), + OperationItem.selectItem(), + ]) + } else { + // 未知消息体 + items = [ + OperationItem.deleteItem(), + ] } + default: items = [ OperationItem.replayItem(), pinItem, OperationItem.deleteItem(), + OperationItem.selectItem(), ] } - if model?.message?.from == NIMSDK.shared().loginManager.currentAccount(), model?.message?.messageType != .rtcCallRecord { + // 自己发送且非未知消息可以撤回 + if model?.message?.from == NIMSDK.shared().loginManager.currentAccount() { + if model?.message?.messageType == .custom, + NECustomAttachment.dataOfCustomMessage(message: model?.message) == nil { + return items + } items.append(OperationItem.recallItem()) } return items } private func indexPathsForTeamMarkRead(_ receipts: [NIMMessageReceipt]) -> [IndexPath] { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", receipts.count: \(receipts.count)") + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", receipts.count: \(receipts.count)") var indexs = [IndexPath]() // find messages that need to update UI for receipt in receipts { @@ -1043,7 +1293,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } private func indexPathsForP2PMarkRead(_ receipts: [NIMMessageReceipt]) -> [IndexPath] { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", receipts.count: \(receipts.count)") + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", receipts.count: \(receipts.count)") var updateIndexs = [IndexPath]() // find messages that need to update UI var i = messages.count - 1 @@ -1059,116 +1309,44 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel return updateIndexs } - // history message insert message at first of messages, send message add last of messages - @discardableResult - private func addTimeMessage(_ message: NIMMessage) -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) - if NotificationMessageUtils.isDiscussSeniorTeamNoti(message: message) { - return false - } - - let lastTs = messages.last?.message?.timestamp ?? 0.0 - let curTs = message.timestamp - let dur = curTs - lastTs - if (dur / 60) > 5 { - messages.append(timeModel(message)) - return true + private func addTimeForHistoryMessage(_ model: MessageModel) { + guard let first = messages.first, + let firstMsg = first.message else { + NELog.errorLog(ModuleName + " " + className(), desc: #function + ", model.message is nil") + return } - return false - } - private func addTimeForHistoryMessage(_ message: NIMMessage) -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) - if NotificationMessageUtils.isDiscussSeniorTeamNoti(message: message) { - return false + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + firstMsg.messageId) + if NotificationMessageUtils.isDiscussSeniorTeamNoti(message: firstMsg) { + return } - let firstTs = messages.first?.message?.timestamp ?? 0.0 - let curTs = message.timestamp + let firstTs = firstMsg.timestamp + let curTs = model.message?.timestamp ?? 0.0 let dur = firstTs - curTs if (dur / 60) > 5 { let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: firstTs)) - let model = MessageTipsModel(message: nil, - initType: .time, - initText: timeText) - messages.insert(model, at: 0) - return true - } - return false - } - - private func timeModel(_ message: NIMMessage) -> MessageModel { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) - let curTs = message.timestamp - let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: curTs)) - let model = MessageTipsModel(message: nil, - initType: .time, - initText: timeText) - return model - } - - private func tipsModel(_ message: NIMMessage) -> MessageModel { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) - let model = MessageTipsModel(message: message) - return model + first.timeContent = timeText + } } - private func modelFromMessage(message: NIMMessage) -> MessageModel { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) - var model: MessageModel - print("message type : ", message.messageType.rawValue) - switch message.messageType { - case .video: - model = MessageVideoModel(message: message) - case .text: - model = MessageTextModel(message: message) - case .image: - model = MessageImageModel(message: message) - case .audio: - model = MessageAudioModel(message: message) - case .notification, .tip: - model = MessageTipsModel(message: message) - case .file: - model = MessageFileModel(message: message) - case .custom: - model = MessageCustomModel(message: message) - case .location: - model = MessageLocationModel(message: message) - case .rtcCallRecord: - model = MessageCallRecordModel(message: message) - default: - // 未识别的消息类型,默认为文本消息类型,text为未知消息 - message.text = "未知消息" - model = MessageContentModel(message: message) - } + // 构建聊天页面UI显示model + open func modelFromMessage(message: NIMMessage) -> MessageModel { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) + let model = ChatMessageHelper.modelFromMessage(message: message) if let uid = message.from { - let user = getUserInfo(userId: uid) - var fullName = uid - var shortName = uid - if let nickName = user?.userInfo?.nickName { - fullName = nickName - shortName = nickName - } + let user = ChatUserCache.getUserInfo(uid) + let fullName = ChatUserCache.getShowName(userId: uid, teamId: session.sessionId) model.avatar = user?.userInfo?.avatarUrl - if session.sessionType == .team { - // team - let teamMember = getTeamMember(userId: uid, teamId: session.sessionId) - if let teamNickname = teamMember?.nickname { - fullName = teamNickname - } - } - if let alias = user?.alias { - fullName = alias - } model.fullName = fullName - model.shortName = getShortName(name: shortName, length: 2) + model.shortName = ChatUserCache.getShortName(name: user?.showName(false) ?? "", length: 2) } model.replyedModel = getReplyMessageWithoutThread(message: message) if let pin = repo.searchMessagePinHistory(message) { model.isPined = true model.pinAccount = pin.accountID let pinID = pin.accountID ?? NIMSDK.shared().loginManager.currentAccount() - model.pinShowName = getShowName(userId: pinID, teamId: session.sessionId) + model.pinShowName = ChatUserCache.getShowName(userId: pinID, teamId: session.sessionId) } else { model.isPined = false } @@ -1176,8 +1354,9 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel return model } - public func getReplyMessage(message: NIMMessage) -> MessageModel? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + // 查找回复消息 + open func getReplyMessage(message: NIMMessage) -> MessageModel? { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) guard let id = message.repliedMessageId, id.count > 0 else { return nil } @@ -1193,8 +1372,8 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel return model } - public func getReplyMessageWithoutThread(message: NIMMessage) -> MessageModel? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + open func getReplyMessageWithoutThread(message: NIMMessage) -> MessageModel? { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) var replyId: String? = message.repliedMessageId if let yxReplyMsg = message.remoteExt?[keyReplyMsgKey] as? [String: Any] { @@ -1217,53 +1396,8 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel return model } - public func getUserInfo(_ userId: String, _ completion: @escaping (User?, NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + userId) - if let user = newUserInfoDic[userId] { - completion(user, nil) - return - } - - UserInfoProvider.shared.fetchUserInfo([userId]) { [weak self] error, users in - if let user = users?.first { - self?.newUserInfoDic[userId] = user - completion(user, nil) - } else { - completion(nil, error) - } - } - } - -// 获取展示的用户名字,p2p: 备注 > 昵称 > ID team: 备注 > 群昵称 > 昵称 > ID - func getShowName(userId: String, teamId: String?, _ showAlias: Bool = true) -> String { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + userId) - let user = getUserInfo(userId: userId) - var fullName = userId - if let nickName = user?.userInfo?.nickName { - fullName = nickName - } - if let tID = teamId, session.sessionType == .team { - // team - let teamMember = getTeamMember(userId: userId, teamId: tID) - if let teamNickname = teamMember?.nickname { - fullName = teamNickname - } - } - if showAlias, let alias = user?.alias { - fullName = alias - } - return fullName - } - -// 全名后几位 - func getShortName(name: String, length: Int) -> String { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", name: " + name) - return name - .count > length ? String(name[name.index(name.endIndex, offsetBy: -length)...]) : name - } - func deleteMessageUpdateUI(_ message: NIMMessage) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) var index = -1 var replyIndex = [Int]() var hasFind = false @@ -1312,7 +1446,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } func revokeMessageUpdateUI(_ message: NIMMessage) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) var index = -1 var replyIndex = [Int]() var hasFind = false @@ -1351,85 +1485,169 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel delegate?.onRevokeMessage(message, atIndexs: indexs) } - public func fetchMessageAttachment(_ message: NIMMessage, didCompleteWithError error: Error?) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) - print("featch message completion : ", error as Any) + open func fetchMessageAttachment(_ message: NIMMessage, didCompleteWithError error: Error?) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) } - public func fetchMessageAttachment(_ message: NIMMessage, progress: Float) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) - print("fetchMessageAttachment progress : ", progress) - /* - var index = -1 - for (i, model) in self.messages.enumerated() { - if model.message?.serverID == message.serverID { - index = i - break - } - } - if index >= 0 { - let indexPath = IndexPath(row: index, section: 0) - delegate?.updateDownloadProgress(message, atIndex: indexPath, progress: progress) - } */ + open func fetchMessageAttachment(_ message: NIMMessage, progress: Float) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) } - public func fetchMessageAttachment(_ message: NIMMessage, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + open func fetchMessageAttachment(_ message: NIMMessage, + _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) repo.downloadMessageAttachment(message, completion) } - public func downLoad(_ urlString: String, _ filePath: String, _ progress: NIMHttpProgressBlock?, - _ completion: NIMDownloadCompleteBlock?) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + urlString) + open func downLoad(_ urlString: String, _ filePath: String, _ progress: NIMHttpProgressBlock?, + _ completion: NIMDownloadCompleteBlock?) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) repo.downloadSource(urlString, filePath, progress, completion) } - public func getUrls() -> [String] { - NELog.infoLog(ModuleName + " " + className, desc: #function) - var urls = [String]() - messages.forEach { model in - if model.type == .image, let message = model.message?.messageObject as? NIMImageObject { - if let url = message.url { - urls.append(url) - } else { - if let path = message.path, FileManager.default.fileExists(atPath: path) { - urls.append(path) + // 转发消息 + open func forwardMessage(_ forwardMessages: [NIMMessage], + _ session: NIMSession, + _ comment: String?, + _ completion: @escaping (Error?) -> Void) { + for message in forwardMessages { + if let forwardMessage = repo.makeForwardMessage(message) { + ChatMessageHelper.clearForwardAtMark(forwardMessage) + repo.sendForwardMessage(forwardMessage, session) + } + } + if let text = comment, !text.isEmpty { + sendTextMessage(text: text, session: session, completion) + } else { + completion(nil) + } + } + + // 合并转发消息 + open func forwardMultiMessage(_ forwardMessages: [NIMMessage], + _ toSession: NIMSession, + _ depth: Int = 0, + _ comment: String?, + _ completion: @escaping (Error?) -> Void) { + if forwardMessages.count <= 0 { + if let text = comment, !text.isEmpty { + sendTextMessage(text: text, session: toSession, completion) + } else { + completion(nil) + } + return + } + + let fromSession = session + let header = ChatMessageHelper.buildHeader(messageCount: forwardMessages.count) + ChatMessageHelper.buildBody(messages: forwardMessages) { body, abstracts in + let multiForwardMsg = header + body + let fileName = multiForwardFileName + "\(Int(Date().timeIntervalSince1970))" + if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(fileName)") + if let multiForwardMsgData = multiForwardMsg.data(using: .utf8) { + do { + try multiForwardMsgData.write(to: filePath) + } catch { + completion(NSError(domain: chatLocalizable("forward_failed"), code: 414)) + print("Error writing string to file: \(error)") + return + } + } + + NIMSDK.shared().resourceManager.upload(filePath.path, progress: nil) { [weak self] url, error in + if let err = error { + completion(err) + } else if let url = url { + let md5 = ChatMessageHelper.getFileChecksum(fileURL: filePath) + + // 删除本地文件 + do { + try FileManager.default.removeItem(atPath: filePath.path) + } catch { + print("无法删除合并转发文件:\(error)") + } + + var data = [String: Any]() + data["sessionId"] = toSession.sessionId + data["sessionName"] = ChatMessageHelper.getSessionName(session: fromSession, showAlias: false) + data["url"] = url + data["md5"] = md5 + data["depth"] = depth + data["abstracts"] = abstracts + + var jsonData = [String: Any]() + jsonData["data"] = data + jsonData["messageType"] = "custom" + jsonData["type"] = customMultiForwardType + + let attah = NECustomAttachment(customType: customMultiForwardType, + cellHeight: customMultiForwardCellHeight, + data: jsonData) + self?.sendCustomMessage(attachment: attah, + remoteExt: nil, + apnsConstent: "[\(chatLocalizable("chat_history"))]", + session: toSession) { error in + if let err = error { + completion(err) + } else { + if let text = comment, !text.isEmpty { + self?.sendTextMessage(text: text, session: toSession, completion) + } else { + completion(nil) + } + } + } } } } } - print("urls:\(urls)") - return urls } - public func forwardUserMessage(_ message: NIMMessage, _ users: [NIMUser]) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + open func forwardUserMessage(_ users: [NIMUser], + _ isMultiForward: Bool, + _ depth: Int, + _ comment: String?, + _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") + + // 排序(发送时间正序) + let forwardMessages = selectedMessages.sorted { msg1, msg2 in + msg1.timestamp < msg2.timestamp + } + users.forEach { user in if let uid = user.userId { let session = NIMSession(uid, type: .P2P) - if let forwardMessage = repo.makeForwardMessage(message) { - clearForwardAtMark(forwardMessage) - repo.sendForwardMessage(forwardMessage, session) + if isMultiForward { + forwardMultiMessage(forwardMessages, session, depth, comment, completion) + } else { + forwardMessage(forwardMessages, session, comment, completion) } } } } - public func forwardTeamMessage(_ message: NIMMessage, _ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + open func forwardTeamMessage(_ team: NIMTeam, + _ isMultiForward: Bool, + _ depth: Int, + _ comment: String?, + _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") if let tid = team.teamId { let session = NIMSession(tid, type: .team) - if let forwardMessage = repo.makeForwardMessage(message) { - clearForwardAtMark(forwardMessage) - repo.sendForwardMessage(forwardMessage, session) + if isMultiForward { + forwardMultiMessage(selectedMessages, session, depth, comment, completion) + } else { + forwardMessage(selectedMessages, session, comment, completion) } } } - public func pinMessage(_ message: NIMMessage, - _ completion: @escaping (Error?, NIMMessagePinItem?, Int) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + // 标记消息 + open func pinMessage(_ message: NIMMessage, + _ completion: @escaping (Error?, NIMMessagePinItem?, Int) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) let item = NIMMessagePinItem(message: message) guard let _ = NIMSDK.shared().conversationManager.messages(in: session, messageIds: [message.messageId]) else { return @@ -1445,7 +1663,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel if message.messageId == model.message?.messageId, !messages[i].isPined { messages[i].isPined = true messages[i].pinAccount = NIMSDK.shared().loginManager.currentAccount() - messages[i].pinShowName = self?.getShowName( + messages[i].pinShowName = ChatUserCache.getShowName( userId: NIMSDK.shared().loginManager.currentAccount(), teamId: message.session?.sessionId ) @@ -1460,10 +1678,11 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } - public func removePinMessage(_ message: NIMMessage, - _ completion: @escaping (Error?, NIMMessagePinItem?, Int) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + // 取消消息标记 + open func removePinMessage(_ message: NIMMessage, + _ completion: @escaping (Error?, NIMMessagePinItem?, Int) + -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) guard let _ = NIMSDK.shared().conversationManager.messages(in: session, messageIds: [message.messageId]) else { return } @@ -1479,22 +1698,24 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } - public func sendInputTypingState() { - NELog.infoLog(ModuleName + " " + className, desc: #function) + // 发送正在输入中状态 + open func sendInputTypingState() { + NELog.infoLog(ModuleName + " " + className(), desc: #function) if session.sessionType == .P2P { setTypingCustom(1) } } - public func sendInputTypingEndState() { - NELog.infoLog(ModuleName + " " + className, desc: #function) + // 发送结束输入中状态 + open func sendInputTypingEndState() { + NELog.infoLog(ModuleName + " " + className(), desc: #function) if session.sessionType == .P2P { setTypingCustom(0) } } func setTypingCustom(_ typing: Int) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", typing: \(typing)") + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", typing: \(typing)") let message = NIMMessage() if message.setting == nil { message.setting = NIMMessageSetting() @@ -1511,17 +1732,18 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } - public func getHandSetEnable() -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function) + open func getHandSetEnable() -> Bool { + NELog.infoLog(ModuleName + " " + className(), desc: #function) return repo.getHandsetMode() } - public func getMessageRead() -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function) + open func getMessageRead() -> Bool { + NELog.infoLog(ModuleName + " " + className(), desc: #function) return repo.getMessageRead() } - public func saveRevokeMessage(_ message: NIMMessage, _ completion: @escaping (Error?) -> Void) { + // 本地保存撤回消息 + open func saveRevokeMessage(_ message: NIMMessage, _ completion: @escaping (Error?) -> Void) { let messageNew = NIMMessage() messageNew.text = chatLocalizable("message_recalled") var muta = [String: Any]() @@ -1529,6 +1751,14 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel if message.messageType == .text { muta[revokeLocalMessageContent] = message.text } + if message.messageType == .custom { + if let title = NECustomAttachment.titleOfRichText(message: message), !title.isEmpty { + muta[revokeLocalMessageContent] = title + } + if let body = NECustomAttachment.bodyOfRichText(message: message), !body.isEmpty { + muta[revokeLocalMessageContent] = body + } + } messageNew.timestamp = message.timestamp messageNew.from = message.from messageNew.localExt = muta @@ -1540,7 +1770,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel repo.saveMessageToDB(messageNew, session, completion) } - private func filterRevokeMessage(_ messages: [MessageModel]) { + open func filterRevokeMessage(_ messages: [MessageModel]) { messages.forEach { model in if let isRevoke = model.message?.localExt?[revokeLocalMessage] as? Bool, isRevoke == true { if let content = model.message?.localExt?[revokeLocalMessageContent] as? String, content.count > 0 { @@ -1552,7 +1782,8 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } - public func refreshReceipts(messages: [NIMMessage]) { + // 刷新已读回执 + open func refreshReceipts(messages: [NIMMessage]) { if session.sessionType != .team { return } @@ -1588,9 +1819,8 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel // MARK: NIMConversationManagerDelegate - // remote - public func onRecvMessagesDeleted(_ messages: [NIMMessage], exts: [String: String]?) { - var messageIDs = Set() + // 多端登录删除消息 + open func onRecvMessagesDeleted(_ messages: [NIMMessage], exts: [String: String]?) { messages.forEach { message in if message.session?.sessionId != session.sessionId { return @@ -1598,39 +1828,29 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel if message.messageId.count <= 0 { return } - messageIDs.insert(message.messageId) - } - if messageIDs.count > 0 { - self.messages.removeAll { model in - if let messageId = model.message?.messageId, messageId.count > 0 { - if messageIDs.contains(messageId) { - return true - } - } - return false - } - if let lastModel = self.messages.last as? MessageTipsModel, lastModel.type == .time { - self.messages.removeLast() - } - delegate?.didRefreshTable() + deleteMessageUpdateUI(message) } } - deinit { - print("deinit") + func fetchPinMessage(_ completion: @escaping () -> Void) { + repo.fetchPinMessage(session.sessionId, session.sessionType) { error, items in + completion() + } } - private func clearForwardAtMark(_ forwardMessage: NIMMessage) { - forwardMessage.remoteExt?.removeValue(forKey: yxAtMsg) - forwardMessage.remoteExt?.removeValue(forKey: keyReplyMsgKey) - if forwardMessage.remoteExt?.count ?? 0 <= 0 { - forwardMessage.remoteExt = nil + // 检查音频消息是否有附件 + open func checkAudioFile(messages: [MessageModel]?) { + messages?.forEach { model in + if let message = model.message { + ChatMessageHelper.downloadAudioFile(message: message) + } } } - func fetchPinMessage(_ completion: @escaping () -> Void) { - repo.fetchPinMessage(session.sessionId, session.sessionType) { error, items in - completion() + open func onTeamMemberChanged(_ team: NIMTeam) { + if session.sessionType == .team, session.sessionId == team.teamId { + self.team = team + delegate?.onTeamMemberChange(team: team) } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift new file mode 100644 index 00000000..38b018c0 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift @@ -0,0 +1,94 @@ +// 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 + +@objc +public protocol MultiForwardViewModelDelegate: NSObjectProtocol { + @objc optional + func getMessageModel(model: MessageModel) +} + +@objcMembers +open class MultiForwardViewModel: NSObject { + public weak var delegate: MultiForwardViewModelDelegate? + public var repo = ChatRepo.shared + public var messages = [MessageModel]() + + func loadData(_ messageAttachmentUrl: String?, + _ filePath: String, + _ md5: String?, + _ completion: @escaping (Error?) -> Void) { + if FileManager.default.fileExists(atPath: filePath) { + decodeMesssage(filePath: filePath, md5: md5, completion) + } else if let urlString = messageAttachmentUrl { + downLoad(urlString, filePath, nil) { [weak self] error in + self?.decodeMesssage(filePath: filePath, md5: md5, completion) + } + } + } + + func decodeMesssage(filePath: String, + md5: String?, + _ completion: (Error?) -> Void) { + // 校验文件 MD5 + if let filePath = URL(string: filePath), + let fileMD5 = ChatMessageHelper.getFileChecksum(fileURL: filePath) { + if fileMD5 != md5 { + completion(NSError(domain: chatLocalizable("file_check_failed"), code: 0)) + return + } + } + + do { + let data = try Data(contentsOf: URL(fileURLWithPath: filePath)) + let strData = String(data: data, encoding: .utf8) + let subStringData = strData?.components(separatedBy: "\n") + if let msgCount = subStringData?.count, msgCount > 1 { + for i in 1 ..< msgCount { + if let msgData = subStringData?[i].data(using: .utf8) { + let msg = NIMSDK.shared().conversationManager.decodeMessage(from: msgData) + let model = modelFromMessage(message: msg) + ChatMessageHelper.addTimeMessage(model, messages.last) + messages.append(model) + } + } + completion(nil) + } + } catch { + completion(error) + } + } + + open func modelFromMessage(message: NIMMessage) -> MessageModel { + var model: MessageModel + switch message.messageType { + case .audio: + message.text = chatLocalizable("msg_audio") + model = MessageTextModel(message: message) + case .rtcCallRecord: + message.text = chatLocalizable("msg_rtc_call") + if let object = message.messageObject as? NIMRtcCallRecordObject { + message.text = object.callType == .audio ? chatLocalizable("msg_rtc_audio") : chatLocalizable("msg_rtc_video") + } + model = MessageTextModel(message: message) + default: + model = ChatMessageHelper.modelFromMessage(message: message) + } + + model.fullName = message.remoteExt?[mergedMessageNickKey] as? String + model.shortName = ChatUserCache.getShortName(name: model.fullName ?? "", length: 2) + model.avatar = message.remoteExt?[mergedMessageAvatarKey] as? String + + delegate?.getMessageModel?(model: model) + return model + } + + open func downLoad(_ urlString: String, _ filePath: String, _ progress: NIMHttpProgressBlock?, + _ completion: NIMDownloadCompleteBlock?) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) + repo.downloadSource(urlString, filePath, progress, completion) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift index 1757dbe9..4890d90d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift @@ -12,7 +12,7 @@ public protocol PinMessageViewModelDelegate: NSObjectProtocol { } @objcMembers -public class PinMessageViewModel: NSObject, ChatExtendProviderDelegate, NIMChatManagerDelegate, NIMConversationManagerDelegate { +open class PinMessageViewModel: NSObject, ChatExtendProviderDelegate, NIMChatManagerDelegate, NIMConversationManagerDelegate { public let chatRepo = ChatRepo.shared public var items = [PinMessageModel]() public var delegate: PinMessageViewModelDelegate? @@ -25,7 +25,7 @@ public class PinMessageViewModel: NSObject, ChatExtendProviderDelegate, NIMChatM NIMSDK.shared().conversationManager.add(self) } - public func onRecvMessagesDeleted(_ messages: [NIMMessage], exts: [String: String]?) { + open func onRecvMessagesDeleted(_ messages: [NIMMessage], exts: [String: String]?) { for message in messages { if message.session?.sessionId == session?.sessionId { delegate?.didNeedRefreshUI() @@ -34,7 +34,7 @@ public class PinMessageViewModel: NSObject, ChatExtendProviderDelegate, NIMChatM } } - public func getPinitems(session: NIMSession, _ completion: @escaping (Error?) -> Void) { + open func getPinitems(session: NIMSession, _ completion: @escaping (Error?) -> Void) { weak var weakSelf = self self.session = session chatRepo.fetchPinMessage(session.sessionId, session.sessionType) { error, pinItems in @@ -92,9 +92,9 @@ public class PinMessageViewModel: NSObject, ChatExtendProviderDelegate, NIMChatM } } - public func removePinMessage(_ message: NIMMessage, - _ completion: @escaping (Error?, NIMMessagePinItem?) - -> Void) { + open func removePinMessage(_ message: NIMMessage, + _ completion: @escaping (Error?, NIMMessagePinItem?) + -> Void) { NELog.infoLog("PinMessageViewModel", desc: #function + ", messageId: " + message.messageId) let item = NIMMessagePinItem(message: message) chatRepo.removeMessagePin(item) { error, pinItem in @@ -102,54 +102,83 @@ public class PinMessageViewModel: NSObject, ChatExtendProviderDelegate, NIMChatM } } - public func forwardUserMessage(_ message: NIMMessage, _ users: [NIMUser]) { + open func sendTextMessage(text: String, session: NIMSession, _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") + if text.count <= 0 { + return + } + chatRepo.sendMessage( + message: MessageUtils.textMessage(text: text), + session: session, + completion + ) + } + + open func forwardUserMessage(_ message: NIMMessage, + _ users: [NIMUser], + _ comment: String?, + _ completion: @escaping (Error?) -> Void) { NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) users.forEach { user in if let uid = user.userId { let session = NIMSession(uid, type: .P2P) if let forwardMessage = chatRepo.makeForwardMessage(message) { - clearForwardAtMark(forwardMessage) + ChatMessageHelper.clearForwardAtMark(forwardMessage) chatRepo.sendForwardMessage(forwardMessage, session) } + if let text = comment { + sendTextMessage(text: text, session: session) { error in + print("sendTextMessage error: \(String(describing: error))") + } + } } } } - public func forwardTeamMessage(_ message: NIMMessage, _ team: NIMTeam) { + open func forwardTeamMessage(_ message: NIMMessage, + _ team: NIMTeam, + _ comment: String?, + _ completion: @escaping (Error?) -> Void) { NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) if let tid = team.teamId { let session = NIMSession(tid, type: .team) if let forwardMessage = chatRepo.makeForwardMessage(message) { - clearForwardAtMark(forwardMessage) + ChatMessageHelper.clearForwardAtMark(forwardMessage) chatRepo.sendForwardMessage(forwardMessage, session) } + if let text = comment { + sendTextMessage(text: text, session: session, completion) + } } } // MARK: NIMChatManagerDelegate - public func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { + open func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { // items = [PinMessageModel]() delegate?.didNeedRefreshUI() } // MARK: ChatExtendProviderDelegate - public func onNotifyAddMessagePin(pinItem: NIMMessagePinItem) { + open func onNotifyAddMessagePin(pinItem: NIMMessagePinItem) { // items = [PinMessageModel]() delegate?.didNeedRefreshUI() } - public func onNotifyRemoveMessagePin(pinItem: NIMMessagePinItem) { + open func onNotifyRemoveMessagePin(pinItem: NIMMessagePinItem) { // items = [PinMessageModel]() delegate?.didNeedRefreshUI() } - private func clearForwardAtMark(_ forwardMessage: NIMMessage) { - forwardMessage.remoteExt?.removeValue(forKey: yxAtMsg) - forwardMessage.remoteExt?.removeValue(forKey: keyReplyMsgKey) - if forwardMessage.remoteExt?.count ?? 0 <= 0 { - forwardMessage.remoteExt = nil - } + open func downLoad(_ urlString: String, _ filePath: String, _ progress: NIMHttpProgressBlock?, + _ completion: NIMDownloadCompleteBlock?) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) + chatRepo.downloadSource(urlString, filePath, progress, completion) + } + + open func getHandSetEnable() -> Bool { + NELog.infoLog(ModuleName + " " + className(), desc: #function) + return chatRepo.getHandsetMode() } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift index 52a74c90..22885f04 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift @@ -4,6 +4,7 @@ import CoreText import Foundation +import NEChatKit import NECoreIMKit import NIMSDK @@ -15,37 +16,23 @@ public protocol TeamChatViewModelDelegate: ChatViewModelDelegate { } @objcMembers -public class TeamChatViewModel: ChatViewModel, NIMTeamManagerDelegate { +open class TeamChatViewModel: ChatViewModel { private let className = "TeamChatViewModel" -// override init(session: NIMSession) { -// super.init(session: session) -// repo.addTeamDelegate(delegate: self) -// -// } override init(session: NIMSession, anchor: NIMMessage?) { super.init(session: session, anchor: anchor) NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId: " + session.sessionId) repo.addTeamDelegate(delegate: self) -// self.session = session -// self.anchor = anchor -// super.init() -// if anchor != nil { -// isHistoryChat = true -// } -// repo.addChatDelegate(delegate: self) -// repo.addConversationDelegate(delegate: self) -// repo.addSystemNotiDelegate(delegate: self) -// repo.addChatExtDelegate(delegate: self) + getTeamMember() } - public func getTeam(teamId: String) -> NIMTeam? { + open func getTeam(teamId: String) -> NIMTeam? { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + teamId) return repo.getTeamInfo(teamId: teamId) } - public func fetchTeamInfo(teamId: String, - _ completion: @escaping (NSError?, NIMTeam?) -> Void) { + open func fetchTeamInfo(teamId: String, + _ completion: @escaping (NSError?, NIMTeam?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + teamId) repo.getTeamInfo(teamId: teamId) { [weak self] error, team in if error == nil { @@ -57,7 +44,7 @@ public class TeamChatViewModel: ChatViewModel, NIMTeamManagerDelegate { // MARK: NIMTeamManagerDelegate - public func onTeamRemoved(_ team: NIMTeam) { + open func onTeamRemoved(_ team: NIMTeam) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + (team.teamId ?? "nil")) if session.sessionId == team.teamId { if let delegate = delegate as? TeamChatViewModelDelegate { @@ -66,7 +53,7 @@ public class TeamChatViewModel: ChatViewModel, NIMTeamManagerDelegate { } } - public func onTeamUpdated(_ team: NIMTeam) { + open func onTeamUpdated(_ team: NIMTeam) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + (team.teamId ?? "nil")) if session.sessionId == team.teamId { self.team = team @@ -76,16 +63,21 @@ public class TeamChatViewModel: ChatViewModel, NIMTeamManagerDelegate { } } - public func onTeamMemberUpdated(_ team: NIMTeam, withMembers memberIDs: [String]?) { + open func onTeamMemberUpdated(_ team: NIMTeam, withMembers memberIDs: [String]?) { guard let membersIds = memberIDs else { return } for memberId in membersIds { - let user = UserInfoProvider.shared.getUserInfo(userId: memberId) - newUserInfoDic[memberId] = user + if let user = UserInfoProvider.shared.getUserInfo(userId: memberId) { + ChatUserCache.updateUserInfo(user) + } } if let delegate = delegate as? TeamChatViewModelDelegate { delegate.onTeamMemberUpdate(team: team) } } + + public func getTeamMember() { + teamMember = getTeamMember(userId: IMKitClient.instance.imAccid(), teamId: session.sessionId) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift index 4a63304d..6eb70637 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift @@ -8,12 +8,12 @@ import NECoreIMKit import NIMSDK @objcMembers -public class TeamMemberSelectVM: NSObject { +open class TeamMemberSelectVM: NSObject { public var chatRepo = ChatRepo.shared private let className = "TeamMemberSelectVM" - public func fetchTeamMembers(sessionId: String, - _ completion: @escaping (Error?, ChatTeamInfoModel?) -> Void) { + open func fetchTeamMembers(sessionId: String, + _ completion: @escaping (Error?, ChatTeamInfoModel?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId: " + sessionId) chatRepo.getTeamInfo(sessionId, completion) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift index 6c3d1b62..2cf6919b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift @@ -13,10 +13,10 @@ protocol UserSettingViewModelDelegate: NSObjectProtocol { } @objcMembers -public class UserSettingViewModel: NSObject { +open class UserSettingViewModel: NSObject { var repo = ChatRepo.shared - var userInfo: User? + var userInfo: NEKitUser? var cellDatas = [UserSettingCellModel]() @@ -69,6 +69,9 @@ public class UserSettingViewModel: NSObject { if let uid = weakSelf?.userInfo?.userId { let session = NIMSession(uid, type: .P2P) if isOpen { + if weakSelf?.getRecenterSession() == nil { + weakSelf?.addRecentetSession() + } let params = NIMAddStickTopSessionParams(session: session) weakSelf?.repo.chatExtendProvider .addStickTopSession(params: params) { error, info in @@ -96,28 +99,21 @@ public class UserSettingViewModel: NSObject { } } } - - /* - let blackList = UserSettingCellModel() - blackList.cornerType = .bottomRight.union(.bottomLeft) - blackList.cellName = "加入黑名单" - if let isBlack = user.imUser?.isInMyBlackList() { - blackList.switchOpen = isBlack - } - blackList.swichChange = { isOpen in - if let uid = weakSelf?.userInfo?.userId { - if isOpen { - weakSelf?.repo.addBlackList(account: uid, { error in - print("add black list : ", error as Any) - }) - }else { - weakSelf?.repo.removeFromBlackList(account: uid, { error in - print("remo black list : ", error as Any) - }) - } - } - } - */ cellDatas.append(contentsOf: [mark, remind, setTop]) } + + public func addRecentetSession() { + if let uid = userInfo?.userId { + let currentSession = NIMSession(uid, type: .P2P) + repo.addRecentSession(currentSession) + } + } + + public func getRecenterSession() -> NIMRecentSession? { + if let uid = userInfo?.userId { + let currentSession = NIMSession(uid, type: .P2P) + return repo.getRecentSession(currentSession) + } + return nil + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift b/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift index ba722371..a891a613 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift @@ -77,16 +77,24 @@ public class MessageProperties: NSObject { // 接收到的消息体的背景色 public var receiveMessageBg: UIColor = .clear + // 背景图片拉伸参数(边距偏移) + public var backgroundImageCapInsets = UIEdgeInsets(top: 35, left: 25, bottom: 10, right: 25) + // 不设置头像的用户所展示的文字头像中的文字颜色 public var userNickColor: UIColor = .white // 不设置头像的用户所展示的文字头像中的文字字体大小 public var userNickTextSize: CGFloat = 12 + // 标记列表字体大小(文本类型) + public var pinMessageTextSize: CGFloat = 14 + // 单聊中是否展示已读未读状态 public var showP2pMessageStatus: Bool = true // 群聊中是否展示已读未读状态 public var showTeamMessageStatus: Bool = true + // 群聊中是否展示好友昵称 + public var showTeamMessageNick: Bool = true // 会话界面是否展示标题栏 public var showTitleBar: Bool = true // 是否展示标题栏右侧图标按钮 diff --git a/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift b/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift index 69b20770..44f6c34e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift @@ -11,7 +11,7 @@ import SDWebImageSVGKitPlugin import SDWebImageWebPCoder @objcMembers -public class ChatRouter: NSObject { +open class ChatRouter: NSObject { public static func setupInit() { NIMKitFileLocationHelper.setStaticAppkey(NIMSDK.shared().appKey()) NIMKitFileLocationHelper.setStaticUserId(NIMSDK.shared().loginManager.currentAccount()) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift index 17070440..d38eb966 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift @@ -6,13 +6,14 @@ import Foundation // 距离cell边缘的距离 -public let chat_cell_margin = 16.0 +public let chat_cell_margin: CGFloat = 16.0 // 控件之间的间距 -public let chat_content_margin = 8.0 +public let chat_content_margin: CGFloat = 8.0 // 头像宽高 -public let chat_headWH = 32.0 +public let chat_headWH: CGFloat = 32.0 + // 时间cell的高度(固定) -public let chat_timeCellH = 21.0 +public let chat_timeCellH: CGFloat = 22.0 // 图片最大宽高 public let chat_pic_size = CGSize(width: 150, height: 200) @@ -21,19 +22,19 @@ public let chat_pic_size = CGSize(width: 150, height: 200) public let chat_file_size = CGSize(width: 254, height: 56) // 单行气泡高度 -public let chat_min_h = 40.0 +public let chat_min_h: CGFloat = 40.0 // 回复消息replyLabel高度 -public let chat_reply_height = 16.0 +public let chat_reply_height: CGFloat = 16.0 // 气泡最大宽度 -public let chat_content_maxW = (kScreenWidth - 136) +public let chat_content_maxW: CGFloat = (kScreenWidth - 136) // 文本内容最大宽度 -public let chat_text_maxW = chat_content_maxW - 2 * chat_content_margin +public let chat_text_maxW: CGFloat = chat_content_maxW - 2 * chat_content_margin // pin消息需要增加的高度 -public let chat_pin_height = 16.0 +public let chat_pin_height: CGFloat = 16.0 -// 群聊起泡上方用户名label高度,p2p无此展示,高度为0 -public let chat_full_name_height = 16.0 +// 群聊气泡上方用户名label高度,p2p无此展示,高度为0 +public let chat_full_name_height: CGFloat = 16.0 diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift index 2bf471bd..6a473b6c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import Foundation +import NEChatKit @_exported import NECommonKit @_exported import NECommonUIKit @_exported import NECoreIMKit @@ -14,7 +15,7 @@ func chatLocalizable(_ key: String) -> String { coreLoader.localizable(key) } -func getJSONStringFromDictionary(_ dictionary: [String: Any]) -> String { +public func getJSONStringFromDictionary(_ dictionary: [String: Any]) -> String { if !JSONSerialization.isValidJSONObject(dictionary) { print("not parse to json string") return "" @@ -26,7 +27,7 @@ func getJSONStringFromDictionary(_ dictionary: [String: Any]) -> String { return "" } -func getDictionaryFromJSONString(_ jsonString: String) -> NSDictionary? { +public func getDictionaryFromJSONString(_ jsonString: String) -> NSDictionary? { if let jsonData = jsonString.data(using: .utf8), let dict = try? JSONSerialization.jsonObject( with: jsonData, @@ -112,6 +113,9 @@ let DefaultTextFont: ((Float) -> UIFont) = { let TextNormalColor: UIColor = HexRGB(0x333333) let SubTextColor: UIColor = HexRGB(0x666666) let PlaceholderTextColor: UIColor = HexRGB(0xA6ADB6) +let multiForwardLineColor: UIColor = HexRGB(0xF0F1F5) +let forwardLineColor: UIColor = HexRGB(0xE1E6E8) +let multiForwardborderColor: UIColor = HexRGB(0xE4E9F2) let HexRGB: ((Int) -> UIColor) = { (rgbValue: Int) -> UIColor in HexRGBAlpha(rgbValue, 1.0) @@ -128,8 +132,7 @@ let HexRGBAlpha: ((Int, Float) -> UIColor) = { (rgbValue: Int, alpha: Float) -> // MARK: notificationkey -enum NotificationName { - static let updateFriendInfo = Notification.Name("chat.updateFriendInfo") +extension NENotificationName { // 参数 serverId: string static let createServer = Notification.Name(rawValue: "qchat.createServer") // param channel: ChatChannel @@ -137,6 +140,7 @@ enum NotificationName { static let updateChannel = Notification.Name(rawValue: "qchat.updateChannel") static let deleteChannel = Notification.Name(rawValue: "qchat.deleteChannel") static let leaveTeamBySelf = Notification.Name(rawValue: "team.leaveTeamBySelf") + static let popGroupChatVC = Notification.Name(rawValue: "team.popGroupChatVC") // static let login = Notification.Name(rawValue:"qchat.login") static let logout = Notification.Name(rawValue: "qchat.logout") diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift index 16a7ce23..18a155ad 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift @@ -8,7 +8,7 @@ import UIKit @objc @objcMembers -public class NEChatUIKitClient: NSObject { +open class NEChatUIKitClient: NSObject { public static let instance = NEChatUIKitClient() private var customRegisterDic = [String: UITableViewCell.Type]() public var moreAction = [NEMoreItemModel]() @@ -43,7 +43,7 @@ public class NEChatUIKitClient: NSObject { /// 获取更多面板数据 /// - Returns: 返回更多操作数据 - public func getMoreActionData(sessionType: NIMSessionType) -> [NEMoreItemModel] { + open func getMoreActionData(sessionType: NIMSessionType) -> [NEMoreItemModel] { var more = [NEMoreItemModel]() moreAction.forEach { model in if model.type != .rtc { @@ -61,13 +61,13 @@ public class NEChatUIKitClient: NSObject { } /// 新增聊天页针对自定义消息的cell扩展,以及现有cell样式覆盖 - public func regsiterCustomCell(_ registerDic: [String: UITableViewCell.Type]) { + open func regsiterCustomCell(_ registerDic: [String: UITableViewCell.Type]) { registerDic.forEach { (key: String, value: UITableViewCell.Type) in customRegisterDic[key] = value } } - public func getRegisterCustomCell() -> [String: UITableViewCell.Type] { + open func getRegisterCustomCell() -> [String: UITableViewCell.Type] { customRegisterDic } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift index fe86ebc8..1bca66c5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift @@ -13,7 +13,7 @@ var tempLabelForCalc: UILabel = { }() extension String { - // 计算文字size + /// 计算 string 的 size static func getTextRectSize(_ text: String, font: UIFont, size: CGSize) -> CGSize { let attributes = [NSAttributedString.Key.font: font] let option = NSStringDrawingOptions.usesLineFragmentOrigin @@ -23,10 +23,19 @@ extension String { } /// 计算 string 的行数,使用 font 的 lineHeight - static func calculateMaxLines(width: CGFloat, string: String, font: UIFont) -> Int { + static func calculateMaxLines(width: CGFloat, string: String?, font: UIFont) -> Int { let maxSize = CGSize(width: width, height: CGFloat(Float.infinity)) let charSize = font.lineHeight - let textSize = getTextRectSize(string, font: font, size: maxSize) + let textSize = string?.finalSize(font, maxSize) ?? .zero + let lines = Int(textSize.height / charSize) + return lines + } + + /// 计算 label 的行数,使用 font 的 lineHeight + static func calculateMaxLines(width: CGFloat, attributeString: NSAttributedString?, font: UIFont) -> Int { + let maxSize = CGSize(width: width, height: CGFloat(Float.infinity)) + let charSize = font.lineHeight + let textSize = attributeString?.finalSize(font, maxSize) ?? .zero let lines = Int(textSize.height / charSize) return lines } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift index 9f649440..f09afd29 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift @@ -126,11 +126,8 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco timeLabelRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) if let m = model as? MessageAudioModel { if isSend { timeLabelRight.text = "\(m.duration)" + "″" diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift index 06eeacb3..f586fed8 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift @@ -9,16 +9,20 @@ open class FunChatMessageBaseCell: NEBaseChatMessageCell { public let funMargin: CGFloat = 5.2 override open func initProperty() { super.initProperty() + timeLabel.backgroundColor = .funChatBackgroundColor + readView.borderLayer.strokeColor = UIColor.funChatThemeColor.cgColor readView.sectorLayer.fillColor = UIColor.funChatThemeColor.cgColor var image = NEKitChatConfig.shared.ui.messageProperties.leftBubbleBg ?? UIImage.ne_imageNamed(name: "chat_message_receive_fun") bubbleImageLeft.image = image? - .resizableImage(withCapInsets: UIEdgeInsets(top: 35, left: 25, bottom: 10, right: 25)) + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) image = NEKitChatConfig.shared.ui.messageProperties.rightBubbleBg ?? UIImage.ne_imageNamed(name: "chat_message_send_fun") bubbleImageRight.image = image? - .resizableImage(withCapInsets: UIEdgeInsets(top: 35, left: 25, bottom: 10, right: 25)) + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) + + seletedBtn.setImage(.ne_imageNamed(name: "fun_select"), for: .selected) } override open func baseCommonUI() { @@ -26,8 +30,7 @@ open class FunChatMessageBaseCell: NEBaseChatMessageCell { setAvatarImgSize(size: 42) contentView.updateLayoutConstraint(firstItem: fullNameLabel, seconedItem: avatarImageLeft, attribute: .left, constant: 8 + funMargin) - contentView.removeLayoutConstraint(firstItem: fullNameLabel, seconedItem: avatarImageLeft, attribute: .top) - fullNameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0).isActive = true + contentView.updateLayoutConstraint(firstItem: fullNameLabel, seconedItem: avatarImageLeft, attribute: .top, constant: -4) } override open func initSubviewsLayout() { diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift index bc5fe7a6..ee526efc 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift @@ -62,11 +62,8 @@ open class FunChatMessageCallCell: FunChatMessageBaseCell { contentLabelRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let contentLabel = isSend ? contentLabelRight : contentLabelLeft if let m = model as? MessageCallRecordModel { contentLabel.attributedText = m.attributeStr diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift index c9545d24..5dedf7ea 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift @@ -14,6 +14,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { let view_img = UIImageView() view_img.translatesAutoresizingMaskIntoConstraints = false view_img.backgroundColor = .clear + view_img.accessibilityIdentifier = "id.fileType" return view_img }() @@ -21,6 +22,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { let state = FileStateView() state.translatesAutoresizingMaskIntoConstraints = false state.backgroundColor = .clear + state.accessibilityIdentifier = "id.fileStatus" return state }() @@ -69,6 +71,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { let view_img = UIImageView() view_img.translatesAutoresizingMaskIntoConstraints = false view_img.backgroundColor = .clear + view_img.accessibilityIdentifier = "id.fileType" return view_img }() @@ -76,6 +79,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { let state = FileStateView() state.translatesAutoresizingMaskIntoConstraints = false state.backgroundColor = .clear + state.accessibilityIdentifier = "id.fileStatus" return state }() @@ -149,10 +153,10 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { imgViewLeft.heightAnchor.constraint(equalToConstant: 32), ]) - addSubview(stateViewLeft) + imgViewLeft.addSubview(stateViewLeft) NSLayoutConstraint.activate([ - stateViewLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: 10), - stateViewLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: 10), + stateViewLeft.leftAnchor.constraint(equalTo: imgViewLeft.leftAnchor, constant: 0), + stateViewLeft.topAnchor.constraint(equalTo: imgViewLeft.topAnchor, constant: 0), stateViewLeft.widthAnchor.constraint(equalToConstant: 32), stateViewLeft.heightAnchor.constraint(equalToConstant: 32), ]) @@ -181,10 +185,10 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { imgViewRight.heightAnchor.constraint(equalToConstant: 32), ]) - addSubview(stateViewRight) + imgViewRight.addSubview(stateViewRight) NSLayoutConstraint.activate([ - stateViewRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: 10), - stateViewRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: 10), + stateViewRight.leftAnchor.constraint(equalTo: imgViewRight.leftAnchor, constant: 0), + stateViewRight.topAnchor.constraint(equalTo: imgViewRight.topAnchor, constant: 0), stateViewRight.widthAnchor.constraint(equalToConstant: 32), stateViewRight.heightAnchor.constraint(equalToConstant: 32), ]) @@ -209,11 +213,8 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { labelViewRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let stateView = isSend ? stateViewRight : stateViewLeft let imgView = isSend ? imgViewRight : imgViewLeft let titleLabel = isSend ? titleLabelRight : titleLabelLeft diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift index ea828db0..0c5e1790 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift @@ -59,11 +59,8 @@ open class FunChatMessageImageCell: FunChatMessageBaseCell { contentImageViewRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let contentImageView = isSend ? contentImageViewRight : contentImageViewLeft if let m = model as? MessageImageModel, let imageUrl = m.imageUrl { @@ -76,7 +73,8 @@ open class FunChatMessageImageCell: FunChatMessageBaseCell { completed: nil ) } else { - contentImageView.image = UIImage(contentsOfFile: imageUrl) + let url = URL(fileURLWithPath: imageUrl) + contentImageView.sd_setImage(with: url) } } else { contentImageView.image = nil diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift index e729138c..8e2c9509 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift @@ -89,6 +89,7 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { backgroundViewLeft.layer.cornerRadius = 4 backgroundViewLeft.layer.borderWidth = 1 backgroundViewLeft.layer.borderColor = UIColor.ne_outlineColor.cgColor + backgroundViewLeft.accessibilityIdentifier = "id.mapView" let messageLongPress = UILongPressGestureRecognizer( target: self, @@ -104,6 +105,7 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { ]) let messageTap = UITapGestureRecognizer(target: self, action: #selector(tapMessage)) + messageTap.cancelsTouchesInView = false backgroundViewLeft.addGestureRecognizer(messageTap) backgroundViewLeft.addSubview(titleLabelLeft) @@ -158,6 +160,8 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { backgroundViewRight.layer.cornerRadius = 4 backgroundViewRight.layer.borderWidth = 1 backgroundViewRight.layer.borderColor = UIColor.ne_outlineColor.cgColor + backgroundViewRight.accessibilityIdentifier = "id.mapView" + let messageLongPress = UILongPressGestureRecognizer( target: self, action: #selector(longPress) @@ -171,6 +175,7 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { backgroundViewRight.bottomAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor), ]) let messageTap = UITapGestureRecognizer(target: self, action: #selector(tapMessage)) + messageTap.cancelsTouchesInView = false backgroundViewRight.addGestureRecognizer(messageTap) backgroundViewRight.addSubview(titleLabelRight) @@ -222,11 +227,8 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { backgroundViewRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let titleLabel = isSend ? titleLabelRight : titleLabelLeft let subTitleLabel = isSend ? subTitleLabelRight : subTitleLabelLeft let mapView = isSend ? mapViewRight : mapViewLeft diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift new file mode 100644 index 00000000..15ba90b3 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift @@ -0,0 +1,384 @@ + +// 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 NIMSDK +import UIKit + +@objcMembers +open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { + let contentWidth: CGFloat = 228 + let titleLabelFontSize: CGFloat = 16 + let contentLabelFontSize: CGFloat = 12 + + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open func setupUI() { + setupUIRight() + setupUILeft() + } + + open func setupUILeft() { + bubbleImageLeft.image = nil + let image = UIImage.ne_imageNamed(name: "multiForward_message_receive_fun") + backViewLeft.image = image? + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) + + bubbleImageLeft.addSubview(backViewLeft) + NSLayoutConstraint.activate([ + backViewLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: 0), + backViewLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: 0), + backViewLeft.bottomAnchor.constraint(equalTo: bubbleImageLeft.bottomAnchor, constant: -0), + backViewLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor, constant: -0), + ]) + + backViewLeft.addSubview(titleLabelLeft1) + NSLayoutConstraint.activate([ + titleLabelLeft1.leftAnchor.constraint(equalTo: backViewLeft.leftAnchor, constant: 12 + funMargin), + titleLabelLeft1.rightAnchor.constraint(lessThanOrEqualTo: backViewLeft.rightAnchor, constant: -102), + titleLabelLeft1.topAnchor.constraint(equalTo: backViewLeft.topAnchor, constant: 12), + titleLabelLeft1.heightAnchor.constraint(equalToConstant: 22), + ]) + + backViewLeft.addSubview(titleLabelLeft2) + NSLayoutConstraint.activate([ + titleLabelLeft2.leftAnchor.constraint(equalTo: titleLabelLeft1.rightAnchor), + titleLabelLeft2.centerYAnchor.constraint(equalTo: titleLabelLeft1.centerYAnchor), + titleLabelLeft2.heightAnchor.constraint(equalToConstant: 22), + titleLabelLeft2.widthAnchor.constraint(equalToConstant: 88), + ]) + + backViewLeft.addSubview(contentLabelLeft1) + NSLayoutConstraint.activate([ + contentLabelLeft1.leftAnchor.constraint(equalTo: titleLabelLeft1.leftAnchor), + contentLabelLeft1.topAnchor.constraint(equalTo: titleLabelLeft1.bottomAnchor, constant: 4), + contentLabelLeft1.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelLeft1.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewLeft.addSubview(contentLabelLeft2) + NSLayoutConstraint.activate([ + contentLabelLeft2.leftAnchor.constraint(equalTo: contentLabelLeft1.leftAnchor), + contentLabelLeft2.topAnchor.constraint(equalTo: contentLabelLeft1.bottomAnchor), + contentLabelLeft2.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelLeft2.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewLeft.addSubview(contentLabelLeft3) + NSLayoutConstraint.activate([ + contentLabelLeft3.leftAnchor.constraint(equalTo: contentLabelLeft2.leftAnchor), + contentLabelLeft3.topAnchor.constraint(equalTo: contentLabelLeft2.bottomAnchor), + contentLabelLeft3.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelLeft3.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewLeft.addSubview(contentHistoryLeft) + NSLayoutConstraint.activate([ + contentHistoryLeft.leftAnchor.constraint(equalTo: titleLabelLeft1.leftAnchor), + contentHistoryLeft.bottomAnchor.constraint(equalTo: backViewLeft.bottomAnchor, constant: -8), + contentHistoryLeft.widthAnchor.constraint(equalToConstant: 60), + contentHistoryLeft.heightAnchor.constraint(equalToConstant: 14), + ]) + + backViewLeft.addSubview(dividerLineLeft) + NSLayoutConstraint.activate([ + dividerLineLeft.leftAnchor.constraint(equalTo: backViewLeft.leftAnchor, constant: funMargin), + dividerLineLeft.rightAnchor.constraint(equalTo: backViewLeft.rightAnchor), + dividerLineLeft.topAnchor.constraint(equalTo: contentHistoryLeft.topAnchor, constant: -8), + dividerLineLeft.heightAnchor.constraint(equalToConstant: 1), + ]) + } + + open func setupUIRight() { + bubbleImageRight.image = nil + let image = UIImage.ne_imageNamed(name: "multiForward_message_send_fun") + backViewRight.image = image? + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) + + bubbleImageRight.addSubview(backViewRight) + NSLayoutConstraint.activate([ + backViewRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: 0), + backViewRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: 0), + backViewRight.bottomAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: -0), + backViewRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -0), + ]) + + backViewRight.addSubview(titleLabelRight1) + NSLayoutConstraint.activate([ + titleLabelRight1.leftAnchor.constraint(equalTo: backViewRight.leftAnchor, constant: 12), + titleLabelRight1.rightAnchor.constraint(lessThanOrEqualTo: backViewRight.rightAnchor, constant: -102), + titleLabelRight1.topAnchor.constraint(equalTo: backViewRight.topAnchor, constant: 12), + titleLabelRight1.heightAnchor.constraint(equalToConstant: 22), + ]) + + backViewRight.addSubview(titleLabelRight2) + NSLayoutConstraint.activate([ + titleLabelRight2.leftAnchor.constraint(equalTo: titleLabelRight1.rightAnchor), + titleLabelRight2.centerYAnchor.constraint(equalTo: titleLabelRight1.centerYAnchor), + titleLabelRight2.heightAnchor.constraint(equalToConstant: 22), + titleLabelRight2.widthAnchor.constraint(equalToConstant: 88), + ]) + + backViewRight.addSubview(contentLabelRight1) + NSLayoutConstraint.activate([ + contentLabelRight1.leftAnchor.constraint(equalTo: titleLabelRight1.leftAnchor), + contentLabelRight1.topAnchor.constraint(equalTo: titleLabelRight1.bottomAnchor, constant: 4), + contentLabelRight1.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelRight1.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewRight.addSubview(contentLabelRight2) + NSLayoutConstraint.activate([ + contentLabelRight2.leftAnchor.constraint(equalTo: contentLabelRight1.leftAnchor), + contentLabelRight2.topAnchor.constraint(equalTo: contentLabelRight1.bottomAnchor), + contentLabelRight2.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelRight2.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewRight.addSubview(contentLabelRight3) + NSLayoutConstraint.activate([ + contentLabelRight3.leftAnchor.constraint(equalTo: contentLabelRight2.leftAnchor), + contentLabelRight3.topAnchor.constraint(equalTo: contentLabelRight2.bottomAnchor), + contentLabelRight3.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelRight3.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewRight.addSubview(contentHistoryRight) + NSLayoutConstraint.activate([ + contentHistoryRight.leftAnchor.constraint(equalTo: titleLabelRight1.leftAnchor), + contentHistoryRight.bottomAnchor.constraint(equalTo: backViewRight.bottomAnchor, constant: -8), + contentHistoryRight.widthAnchor.constraint(equalToConstant: 60), + contentHistoryRight.heightAnchor.constraint(equalToConstant: 14), + ]) + + backViewRight.addSubview(dividerLineRight) + NSLayoutConstraint.activate([ + dividerLineRight.leftAnchor.constraint(equalTo: backViewRight.leftAnchor), + dividerLineRight.rightAnchor.constraint(equalTo: backViewRight.rightAnchor, constant: -funMargin), + dividerLineRight.topAnchor.constraint(equalTo: contentHistoryRight.topAnchor, constant: -8), + dividerLineRight.heightAnchor.constraint(equalToConstant: 1), + ]) + } + + override open func showLeftOrRight(showRight: Bool) { + super.showLeftOrRight(showRight: showRight) + } + + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) + guard let data = NECustomAttachment.dataOfCustomMessage(message: model.message) else { + return + } + + let font = UIFont.systemFont(ofSize: contentLabelFontSize) + let bubbleW = isSend ? bubbleWRight : bubbleWLeft + let bubbleH = isSend ? bubbleHRight : bubbleHLeft + let titleLabel = isSend ? titleLabelRight1 : titleLabelLeft1 + let titleLabel2 = isSend ? titleLabelRight2 : titleLabelLeft2 + let contentLabel1 = isSend ? contentLabelRight1 : contentLabelLeft1 + let contentLabel2 = isSend ? contentLabelRight2 : contentLabelLeft2 + let contentLabel3 = isSend ? contentLabelRight3 : contentLabelLeft3 + + bubbleW?.constant = 256 + bubbleH?.constant = 130 + + if let sessionName = data["sessionName"] as? String { + titleLabel.attributedText = + NEEmotionTool.getAttWithStr(str: sessionName, + font: .systemFont(ofSize: titleLabelFontSize), + color: .ne_darkText) + } else { + titleLabel2.text = chatLocalizable("chat_history") + } + + guard let abstracts = data["abstracts"] as? [[String: Any]] else { return } + + contentLabel2.attributedText = nil + contentLabel3.attributedText = nil + for i in 0 ..< abstracts.count { + var contentLabel = contentLabel1 + if i == 1 { + contentLabel = contentLabel2 + } else if i == 2 { + contentLabel = contentLabel3 + } + + var contentText = "" + if var senderNick = abstracts[i]["senderNick"] as? String { + if senderNick.count > 5 { + // 截取字符串 abcdefg -> ab...fg + let leftEndIndex = senderNick.index(senderNick.startIndex, offsetBy: 2) + let rightStartIndex = senderNick.index(senderNick.endIndex, offsetBy: -2) + senderNick = senderNick[senderNick.startIndex ..< leftEndIndex] + "..." + senderNick[rightStartIndex ..< senderNick.endIndex] + } + contentText = senderNick + if let content = abstracts[i]["content"] as? String { + contentText += ":" + content + } + } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 1 // 设置行间距 + paragraphStyle.lineBreakMode = .byTruncatingTail + let attributedText = NEEmotionTool.getAttWithStr(str: contentText, + font: font, + color: .funChatMultiForwardContentColor) + attributedText.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedText.length)) + contentLabel.attributedText = attributedText + } + + let numCount1 = String.calculateMaxLines(width: contentWidth, + attributeString: contentLabel1.attributedText, + font: font) + if numCount1 == 1 { + contentLabel2.numberOfLines = 2 + contentLabel2.isHidden = contentLabel2.attributedText == nil + let numCount2 = String.calculateMaxLines(width: contentWidth, + attributeString: contentLabel2.attributedText, + font: font) + contentLabel3.isHidden = contentLabel3.attributedText == nil || numCount2 >= 2 + } else if numCount1 == 2 { + contentLabel2.numberOfLines = 1 + contentLabel2.isHidden = contentLabel2.attributedText == nil + contentLabel3.isHidden = true + } else { + contentLabel2.isHidden = true + contentLabel3.isHidden = true + } + } + + // MARK: - lazy load + + public lazy var backViewLeft: UIImageView = { + let view = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + public lazy var titleLabelLeft1: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.name1" + return label + }() + + public lazy var titleLabelLeft2: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = chatLocalizable("chat_history_by") + label.textColor = .ne_darkText + label.accessibilityIdentifier = "id.name2" + return label + }() + + public lazy var contentLabelLeft1: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 3 + label.accessibilityIdentifier = "id.content1" + return label + }() + + public lazy var contentLabelLeft2: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 2 + label.accessibilityIdentifier = "id.content2" + return label + }() + + public lazy var contentLabelLeft3: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.content3" + return label + }() + + public lazy var dividerLineLeft: UIView = { + let line = UIView() + line.translatesAutoresizingMaskIntoConstraints = false + line.backgroundColor = multiForwardLineColor + return line + }() + + public lazy var contentHistoryLeft: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .systemFont(ofSize: 12) + label.textColor = .ne_lightText + label.text = chatLocalizable("chat_history") + label.accessibilityIdentifier = "id.contentHistoryLeft" + return label + }() + + public lazy var backViewRight: UIImageView = { + let view = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + public lazy var titleLabelRight1: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.name1" + return label + }() + + public lazy var titleLabelRight2: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = chatLocalizable("chat_history_by") + label.textColor = .ne_darkText + label.accessibilityIdentifier = "id.name2" + return label + }() + + public lazy var contentLabelRight1: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 3 + label.accessibilityIdentifier = "id.content1" + return label + }() + + public lazy var contentLabelRight2: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 2 + label.accessibilityIdentifier = "id.content2" + return label + }() + + public lazy var contentLabelRight3: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.content3" + return label + }() + + public lazy var dividerLineRight: UIView = { + let line = UIView() + line.translatesAutoresizingMaskIntoConstraints = false + line.backgroundColor = multiForwardLineColor + return line + }() + + public lazy var contentHistoryRight: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .systemFont(ofSize: 12) + label.textColor = .ne_lightText + label.text = chatLocalizable("chat_history") + label.accessibilityIdentifier = "id.contentHistoryRight" + return label + }() +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift index 11b6dd7e..57cd1925 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift @@ -9,10 +9,9 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { public lazy var replyLabelLeft: UILabel = { let replyLabelLeft = UILabel() replyLabelLeft.numberOfLines = 2 - replyLabelLeft.textColor = UIColor(hexString: "#666666") // 换肤颜色提取 + replyLabelLeft.textColor = .ne_greyText replyLabelLeft.translatesAutoresizingMaskIntoConstraints = false replyLabelLeft.font = UIFont.systemFont(ofSize: 13) - replyLabelLeft.textAlignment = .justified return replyLabelLeft }() @@ -21,6 +20,7 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyTextView.translatesAutoresizingMaskIntoConstraints = false replyTextView.backgroundColor = .funChatInputReplyBg replyTextView.layer.cornerRadius = 4 + replyTextView.accessibilityIdentifier = "id.replyTextView" return replyTextView }() @@ -32,7 +32,6 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyLabelRight.textColor = .ne_greyText replyLabelRight.translatesAutoresizingMaskIntoConstraints = false replyLabelRight.font = UIFont.systemFont(ofSize: 13) - replyLabelRight.textAlignment = .justified return replyLabelRight }() @@ -41,9 +40,13 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyTextView.translatesAutoresizingMaskIntoConstraints = false replyTextView.backgroundColor = .funChatInputReplyBg replyTextView.layer.cornerRadius = 4 + replyTextView.accessibilityIdentifier = "id.replyTextView" return replyTextView }() + public var funPinLabelLeftTopAnchor: NSLayoutConstraint? // 左侧标记文案顶部布局约束 + public var funPinLabelRightTopAnchor: NSLayoutConstraint? // 右侧标记文案顶部布局约束 + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonUI() @@ -69,9 +72,10 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyTextViewLeft.widthAnchor.constraint(lessThanOrEqualToConstant: chat_content_maxW - funMargin), ]) - contentView.removeLayoutConstraint(firstItem: pinLabelLeft, seconedItem: bubbleImageLeft, attribute: .top) contentView.updateLayoutConstraint(firstItem: pinLabelLeft, seconedItem: bubbleImageLeft, attribute: .left, constant: 14 + funMargin) - pinLabelLeft.topAnchor.constraint(equalTo: replyTextViewLeft.bottomAnchor, constant: 4).isActive = true + pinLabelLeftTopAnchor?.isActive = false + funPinLabelLeftTopAnchor = pinLabelLeft.topAnchor.constraint(equalTo: replyTextViewLeft.bottomAnchor, constant: 4) + funPinLabelLeftTopAnchor?.isActive = true replyTextViewLeft.addSubview(replyLabelLeft) NSLayoutConstraint.activate([ @@ -91,9 +95,10 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyTextViewRight.widthAnchor.constraint(lessThanOrEqualToConstant: chat_content_maxW - funMargin), ]) - contentView.removeLayoutConstraint(firstItem: pinLabelRight, seconedItem: bubbleImageRight, attribute: .top) contentView.updateLayoutConstraint(firstItem: pinLabelRight, seconedItem: bubbleImageRight, attribute: .right, constant: -funMargin) - pinLabelRight.topAnchor.constraint(equalTo: replyTextViewRight.bottomAnchor, constant: 4).isActive = true + pinLabelRightTopAnchor?.isActive = false + funPinLabelRightTopAnchor = pinLabelRight.topAnchor.constraint(equalTo: replyTextViewRight.bottomAnchor, constant: 4) + funPinLabelRightTopAnchor?.isActive = true replyTextViewRight.addSubview(replyLabelRight) NSLayoutConstraint.activate([ @@ -112,8 +117,11 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { open func addReplyGesture() { let replyViewTapLeft = UITapGestureRecognizer(target: self, action: #selector(tapReplyView(tap:))) + replyViewTapLeft.cancelsTouchesInView = false replyTextViewLeft.addGestureRecognizer(replyViewTapLeft) + let replyViewTapRight = UITapGestureRecognizer(target: self, action: #selector(tapReplyView(tap:))) + replyViewTapRight.cancelsTouchesInView = false replyTextViewRight.addGestureRecognizer(replyViewTapRight) } @@ -122,19 +130,15 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { delegate?.didTapMessageView(self, contentModel) } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let replyLabel = isSend ? replyLabelRight : replyLabelLeft if let text = model.replyText, let font = replyLabel.font { - let normalReplyText = NEEmotionTool.getAttWithStr(str: text, - font: font, - color: replyLabel.textColor) - replyLabel.attributedText = normalReplyText.attributedSubstring(from: NSRange(location: 1, length: normalReplyText.length - 1)) + replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, + font: font, + color: replyLabel.textColor) } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift index 405c928f..33dd595e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift @@ -35,7 +35,7 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { NSLayoutConstraint.activate([ revokeLabelLeft.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), revokeLabelLeft.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), - revokeLabelLeft.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + revokeLabelLeft.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 6), revokeLabelLeft.heightAnchor.constraint(equalToConstant: 16), ]) } @@ -50,7 +50,7 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { revokeLabelRightXAnchor?.isActive = true NSLayoutConstraint.activate([ revokeLabelRight.widthAnchor.constraint(equalToConstant: 120), - revokeLabelRight.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + revokeLabelRight.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 6), revokeLabelRight.heightAnchor.constraint(equalToConstant: 16), ]) @@ -69,22 +69,27 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { } override open func showLeftOrRight(showRight: Bool) { - super.showLeftOrRight(showRight: showRight) - revokeLabelLeft.isHidden = showRight - reeditButton.isHidden = !showRight - revokeLabelRight.isHidden = !showRight avatarImageLeft.isHidden = true + nameLabelLeft.isHidden = true bubbleImageLeft.isHidden = true + pinImageLeft.isHidden = true + pinLabelLeft.isHidden = true + fullNameLabel.isHidden = true + avatarImageRight.isHidden = true + nameLabelRight.isHidden = true bubbleImageRight.isHidden = true - seletedBtn.isHidden = true - pinLabelLeft.isHidden = true - pinImageLeft.isHidden = true - pinLabelRight.isHidden = true pinImageRight.isHidden = true + pinLabelRight.isHidden = true + activityView.isHidden = true + readView.isHidden = true + seletedBtn.isHidden = true + + revokeLabelLeft.isHidden = showRight + revokeLabelRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { if let time = model.message?.timestamp { let date = Date() let currentTime = date.timeIntervalSince1970 @@ -93,14 +98,11 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { } } - guard let isSend = model.message?.isOutgoingMsg else { - return - } let revokeLabel = isSend ? revokeLabelRight : revokeLabelLeft model.contentSize = CGSize(width: kScreenWidth, height: 0) - super.setModel(model) - fullNameLabel.isHidden = true + super.setModel(model, isSend) + showLeftOrRight(showRight: isSend) revokeLabel.textColor = .funChatInputViewPlaceholderTextColor if isSend { @@ -108,7 +110,6 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { } else { revokeLabel.text = (model.fullName ?? "") + " " + chatLocalizable("withdrew_message") } - reeditButton.setTitle(chatLocalizable("message_reedit"), for: .normal) if isSend, model.isRevokedText == true { if model.timeOut == true { @@ -116,10 +117,12 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { revokeLabelRightXAnchor?.constant = 0 } else { reeditButton.isHidden = false + reeditButton.setTitle(chatLocalizable("message_reedit"), for: .normal) revokeLabelRightXAnchor?.constant = -32 } } else { reeditButton.isHidden = true + revokeLabelRightXAnchor?.constant = 0 } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift new file mode 100644 index 00000000..90300fee --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift @@ -0,0 +1,117 @@ +// 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 FunChatMessageRichTextCell: FunChatMessageReplyCell { + public lazy var titleLabelLeft: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.isEnabled = false + label.numberOfLines = 0 + label.isUserInteractionEnabled = false + label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) + label.backgroundColor = .clear + return label + }() + + public lazy var titleLabelRight: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.isEnabled = false + label.numberOfLines = 0 + label.isUserInteractionEnabled = false + label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) + label.backgroundColor = .clear + return label + }() + + public var titleLabelLeftHeightAnchor: NSLayoutConstraint? + public var titleLabelRightHeightAnchor: NSLayoutConstraint? + public var contentLabelLeftHeightAnchor: NSLayoutConstraint? + public var contentLabelRightHeightAnchor: NSLayoutConstraint? + + override open func commonUI() { + /// left + bubbleImageLeft.addSubview(titleLabelLeft) + titleLabelLeftHeightAnchor = titleLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + NSLayoutConstraint.activate([ + titleLabelLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor, constant: -chat_content_margin), + titleLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: chat_content_margin + funMargin), + titleLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: chat_content_margin), + titleLabelLeftHeightAnchor!, + ]) + + bubbleImageLeft.addSubview(contentLabelLeft) + contentLabelLeftHeightAnchor = contentLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + NSLayoutConstraint.activate([ + contentLabelLeft.rightAnchor.constraint(equalTo: titleLabelLeft.rightAnchor, constant: -0), + contentLabelLeft.leftAnchor.constraint(equalTo: titleLabelLeft.leftAnchor, constant: 0), + contentLabelLeft.topAnchor.constraint(equalTo: titleLabelLeft.bottomAnchor, constant: chat_content_margin), + contentLabelLeftHeightAnchor!, + ]) + + commonUILeft() + + /// right + bubbleImageRight.addSubview(titleLabelRight) + titleLabelRightHeightAnchor = titleLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + NSLayoutConstraint.activate([ + titleLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -chat_content_margin - funMargin), + titleLabelRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: chat_content_margin), + titleLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: chat_content_margin), + titleLabelRightHeightAnchor!, + ]) + + bubbleImageRight.addSubview(contentLabelRight) + contentLabelRightHeightAnchor = contentLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + NSLayoutConstraint.activate([ + contentLabelRight.rightAnchor.constraint(equalTo: titleLabelRight.rightAnchor, constant: -0), + contentLabelRight.leftAnchor.constraint(equalTo: titleLabelRight.leftAnchor, constant: 0), + contentLabelRight.topAnchor.constraint(equalTo: titleLabelRight.bottomAnchor, constant: chat_content_margin), + contentLabelRightHeightAnchor!, + ]) + + commonUIRight() + } + + override open func showLeftOrRight(showRight: Bool) { + super.showLeftOrRight(showRight: showRight) + titleLabelLeft.isHidden = showRight + titleLabelRight.isHidden = !showRight + } + + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) + let replyView = isSend ? replyTextViewRight : replyTextViewLeft + let titleLabel = isSend ? titleLabelRight : titleLabelLeft + let titleLabelHeightAnchor = isSend ? titleLabelRightHeightAnchor : titleLabelLeftHeightAnchor + let contentLabelHeightAnchor = isSend ? contentLabelRightHeightAnchor : contentLabelLeftHeightAnchor + let pinLabelTopAnchor = isSend ? pinLabelRightTopAnchor : pinLabelLeftTopAnchor + let funPinLabelTopAnchor = isSend ? funPinLabelRightTopAnchor : funPinLabelLeftTopAnchor + + if model.replyText == nil || model.replyText!.isEmpty { + replyView.isHidden = true + funPinLabelTopAnchor?.isActive = false + pinLabelTopAnchor?.isActive = true + } else { + replyView.isHidden = false + funPinLabelTopAnchor?.isActive = true + pinLabelTopAnchor?.isActive = false + } + + if let m = model as? MessageTextModel { + contentLabelHeightAnchor?.constant = m.textHeight + } + + if let m = model as? MessageRichTextModel { + titleLabel.attributedText = m.titleAttributeStr + titleLabelHeightAnchor?.constant = m.titleTextHeight + if m.message?.text?.isEmpty == true { + titleLabelHeightAnchor?.constant = 26 + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift index 7ae9c753..19656f6e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift @@ -14,7 +14,6 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) label.backgroundColor = .clear - label.textAlignment = .justified return label }() @@ -26,7 +25,6 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) label.backgroundColor = .clear - label.textAlignment = .justified return label }() @@ -65,11 +63,8 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { contentLabelRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let contentLabel = isSend ? contentLabelRight : contentLabelLeft let bubbleW = isSend ? bubbleWRight : bubbleWLeft diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTipCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTipCell.swift index d607b0d7..04e46c6d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTipCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTipCell.swift @@ -9,7 +9,7 @@ import UIKit open class FunChatMessageTipCell: NEBaseChatMessageTipCell { override open func commonUI() { super.commonUI() - timeLabel.font = .systemFont(ofSize: 14) - timeLabel.textColor = .funRecordAudioTextColor + contentLabel.font = .systemFont(ofSize: 14) + contentLabel.textColor = .funRecordAudioTextColor } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift index 157f09b5..c5b900dc 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift @@ -120,11 +120,8 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { ]) } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let contentImageView = isSend ? contentImageViewRight : contentImageViewLeft let timeView = isSend ? timeViewRight : timeViewLeft let timeLabel = isSend ? timeLabelRight : timeLabelLeft diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageAudioCell.swift index c88c50d8..e07ecd30 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageAudioCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageAudioCell.swift @@ -15,6 +15,6 @@ open class FunPinMessageAudioCell: NEBasePinMessageAudioCell { headerView.layer.cornerRadius = 4.0 let image = NEKitChatConfig.shared.ui.messageProperties.leftBubbleBg ?? UIImage.ne_imageNamed(name: "fun_pin_message_audio_bg") bubbleImage.image = image? - .resizableImage(withCapInsets: UIEdgeInsets(top: 35, left: 25, bottom: 10, right: 25)) + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageImageCell.swift index 9e8f6925..e468d395 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageImageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageImageCell.swift @@ -1,3 +1,4 @@ + // 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. diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageMultiForwardCell.swift new file mode 100644 index 00000000..89491e67 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageMultiForwardCell.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 FunPinMessageMultiForwardCell: NEBasePinMessageMultiForwardCell { + override open func setupUI() { + super.setupUI() + backLeftConstraint?.constant = 0 + backRightConstraint?.constant = 0 + backView.layer.cornerRadius = 0 + headerView.layer.cornerRadius = 4.0 + + titleLabelFontSize = 16 + contentLabelFontSize = 12 + contentLabelColor = .funChatMultiForwardContentColor + + backViewLeft.addSubview(titleLabelLeft1) + NSLayoutConstraint.activate([ + titleLabelLeft1.leftAnchor.constraint(equalTo: backViewLeft.leftAnchor, constant: 12 + funMargin), + titleLabelLeft1.rightAnchor.constraint(lessThanOrEqualTo: backViewLeft.rightAnchor, constant: -102), + titleLabelLeft1.topAnchor.constraint(equalTo: backViewLeft.topAnchor, constant: 12), + titleLabelLeft1.heightAnchor.constraint(equalToConstant: 22), + ]) + + backViewLeft.addSubview(titleLabelLeft2) + NSLayoutConstraint.activate([ + titleLabelLeft2.leftAnchor.constraint(equalTo: titleLabelLeft1.rightAnchor), + titleLabelLeft2.centerYAnchor.constraint(equalTo: titleLabelLeft1.centerYAnchor), + titleLabelLeft2.heightAnchor.constraint(equalToConstant: 22), + titleLabelLeft2.widthAnchor.constraint(equalToConstant: 88), + ]) + + backViewLeft.addSubview(contentLabelLeft1) + NSLayoutConstraint.activate([ + contentLabelLeft1.leftAnchor.constraint(equalTo: titleLabelLeft1.leftAnchor), + contentLabelLeft1.topAnchor.constraint(equalTo: titleLabelLeft1.bottomAnchor, constant: 4), + contentLabelLeft1.widthAnchor.constraint(equalToConstant: contentW), + contentLabelLeft1.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewLeft.addSubview(contentLabelLeft2) + NSLayoutConstraint.activate([ + contentLabelLeft2.leftAnchor.constraint(equalTo: contentLabelLeft1.leftAnchor), + contentLabelLeft2.topAnchor.constraint(equalTo: contentLabelLeft1.bottomAnchor), + contentLabelLeft2.widthAnchor.constraint(equalToConstant: contentW), + contentLabelLeft2.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewLeft.addSubview(contentLabelLeft3) + NSLayoutConstraint.activate([ + contentLabelLeft3.leftAnchor.constraint(equalTo: contentLabelLeft2.leftAnchor), + contentLabelLeft3.topAnchor.constraint(equalTo: contentLabelLeft2.bottomAnchor), + contentLabelLeft3.widthAnchor.constraint(equalToConstant: contentW), + contentLabelLeft3.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + +// backViewLeft.addSubview(contentHistoryLeft) +// NSLayoutConstraint.activate([ +// contentHistoryLeft.leftAnchor.constraint(equalTo: titleLabelLeft1.leftAnchor), +// contentHistoryLeft.bottomAnchor.constraint(equalTo: backViewLeft.bottomAnchor, constant: -8), +// contentHistoryLeft.widthAnchor.constraint(equalToConstant: 60), +// contentHistoryLeft.heightAnchor.constraint(equalToConstant: 14), +// ]) +// +// backViewLeft.addSubview(dividerLineLeft) +// NSLayoutConstraint.activate([ +// dividerLineLeft.leftAnchor.constraint(equalTo: backViewLeft.leftAnchor, constant: funMargin), +// dividerLineLeft.rightAnchor.constraint(equalTo: backViewLeft.rightAnchor), +// dividerLineLeft.topAnchor.constraint(equalTo: contentHistoryLeft.topAnchor, constant: -8), +// dividerLineLeft.heightAnchor.constraint(equalToConstant: 1), +// ]) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageRichTextCell.swift new file mode 100644 index 00000000..8af629c1 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/PinCell/FunPinMessageRichTextCell.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. + +import UIKit + +@objcMembers +open class FunPinMessageRichTextCell: NEBasePinMessageRichTextCell { + override open func setupUI() { + super.setupUI() + backLeftConstraint?.constant = 0 + backRightConstraint?.constant = 0 + backView.layer.cornerRadius = 0 + headerView.layer.cornerRadius = 4.0 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift index 18265aed..a95e4c6c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.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 @@ -9,22 +10,10 @@ import UIKit @objcMembers open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, NIMUserManagerDelegate, FunChatRecordViewDelegate { public weak var recordView: FunRecordAudioView? - public var currentKeyboardHeight: CGFloat = 0 override public init(session: NIMSession) { super.init(session: session) - cellRegisterDic = [ - "\(MessageType.text.rawValue)": FunChatMessageTextCell.self, - "\(MessageType.rtcCallRecord.rawValue)": FunChatMessageCallCell.self, - "\(MessageType.audio.rawValue)": FunChatMessageAudioCell.self, - "\(MessageType.image.rawValue)": FunChatMessageImageCell.self, - "\(MessageType.revoke.rawValue)": FunChatMessageRevokeCell.self, - "\(MessageType.video.rawValue)": FunChatMessageVideoCell.self, - "\(MessageType.file.rawValue)": FunChatMessageFileCell.self, - "\(MessageType.reply.rawValue)": FunChatMessageReplyCell.self, - "\(MessageType.location.rawValue)": FunChatMessageLocationCell.self, - "\(MessageType.time.rawValue)": FunChatMessageTipCell.self, - ] + cellRegisterDic = ChatMessageHelper.getChatCellRegisterDic(isFun: true) normalInputHeight = 90 brokenNetworkViewHeight = 48 @@ -45,25 +34,9 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, getFunInputView()?.funDelegate = self } - override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let m = viewmodel.messages[indexPath.row] - if m.type == .custom { - if let object = m.message?.messageObject as? NIMCustomObject, let custom = object.attachment as? NECustomAttachmentProtocol { - return custom.cellHeight - } - } - - if let contentModel = m as? MessageContentModel { - if contentModel.type == .revoke { - return 28 - } - } - - return m.cellHeight() - } - override open func getMenuView() -> NEBaseChatInputView { let input = FunChatInputView() + input.multipleLineDelegate = self let gesture = UILongPressGestureRecognizer(target: self, action: #selector(holdToSpeak(gesture:))) input.holdToSpeakView.addGestureRecognizer(gesture) return input @@ -73,24 +46,49 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, FunForwardAlertViewController() } + override open func getMultiForwardViewController(_ messageAttachmentUrl: String?, + _ messageAttachmentFilePath: String, + _ messageAttachmentMD5: String?) -> MultiForwardViewController { + FunMultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) + } + override func getUserSelectVC() -> NEBaseSelectUserViewController { FunSelectUserViewController(sessionId: viewmodel.session.sessionId, showSelf: false) } - override func getTextViewController(text: String) -> TextViewController { - let textViewController = super.getTextViewController(text: text) + override func getTextViewController(title: String?, body: String?) -> TextViewController { + let textViewController = super.getTextViewController(title: title, body: body) textViewController.view.backgroundColor = .funChatBackgroundColor return textViewController } open func recordModeChangeDidClick() { normalOffset = 0 + if chatInputView.chatInpuMode == .multipleSend { + normalInputHeight = 90 + if let inputView = chatInputView as? FunChatInputView { + inputView.setRecordNormalStyle() + } + } + layoutInputView(offset: 0) + UIApplication.shared.keyWindow?.endEditing(true) } + open func didHideRecordMode() { + if chatInputView.chatInpuMode == .multipleSend { + normalInputHeight = 130 + if let inputView = chatInputView as? FunChatInputView { + inputView.resotreMutipleModeFromRecordMode() + } + layoutInputView(offset: 0) + } + } + open func didHideReplyMode() { viewmodel.isReplying = false + if currentKeyboardHeight > 0 { normalOffset = 30 } else { @@ -99,7 +97,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, layoutInputView(offset: currentKeyboardHeight) } - public func didShowReplyMode() { + open func didShowReplyMode() { viewmodel.isReplying = true chatInputView.textView.becomeFirstResponder() } @@ -141,50 +139,19 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, showCustomActionSheet([videoCallAction, audioCallAction]) } - override open func forwardMessage() { - if let message = viewmodel.operationModel?.message { - if IMKitClient.instance.getConfigCenter().teamEnable { - weak var weakSelf = self - let userAction = NECustomAlertAction(title: chatLocalizable("contact_user")) { - weakSelf?.forwardMessageToUser(message: message) - } - - let teamAction = NECustomAlertAction(title: chatLocalizable("team")) { - weakSelf?.forwardMessageToTeam(message: message) - } + override func getUserSettingViewController() -> NEBaseUserSettingViewController { + FunUserSettingViewController(userId: viewmodel.session.sessionId) + } - showCustomActionSheet([teamAction, userAction]) + override open func keyBoardWillShow(_ notification: Notification) { + if chatInputView.chatInpuMode == .normal || chatInputView.chatInpuMode == .multipleSend { + if viewmodel.isReplying { + normalOffset = -10 } else { - forwardMessageToUser(message: message) + normalOffset = 30 } } - } - - /// 设置按钮点击事件 - override open func toSetting() { - if let block = NEKitChatConfig.shared.ui.messageProperties.titleBarRightClick { - block() - return - } - if viewmodel.session.sessionType == .team { - Router.shared.use( - TeamSettingViewRouter, - parameters: ["nav": navigationController as Any, - "teamid": viewmodel.session.sessionId], - closure: nil - ) - } else if viewmodel.session.sessionType == .P2P { - let userSetting = FunUserSettingViewController(userId: viewmodel.session.sessionId) - navigationController?.pushViewController(userSetting, animated: true) - } - } - override open func keyBoardWillShow(_ notification: Notification) { - if viewmodel.isReplying { - normalOffset = -10 - } else { - normalOffset = 30 - } let keyboardRect = (notification .userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue currentKeyboardHeight = keyboardRect.height @@ -192,11 +159,14 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } override open func keyBoardWillHide(_ notification: Notification) { - if viewmodel.isReplying { - normalOffset = -30 - } else { - normalOffset = 0 + if chatInputView.chatInpuMode == .normal || chatInputView.chatInpuMode == .multipleSend { + if viewmodel.isReplying { + normalOffset = -30 + } else { + normalOffset = 0 + } } + currentKeyboardHeight = 0 super.keyBoardWillHide(notification) } @@ -276,7 +246,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, recordView = nil } - public func didEndRecord(view: FunRecordAudioView) { + open func didEndRecord(view: FunRecordAudioView) { endRecord(insideView: true) view.removeFromSuperview() if let hodlToSpeakView = getFunInputView()?.holdToSpeakView { @@ -285,12 +255,24 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } func getFunInputView() -> FunChatInputView? { - if let funInput = inputView as? FunChatInputView { + if let funInput = chatInputView as? FunChatInputView { return funInput } return nil } + override open func setMutilSelectBottomView() { + mutilSelectBottomView.backgroundColor = .white + mutilSelectBottomView.buttonTopAnchor?.constant = 6 + mutilSelectBottomView.multiForwardButton.setImage(.ne_imageNamed(name: "fun_select_multiForward"), for: .normal) + mutilSelectBottomView.multiForwardButton.setImage(.ne_imageNamed(name: "fun_unselect_multiForward"), for: .disabled) + mutilSelectBottomView.singleForwardButton.setImage(.ne_imageNamed(name: "fun_select_singleForward"), for: .normal) + mutilSelectBottomView.singleForwardButton.setImage(.ne_imageNamed(name: "fun_unselect_singleForward"), for: .disabled) + mutilSelectBottomView.deleteButton.setImage(.ne_imageNamed(name: "fun_select_delete"), for: .normal) + mutilSelectBottomView.deleteButton.setImage(.ne_imageNamed(name: "fun_unselect_delete"), for: .disabled) + mutilSelectBottomView.setLabelColor(color: .funChatInputHoldspeakTextColor) + } + override open func closeReply(button: UIButton?) { viewmodel.isReplying = false getFunInputView()?.hideReplyMode() @@ -312,38 +294,18 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } else { var text = chatLocalizable("msg_reply") if let uid = message.from { - var showName = viewmodel.getShowName(userId: uid, teamId: viewmodel.session.sessionId, false) + var showName = ChatUserCache.getShowName(userId: uid, teamId: viewmodel.session.sessionId, false) if viewmodel.session.sessionType != .P2P, !IMKitClient.instance.isMySelf(uid) { addToAtUsers(addText: "@" + showName + "", isReply: true, accid: uid) } let user = viewmodel.getUserInfo(userId: uid) - if let alias = user?.alias { + if let alias = user?.alias, !alias.isEmpty { showName = alias } text += " " + showName } - text += ": " - switch message.messageType { - case .text: - if let t = message.text { - text += t - } - case .image: - text += "[\(chatLocalizable("msg_image"))]" - case .audio: - text += "[\(chatLocalizable("msg_audio"))]" - case .video: - text += "[\(chatLocalizable("msg_video"))]" - case .file: - text += "[\(chatLocalizable("msg_file"))]" - case .location: - text += "[\(chatLocalizable("msg_location"))]" - case .custom: - text += "[\(chatLocalizable("msg_custom"))]" - default: - text += "[\(chatLocalizable("msg_unknown"))]" - } + text += ": \(ChatMessageHelper.contentOfMessage(message))" getFunInputView()?.replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, font: .systemFont(ofSize: 13), color: .ne_greyText) @@ -357,14 +319,31 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } } - override open func didTapReadView(_ cell: UITableViewCell, _ model: MessageContentModel?) { - if let msg = model?.message, msg.session?.sessionType == .team { - let readVC = FunReadViewController(message: msg) - navigationController?.pushViewController(readVC, animated: true) + override open func getReadView(_ message: NIMMessage) -> NEBaseReadViewController { + FunReadViewController(message: message) + } + + override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let model = viewmodel.messages[indexPath.row] + + if let contentModel = model as? MessageContentModel { + if let tipModel = model as? MessageTipsModel { + tipModel.commonInit() + return tipModel.cellHeight() + chat_content_margin + } + + if contentModel.type == .revoke { + if let time = contentModel.timeContent, !time.isEmpty { + return 28 + chat_timeCellH + } + return 28 + } } + + return model.cellHeight() } - public func getMessageModel(model: MessageModel) { + open func getMessageModel(model: MessageModel) { if model.type == .tip || model.type == .notification || model.type == .time { @@ -372,7 +351,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, tipModel.contentSize = String.getTextRectSize(tipModel.text ?? "", font: .systemFont(ofSize: 14), size: CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) - tipModel.height = Float(max(tipModel.contentSize.height + chat_content_margin, 28)) + tipModel.height = max(tipModel.contentSize.height + chat_content_margin, 28) } return } @@ -402,4 +381,99 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, getFunInputView()?.hideRecordMode() super.addToAtUsers(addText: addText, isReply: isReply, accid: accid, isLongPress) } + + // MARK: NEMutilSelectBottomViewDelegate + + override open func multiForwardForward(_ depth: Int) { + weak var weakSelf = self + if IMKitClient.instance.getConfigCenter().teamEnable { + let userAction = NECustomAlertAction(title: chatLocalizable("contact_user")) { + weakSelf?.forwardMessageToUser(isMultiForward: true, depth: depth) { + weakSelf?.cancelMutilSelect() + } + } + + let teamAction = NECustomAlertAction(title: chatLocalizable("team")) { + weakSelf?.forwardMessageToTeam(isMultiForward: true, depth: depth) { + weakSelf?.cancelMutilSelect() + } + } + + showCustomActionSheet([teamAction, userAction]) + } else { + forwardMessageToUser(isMultiForward: true, depth: depth) { + weakSelf?.cancelMutilSelect() + } + } + } + + override open func singleForward() { + weak var weakSelf = self + if IMKitClient.instance.getConfigCenter().teamEnable { + let userAction = NECustomAlertAction(title: chatLocalizable("contact_user")) { + weakSelf?.forwardMessageToUser { + weakSelf?.cancelMutilSelect() + } + } + + let teamAction = NECustomAlertAction(title: chatLocalizable("team")) { + weakSelf?.forwardMessageToTeam { + weakSelf?.cancelMutilSelect() + } + } + + showCustomActionSheet([teamAction, userAction]) + } else { + forwardMessageToUser { + weakSelf?.cancelMutilSelect() + } + } + } + + override open func expandButtonDidClick() { + super.expandButtonDidClick() + print("expandButtonDidClick ") + chatInputView.changeToMultipleLineStyle() + normalInputHeight = 295 + bottomViewTopAnchor?.constant = -normalInputHeight + } + + override open func didHideMultipleButtonClick() { + super.didHideMultipleButtonClick() + setInputValue() + layoutInputViewWithAnimation(offset: 0) + } + + override open func titleTextDidClearEmpty() { + if chatInputView.chatInpuMode == .multipleSend { + chatInputView.chatInpuMode = .normal + setInputValue() + chatInputView.restoreNormalInputStyle() + chatInputView.textView.becomeFirstResponder() + layoutInputViewWithAnimation(offset: currentKeyboardHeight) + } + } + + func setInputValue() { + if chatInputView.chatInpuMode == .normal { + normalInputHeight = 90 + } else if chatInputView.chatInpuMode == .multipleSend { + normalInputHeight = 130 + } + + if viewmodel.isReplying { + normalOffset = -30 + } else { + normalOffset = 0 + } + } + + // 不隐藏键盘 + override open func didHideMultiple() { + normalInputHeight = 90 + if currentKeyboardHeight > 0 { + normalOffset = 30 + } + layoutInputViewWithAnimation(offset: currentKeyboardHeight) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift index e7baeaae..0d583f0d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift @@ -8,7 +8,7 @@ import NECommonUIKit import UIKit @objcMembers -public class FunForwardUserCell: NEBaseForwardUserCell { +open class FunForwardUserCell: NEBaseForwardUserCell { override func setupUI() { super.setupUI() userHeader.layer.cornerRadius = 4 @@ -16,8 +16,8 @@ public class FunForwardUserCell: NEBaseForwardUserCell { } @objcMembers -public class FunForwardAlertViewController: NEBaseForwardAlertViewController { - override public func setupUI() { +open class FunForwardAlertViewController: NEBaseForwardAlertViewController { + override open func setupUI() { super.setupUI() tip.font = .systemFont(ofSize: 16, weight: .semibold) oneUserHead.layer.cornerRadius = 4.0 @@ -28,7 +28,7 @@ public class FunForwardAlertViewController: NEBaseForwardAlertViewController { ) } - override public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { if let cell = collectionView.dequeueReusableCell( withReuseIdentifier: "\(FunForwardUserCell.self)", for: indexPath diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunGroupChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunGroupChatViewController.swift index f52f99fa..33b4726b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunGroupChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunGroupChatViewController.swift @@ -2,13 +2,16 @@ // 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 FunGroupChatViewController: FunChatViewController, TeamChatViewModelDelegate { - private var isLeaveTeamBySelf = false // 是否是主动退出群聊 + private var isLeaveTeamByOther = false // 是否被移出群聊 + private var isLeaveTeamBySelf = false // 是否多端登录另一端退出群聊 private var isdismissTeam = false // 群聊是否已解散 + private var isdismissDiscuss = false // 讨论组是否已解散 private var onCurrentPage = false // 是否位于聊天详情页 public init(session: NIMSession, anchor: NIMMessage?) { @@ -28,12 +31,30 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD fatalError("init(coder:) has not been implemented") } + deinit { + NotificationCenter.default.removeObserver(self) + } + override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) onCurrentPage = true - // 被动解散群聊 + + // 多端登录另一端解散、退出讨论组 + // 多端登录另一端退出群聊 + if isdismissDiscuss || isLeaveTeamBySelf { + popGroupChatVC() + } + + weak var weakSelf = self + // 被移除群聊 + if isLeaveTeamByOther { + showSingleAlert(message: chatLocalizable("team_has_been_quit")) { + weakSelf?.navigationController?.popViewController(animated: true) + } + } + + // 解散群聊 if isdismissTeam { - weak var weakSelf = self showSingleAlert(message: chatLocalizable("team_has_been_removed")) { weakSelf?.navigationController?.popViewController(animated: true) } @@ -47,7 +68,7 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD override open func viewDidLoad() { super.viewDidLoad() - NotificationCenter.default.addObserver(self, selector: #selector(leaveTeamBySelf), name: NotificationName.leaveTeamBySelf, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(popGroupChatVC), name: NENotificationName.popGroupChatVC, object: nil) } override open func getSessionInfo(session: NIMSession) { @@ -60,9 +81,21 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD // MARK: private method - func leaveTeamBySelf(noti: Notification) { - if let flag = noti.object as? Bool { - isLeaveTeamBySelf = flag + func popGroupChatVC() { + var beforeChat: UIViewController? + var loopCount = 0 + for vc in navigationController?.viewControllers ?? [] { + if vc.isKind(of: ChatViewController.self) { + if loopCount <= 1 { + navigationController?.popToRootViewController(animated: true) + return + } + navigationController?.popToViewController(beforeChat!, animated: true) + return + } else { + beforeChat = vc + loopCount += 1 + } } } @@ -79,51 +112,83 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD open func updateTeamInfo(team: NIMTeam) { title = team.getShowName() - if team.inAllMuteMode(), team.owner != NIMSDK.shared().loginManager.currentAccount() { + if team.inAllMuteMode(), viewmodel.teamMember?.type != .manager, viewmodel.teamMember?.type != .owner { // 群禁言 + isMute = true chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("team_mute")) chatInputView.textView.backgroundColor = .funChatInputViewBackgroundColorInMute layoutInputView(offset: 0) getFunInputView()?.hideRecordMode() chatInputView.isUserInteractionEnabled = false + chatInputView.setMuteInputStyle() + if chatInputView.chatInpuMode != .normal { + chatInputView.titleField.text = nil + didHideMultipleButtonClick() + } + if getFunInputView()?.replyLabel.text?.count ?? 0 > 0 { + getFunInputView()?.hideReplyMode() + } } else { // 解除群禁言 + isMute = false chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("fun_chat_input_placeholder")) chatInputView.textView.backgroundColor = .white chatInputView.isUserInteractionEnabled = true + chatInputView.setUnMuteInputStyle() } } - // MARK: TeamChatViewModelDelegate - - open func onTeamRemoved(team: NIMTeam) { - // 退出讨论组 - if team.isDisscuss() == true { - navigationController?.popViewController(animated: true) - return - } + override open func onRecvMessages(_ messages: [NIMMessage]) { + super.onRecvMessages(messages) + for message in messages { + if let object = message.messageObject as? NIMNotificationObject, + let content = object.content as? NIMTeamNotificationContent { + if content.operationType == .leave, + IMKitClient.instance.isMySelf(content.sourceID) { + isLeaveTeamBySelf = true + if onCurrentPage { + popGroupChatVC() + } + } else if content.operationType == .kick, + let targetIDs = content.targetIDs, + targetIDs.contains(IMKitClient.instance.imAccid()) { + // 被移出群聊 + isLeaveTeamByOther = true + if onCurrentPage { + showSingleAlert(message: chatLocalizable("team_has_been_quit")) { [weak self] in + self?.navigationController?.popViewController(animated: true) + } + } + } else if content.operationType == .dismiss { + if isdismissDiscuss { + return + } - // 离开群聊 - if team.teamId == viewmodel.session.sessionId { - if team.owner != NIMSDK.shared().loginManager.currentAccount() { // 退出群聊 - if isLeaveTeamBySelf { - navigationController?.popViewController(animated: true) - } else { + // 解散群聊 isdismissTeam = true - // 被动解散群聊 if onCurrentPage { - weak var weakSelf = self - showSingleAlert(message: chatLocalizable("team_has_been_removed")) { - weakSelf?.navigationController?.popViewController(animated: true) + showSingleAlert(message: chatLocalizable("team_has_been_removed")) { [weak self] in + self?.navigationController?.popViewController(animated: true) } } } - } else { // 主动解散 - navigationController?.popViewController(animated: true) } } } + // MARK: TeamChatViewModelDelegate + + open func onTeamRemoved(team: NIMTeam) { + // 多端登录另一端解散、退出讨论组 + if team.isDisscuss() == true { + isdismissDiscuss = true + if onCurrentPage { + popGroupChatVC() + } + return + } + } + open func onTeamUpdate(team: NIMTeam) { if team.teamId != viewmodel.session.sessionId { return @@ -131,7 +196,15 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD updateTeamInfo(team: team) } - public func onTeamMemberUpdate(team: NIMTeam) { + open func onTeamMemberUpdate(team: NIMTeam) { didRefreshTable() } + + override public func onTeamMemberChange(team: NIMTeam) { + if viewmodel.session.sessionId != team.teamId { + return + } + (viewmodel as? TeamChatViewModel)?.getTeamMember() + updateTeamInfo(team: team) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift new file mode 100644 index 00000000..390085f0 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift @@ -0,0 +1,69 @@ +// 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 NIMSDK +import UIKit + +@objcMembers +open class FunMultiForwardViewController: MultiForwardViewController { + override public init(_ attachmentUrl: String?, + _ attachmentFilePath: String, + _ attachmentMD5: String?) { + super.init(attachmentUrl, attachmentFilePath, attachmentMD5) + brokenNetworkViewHeight = 48 + cellRegisterDic = ChatMessageHelper.getChatCellRegisterDic(isFun: true) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .funChatBackgroundColor // 换肤颜色提取 + brokenNetworkView.errorIcon.isHidden = false + brokenNetworkView.backgroundColor = .funChatNetworkBrokenBackgroundColor + brokenNetworkView.content.textColor = .funChatNetworkBrokenTitleColor + navigationView.backgroundColor = .funChatBackgroundColor + navigationView.titleBarBottomLine.backgroundColor = .funChatNavigationBottomLineColor + } + + override open func getMultiForwardViewController(_ messageAttachmentUrl: String?, + _ messageAttachmentFilePath: String, + _ messageAttachmentMD5: String?) -> MultiForwardViewController { + FunMultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) + } + + override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + viewmodel.messages[indexPath.row].cellHeight() + } + + open func getMessageModel(model: MessageModel) { + if model.type == .tip || + model.type == .notification || + model.type == .time { + if let tipModel = model as? MessageTipsModel { + tipModel.contentSize = String.getTextRectSize(tipModel.text ?? "", + font: .systemFont(ofSize: 14), + size: CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) + tipModel.height = max(tipModel.contentSize.height + chat_content_margin, 28) + } + return + } + + let contentWidth = model.contentSize.width + let contentHeight = model.contentSize.height + if contentHeight < 42 { + let subHeight = 42 - contentHeight + model.contentSize = CGSize(width: contentWidth, height: 42) + model.offset = CGFloat(subHeight) + } + + if model.type == .rtcCallRecord { + model.contentSize = CGSize(width: contentWidth, height: contentHeight - 2) + model.offset = -2 + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift index eb8282d6..733572a6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift @@ -21,7 +21,7 @@ open class FunP2PChatViewController: FunChatViewController { override open func getSessionInfo(session: NIMSession) { var showName = session.sessionId - viewmodel.getUserInfo(session.sessionId) { [weak self] user, error in + ChatUserCache.getUserInfo(session.sessionId) { [weak self] user, error in if let name = user?.showName() { showName = name } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift index 7e87e147..1a942c49 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift @@ -7,23 +7,23 @@ import UIKit @objcMembers open class FunPinMessageViewController: NEBasePinMessageViewController { - override public func viewDidLoad() { + override public init(session: NIMSession) { + super.init(session: session) + pin_content_maxW = (kScreenWidth - 32) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funChatBackgroundColor emptyView.setEmptyImage(name: "fun_user_empty") } - override open func getRegisterCellDic() -> [Int: NEBasePinMessageCell.Type] { - let cellClassDic = [ - NIMMessageType.text.rawValue: FunPinMessageTextCell.self, - NIMMessageType.image.rawValue: FunPinMessageImageCell.self, - NIMMessageType.audio.rawValue: FunPinMessageAudioCell.self, - NIMMessageType.video.rawValue: FunPinMessageVideoCell.self, - NIMMessageType.location.rawValue: FunPinMessageLocationCell.self, - NIMMessageType.file.rawValue: FunPinMessageFileCell.self, - PinMessageDefaultType: FunPinMessageDefaultCell.self, - ] - return cellClassDic + override open func getRegisterCellDic() -> [String: NEBasePinMessageCell.Type] { + ChatMessageHelper.getPinCellRegisterDic(isFun: true) } override open func showAction(item: PinMessageModel) { @@ -71,4 +71,10 @@ open class FunPinMessageViewController: NEBasePinMessageViewController { forwardMessageToUser(message) } } + + override open func getMultiForwardViewController(_ messageAttachmentUrl: String?, + _ messageAttachmentFilePath: String, + _ messageAttachmentMD5: String?) -> MultiForwardViewController { + FunMultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift index 513e8cfd..ccd2b5c8 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift @@ -9,7 +9,7 @@ import UIKit @objcMembers open class FunReadViewController: NEBaseReadViewController { - override public func commonUI() { + override open func commonUI() { super.commonUI() navigationController?.navigationBar.backgroundColor = .white navigationView.backgroundColor = .white @@ -26,13 +26,13 @@ open class FunReadViewController: NEBaseReadViewController { emptyView.setEmptyImage(name: "fun_emptyView") } - override public func readButtonEvent(button: UIButton) { + override open func readButtonEvent(button: UIButton) { super.readButtonEvent(button: button) readButton.setTitleColor(UIColor.funChatThemeColor, for: .normal) unreadButton.setTitleColor(UIColor.ne_darkText, for: .normal) } - override public func unreadButtonEvent(button: UIButton) { + override open func unreadButtonEvent(button: UIButton) { super.unreadButtonEvent(button: button) readButton.setTitleColor(UIColor.ne_darkText, for: .normal) unreadButton.setTitleColor(UIColor.funChatThemeColor, for: .normal) diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift index d02183e8..d7816365 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift @@ -34,11 +34,11 @@ open class FunSelectUserViewController: NEBaseSelectUserViewController { withIdentifier: "\(FunChatTeamMemberCell.self)", for: indexPath ) as! FunChatTeamMemberCell - if indexPath.row == 0 { + if indexPath.section == 0 { cell.headerView.image = UIImage.ne_imageNamed(name: "fun_all") cell.nameLabel.text = chatLocalizable("user_select_all") } else { - if let model = teamInfo?.users[indexPath.row - 1] { + if let model = teamInfo?.users[indexPath.row] { cell.configure(model) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift index f3994163..e53c3c03 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift @@ -20,7 +20,7 @@ open class FunUserSettingViewController: NEBaseUserSettingViewController { fatalError("init(coder:) has not been implemented") } - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funChatBackgroundColor viewmodel.cellDatas.forEach { cellModel in @@ -38,7 +38,7 @@ open class FunUserSettingViewController: NEBaseUserSettingViewController { contentTable.rowHeight = 56 } - override public func headerView() -> UIView { + override open func headerView() -> UIView { let header = UIView(frame: CGRect(x: 0, y: 0, width: view.width, height: 117)) header.backgroundColor = .clear let cornerBack = UIView() @@ -59,7 +59,7 @@ open class FunUserSettingViewController: NEBaseUserSettingViewController { tap.numberOfTapsRequired = 1 tap.numberOfTouchesRequired = 1 - if let url = viewmodel.userInfo?.userInfo?.avatarUrl { + if let url = viewmodel.userInfo?.userInfo?.avatarUrl, !url.isEmpty { userHeader.sd_setImage(with: URL(string: url), completed: nil) userHeader.setTitle("") userHeader.backgroundColor = .clear @@ -116,7 +116,7 @@ open class FunUserSettingViewController: NEBaseUserSettingViewController { return header } - override public func filterStackViewController() -> [UIViewController]? { + override open func filterStackViewController() -> [UIViewController]? { navigationController?.viewControllers.filter { if $0.isKind(of: FunP2PChatViewController.self) || $0 .isKind(of: FunUserSettingViewController.self) { diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift index 058971e3..bae3339b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift @@ -25,7 +25,8 @@ public extension ChatRouter { return } let anchor = param["anchor"] as? NIMMessage - var p2pChatVC = FunP2PChatViewController(session: session, anchor: anchor) + let p2pChatVC = FunP2PChatViewController(session: session, anchor: anchor) + for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { nav?.viewControllers[i] = p2pChatVC @@ -33,6 +34,11 @@ public extension ChatRouter { return } } + + if let remove = param["removeUserVC"] as? Bool, remove { + nav?.viewControllers.removeLast() + } + nav?.pushViewController(p2pChatVC, animated: true) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatUIColor.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatUIColor.swift index 88f81456..08f003f8 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatUIColor.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatUIColor.swift @@ -26,4 +26,6 @@ public extension UIColor { static let funChatInputViewBackgroundColorInMute = UIColor(hexString: "#E0E0E0") static let funChatNetworkBrokenBackgroundColor = UIColor(hexString: "#FCEEEE") static let funChatNetworkBrokenTitleColor = UIColor(white: 0, alpha: 0.5) + + static let funChatMultiForwardContentColor = UIColor(hexString: "#BBBBBB") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift index 80e94f83..38cc86cd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift @@ -11,6 +11,7 @@ let showPhotoTag = 2 @objc public protocol FunChatInputViewDelegate: NSObjectProtocol { func recordModeChangeDidClick() + func didHideRecordMode() func didHideReplyMode() func didShowReplyMode() // 内部状态转换的回调,上次是回复UI样式,转转成录音模式后再转换回来,还要保持回复样式,此场景下对外回调 } @@ -31,10 +32,18 @@ open class FunChatInputView: NEBaseChatInputView { var defaultReplyTopSpace: CGFloat = 8 + var multipleReplyTopSpace: CGFloat = 48 + + public var backViewHeightConstaint: NSLayoutConstraint? + + public var textViewTop: NSLayoutConstraint? + +// public var textViewHeight: NSLayoutConstraint? + public var replyBackView: UIView = { let back = UIView() back.translatesAutoresizingMaskIntoConstraints = false - back.layer.cornerRadius = 4.0 + back.layer.cornerRadius = 8.0 back.clipsToBounds = true back.backgroundColor = UIColor.funChatInputReplyBg return back @@ -111,23 +120,43 @@ open class FunChatInputView: NEBaseChatInputView { textView.layer.cornerRadius = 4.0 textView.delegate = self textviewLeftConstraint = textView.leftAnchor.constraint(equalTo: leftAnchor, constant: 48) - textviewRightConstraint = textView.rightAnchor.constraint(equalTo: rightAnchor, constant: -88) + textviewRightConstraint = textView.rightAnchor.constraint(equalTo: rightAnchor, constant: -132) + textViewTop = textView.topAnchor.constraint(equalTo: topAnchor, constant: 8) NSLayoutConstraint.activate([ textviewLeftConstraint!, textviewRightConstraint!, - textView.topAnchor.constraint(equalTo: topAnchor, constant: 8), + textViewTop!, textView.heightAnchor.constraint(equalToConstant: 40), ]) textInput = textView - insertSubview(replyBackView, belowSubview: textView) + addSubview(expandButton) + NSLayoutConstraint.activate([ + expandButton.topAnchor.constraint(equalTo: topAnchor, constant: 8), + expandButton.rightAnchor.constraint(equalTo: rightAnchor, constant: -88), + expandButton.heightAnchor.constraint(equalToConstant: 40), + expandButton.widthAnchor.constraint(equalToConstant: 44.0), + ]) + expandButton.setImage(coreLoader.loadImage("fun_input_unfold"), for: .normal) + expandButton.addTarget(self, action: #selector(didClickExpandButton), for: .touchUpInside) + + backViewHeightConstaint = backView.heightAnchor.constraint(equalToConstant: 40) + insertSubview(backView, belowSubview: textView) + NSLayoutConstraint.activate([ + backView.leftAnchor.constraint(equalTo: leftAnchor, constant: 48), + backView.rightAnchor.constraint(equalTo: rightAnchor, constant: -88), + backView.topAnchor.constraint(equalTo: topAnchor, constant: 8), + backViewHeightConstaint!, + ]) + + insertSubview(replyBackView, belowSubview: backView) replyViewTopConstraint = replyBackView.topAnchor.constraint(equalTo: topAnchor, constant: defaultReplyTopSpace) NSLayoutConstraint.activate([ replyViewTopConstraint!, replyBackView.heightAnchor.constraint(equalToConstant: 40), - replyBackView.leftAnchor.constraint(equalTo: textView.leftAnchor), - replyBackView.rightAnchor.constraint(equalTo: textView.rightAnchor), + replyBackView.leftAnchor.constraint(equalTo: leftAnchor, constant: 48), + replyBackView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: 0), ]) replyBackView.addSubview(replyLabel) @@ -201,12 +230,15 @@ open class FunChatInputView: NEBaseChatInputView { addSubview(holdToSpeakView) NSLayoutConstraint.activate([ - holdToSpeakView.leftAnchor.constraint(equalTo: textView.leftAnchor), - holdToSpeakView.rightAnchor.constraint(equalTo: textView.rightAnchor), - holdToSpeakView.topAnchor.constraint(equalTo: textView.topAnchor), - holdToSpeakView.bottomAnchor.constraint(equalTo: textView.bottomAnchor), + holdToSpeakView.leftAnchor.constraint(equalTo: leftAnchor, constant: 48), + holdToSpeakView.rightAnchor.constraint(equalTo: backView.rightAnchor), + holdToSpeakView.topAnchor.constraint(equalTo: backView.topAnchor), + holdToSpeakView.bottomAnchor.constraint(equalTo: backView.bottomAnchor), ]) holdToSpeakView.isHidden = true + + setupMultipleLineView() + multipleLineExpandButton.setImage(coreLoader.loadImage("fun_input_fold"), for: .normal) } open func changeToRecordMode(_ button: UIButton) { @@ -214,6 +246,7 @@ open class FunChatInputView: NEBaseChatInputView { if button.isSelected == true { showRecordMode() } else { + funDelegate?.didHideRecordMode() hideRecordMode() } } @@ -240,7 +273,11 @@ open class FunChatInputView: NEBaseChatInputView { } open func showReplyMode() { - replyViewTopConstraint?.constant = 52 + if chatInpuMode == .normal { + replyViewTopConstraint?.constant = 52 + } else { + replyViewTopConstraint?.constant = 90 + } if replyLabel.attributedText == nil { hideRecordMode() } else if let replyText = replyLabel.attributedText, replyText.length <= 0 { @@ -252,7 +289,11 @@ open class FunChatInputView: NEBaseChatInputView { if let topSpace = replyViewTopConstraint?.constant, topSpace == defaultReplyTopSpace { return } - replyViewTopConstraint?.constant = 8 + if chatInpuMode == .normal { + replyViewTopConstraint?.constant = 8 + } else { + replyViewTopConstraint?.constant = multipleReplyTopSpace + } funDelegate?.didHideReplyMode() } @@ -262,6 +303,9 @@ open class FunChatInputView: NEBaseChatInputView { @objc private func moreBtnClick() { + if changeRecordModeBtn.isSelected == true, chatInpuMode == .multipleSend { + funDelegate?.didHideRecordMode() + } hideRecordMode() changeRecordModeBtn.isSelected = false buttonEvent(button: showMoreActionBtn) @@ -269,8 +313,117 @@ open class FunChatInputView: NEBaseChatInputView { @objc private func emojBtnClick() { + if changeRecordModeBtn.isSelected == true, chatInpuMode == .multipleSend { + funDelegate?.didHideRecordMode() + } hideRecordMode() changeRecordModeBtn.isSelected = false buttonEvent(button: showEmojBtn) } + + override open func restoreNormalInputStyle() { + super.restoreNormalInputStyle() + + contentSubView?.isHidden = true + textView.returnKeyType = .send + textView.removeAllAutoLayout() + insertSubview(textView, belowSubview: holdToSpeakView) + textView.removeConstraints(textView.constraints) + + if chatInpuMode == .normal { + textviewLeftConstraint = textView.leftAnchor.constraint(equalTo: leftAnchor, constant: 48) + textviewRightConstraint = textView.rightAnchor.constraint(equalTo: rightAnchor, constant: -132) + textViewTop = textView.topAnchor.constraint(equalTo: topAnchor, constant: 8) + NSLayoutConstraint.activate([ + textviewLeftConstraint!, + textviewRightConstraint!, + textViewTop!, + textView.heightAnchor.constraint(equalToConstant: 40), + ]) + backViewHeightConstaint?.constant = 40 + if let replyText = replyLabel.attributedText, replyText.length > 0 { + replyViewTopConstraint?.constant = 51 + } else { + replyViewTopConstraint?.constant = defaultReplyTopSpace + } + } else if chatInpuMode == .multipleSend { + titleField.removeAllAutoLayout() + insertSubview(titleField, aboveSubview: backView) + NSLayoutConstraint.activate([ + titleField.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 4), + titleField.rightAnchor.constraint(equalTo: expandButton.leftAnchor, constant: 2), + titleField.topAnchor.constraint(equalTo: backView.topAnchor), + titleField.heightAnchor.constraint(equalToConstant: 40), + ]) + + textviewLeftConstraint = textView.leftAnchor.constraint(equalTo: leftAnchor, constant: 48) + textviewRightConstraint = textView.rightAnchor.constraint(equalTo: rightAnchor, constant: -132) + textViewTop = textView.topAnchor.constraint(equalTo: topAnchor, constant: 40) + + NSLayoutConstraint.activate([ + textviewLeftConstraint!, + textviewRightConstraint!, + textViewTop!, + textView.heightAnchor.constraint(equalToConstant: 40), + ]) + backViewHeightConstaint?.constant = 80 + if let replyText = replyLabel.attributedText, replyText.length > 0 { + replyViewTopConstraint?.constant = 90 + } else { + replyViewTopConstraint?.constant = multipleReplyTopSpace + } + } + } + + override open func changeToMultipleLineStyle() { + super.changeToMultipleLineStyle() + textView.removeAllAutoLayout() + textView.returnKeyType = .default + multipleLineView.addSubview(textView) + NSLayoutConstraint.activate([ + textView.leftAnchor.constraint(equalTo: multipleLineView.leftAnchor, constant: 13), + textView.rightAnchor.constraint(equalTo: multipleLineView.rightAnchor, constant: -16), + textView.topAnchor.constraint(equalTo: multipleLineView.topAnchor, constant: 48), + textView.heightAnchor.constraint(equalToConstant: 183), + ]) + + if titleField.superview == nil || titleField.superview != multipleLineView { + titleField.removeAllAutoLayout() + multipleLineView.addSubview(titleField) + NSLayoutConstraint.activate([ + titleField.leftAnchor.constraint(equalTo: multipleLineView.leftAnchor, constant: 16), + titleField.rightAnchor.constraint(equalTo: multipleLineView.rightAnchor, constant: -56), + titleField.topAnchor.constraint(equalTo: multipleLineView.topAnchor, constant: 5), + titleField.heightAnchor.constraint(equalToConstant: 40), + ]) + } + } + + override open func setMuteInputStyle() { + super.setMuteInputStyle() + backView.backgroundColor = .funChatInputViewBackgroundColorInMute + } + + override open func setUnMuteInputStyle() { + super.setUnMuteInputStyle() + backView.backgroundColor = .white + } + + // 多行输入模式下进入录音模式,切换回单行样式 + open func setRecordNormalStyle() { + backViewHeightConstaint?.constant = 40 + textViewTop?.constant = 8 + replyViewTopConstraint?.constant = defaultReplyTopSpace + } + + // 从录音模式切换回多行模式 + open func resotreMutipleModeFromRecordMode() { + backViewHeightConstaint?.constant = 80 + textViewTop?.constant = 40 + if let replyText = replyLabel.attributedText, replyText.length > 0 { + replyViewTopConstraint?.constant = 90 + } else { + replyViewTopConstraint?.constant = multipleReplyTopSpace + } + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift index 5c66cc9f..8b298f69 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift @@ -219,7 +219,7 @@ open class FunRecordAudioView: UIView { return windowWidth / 375.0 * 128.0 } - public func changeToCancelStyle() { + open func changeToCancelStyle() { if releaseToSendLabel.isHidden == true { return } @@ -231,7 +231,7 @@ open class FunRecordAudioView: UIView { triangleView.backgroundColor = UIColor.funRecordAudioProgressCancelColor } - public func changeToNormalStyle() { + open func changeToNormalStyle() { if releaseToSendLabel.isHidden == false { return } @@ -243,7 +243,7 @@ open class FunRecordAudioView: UIView { triangleView.backgroundColor = UIColor.funRecordAudioProgressNormalColor } - public func isRecordNormalStyle() -> Bool { + open func isRecordNormalStyle() -> Bool { if releaseToSendLabel.isHidden == false { return true } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift index 4948d4d4..7bf3d4be 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift @@ -131,11 +131,8 @@ open class ChatMessageAudioCell: NormalChatMessageBaseCell, ChatAudioCellProtoco timeLabelRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) if let m = model as? MessageAudioModel { if isSend { timeLabelRight.text = "\(m.duration)" + "s" diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift index 7cb6175b..b77bd2ad 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift @@ -66,14 +66,11 @@ open class ChatMessageCallCell: NormalChatMessageBaseCell { contentLabelRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) if let m = model as? MessageCallRecordModel { - if let isSend = model.message?.isOutgoingMsg, isSend { - contentLabelRight.attributedText = m.attributeStr - return - } - contentLabelLeft.attributedText = m.attributeStr + let contentLabel = isSend ? contentLabelRight : contentLabelLeft + contentLabel.attributedText = m.attributeStr } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift index 361cdc24..619af615 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift @@ -23,6 +23,7 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { let state = FileStateView() state.translatesAutoresizingMaskIntoConstraints = false state.backgroundColor = .clear + state.accessibilityIdentifier = "id.fileStatus" return state }() @@ -81,6 +82,7 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { let state = FileStateView() state.translatesAutoresizingMaskIntoConstraints = false state.backgroundColor = .clear + state.accessibilityIdentifier = "id.fileStatus" return state }() @@ -156,10 +158,10 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { imgViewLeft.heightAnchor.constraint(equalToConstant: 32), ]) - contentView.addSubview(stateViewLeft) + imgViewLeft.addSubview(stateViewLeft) NSLayoutConstraint.activate([ - stateViewLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: 10), - stateViewLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: 10), + stateViewLeft.leftAnchor.constraint(equalTo: imgViewLeft.leftAnchor, constant: 0), + stateViewLeft.topAnchor.constraint(equalTo: imgViewLeft.topAnchor, constant: 0), stateViewLeft.widthAnchor.constraint(equalToConstant: 32), stateViewLeft.heightAnchor.constraint(equalToConstant: 32), ]) @@ -188,10 +190,10 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { imgViewRight.heightAnchor.constraint(equalToConstant: 32), ]) - contentView.addSubview(stateViewRight) + imgViewRight.addSubview(stateViewRight) NSLayoutConstraint.activate([ - stateViewRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: 10), - stateViewRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: 10), + stateViewRight.leftAnchor.constraint(equalTo: imgViewRight.leftAnchor, constant: 0), + stateViewRight.topAnchor.constraint(equalTo: imgViewRight.topAnchor, constant: 0), stateViewRight.widthAnchor.constraint(equalToConstant: 32), stateViewRight.heightAnchor.constraint(equalToConstant: 32), ]) @@ -216,11 +218,8 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { labelViewRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let stateView = isSend ? stateViewRight : stateViewLeft let imgView = isSend ? imgViewRight : imgViewLeft let titleLabel = isSend ? titleLabelRight : titleLabelLeft diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift index 2f01c523..f1d21e87 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift @@ -75,11 +75,8 @@ open class ChatMessageImageCell: NormalChatMessageBaseCell { contentImageViewRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let contentImageView = isSend ? contentImageViewRight : contentImageViewLeft if let m = model as? MessageImageModel, let imageUrl = m.imageUrl { @@ -92,7 +89,8 @@ open class ChatMessageImageCell: NormalChatMessageBaseCell { completed: nil ) } else { - contentImageView.image = UIImage(contentsOfFile: imageUrl) + let url = URL(fileURLWithPath: imageUrl) + contentImageView.sd_setImage(with: url) } } else { contentImageView.image = nil diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift index dfe0b813..b7305f54 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift @@ -93,6 +93,7 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { backgroundViewLeft.layer.cornerRadius = 4 backgroundViewLeft.layer.borderWidth = 1 backgroundViewLeft.layer.borderColor = UIColor.ne_outlineColor.cgColor + backgroundViewLeft.accessibilityIdentifier = "id.mapView" let messageLongPress = UILongPressGestureRecognizer( target: self, @@ -108,6 +109,7 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { ]) let messageTap = UITapGestureRecognizer(target: self, action: #selector(tapMessage)) + messageTap.cancelsTouchesInView = false backgroundViewLeft.addGestureRecognizer(messageTap) backgroundViewLeft.addSubview(titleLabelLeft) @@ -162,6 +164,8 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { backgroundViewRight.layer.cornerRadius = 4 backgroundViewRight.layer.borderWidth = 1 backgroundViewRight.layer.borderColor = UIColor.ne_outlineColor.cgColor + backgroundViewRight.accessibilityIdentifier = "id.mapView" + let messageLongPress = UILongPressGestureRecognizer( target: self, action: #selector(longPress) @@ -175,6 +179,7 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { backgroundViewRight.bottomAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor), ]) let messageTap = UITapGestureRecognizer(target: self, action: #selector(tapMessage)) + messageTap.cancelsTouchesInView = false backgroundViewRight.addGestureRecognizer(messageTap) backgroundViewRight.addSubview(titleLabelRight) @@ -226,11 +231,8 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { backgroundViewRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let titleLabel = isSend ? titleLabelRight : titleLabelLeft let subTitleLabel = isSend ? subTitleLabelRight : subTitleLabelLeft let mapView = isSend ? mapViewRight : mapViewLeft diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift new file mode 100644 index 00000000..587fd1ff --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift @@ -0,0 +1,382 @@ + +// 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 NIMSDK +import UIKit + +@objcMembers +open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { + let contentWidth: CGFloat = 234 + let titleLabelFontSize: CGFloat = 14 + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + open func setupUI() { + setupUIRight() + setupUILeft() + } + + open func setupUILeft() { + bubbleImageLeft.image = nil + let image = UIImage.ne_imageNamed(name: "multiForward_message_receive") + backViewLeft.image = image? + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) + bubbleImageLeft.addSubview(backViewLeft) + NSLayoutConstraint.activate([ + backViewLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor), + backViewLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor), + backViewLeft.bottomAnchor.constraint(equalTo: bubbleImageLeft.bottomAnchor), + backViewLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor), + ]) + + backViewLeft.addSubview(titleLabelLeft1) + NSLayoutConstraint.activate([ + titleLabelLeft1.leftAnchor.constraint(equalTo: backViewLeft.leftAnchor, constant: 16), + titleLabelLeft1.rightAnchor.constraint(lessThanOrEqualTo: backViewLeft.rightAnchor, constant: -84), + titleLabelLeft1.topAnchor.constraint(equalTo: backViewLeft.topAnchor, constant: 10), + titleLabelLeft1.heightAnchor.constraint(equalToConstant: 22), + ]) + + backViewLeft.addSubview(titleLabelLeft2) + NSLayoutConstraint.activate([ + titleLabelLeft2.leftAnchor.constraint(equalTo: titleLabelLeft1.rightAnchor), + titleLabelLeft2.centerYAnchor.constraint(equalTo: titleLabelLeft1.centerYAnchor), + titleLabelLeft2.heightAnchor.constraint(equalToConstant: 22), + titleLabelLeft2.widthAnchor.constraint(equalToConstant: 74), + ]) + + backViewLeft.addSubview(contentLabelLeft1) + NSLayoutConstraint.activate([ + contentLabelLeft1.leftAnchor.constraint(equalTo: titleLabelLeft1.leftAnchor), + contentLabelLeft1.topAnchor.constraint(equalTo: titleLabelLeft1.bottomAnchor, constant: 2), + contentLabelLeft1.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelLeft1.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewLeft.addSubview(contentLabelLeft2) + NSLayoutConstraint.activate([ + contentLabelLeft2.leftAnchor.constraint(equalTo: contentLabelLeft1.leftAnchor), + contentLabelLeft2.topAnchor.constraint(equalTo: contentLabelLeft1.bottomAnchor), + contentLabelLeft2.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelLeft2.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewLeft.addSubview(contentLabelLeft3) + NSLayoutConstraint.activate([ + contentLabelLeft3.leftAnchor.constraint(equalTo: contentLabelLeft2.leftAnchor), + contentLabelLeft3.topAnchor.constraint(equalTo: contentLabelLeft2.bottomAnchor), + contentLabelLeft3.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelLeft3.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewLeft.addSubview(contentHistoryLeft) + NSLayoutConstraint.activate([ + contentHistoryLeft.leftAnchor.constraint(equalTo: titleLabelLeft1.leftAnchor), + contentHistoryLeft.bottomAnchor.constraint(equalTo: backViewLeft.bottomAnchor, constant: -12), + contentHistoryLeft.widthAnchor.constraint(equalToConstant: 60), + contentHistoryLeft.heightAnchor.constraint(equalToConstant: 14), + ]) + + backViewLeft.addSubview(dividerLineLeft) + NSLayoutConstraint.activate([ + dividerLineLeft.leftAnchor.constraint(equalTo: backViewLeft.leftAnchor, constant: 6), + dividerLineLeft.rightAnchor.constraint(equalTo: backViewLeft.rightAnchor, constant: -6), + dividerLineLeft.topAnchor.constraint(equalTo: contentHistoryLeft.topAnchor, constant: -6), + dividerLineLeft.heightAnchor.constraint(equalToConstant: 1), + ]) + } + + open func setupUIRight() { + bubbleImageRight.image = nil + let image = UIImage.ne_imageNamed(name: "multiForward_message_send") + backViewRight.image = image? + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) + bubbleImageRight.addSubview(backViewRight) + NSLayoutConstraint.activate([ + backViewRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor), + backViewRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor), + backViewRight.bottomAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor), + backViewRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor), + ]) + + backViewRight.addSubview(titleLabelRight1) + NSLayoutConstraint.activate([ + titleLabelRight1.leftAnchor.constraint(equalTo: backViewRight.leftAnchor, constant: 16), + titleLabelRight1.rightAnchor.constraint(lessThanOrEqualTo: backViewRight.rightAnchor, constant: -84), + titleLabelRight1.topAnchor.constraint(equalTo: backViewRight.topAnchor, constant: 10), + titleLabelRight1.heightAnchor.constraint(equalToConstant: 22), + ]) + + backViewRight.addSubview(titleLabelRight2) + NSLayoutConstraint.activate([ + titleLabelRight2.leftAnchor.constraint(equalTo: titleLabelRight1.rightAnchor), + titleLabelRight2.centerYAnchor.constraint(equalTo: titleLabelRight1.centerYAnchor), + titleLabelRight2.heightAnchor.constraint(equalToConstant: 22), + titleLabelRight2.widthAnchor.constraint(equalToConstant: 74), + ]) + + backViewRight.addSubview(contentLabelRight1) + NSLayoutConstraint.activate([ + contentLabelRight1.leftAnchor.constraint(equalTo: titleLabelRight1.leftAnchor), + contentLabelRight1.topAnchor.constraint(equalTo: titleLabelRight1.bottomAnchor, constant: 2), + contentLabelRight1.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelRight1.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewRight.addSubview(contentLabelRight2) + NSLayoutConstraint.activate([ + contentLabelRight2.leftAnchor.constraint(equalTo: contentLabelRight1.leftAnchor), + contentLabelRight2.topAnchor.constraint(equalTo: contentLabelRight1.bottomAnchor), + contentLabelRight2.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelRight2.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewRight.addSubview(contentLabelRight3) + NSLayoutConstraint.activate([ + contentLabelRight3.leftAnchor.constraint(equalTo: contentLabelRight2.leftAnchor), + contentLabelRight3.topAnchor.constraint(equalTo: contentLabelRight2.bottomAnchor), + contentLabelRight3.widthAnchor.constraint(equalToConstant: contentWidth), + contentLabelRight3.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewRight.addSubview(contentHistoryRight) + NSLayoutConstraint.activate([ + contentHistoryRight.leftAnchor.constraint(equalTo: titleLabelRight1.leftAnchor), + contentHistoryRight.bottomAnchor.constraint(equalTo: backViewRight.bottomAnchor, constant: -12), + contentHistoryRight.widthAnchor.constraint(equalToConstant: 60), + contentHistoryRight.heightAnchor.constraint(equalToConstant: 14), + ]) + + backViewRight.addSubview(dividerLineRight) + NSLayoutConstraint.activate([ + dividerLineRight.leftAnchor.constraint(equalTo: backViewRight.leftAnchor, constant: 6), + dividerLineRight.rightAnchor.constraint(equalTo: backViewRight.rightAnchor, constant: -6), + dividerLineRight.topAnchor.constraint(equalTo: contentHistoryRight.topAnchor, constant: -6), + dividerLineRight.heightAnchor.constraint(equalToConstant: 1), + ]) + } + + override open func showLeftOrRight(showRight: Bool) { + super.showLeftOrRight(showRight: showRight) + } + + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) + guard let data = NECustomAttachment.dataOfCustomMessage(message: model.message) else { + return + } + + let font = UIFont.systemFont(ofSize: titleLabelFontSize) + let bubbleW = isSend ? bubbleWRight : bubbleWLeft + let bubbleH = isSend ? bubbleHRight : bubbleHLeft + let titleLabel = isSend ? titleLabelRight1 : titleLabelLeft1 + let titleLabel2 = isSend ? titleLabelRight2 : titleLabelLeft2 + let contentLabel1 = isSend ? contentLabelRight1 : contentLabelLeft1 + let contentLabel2 = isSend ? contentLabelRight2 : contentLabelLeft2 + let contentLabel3 = isSend ? contentLabelRight3 : contentLabelLeft3 + + bubbleW?.constant = 266 + bubbleH?.constant = 130 + + if let sessionName = data["sessionName"] as? String { + titleLabel.attributedText = + NEEmotionTool.getAttWithStr(str: sessionName, + font: .systemFont(ofSize: titleLabelFontSize, weight: .semibold), + color: .ne_darkText) + } else { + titleLabel2.text = chatLocalizable("chat_history") + } + + guard let abstracts = data["abstracts"] as? [[String: Any]] else { return } + + contentLabel2.attributedText = nil + contentLabel3.attributedText = nil + for i in 0 ..< abstracts.count { + var contentLabel = contentLabel1 + if i == 1 { + contentLabel = contentLabel2 + } else if i == 2 { + contentLabel = contentLabel3 + } + + var contentText = "" + if var senderNick = abstracts[i]["senderNick"] as? String { + if senderNick.count > 5 { + // 截取字符串 abcdefg -> ab...fg + let leftEndIndex = senderNick.index(senderNick.startIndex, offsetBy: 2) + let rightStartIndex = senderNick.index(senderNick.endIndex, offsetBy: -2) + senderNick = senderNick[senderNick.startIndex ..< leftEndIndex] + "..." + senderNick[rightStartIndex ..< senderNick.endIndex] + } + contentText = senderNick + if let content = abstracts[i]["content"] as? String { + contentText += ":" + content + } + } + + let paragraphStyle = NSMutableParagraphStyle() + paragraphStyle.lineSpacing = 1 // 设置行间距 + paragraphStyle.lineBreakMode = .byTruncatingTail + let attributedText = NEEmotionTool.getAttWithStr(str: contentText, + font: font, + color: .ne_lightText) + attributedText.addAttribute(NSAttributedString.Key.paragraphStyle, value: paragraphStyle, range: NSMakeRange(0, attributedText.length)) + contentLabel.attributedText = attributedText + } + + let numCount1 = String.calculateMaxLines(width: contentWidth, + attributeString: contentLabel1.attributedText, + font: font) + if numCount1 == 1 { + contentLabel2.numberOfLines = 2 + contentLabel2.isHidden = contentLabel2.attributedText == nil + let numCount2 = String.calculateMaxLines(width: contentWidth, + attributeString: contentLabel2.attributedText, + font: font) + contentLabel3.isHidden = contentLabel3.attributedText == nil || numCount2 >= 2 + } else if numCount1 == 2 { + contentLabel2.numberOfLines = 1 + contentLabel2.isHidden = contentLabel2.attributedText == nil + contentLabel3.isHidden = true + } else { + contentLabel2.isHidden = true + contentLabel3.isHidden = true + } + } + + // MARK: - lazy load + + public lazy var backViewLeft: UIImageView = { + let view = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + public lazy var titleLabelLeft1: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.name1" + return label + }() + + public lazy var titleLabelLeft2: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = chatLocalizable("chat_history_by") + label.textColor = .ne_darkText + label.font = .systemFont(ofSize: titleLabelFontSize, weight: .semibold) + label.accessibilityIdentifier = "id.name2" + return label + }() + + public lazy var contentLabelLeft1: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 3 + label.accessibilityIdentifier = "id.content1" + return label + }() + + public lazy var contentLabelLeft2: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 2 + label.accessibilityIdentifier = "id.content2" + return label + }() + + public lazy var contentLabelLeft3: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.content3" + return label + }() + + public lazy var dividerLineLeft: UIView = { + let line = UIView() + line.translatesAutoresizingMaskIntoConstraints = false + line.backgroundColor = multiForwardLineColor + return line + }() + + public lazy var contentHistoryLeft: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .systemFont(ofSize: 12) + label.textColor = .ne_lightText + label.text = chatLocalizable("chat_history") + label.accessibilityIdentifier = "id.contentHistoryLeft" + return label + }() + + public lazy var backViewRight: UIImageView = { + let view = UIImageView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + public lazy var titleLabelRight1: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.name1" + return label + }() + + public lazy var titleLabelRight2: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = chatLocalizable("chat_history_by") + label.textColor = .ne_darkText + label.font = .systemFont(ofSize: titleLabelFontSize, weight: .semibold) + label.accessibilityIdentifier = "id.name2" + return label + }() + + public lazy var contentLabelRight1: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 3 + label.accessibilityIdentifier = "id.content1" + return label + }() + + public lazy var contentLabelRight2: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 2 + label.accessibilityIdentifier = "id.content2" + return label + }() + + public lazy var contentLabelRight3: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.content3" + return label + }() + + public lazy var dividerLineRight: UIView = { + let line = UIView() + line.translatesAutoresizingMaskIntoConstraints = false + line.backgroundColor = multiForwardLineColor + return line + }() + + public lazy var contentHistoryRight: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .systemFont(ofSize: 12) + label.textColor = .ne_lightText + label.text = chatLocalizable("chat_history") + label.accessibilityIdentifier = "id.contentHistoryRight" + return label + }() +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift index 30ff3250..7a38dfa1 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift @@ -11,8 +11,8 @@ open class ChatMessageReplyCell: ChatMessageTextCell { let replyLabel = UILabel() replyLabel.font = UIFont.systemFont(ofSize: 13) replyLabel.textColor = UIColor(hexString: "#929299") - replyLabel.textAlignment = .justified replyLabel.translatesAutoresizingMaskIntoConstraints = false + replyLabel.accessibilityIdentifier = "id.messageReply" return replyLabel }() @@ -20,8 +20,8 @@ open class ChatMessageReplyCell: ChatMessageTextCell { let replyLabel = UILabel() replyLabel.font = UIFont.systemFont(ofSize: 13) replyLabel.textColor = UIColor(hexString: "#929299") - replyLabel.textAlignment = .justified replyLabel.translatesAutoresizingMaskIntoConstraints = false + replyLabel.accessibilityIdentifier = "id.messageReply" return replyLabel }() @@ -81,15 +81,12 @@ open class ChatMessageReplyCell: ChatMessageTextCell { replyLabelRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { let replyLabel = isSend ? replyLabelRight : replyLabelLeft if let text = model.replyText, let font = replyLabel.font { - replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, + replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: "| " + text, font: font, color: replyLabel.textColor) if let attriText = replyLabel.attributedText { @@ -98,6 +95,6 @@ open class ChatMessageReplyCell: ChatMessageTextCell { } } - super.setModel(model) + super.setModel(model, isSend) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift index 2960f211..0ed2ed6d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift @@ -37,6 +37,7 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { revokeLabelLeft.translatesAutoresizingMaskIntoConstraints = false revokeLabelLeft.textColor = UIColor.ne_greyText revokeLabelLeft.font = UIFont.systemFont(ofSize: 16.0) + revokeLabelLeft.accessibilityIdentifier = "id.messageText" bubbleImageLeft.addSubview(revokeLabelLeft) NSLayoutConstraint.activate([ revokeLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: 16), @@ -50,6 +51,7 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { revokeLabelRight.translatesAutoresizingMaskIntoConstraints = false revokeLabelRight.textColor = UIColor.ne_greyText revokeLabelRight.font = UIFont.systemFont(ofSize: 16.0) + revokeLabelRight.accessibilityIdentifier = "id.messageText" bubbleImageRight.addSubview(revokeLabelRight) NSLayoutConstraint.activate([ revokeLabelRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: 16), @@ -59,6 +61,7 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { ]) reeditButton.translatesAutoresizingMaskIntoConstraints = false + reeditButton.accessibilityIdentifier = "id.reeditButton" reeditButton.setImage(UIImage.ne_imageNamed(name: "right_arrow"), for: .normal) reeditButton.titleLabel?.font = UIFont.systemFont(ofSize: 16.0) reeditButton.setTitleColor(UIColor.ne_blueText, for: .normal) @@ -80,6 +83,10 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { super.showLeftOrRight(showRight: showRight) revokeLabelLeft.isHidden = showRight revokeLabelRight.isHidden = !showRight +// reeditButton.isHidden = !showRight + + activityView.isHidden = true + readView.isHidden = true seletedBtn.isHidden = true pinLabelLeft.isHidden = true pinImageLeft.isHidden = true @@ -87,7 +94,7 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { pinImageRight.isHidden = true } - override open func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { if let time = model.message?.timestamp { let date = Date() let currentTime = date.timeIntervalSince1970 @@ -95,24 +102,22 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { model.timeOut = true } } - if let isSend = model.message?.isOutgoingMsg, isSend, model.isRevokedText == true, model.timeOut == false { + if isSend, + model.isRevokedText == true, + model.timeOut == false { reeditButtonW?.constant = 86 reeditButton.isHidden = false + reeditButton.setTitle(chatLocalizable("message_reedit"), for: .normal) model.contentSize = CGSize(width: 218, height: chat_min_h) } else { reeditButtonW?.constant = 0 reeditButton.isHidden = true model.contentSize = CGSize(width: 130, height: chat_min_h) } - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + super.setModel(model, isSend) let revokeLabel = isSend ? revokeLabelRight : revokeLabelLeft - - revokeLabel.text = chatLocalizable("message_has_be_withdrawn") - reeditButton.setTitle(chatLocalizable("message_reedit"), for: .normal) + revokeLabel.text = chatLocalizable("message_recalled") } func reeditEvent(button: UIButton) { diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift new file mode 100644 index 00000000..b6c4956f --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift @@ -0,0 +1,134 @@ + +// 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 ChatMessageRichTextCell: ChatMessageReplyCell { + public lazy var titleLabelLeft: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.isEnabled = false + label.numberOfLines = 0 + label.isUserInteractionEnabled = false + label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) + label.backgroundColor = .clear + label.accessibilityIdentifier = "id.messageTitle" + return label + }() + + public lazy var titleLabelRight: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.isEnabled = false + label.numberOfLines = 0 + label.isUserInteractionEnabled = false + label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) + label.backgroundColor = .clear + label.accessibilityIdentifier = "id.messageTitle" + return label + }() + + public var replyLabelLeftHeightAnchor: NSLayoutConstraint? + public var replyLabelRightHeightAnchor: NSLayoutConstraint? + public var titleLabelLeftTopAnchor: NSLayoutConstraint? + public var titleLabelLeftHeightAnchor: NSLayoutConstraint? + public var titleLabelRightTopAnchor: NSLayoutConstraint? + public var titleLabelRightHeightAnchor: NSLayoutConstraint? + public var contentLabelLeftHeightAnchor: NSLayoutConstraint? + public var contentLabelRightHeightAnchor: NSLayoutConstraint? + + override open func commonUI() { + /// left + bubbleImageLeft.addSubview(replyLabelLeft) + replyLabelLeftHeightAnchor = replyLabelLeft.heightAnchor.constraint(equalToConstant: 16.0) + NSLayoutConstraint.activate([ + replyLabelLeft.leadingAnchor.constraint(equalTo: bubbleImageLeft.leadingAnchor, constant: chat_content_margin), + replyLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: chat_content_margin), + replyLabelLeftHeightAnchor!, + replyLabelLeft.trailingAnchor.constraint(equalTo: bubbleImageLeft.trailingAnchor, constant: -chat_content_margin), + ]) + + bubbleImageLeft.addSubview(titleLabelLeft) + titleLabelLeftTopAnchor = titleLabelLeft.topAnchor.constraint(equalTo: replyLabelLeft.bottomAnchor, constant: chat_content_margin) + titleLabelLeftHeightAnchor = titleLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + NSLayoutConstraint.activate([ + titleLabelLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor, constant: -chat_content_margin), + titleLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: chat_content_margin), + titleLabelLeftTopAnchor!, + titleLabelLeftHeightAnchor!, + ]) + + bubbleImageLeft.addSubview(contentLabelLeft) + contentLabelLeftHeightAnchor = contentLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + NSLayoutConstraint.activate([ + contentLabelLeft.rightAnchor.constraint(equalTo: titleLabelLeft.rightAnchor, constant: 0), + contentLabelLeft.leftAnchor.constraint(equalTo: titleLabelLeft.leftAnchor, constant: 0), + contentLabelLeft.topAnchor.constraint(equalTo: titleLabelLeft.bottomAnchor, constant: chat_content_margin), + contentLabelLeftHeightAnchor!, + ]) + + /// right + bubbleImageRight.addSubview(replyLabelRight) + replyLabelRightHeightAnchor = replyLabelRight.heightAnchor.constraint(equalToConstant: 16.0) + NSLayoutConstraint.activate([ + replyLabelRight.leadingAnchor.constraint(equalTo: bubbleImageRight.leadingAnchor, constant: chat_content_margin), + replyLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: chat_content_margin), + replyLabelRightHeightAnchor!, + replyLabelRight.trailingAnchor.constraint(equalTo: bubbleImageRight.trailingAnchor, constant: -chat_content_margin), + ]) + + bubbleImageRight.addSubview(titleLabelRight) + titleLabelRightTopAnchor = titleLabelRight.topAnchor.constraint(equalTo: replyLabelRight.bottomAnchor, constant: chat_content_margin) + titleLabelRightHeightAnchor = titleLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + NSLayoutConstraint.activate([ + titleLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -chat_content_margin), + titleLabelRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: chat_content_margin), + titleLabelRightTopAnchor!, + titleLabelRightHeightAnchor!, + ]) + + bubbleImageRight.addSubview(contentLabelRight) + contentLabelRightHeightAnchor = contentLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + NSLayoutConstraint.activate([ + contentLabelRight.rightAnchor.constraint(equalTo: titleLabelRight.rightAnchor, constant: -0), + contentLabelRight.leftAnchor.constraint(equalTo: titleLabelRight.leftAnchor, constant: 0), + contentLabelRight.topAnchor.constraint(equalTo: titleLabelRight.bottomAnchor, constant: chat_content_margin), + contentLabelRightHeightAnchor!, + ]) + } + + override open func showLeftOrRight(showRight: Bool) { + super.showLeftOrRight(showRight: showRight) + titleLabelLeft.isHidden = showRight + titleLabelRight.isHidden = !showRight + } + + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) + let replyLabelHeightAnchor = isSend ? replyLabelRightHeightAnchor : replyLabelLeftHeightAnchor + let titleLabel = isSend ? titleLabelRight : titleLabelLeft + let titleLabelTopAnchor = isSend ? titleLabelRightTopAnchor : titleLabelLeftTopAnchor + let titleLabelHeightAnchor = isSend ? titleLabelRightHeightAnchor : titleLabelLeftHeightAnchor + let contentLabelHeightAnchor = isSend ? contentLabelRightHeightAnchor : contentLabelLeftHeightAnchor + + if let text = model.replyText { + replyLabelHeightAnchor?.constant = 16 + titleLabelTopAnchor?.constant = chat_content_margin + } else { + replyLabelHeightAnchor?.constant = 0 + titleLabelTopAnchor?.constant = 0 + } + + if let m = model as? MessageTextModel { + contentLabelHeightAnchor?.constant = m.textHeight + } + + if let m = model as? MessageRichTextModel { + titleLabel.attributedText = m.titleAttributeStr + titleLabelHeightAnchor?.constant = m.titleTextHeight + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift index a5bb8cac..219e816a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift @@ -15,7 +15,6 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) label.backgroundColor = .clear - label.textAlignment = .justified label.accessibilityIdentifier = "id.messageText" return label }() @@ -28,7 +27,6 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) label.backgroundColor = .clear - label.textAlignment = .justified label.accessibilityIdentifier = "id.messageText" return label }() @@ -66,13 +64,9 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { contentLabelRight.isHidden = !showRight } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let contentLabel = isSend ? contentLabelRight : contentLabelLeft - if let m = model as? MessageTextModel { contentLabel.attributedText = m.attributeStr } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift index 5cf8151c..91a481b5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift @@ -123,11 +123,8 @@ open class ChatMessageVideoCell: ChatMessageImageCell { ]) } - override open func setModel(_ model: MessageContentModel) { - super.setModel(model) - guard let isSend = model.message?.isOutgoingMsg else { - return - } + override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { + super.setModel(model, isSend) let contentImageView = isSend ? contentImageViewRight : contentImageViewLeft let timeView = isSend ? timeViewRight : timeViewLeft let timeLabel = isSend ? timeLabelRight : timeLabelLeft diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/PinCell/PinMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/PinCell/PinMessageMultiForwardCell.swift new file mode 100644 index 00000000..38540682 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/PinCell/PinMessageMultiForwardCell.swift @@ -0,0 +1,68 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NIMSDK +import UIKit + +@objcMembers +open class PinMessageMultiForwardCell: NEBasePinMessageMultiForwardCell { + override open func setupUI() { + super.setupUI() + backViewLeft.addSubview(titleLabelLeft1) + NSLayoutConstraint.activate([ + titleLabelLeft1.leftAnchor.constraint(equalTo: backViewLeft.leftAnchor, constant: 16), + titleLabelLeft1.rightAnchor.constraint(lessThanOrEqualTo: backViewLeft.rightAnchor, constant: -84), + titleLabelLeft1.topAnchor.constraint(equalTo: backViewLeft.topAnchor, constant: 10), + titleLabelLeft1.heightAnchor.constraint(equalToConstant: 22), + ]) + + backViewLeft.addSubview(titleLabelLeft2) + NSLayoutConstraint.activate([ + titleLabelLeft2.leftAnchor.constraint(equalTo: titleLabelLeft1.rightAnchor), + titleLabelLeft2.centerYAnchor.constraint(equalTo: titleLabelLeft1.centerYAnchor), + titleLabelLeft2.heightAnchor.constraint(equalToConstant: 22), + titleLabelLeft2.widthAnchor.constraint(equalToConstant: 74), + ]) + + backViewLeft.addSubview(contentLabelLeft1) + NSLayoutConstraint.activate([ + contentLabelLeft1.leftAnchor.constraint(equalTo: titleLabelLeft1.leftAnchor), + contentLabelLeft1.topAnchor.constraint(equalTo: titleLabelLeft1.bottomAnchor, constant: 2), + contentLabelLeft1.widthAnchor.constraint(equalToConstant: contentW), + contentLabelLeft1.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewLeft.addSubview(contentLabelLeft2) + NSLayoutConstraint.activate([ + contentLabelLeft2.leftAnchor.constraint(equalTo: contentLabelLeft1.leftAnchor), + contentLabelLeft2.topAnchor.constraint(equalTo: contentLabelLeft1.bottomAnchor), + contentLabelLeft2.widthAnchor.constraint(equalToConstant: contentW), + contentLabelLeft2.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + + backViewLeft.addSubview(contentLabelLeft3) + NSLayoutConstraint.activate([ + contentLabelLeft3.leftAnchor.constraint(equalTo: contentLabelLeft2.leftAnchor), + contentLabelLeft3.topAnchor.constraint(equalTo: contentLabelLeft2.bottomAnchor), + contentLabelLeft3.widthAnchor.constraint(equalToConstant: contentW), + contentLabelLeft3.heightAnchor.constraint(greaterThanOrEqualToConstant: 20), + ]) + +// backViewLeft.addSubview(contentHistoryLeft) +// NSLayoutConstraint.activate([ +// contentHistoryLeft.leftAnchor.constraint(equalTo: titleLabelLeft1.leftAnchor), +// contentHistoryLeft.bottomAnchor.constraint(equalTo: backViewLeft.bottomAnchor, constant: -12), +// contentHistoryLeft.widthAnchor.constraint(equalToConstant: 60), +// contentHistoryLeft.heightAnchor.constraint(equalToConstant: 14), +// ]) +// +// backViewLeft.addSubview(dividerLineLeft) +// NSLayoutConstraint.activate([ +// dividerLineLeft.leftAnchor.constraint(equalTo: backViewLeft.leftAnchor, constant: 6), +// dividerLineLeft.rightAnchor.constraint(equalTo: backViewLeft.rightAnchor, constant: -6), +// dividerLineLeft.topAnchor.constraint(equalTo: contentHistoryLeft.topAnchor, constant: -6), +// dividerLineLeft.heightAnchor.constraint(equalToConstant: 1), +// ]) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/PinCell/PinMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/PinCell/PinMessageRichTextCell.swift new file mode 100644 index 00000000..137c9d09 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/PinCell/PinMessageRichTextCell.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 PinMessageRichTextCell: NEBasePinMessageRichTextCell {} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ForwardAlertViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ForwardAlertViewController.swift index 77e904b0..1df9eb9e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ForwardAlertViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ForwardAlertViewController.swift @@ -8,7 +8,7 @@ import NECommonUIKit import UIKit @objcMembers -public class ForwardUserCell: NEBaseForwardUserCell { +open class ForwardUserCell: NEBaseForwardUserCell { override func setupUI() { super.setupUI() userHeader.layer.cornerRadius = 16 @@ -16,8 +16,8 @@ public class ForwardUserCell: NEBaseForwardUserCell { } @objcMembers -public class ForwardAlertViewController: NEBaseForwardAlertViewController { - override public func setupUI() { +open class ForwardAlertViewController: NEBaseForwardAlertViewController { + override open func setupUI() { super.setupUI() oneUserHead.layer.cornerRadius = 16.0 userCollection.register( @@ -26,7 +26,7 @@ public class ForwardAlertViewController: NEBaseForwardAlertViewController { ) } - override public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { if let cell = collectionView.dequeueReusableCell( withReuseIdentifier: "\(ForwardUserCell.self)", for: indexPath diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/GroupChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/GroupChatViewController.swift index ff512a0a..e30091d5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/GroupChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/GroupChatViewController.swift @@ -3,14 +3,17 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECoreIMKit import NIMSDK import UIKit @objcMembers open class GroupChatViewController: NormalChatViewController, TeamChatViewModelDelegate { - private var isLeaveTeamBySelf = false // 是否是主动退出群聊 + private var isLeaveTeamByOther = false // 是否被移出群聊 + private var isLeaveTeamBySelf = false // 是否多端登录另一端退出群聊 private var isdismissTeam = false // 群聊是否已解散 + private var isdismissDiscuss = false // 讨论组是否已解散 private var onCurrentPage = false // 是否位于聊天详情页 public init(session: NIMSession, anchor: NIMMessage?) { @@ -31,12 +34,30 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD fatalError("init(coder:) has not been implemented") } + deinit { + NotificationCenter.default.removeObserver(self) + } + override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) onCurrentPage = true - // 被动解散群聊 + + // 多端登录另一端解散、退出讨论组 + // 多端登录另一端退出群聊 + if isdismissDiscuss || isLeaveTeamBySelf { + popGroupChatVC() + } + + weak var weakSelf = self + // 被移除群聊 + if isLeaveTeamByOther { + showSingleAlert(message: chatLocalizable("team_has_been_quit")) { + weakSelf?.navigationController?.popViewController(animated: true) + } + } + + // 解散群聊 if isdismissTeam { - weak var weakSelf = self showSingleAlert(message: chatLocalizable("team_has_been_removed")) { weakSelf?.navigationController?.popViewController(animated: true) } @@ -50,7 +71,7 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD override open func viewDidLoad() { super.viewDidLoad() - NotificationCenter.default.addObserver(self, selector: #selector(leaveTeamBySelf), name: NotificationName.leaveTeamBySelf, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(popGroupChatVC), name: NENotificationName.popGroupChatVC, object: nil) } override open func getSessionInfo(session: NIMSession) { @@ -63,9 +84,21 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD // MARK: private method - func leaveTeamBySelf(noti: Notification) { - if let flag = noti.object as? Bool { - isLeaveTeamBySelf = flag + func popGroupChatVC() { + var beforeChat: UIViewController? + var loopCount = 0 + for vc in navigationController?.viewControllers ?? [] { + if vc.isKind(of: ChatViewController.self) { + if loopCount <= 1 { + navigationController?.popToRootViewController(animated: true) + return + } + navigationController?.popToViewController(beforeChat!, animated: true) + return + } else { + beforeChat = vc + loopCount += 1 + } } } @@ -82,50 +115,84 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD open func updateTeamInfo(team: NIMTeam) { title = team.getShowName() - if team.inAllMuteMode(), team.owner != NIMSDK.shared().loginManager.currentAccount() { + + if team.inAllMuteMode(), viewmodel.teamMember?.type != .manager, viewmodel.teamMember?.type != .owner { + // 群禁言 + isMute = true chatInputView.textView.isEditable = false chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("team_mute")) chatInputView.textView.backgroundColor = UIColor(hexString: "#E3E4E4") layoutInputView(offset: 0) chatInputView.stackView.isUserInteractionEnabled = false + chatInputView.setMuteInputStyle() + if chatInputView.chatInpuMode != .normal { + chatInputView.titleField.text = nil + didHideMultipleButtonClick() + } + closeReply(button: nil) } else { + // 解除群禁言 + isMute = false chatInputView.textView.isEditable = true chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: "\(chatLocalizable("send_to"))\(team.getShowName())") + chatInputView.textView.backgroundColor = .white chatInputView.stackView.isUserInteractionEnabled = true + chatInputView.setUnMuteInputStyle() } } - // MARK: TeamChatViewModelDelegate - - open func onTeamRemoved(team: NIMTeam) { - // 退出讨论组 - if team.isDisscuss() == true { - navigationController?.popViewController(animated: true) - return - } + override open func onRecvMessages(_ messages: [NIMMessage]) { + super.onRecvMessages(messages) + for message in messages { + if let object = message.messageObject as? NIMNotificationObject, + let content = object.content as? NIMTeamNotificationContent { + if content.operationType == .leave, + IMKitClient.instance.isMySelf(content.sourceID) { + isLeaveTeamBySelf = true + if onCurrentPage { + popGroupChatVC() + } + } else if content.operationType == .kick, + let targetIDs = content.targetIDs, + targetIDs.contains(IMKitClient.instance.imAccid()) { + // 被移出群聊 + isLeaveTeamByOther = true + if onCurrentPage { + showSingleAlert(message: chatLocalizable("team_has_been_quit")) { [weak self] in + self?.navigationController?.popViewController(animated: true) + } + } + } else if content.operationType == .dismiss { + if isdismissDiscuss { + return + } - // 离开群聊 - if team.teamId == viewmodel.session.sessionId { - if team.owner != NIMSDK.shared().loginManager.currentAccount() { // 退出群聊 - if isLeaveTeamBySelf { - navigationController?.popViewController(animated: true) - } else { + // 解散群聊 isdismissTeam = true - // 被动解散群聊 if onCurrentPage { - weak var weakSelf = self - showSingleAlert(message: chatLocalizable("team_has_been_removed")) { - weakSelf?.navigationController?.popViewController(animated: true) + showSingleAlert(message: chatLocalizable("team_has_been_removed")) { [weak self] in + self?.navigationController?.popViewController(animated: true) } } } - } else { // 主动解散 - navigationController?.popViewController(animated: true) } } } + // MARK: TeamChatViewModelDelegate + + open func onTeamRemoved(team: NIMTeam) { + // 多端登录另一端解散、退出讨论组 + if team.isDisscuss() == true { + isdismissDiscuss = true + if onCurrentPage { + popGroupChatVC() + } + return + } + } + open func onTeamUpdate(team: NIMTeam) { if team.teamId != viewmodel.session.sessionId { return @@ -133,7 +200,15 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD updateTeamInfo(team: team) } - public func onTeamMemberUpdate(team: NIMTeam) { + open func onTeamMemberUpdate(team: NIMTeam) { didRefreshTable() } + + override public func onTeamMemberChange(team: NIMTeam) { + if viewmodel.session.sessionId != team.teamId { + return + } + (viewmodel as? TeamChatViewModel)?.getTeamMember() + updateTeamInfo(team: team) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift index 0d74d428..1aa5b508 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift @@ -11,18 +11,7 @@ open class NormalChatViewController: ChatViewController { super.init(session: session) navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white - cellRegisterDic = [ - "\(MessageType.text.rawValue)": ChatMessageTextCell.self, - "\(MessageType.rtcCallRecord.rawValue)": ChatMessageCallCell.self, - "\(MessageType.audio.rawValue)": ChatMessageAudioCell.self, - "\(MessageType.image.rawValue)": ChatMessageImageCell.self, - "\(MessageType.revoke.rawValue)": ChatMessageRevokeCell.self, - "\(MessageType.video.rawValue)": ChatMessageVideoCell.self, - "\(MessageType.file.rawValue)": ChatMessageFileCell.self, - "\(MessageType.reply.rawValue)": ChatMessageReplyCell.self, - "\(MessageType.location.rawValue)": ChatMessageLocationCell.self, - "\(MessageType.time.rawValue)": ChatMessageTipCell.self, - ] + cellRegisterDic = ChatMessageHelper.getChatCellRegisterDic(isFun: false) } public required init?(coder: NSCoder) { @@ -34,50 +23,116 @@ open class NormalChatViewController: ChatViewController { } override open func getMenuView() -> NEBaseChatInputView { - ChatInputView() + let chat = ChatInputView() + chat.multipleLineDelegate = self + return chat } override open func getForwardAlertController() -> NEBaseForwardAlertViewController { ForwardAlertViewController() } + override open func getMultiForwardViewController(_ messageAttachmentUrl: String?, + _ messageAttachmentFilePath: String, + _ messageAttachmentMD5: String?) -> MultiForwardViewController { + NormalMultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) + } + override func getUserSelectVC() -> NEBaseSelectUserViewController { SelectUserViewController(sessionId: viewmodel.session.sessionId, showSelf: false) } - override open func toSetting() { - if let block = NEKitChatConfig.shared.ui.messageProperties.titleBarRightClick { - block() - return - } - if viewmodel.session.sessionType == .team { - Router.shared.use( - TeamSettingViewRouter, - parameters: ["nav": navigationController as Any, - "teamid": viewmodel.session.sessionId], - closure: nil + open func getMessageModel(model: MessageModel) { + if model.type == .reply { + let normalMoreHeight = chat_reply_height + chat_content_margin + model.contentSize = CGSize( + width: model.contentSize.width, + height: model.contentSize.height + normalMoreHeight ) - } else if viewmodel.session.sessionType == .P2P { - let userSetting = UserSettingViewController(userId: viewmodel.session.sessionId) - navigationController?.pushViewController(userSetting, animated: true) + model.height += normalMoreHeight } } - override open func didTapReadView(_ cell: UITableViewCell, _ model: MessageContentModel?) { - if let msg = model?.message, msg.session?.sessionType == .team { - let readVC = ReadViewController(message: msg) - navigationController?.pushViewController(readVC, animated: true) + override open func expandButtonDidClick() { + print("expandButtonDidClick ") + super.expandButtonDidClick() + chatInputView.changeToMultipleLineStyle() + normalInputHeight = 296 + bottomViewTopAnchor?.constant = -normalInputHeight + chatInputView.textView.resignFirstResponder() + chatInputView.titleField.resignFirstResponder() + checkAndRemoveReplyView() + } + + override open func didHideMultipleButtonClick() { + super.didHideMultipleButtonClick() + + if chatInputView.chatInpuMode == .normal { + normalInputHeight = 100 + } else { + normalInputHeight = 150 } + bottomViewTopAnchor?.constant = -normalInputHeight + checkAndRestoreReplyView() } - public func getMessageModel(model: MessageModel) { - if model.type == .reply { - let normalMoreHeight = chat_reply_height + chat_content_margin - model.contentSize = CGSize( - width: model.contentSize.width, - height: model.contentSize.height + normalMoreHeight - ) - model.height += Float(normalMoreHeight) + // 切换到多行消息模式隐藏回复 + func checkAndRemoveReplyView() { + if chatInputView.chatInpuMode == .multipleReturn { + if replyView.superview != nil { + replyView.removeFromSuperview() + } + } + } + + // 切换到单行输入框如果有回复显示回复视图 + 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), + ]) + } + } + + override open func titleTextDidClearEmpty() { + if chatInputView.chatInpuMode == .multipleSend { + chatInputView.chatInpuMode = .normal + if chatInputView.chatInpuMode == .normal { + normalInputHeight = 100 + } else { + normalInputHeight = 150 + } + chatInputView.restoreNormalInputStyle() + layoutInputViewWithAnimation(offset: currentKeyboardHeight) + chatInputView.textView.becomeFirstResponder() + } + } + + override open func keyBoardWillShow(_ notification: Notification) { + let keyboardRect = (notification + .userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue + currentKeyboardHeight = keyboardRect.height + super.keyBoardWillShow(notification) + } + + override open func keyBoardWillHide(_ notification: Notification) { + currentKeyboardHeight = 0 + super.keyBoardWillHide(notification) + } + + // 减小多行输入框高度,不收回键盘 + override open func didHideMultiple() { + if chatInputView.chatInpuMode == .normal { + normalInputHeight = 100 + } else { + normalInputHeight = 150 } + bottomViewTopAnchor?.constant = -(normalInputHeight + currentKeyboardHeight) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift new file mode 100644 index 00000000..ff2087df --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.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 NIMSDK +import UIKit + +@objcMembers +open class NormalMultiForwardViewController: MultiForwardViewController { + override public init(_ attachmentUrl: String?, + _ attachmentFilePath: String, + _ attachmentMD5: String?) { + super.init(attachmentUrl, attachmentFilePath, attachmentMD5) + navigationView.backgroundColor = .white + navigationController?.navigationBar.backgroundColor = .white + cellRegisterDic = ChatMessageHelper.getChatCellRegisterDic(isFun: false) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func getMultiForwardViewController(_ messageAttachmentUrl: String?, + _ messageAttachmentFilePath: String, + _ messageAttachmentMD5: String?) -> MultiForwardViewController { + NormalMultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift index a68daf8f..eb1b6ad1 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift @@ -22,7 +22,7 @@ open class P2PChatViewController: NormalChatViewController { override open func getSessionInfo(session: NIMSession) { var showName = session.sessionId - viewmodel.getUserInfo(session.sessionId) { [weak self] user, error in + ChatUserCache.getUserInfo(session.sessionId) { [weak self] user, error in if let name = user?.showName() { showName = name } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/PinMessageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/PinMessageViewController.swift index 9f2b85d5..eaa75e12 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/PinMessageViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/PinMessageViewController.swift @@ -7,27 +7,24 @@ import UIKit @objcMembers open class PinMessageViewController: NEBasePinMessageViewController { - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .ne_lightBackgroundColor navigationView.backgroundColor = .ne_lightBackgroundColor navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor } - override open func getRegisterCellDic() -> [Int: NEBasePinMessageCell.Type] { - let cellClassDic = [ - NIMMessageType.text.rawValue: PinMessageTextCell.self, - NIMMessageType.image.rawValue: PinMessageImageCell.self, - NIMMessageType.audio.rawValue: PinMessageAudioCell.self, - NIMMessageType.video.rawValue: PinMessageVideoCell.self, - NIMMessageType.location.rawValue: PinMessageLocationCell.self, - NIMMessageType.file.rawValue: PinMessageFileCell.self, - PinMessageDefaultType: PinMessageDefaultCell.self, - ] - return cellClassDic + override open func getRegisterCellDic() -> [String: NEBasePinMessageCell.Type] { + ChatMessageHelper.getPinCellRegisterDic(isFun: false) } override open func getForwardAlertController() -> NEBaseForwardAlertViewController { ForwardAlertViewController() } + + override open func getMultiForwardViewController(_ messageAttachmentUrl: String?, + _ messageAttachmentFilePath: String, + _ messageAttachmentMD5: String?) -> MultiForwardViewController { + NormalMultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift index 2fe49747..06781a12 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift @@ -20,7 +20,7 @@ open class ReadViewController: NEBaseReadViewController { fatalError("init(coder:) has not been implemented") } - override public func commonUI() { + override open func commonUI() { super.commonUI() navigationView.titleBarBottomLine.isHidden = false readButton.setTitleColor(UIColor.ne_darkText, for: .normal) diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift index 896ab813..9e9db17b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift @@ -34,11 +34,11 @@ open class SelectUserViewController: NEBaseSelectUserViewController { withIdentifier: "\(ChatTeamMemberCell.self)", for: indexPath ) as! ChatTeamMemberCell - if indexPath.row == 0 { + if indexPath.section == 0 { cell.headerView.image = UIImage.ne_imageNamed(name: "chat_team") cell.nameLabel.text = chatLocalizable("user_select_all") } else { - if let model = teamInfo?.users[indexPath.row - 1] { + if let model = teamInfo?.users[indexPath.row] { cell.configure(model) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift index 69c6a80c..932a3f18 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift @@ -40,7 +40,8 @@ public extension ChatRouter { return } let anchor = param["anchor"] as? NIMMessage - var p2pChatVC = P2PChatViewController(session: session, anchor: anchor) + let p2pChatVC = P2PChatViewController(session: session, anchor: anchor) + for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { nav?.viewControllers[i] = p2pChatVC @@ -48,6 +49,11 @@ public extension ChatRouter { return } } + + if let remove = param["removeUserVC"] as? Bool, remove { + nav?.viewControllers.removeLast() + } + nav?.pushViewController(p2pChatVC, animated: true) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/View/ChatInpuView.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/View/ChatInpuView.swift index 110df866..45559500 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/View/ChatInpuView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/View/ChatInpuView.swift @@ -6,12 +6,15 @@ import Foundation @objcMembers open class ChatInputView: NEBaseChatInputView { - override public func commonUI() { + public var backViewHeightConstraint: NSLayoutConstraint? + public var toolsBarTopMargin: NSLayoutConstraint? + + override open func commonUI() { backgroundColor = UIColor.normalChatInputBg addSubview(textView) textView.delegate = self textviewLeftConstraint = textView.leftAnchor.constraint(equalTo: leftAnchor, constant: 7) - textviewRightConstraint = textView.rightAnchor.constraint(equalTo: rightAnchor, constant: -7) + textviewRightConstraint = textView.rightAnchor.constraint(equalTo: rightAnchor, constant: -44) NSLayoutConstraint.activate([ textviewLeftConstraint!, textviewRightConstraint!, @@ -20,6 +23,25 @@ open class ChatInputView: NEBaseChatInputView { ]) textInput = textView + backViewHeightConstraint = backView.heightAnchor.constraint(equalToConstant: 40) + insertSubview(backView, belowSubview: textView) + NSLayoutConstraint.activate([ + backView.leftAnchor.constraint(equalTo: leftAnchor, constant: 7), + backView.rightAnchor.constraint(equalTo: rightAnchor, constant: -7), + backView.topAnchor.constraint(equalTo: topAnchor, constant: 6), + backViewHeightConstraint!, + ]) + + addSubview(expandButton) + NSLayoutConstraint.activate([ + expandButton.topAnchor.constraint(equalTo: topAnchor, constant: 7), + expandButton.rightAnchor.constraint(equalTo: rightAnchor, constant: 0), + expandButton.heightAnchor.constraint(equalToConstant: 40), + expandButton.widthAnchor.constraint(equalToConstant: 44.0), + ]) + expandButton.setImage(coreLoader.loadImage("normal_input_unfold"), for: .normal) + expandButton.addTarget(self, action: #selector(didClickExpandButton), for: .touchUpInside) + let imageNames = ["mic", "emoji", "photo", "add"] let imageNamesSelected = ["mic_selected", "emoji_selected", "photo", "add_selected"] @@ -42,23 +64,25 @@ open class ChatInputView: NEBaseChatInputView { stackView = UIStackView(arrangedSubviews: items) stackView.translatesAutoresizingMaskIntoConstraints = false stackView.distribution = .fillEqually + + toolsBarTopMargin = stackView.topAnchor.constraint(equalTo: topAnchor, constant: 46) addSubview(stackView) NSLayoutConstraint.activate([ stackView.leftAnchor.constraint(equalTo: leftAnchor), stackView.rightAnchor.constraint(equalTo: rightAnchor), stackView.heightAnchor.constraint(equalToConstant: 54), - stackView.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 0), + toolsBarTopMargin!, ]) greyView.translatesAutoresizingMaskIntoConstraints = false - greyView.backgroundColor = UIColor(hexString: "#EFF1F3") + greyView.backgroundColor = .ne_backgroundColor greyView.isHidden = true addSubview(greyView) NSLayoutConstraint.activate([ greyView.leftAnchor.constraint(equalTo: leftAnchor, constant: 0), greyView.topAnchor.constraint(equalTo: topAnchor, constant: 0), greyView.rightAnchor.constraint(equalTo: rightAnchor, constant: 0), - greyView.heightAnchor.constraint(equalToConstant: 100), + greyView.heightAnchor.constraint(equalToConstant: 400), ]) addSubview(contentView) @@ -85,5 +109,86 @@ open class ChatInputView: NEBaseChatInputView { contentView.addSubview(emojiView) contentView.addSubview(chatAddMoreView) + + setupMultipleLineView() + multipleLineExpandButton.setImage(coreLoader.loadImage("normal_input_fold"), for: .normal) + } + + override open func restoreNormalInputStyle() { + super.restoreNormalInputStyle() + textView.returnKeyType = .send + textView.removeAllAutoLayout() + textView.removeConstraints(textView.constraints) + insertSubview(textView, aboveSubview: backView) + textviewLeftConstraint = textView.leftAnchor.constraint(equalTo: leftAnchor, constant: 7) + textviewRightConstraint = textView.rightAnchor.constraint(equalTo: rightAnchor, constant: -44) + if chatInpuMode == .normal { + NSLayoutConstraint.activate([ + textviewLeftConstraint!, + textviewRightConstraint!, + textView.topAnchor.constraint(equalTo: topAnchor, constant: 6), + textView.heightAnchor.constraint(equalToConstant: 40), + ]) + backViewHeightConstraint?.constant = 46 + toolsBarTopMargin?.constant = 46 + titleField.isHidden = true + } else if chatInpuMode == .multipleSend { + titleField.isHidden = false + NSLayoutConstraint.activate([ + textviewLeftConstraint!, + textviewRightConstraint!, + textView.topAnchor.constraint(equalTo: topAnchor, constant: 45), + textView.heightAnchor.constraint(equalToConstant: 45), + ]) + + titleField.removeAllAutoLayout() + insertSubview(titleField, belowSubview: textView) + NSLayoutConstraint.activate([ + titleField.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 4), + titleField.rightAnchor.constraint(equalTo: expandButton.leftAnchor), + titleField.topAnchor.constraint(equalTo: backView.topAnchor), + titleField.heightAnchor.constraint(equalToConstant: 40), + ]) + + backViewHeightConstraint?.constant = 100 + toolsBarTopMargin?.constant = 100 + } + } + + override open func changeToMultipleLineStyle() { + super.changeToMultipleLineStyle() + textView.removeAllAutoLayout() + multipleLineView.addSubview(textView) + textView.removeConstraints(textView.constraints) + textView.returnKeyType = .default + titleField.isHidden = false + + NSLayoutConstraint.activate([ + textView.leftAnchor.constraint(equalTo: multipleLineView.leftAnchor, constant: 13), + textView.rightAnchor.constraint(equalTo: multipleLineView.rightAnchor, constant: -16), + textView.topAnchor.constraint(equalTo: multipleLineView.topAnchor, constant: 48), + textView.heightAnchor.constraint(equalToConstant: 183), + ]) + + if titleField.superview == nil || titleField.superview != multipleLineView { + titleField.removeAllAutoLayout() + multipleLineView.addSubview(titleField) + NSLayoutConstraint.activate([ + titleField.leftAnchor.constraint(equalTo: multipleLineView.leftAnchor, constant: 16), + titleField.rightAnchor.constraint(equalTo: multipleLineView.rightAnchor, constant: -56), + titleField.topAnchor.constraint(equalTo: multipleLineView.topAnchor, constant: 5), + titleField.heightAnchor.constraint(equalToConstant: 40), + ]) + } + } + + override open func setMuteInputStyle() { + super.setMuteInputStyle() + backView.backgroundColor = UIColor(hexString: "#E3E4E4") + } + + override open func setUnMuteInputStyle() { + super.setUnMuteInputStyle() + backView.backgroundColor = .white } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Protocol/ChatInputViewDelegate.swift b/NEChatUIKit/NEChatUIKit/Classes/Protocol/ChatInputViewDelegate.swift index 4adcef46..c4a72527 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Protocol/ChatInputViewDelegate.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Protocol/ChatInputViewDelegate.swift @@ -17,7 +17,8 @@ public protocol ChatInputViewDelegate: NSObjectProtocol { func moveOutView() func moveInView() func endRecord(insideView: Bool) - func textFieldDidChange(_ textField: UITextView) - func textFieldDidEndEditing(_ textField: UITextView) - func textFieldDidBeginEditing(_ textField: UITextView) + func textFieldDidChange(_ text: String?) + func textFieldDidEndEditing(_ text: String?) + func textFieldDidBeginEditing(_ text: String?) + func titleTextDidClearEmpty() } diff --git a/NEContactUIKit/NEContactUIKit.podspec b/NEContactUIKit/NEContactUIKit.podspec index 4e12d7f4..39a52c66 100644 --- a/NEContactUIKit/NEContactUIKit.podspec +++ b/NEContactUIKit/NEContactUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEContactUIKit' - s.version = '9.6.5' + s.version = '9.7.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift index edd1ad32..88fc284c 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift @@ -35,6 +35,7 @@ open class NEBaseContactViewCell: UITableViewCell { label.layer.cornerRadius = 9 label.clipsToBounds = true label.isHidden = true + label.accessibilityIdentifier = "id.unread" return label }() @@ -45,7 +46,7 @@ open class NEBaseContactViewCell: UITableViewCell { name.textAlignment = .center name.font = UIFont.systemFont(ofSize: 14.0) name.adjustsFontSizeToFitWidth = true - name.accessibilityIdentifier = "id.avatar" + name.accessibilityIdentifier = "id.noAvatar" return name }() @@ -71,17 +72,6 @@ open class NEBaseContactViewCell: UITableViewCell { var leftConstraint: NSLayoutConstraint? - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } - override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift index 7290e1f1..7b9b28ab 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift @@ -14,7 +14,7 @@ protocol BlackListCellDelegate: AnyObject { open class NEBaseBlackListCell: NEBaseTeamTableViewCell { weak var delegate: BlackListCellDelegate? var index = 0 - private var model: User? + private var model: NEKitUser? var button = UIButton() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -58,8 +58,8 @@ open class NEBaseBlackListCell: NEBaseTeamTableViewCell { delegate?.removeUser(account: model?.userId, index: index) } - override public func setModel(_ model: Any) { - guard let user = model as? User else { + override open func setModel(_ model: Any) { + guard let user = model as? NEKitUser else { return } self.model = user @@ -68,7 +68,7 @@ open class NEBaseBlackListCell: NEBaseTeamTableViewCell { titleLabel.text = user.showName() // avatar - if let imageUrl = user.userInfo?.avatarUrl { + if let imageUrl = user.userInfo?.avatarUrl, !imageUrl.isEmpty { nameLabel.text = "" avatarImage.sd_setImage(with: URL(string: imageUrl), completed: nil) avatarImage.backgroundColor = .clear diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift index 6e2bce8e..ba573f1b 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift @@ -14,10 +14,10 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, public let navigationView = NENavigationView() var tableView = UITableView(frame: .zero, style: .plain) var viewModel = BlackListViewModel() - public var blackList: [User]? + public var blackList: [NEKitUser]? var className = "BlackListBaseViewController" - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white navigationController?.interactivePopGestureRecognizer?.delegate = self @@ -96,12 +96,12 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, tableView.reloadData() } - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { blackList?.count ?? 0 } - public func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { UITableViewCell() } @@ -117,7 +117,7 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, let contactSelectVC = getContactSelectVC() navigationController?.pushViewController(contactSelectVC, animated: true) contactSelectVC.callBack = { [weak self] selectMemberarray in - var users = [User]() + var users = [NEKitUser]() selectMemberarray.forEach { memberInfo in if let u = memberInfo.user { users.append(u) @@ -127,9 +127,9 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, } } - func addBlackUsers(users: [User]) { + func addBlackUsers(users: [NEKitUser]) { var num = users.count - var suc = [User]() + var suc = [NEKitUser]() for user in users { viewModel.addBlackList(account: user.userId ?? "") { [weak self] error in NELog.infoLog( @@ -179,9 +179,9 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, // MARK: FriendProviderDelegate extension NEBaseBlackListViewController: FriendProviderDelegate { - public func onFriendChanged(user: NECoreIMKit.User) {} + public func onFriendChanged(user: NECoreIMKit.NEKitUser) {} - public func onUserInfoChanged(user: NECoreIMKit.User) {} + public func onUserInfoChanged(user: NECoreIMKit.NEKitUser) {} public func onBlackListChanged() { loadData() diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift index 92a760a8..93c45992 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift @@ -8,7 +8,7 @@ import NECoreIMKit import NECoreKit @objcMembers -public class BlackListViewModel: NSObject, FriendProviderDelegate { +open class BlackListViewModel: NSObject, FriendProviderDelegate { var contactRepo = ContactRepo.shared public weak var delegate: FriendProviderDelegate? private let className = "BlackListViewModel" @@ -19,7 +19,7 @@ public class BlackListViewModel: NSObject, FriendProviderDelegate { contactRepo.addContactDelegate(delegate: self) } - func getBlackList() -> [User]? { + func getBlackList() -> [NEKitUser]? { NELog.infoLog(ModuleName + " " + className, desc: #function) return contactRepo.getBlackList() } @@ -36,12 +36,12 @@ public class BlackListViewModel: NSObject, FriendProviderDelegate { // MARK: callback - public func onFriendChanged(user: User) { + public func onFriendChanged(user: NEKitUser) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:\(user.userId ?? "nil")") delegate?.onFriendChanged(user: user) } - public func onUserInfoChanged(user: User) { + public func onUserInfoChanged(user: NEKitUser) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:\(user.userId ?? "nil")") delegate?.onUserInfoChanged(user: user) } @@ -51,7 +51,7 @@ public class BlackListViewModel: NSObject, FriendProviderDelegate { delegate?.onBlackListChanged() } - public func onRecieveNotification(notification: XNotification) { + public func onRecieveNotification(notification: NENotification) { NELog.infoLog(ModuleName + " " + className, desc: #function) print(#file + #function) } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift b/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift index 1f5c61cd..c43dccce 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift @@ -22,9 +22,3 @@ func localizable(_ key: String) -> String { } public let ModuleName = "NEContactUIKit" - -// MARK: notificationkey - -enum NotificationName { - static let updateFriendInfo = Notification.Name("chat.updateFriendInfo") -} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift b/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift index 057af4b5..addd0380 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift @@ -9,4 +9,4 @@ import NECoreKit import NIMSDK @objcMembers -public class ContactRouter: NSObject {} +open class ContactRouter: NSObject {} diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift index dae4c46b..bd6c2984 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift @@ -22,6 +22,8 @@ open class FunContactTableViewCell: NEBaseContactTableViewCell { override open func commonUI() { super.commonUI() bottomLine.backgroundColor = .funContactLineBorderColor + contentView.removeLayoutConstraint(firstItem: redAngleView, seconedItem: arrow, attribute: .right) + redAngleView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10).isActive = true } override open func initSubviewsLayout() { diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift index a404f3f3..508b2a8f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift @@ -13,10 +13,8 @@ public extension ContactRouter { Router.shared.register(ContactUserSelectRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - let contactSelectVC = FunContactsSelectedViewController() - if let fiters = param["filters"] as? Set { - contactSelectVC.filterUsers = fiters - } + let filters = param["filters"] as? Set + let contactSelectVC = FunContactsSelectedViewController(filterUsers: filters) if let limit = param["limit"] as? Int, limit > 0 { contactSelectVC.limit = limit } @@ -34,10 +32,10 @@ public extension ContactRouter { Router.shared.register(ContactUserInfoPageRouter) { param in if let nav = param["nav"] as? UINavigationController { - if let user = param["user"] as? User { + if let user = param["user"] as? NEKitUser { let userInfoVC = FunContactUserViewController(user: user) nav.pushViewController(userInfoVC, animated: true) - } else if let nimUser = param["nim_user"] as? User { + } else if let nimUser = param["nim_user"] as? NEKitUser { let userInfoVC = FunContactUserViewController(user: nimUser) nav.pushViewController(userInfoVC, animated: true) } else if let uid = param["uid"] as? String { diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift index e06be1f8..3ec4e7df 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift @@ -7,7 +7,7 @@ import UIKit @objcMembers open class FunUserInfoHeaderView: NEBaseUserInfoHeaderView { - override public func commonUI() { + override open func commonUI() { super.commonUI() avatarImage.layer.cornerRadius = 4 @@ -19,7 +19,7 @@ open class FunUserInfoHeaderView: NEBaseUserInfoHeaderView { ]) } - override public func setData(user: User?) { + override open func setData(user: NEKitUser?) { super.setData(user: user) guard let u = user else { return diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift index 1e10cadc..e33964a0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift @@ -28,7 +28,7 @@ open class FunBlackListViewController: NEBaseBlackListViewController { tableView.rowHeight = 64 } - override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: "\(NSStringFromClass(FunBlackListCell.self))", for: indexPath @@ -39,7 +39,7 @@ open class FunBlackListViewController: NEBaseBlackListViewController { return cell } - override public func getContactSelectVC() -> NEBaseContactsSelectedViewController { + override open func getContactSelectVC() -> NEBaseContactsSelectedViewController { FunContactsSelectedViewController() } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift index 904d4bcc..13b2a39e 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift @@ -14,7 +14,7 @@ open class FunContactUserViewController: NEBaseContactUserViewController { headerView = FunUserInfoHeaderView() } - override public init(user: User?) { + override public init(user: NEKitUser?) { super.init(user: user) initFun() } @@ -56,11 +56,11 @@ open class FunContactUserViewController: NEBaseContactUserViewController { return 46 } - override public func getContactRemakNameViewController() -> NEBaseContactRemakNameViewController { + override open func getContactRemakNameViewController() -> NEBaseContactRemakNameViewController { FunContactRemakNameViewController() } - override public func deleteFriend(user: User?) { + override open func deleteFriend(user: NEKitUser?) { let titleAction = NECustomAlertAction(title: String(format: localizable("delete_title"), user?.showName(true) ?? "")) {} titleAction.contentText.font = .systemFont(ofSize: 13) titleAction.contentText.textColor = UIColor(hexString: "#8F8F8F") diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsSelectedViewController.swift index 4436ca01..bdc26ed0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsSelectedViewController.swift @@ -8,8 +8,8 @@ import UIKit @objcMembers open class FunContactsSelectedViewController: NEBaseContactsSelectedViewController { - override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + override init(filterUsers: Set? = nil) { + super.init(filterUsers: filterUsers) customCells = [ContactCellType.ContactPerson.rawValue: FunContactSelectedCell.self] view.backgroundColor = .funContactBackgroundColor } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunTeamListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunTeamListViewController.swift index b7c5ef7c..3623240a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunTeamListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunTeamListViewController.swift @@ -18,8 +18,8 @@ open class FunTeamListViewController: NEBaseTeamListViewController { tableView.rowHeight = 72 } - override public func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { + override open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: "\(NSStringFromClass(FunTeamTableViewCell.self))", for: indexPath diff --git a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift index 5504ed86..9ffbfeee 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift @@ -8,13 +8,18 @@ import NECoreIMKit import NECoreKit import UIKit +/* + 通讯录 cell 数据模型 + // contactCellType: 自定义 UI 类型,其在注册会话列表 cell 时作为 key 与自定义 cell 进行绑定 + // localExtension: 本地扩展字段,可根据业务需求添加数据,与 contactCellType 结合可实现多种自定义 cell 的展示 + */ @objcMembers open class ContactInfo: NSObject { func getRowHeight() -> CGFloat? { nil } - public var user: User? + public var user: NEKitUser? public var contactCellType = ContactCellType.ContactPerson.rawValue public var router = ContactPersonRouter public var isSelected = false diff --git a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift index 456f2523..d2eee358 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift @@ -7,6 +7,11 @@ import Foundation import NEChatKit import NECoreIMKit +/* + 通讯录 section 数据模型 + // initial: tableView 对应 section 的标题 + // contacts: tableView 对应 section 的数据 + */ @objcMembers open class ContactSection { public var initial: String diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift index 601161a6..20486958 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift @@ -13,10 +13,8 @@ public extension ContactRouter { Router.shared.register(ContactUserSelectRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - let contactSelectVC = ContactsSelectedViewController() - if let fiters = param["filters"] as? Set { - contactSelectVC.filterUsers = fiters - } + let filters = param["filters"] as? Set + let contactSelectVC = ContactsSelectedViewController(filterUsers: filters) if let limit = param["limit"] as? Int, limit > 0 { contactSelectVC.limit = limit } @@ -34,10 +32,10 @@ public extension ContactRouter { Router.shared.register(ContactUserInfoPageRouter) { param in if let nav = param["nav"] as? UINavigationController { - if let user = param["user"] as? User { + if let user = param["user"] as? NEKitUser { let userInfoVC = ContactUserViewController(user: user) nav.pushViewController(userInfoVC, animated: true) - } else if let nimUser = param["nim_user"] as? User { + } else if let nimUser = param["nim_user"] as? NEKitUser { let userInfoVC = ContactUserViewController(user: nimUser) nav.pushViewController(userInfoVC, animated: true) } else if let uid = param["uid"] as? String { diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift index b3efc216..c390bcb2 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift @@ -8,7 +8,7 @@ import UIKit @objcMembers open class UserInfoHeaderView: NEBaseUserInfoHeaderView { - override public func commonUI() { + override open func commonUI() { super.commonUI() avatarImage.layer.cornerRadius = 30 @@ -20,7 +20,7 @@ open class UserInfoHeaderView: NEBaseUserInfoHeaderView { ]) } - override public func setData(user: User?) { + override open func setData(user: NEKitUser?) { super.setData(user: user) guard let u = user else { return diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift index b4b3e5e9..093dcd78 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift @@ -34,7 +34,7 @@ open class BlackListViewController: NEBaseBlackListViewController { ContactsSelectedViewController() } - override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: "\(NSStringFromClass(BlackListCell.self))", for: indexPath diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift index afb59116..52cd8e37 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift @@ -15,7 +15,7 @@ open class ContactUserViewController: NEBaseContactUserViewController { headerView = UserInfoHeaderView() } - override public init(user: User?) { + override public init(user: NEKitUser?) { super.init(user: user) initNormal() } @@ -34,7 +34,7 @@ open class ContactUserViewController: NEBaseContactUserViewController { tableView.rowHeight = 62 } - override public func getContactRemakNameViewController() -> NEBaseContactRemakNameViewController { + override open func getContactRemakNameViewController() -> NEBaseContactRemakNameViewController { ContactRemakNameViewController() } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsSelectedViewController.swift index 71928f50..db8c3f96 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsSelectedViewController.swift @@ -8,8 +8,8 @@ import UIKit @objcMembers open class ContactsSelectedViewController: NEBaseContactsSelectedViewController { - override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + override init(filterUsers: Set? = nil) { + super.init(filterUsers: filterUsers) customCells = [ContactCellType.ContactPerson.rawValue: ContactSelectedCell.self] view.backgroundColor = .ne_backcolor navigationView.backgroundColor = .white diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/TeamListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/TeamListViewController.swift index de64abf2..8d715237 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/TeamListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/TeamListViewController.swift @@ -20,8 +20,8 @@ open class TeamListViewController: NEBaseTeamListViewController { tableView.rowHeight = 62 } - override public func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { + override open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( withIdentifier: "\(NSStringFromClass(TeamTableViewCell.self))", for: indexPath diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift index 609446eb..7798afdf 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift @@ -69,8 +69,8 @@ open class NEBaseTeamTableViewCell: UITableViewCell { // ]) } - public func setModel(_ model: Any) { - guard let team = model as? Team else { + open func setModel(_ model: Any) { + guard let team = model as? NETeam else { return } guard let name = team.teamName else { @@ -78,7 +78,7 @@ open class NEBaseTeamTableViewCell: UITableViewCell { } titleLabel.text = name // self.nameLabel.text = name.count > 2 ? String(name[name.index(name.endIndex, offsetBy: -2)...]) : name - if let url = team.thumbAvatarUrl { + if let url = team.thumbAvatarUrl, !url.isEmpty { avatarImage.sd_setImage(with: URL(string: url), completed: nil) avatarImage.backgroundColor = .clear } else { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift index 5588d158..672b98eb 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift @@ -14,7 +14,7 @@ open class NEBaseTeamListViewController: UIViewController, UITableViewDelegate, var viewModel = TeamListViewModel() var isClickCallBack = false - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .white navigationController?.interactivePopGestureRecognizer?.delegate = self @@ -76,16 +76,16 @@ open class NEBaseTeamListViewController: UIViewController, UITableViewDelegate, tableView.reloadData() } - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { viewModel.teamList.count } - public func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { UITableViewCell() } - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let model = viewModel.teamList[indexPath.row] if isClickCallBack == true { Router.shared.use( diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift index 03d0f459..dc08705b 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift @@ -8,10 +8,10 @@ import NECoreIMKit import NECoreKit @objcMembers -public class TeamListViewModel: NSObject, NIMTeamManagerDelegate { +open class TeamListViewModel: NSObject, NIMTeamManagerDelegate { var contactRepo = ContactRepo.shared var refresh: () -> Void = {} - public var teamList = [Team]() + public var teamList = [NETeam]() private let className = "TeamListViewModel" override public init() { @@ -23,7 +23,7 @@ public class TeamListViewModel: NSObject, NIMTeamManagerDelegate { contactRepo.removeTeamDelegate(delegate: self) } - func getTeamList() -> [Team]? { + func getTeamList() -> [NETeam]? { NELog.infoLog(ModuleName + " " + className, desc: #function) teamList = contactRepo.getTeamList() teamList.sort(by: { team1, team2 in @@ -35,14 +35,14 @@ public class TeamListViewModel: NSObject, NIMTeamManagerDelegate { // MARK: NIMTeamManagerDelegate public func onTeamAdded(_ team: NIMTeam) { - teamList.insert(Team(teamInfo: team), at: 0) + teamList.insert(NETeam(teamInfo: team), at: 0) refresh() } public func onTeamUpdated(_ team: NIMTeam) { for (i, t) in teamList.enumerated() { if t.teamId == team.teamId { - teamList[i] = Team(teamInfo: team) + teamList[i] = NETeam(teamInfo: team) refresh() break } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift index a0c17c8f..02b3820c 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift @@ -97,7 +97,7 @@ open class NEBaseValidationMessageViewController: NEBaseContactViewController { } } - public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { viewModel.clearNotiUnreadCount() return true } @@ -120,8 +120,8 @@ extension NEBaseValidationMessageViewController: UITableViewDelegate, UITableVie } extension NEBaseValidationMessageViewController: SystemNotificationCellDelegate { - public func changeValidationStatus(notifiModel: XNotification, notiStatus: IMHandleStatus) { - var notifiModels = [XNotification]() + open func changeValidationStatus(notifiModel: NENotification, notiStatus: NEHandleStatus) { + var notifiModels = [NENotification]() if let msgList = notifiModel.msgList, msgList.count > 0 { for msg in msgList { @@ -141,7 +141,7 @@ extension NEBaseValidationMessageViewController: SystemNotificationCellDelegate loadData() } - open func onAccept(_ notifiModel: XNotification) { + open func onAccept(_ notifiModel: NENotification) { weak var weakSelf = self guard let teamId = notifiModel.targetID, let invitorId = notifiModel.sourceID else { return @@ -157,7 +157,7 @@ extension NEBaseValidationMessageViewController: SystemNotificationCellDelegate NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "❌CALLBACK acceptInviteWithTeam failed,error = \(error!.localizedDescription)") if err.code == 807 || err.code == 809 { weakSelf?.showToast(localizable("validate_processed")) - } else if err.code == 803 { + } else if err.code == teamNotExistCode { weakSelf?.showToast(localizable("team_not_exist")) } else { weakSelf?.showToast(localizable("failed_operation")) @@ -186,7 +186,7 @@ extension NEBaseValidationMessageViewController: SystemNotificationCellDelegate } } - open func onRefuse(_ notifiModel: XNotification) { + open func onRefuse(_ notifiModel: NENotification) { weak var weakSelf = self guard let teamId = notifiModel.targetID, let invitorId = notifiModel.sourceID else { return @@ -202,7 +202,7 @@ extension NEBaseValidationMessageViewController: SystemNotificationCellDelegate NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "❌CALLBACK rejectInviteWithTeam failed,error = \(error!.localizedDescription)") if err.code == 807 || err.code == 809 { weakSelf?.showToast(localizable("validate_processed")) - } else if err.code == 803 { + } else if err.code == teamNotExistCode { weakSelf?.showToast(localizable("team_not_exist")) } else { weakSelf?.showToast(localizable("failed_operation")) diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift index a85ab45b..4c6265dd 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift @@ -8,13 +8,13 @@ import NECoreIMKit import NECoreKit @objcMembers -public class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate { +open class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate { typealias DataRefresh = () -> Void var dataRefresh: DataRefresh? private let className = "ValidationMessageViewModel" let contactRepo = ContactRepo.shared - var datas = [XNotification]() + var datas = [NENotification]() override init() { NELog.infoLog(ModuleName + " " + className, desc: #function) @@ -22,10 +22,10 @@ public class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate contactRepo.notiDelegate = self } - public func onNotificationUnreadCountChanged(_ count: Int) {} + open func onNotificationUnreadCountChanged(_ count: Int) {} // 内容待完善 -// public func onRecieveNotification(_ notification: XNotification) { +// open func onRecieveNotification(_ notification: XNotification) { // NELog.infoLog(className, desc: #function) // var isInsert = true // for notify in datas { @@ -56,7 +56,7 @@ public class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate // } // } - func isExist(xNoti: inout XNotification, list: inout [XNotification]) -> Bool { + func isExist(xNoti: inout NENotification, list: inout [NENotification]) -> Bool { for loopList in list { if xNoti.isEqualTo(noti: loopList) { if loopList.msgList == nil { @@ -83,7 +83,7 @@ public class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate return false } - public func onRecieveNotification(_ notification: XNotification) { + open func onRecieveNotification(_ notification: NENotification) { NELog.infoLog(ModuleName + " " + className, desc: #function) var noti = notification if !isExist(xNoti: ¬i, list: &datas) { @@ -100,7 +100,7 @@ public class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate func getValidationMessage(_ completin: @escaping () -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function) contactRepo.getNotificationList(limit: 500) { [weak self] xNotiList in - var data = [XNotification]() + var data = [NENotification]() let dateNow = Date().timeIntervalSince1970 for xNoti in xNotiList { var noti = xNoti @@ -140,21 +140,21 @@ public class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate completion() } - public func acceptInviteWithTeam(_ teamId: String, _ invitorId: String, - _ completion: @escaping (Error?) -> Void) { + open func acceptInviteWithTeam(_ teamId: String, _ invitorId: String, + _ completion: @escaping (Error?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") contactRepo.acceptTeamInvite(teamId, invitorId, completion) } - public func rejectInviteWithTeam(_ teamId: String, _ invitorId: String, - _ completion: @escaping (Error?) -> Void) { + open func rejectInviteWithTeam(_ teamId: String, _ invitorId: String, + _ completion: @escaping (Error?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") contactRepo.rejectTeamInvite(teamId, invitorId, completion) } func agreeRequest(_ account: String, _ completion: @escaping (NSError?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", account:\(account)") - let request = AddFriendRequest() + let request = NEAddFriendRequest() request.account = account request.operationType = .verify contactRepo.addFriend(request: request, completion) @@ -163,7 +163,7 @@ public class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate func refuseRequest(_ account: String, _ completion: @escaping (NSError?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", account:\(account)") print("account : ", account) - let request = AddFriendRequest() + let request = NEAddFriendRequest() request.account = account request.operationType = .reject contactRepo.addFriend(request: request, completion) diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift index 577d3c24..09ef5d3d 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift @@ -11,7 +11,7 @@ import UIKit @objcMembers open class NEBaseSystemNotificationCell: NEBaseValidationCell { - private var notifModel: XNotification? + private var notifModel: NENotification? public weak var delegate: SystemNotificationCellDelegate? override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -58,7 +58,7 @@ open class NEBaseSystemNotificationCell: NEBaseValidationCell { ]) } - override open func confige(_ model: XNotification) { + override open func confige(_ model: NENotification) { super.confige(model) notifModel = model let hideActionButton = shouldHideActionButton() diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift index d932a19e..d8dab8b7 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift @@ -8,8 +8,8 @@ import NIMSDK import UIKit public protocol SystemNotificationCellDelegate: AnyObject { - func onAccept(_ notifiModel: XNotification) - func onRefuse(_ notifiModel: XNotification) + func onAccept(_ notifiModel: NENotification) + func onRefuse(_ notifiModel: NENotification) } enum NotificationHandleType: Int { @@ -24,17 +24,6 @@ open class NEBaseValidationCell: NEBaseContactViewCell { public var titleLabelRightMargin: NSLayoutConstraint? let line = UIView() - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupUI() @@ -83,7 +72,7 @@ open class NEBaseValidationCell: NEBaseContactViewCell { line.backgroundColor = UIColor(hexString: "#F5F8FC") } - open func confige(_ model: XNotification) { + open func confige(_ model: NENotification) { var optionLabelContent = "" var nickName = "" var teamName = "" @@ -99,9 +88,9 @@ open class NEBaseValidationCell: NEBaseContactViewCell { if model.userInfo == nil, let uid = model.sourceID { let user = NIMSDK.shared().userManager.userInfo(uid) - if let alias = user?.alias { + if let alias = user?.alias, !alias.isEmpty { nickName = alias - } else if let nick = user?.userInfo?.nickName { + } else if let nick = user?.userInfo?.nickName, !nick.isEmpty { nickName = nick } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactGroup.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactGroup.swift index bfbec8e0..0818080f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactGroup.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactGroup.swift @@ -6,4 +6,4 @@ import Foundation @objcMembers -public class ContactGroup {} +open class ContactGroup {} diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift index e13150e0..ad7bd690 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift @@ -9,52 +9,52 @@ import NECoreIMKit import NECoreKit @objcMembers -public class ContactUserViewModel: NSObject { +open class ContactUserViewModel: NSObject { let contactRepo = ContactRepo.shared private let className = "ContactUserViewModel" func addFriend(_ account: String, _ completion: @escaping (NSError?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) - let request = AddFriendRequest() + let request = NEAddFriendRequest() request.account = account request.operationType = .addRequest contactRepo.addFriend(request: request, completion) } - public func deleteFriend(account: String, _ completion: @escaping (NSError?) -> Void) { + open func deleteFriend(account: String, _ completion: @escaping (NSError?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) contactRepo.deleteFriend(account: account, completion) } - public func isFriend(account: String) -> Bool { + open func isFriend(account: String) -> Bool { NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) return contactRepo.isFriend(account: account) } - public func isBlack(account: String) -> Bool { + open func isBlack(account: String) -> Bool { NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) return contactRepo.isBlackList(account: account) } - public func removeBlackList(account: String, _ completion: @escaping (NSError?) -> Void) { + open func removeBlackList(account: String, _ completion: @escaping (NSError?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) return contactRepo.removeBlackList(account: account, completion) } - public func update(_ user: User, _ completion: @escaping (Error?) -> Void) { + open func update(_ user: NEKitUser, _ completion: @escaping (Error?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + (user.userId ?? "nil")) contactRepo.updateUser(user, completion) } - public func getUserInfo(_ uid: String, _ completion: @escaping (Error?, User?) -> Void) { + open func getUserInfo(_ uid: String, _ completion: @escaping (Error?, NEKitUser?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", uid: " + uid) contactRepo.getUserInfo(uid) { error, users in completion(error, users?.first) } } - public func fetchUserInfo(accountList: [String], - _ completion: @escaping ([User]?, NSError?) -> Void) { + open func fetchUserInfo(accountList: [String], + _ completion: @escaping ([NEKitUser]?, NSError?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", uid: \(accountList)") contactRepo.fetchUserInfo(accountList: accountList) { users, error in completion(users, error) diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift index e7be32c9..579710d7 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift @@ -9,7 +9,7 @@ import NECoreKit import UIKit @objcMembers -public class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { +open class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { typealias RefreshBlock = () -> Void public var contacts: [ContactSection] = [] public var indexs: [String]? @@ -32,7 +32,7 @@ public class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { self.contactHeaders = contactHeaders } - public func onNotificationUnreadCountChanged(_ count: Int) { + open func onNotificationUnreadCountChanged(_ count: Int) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", count: \(count)") print("onNotificationUnreadCountChanged : ", count) unreadCount = count @@ -41,14 +41,14 @@ public class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { } } - public func onRecieveNotification(_ notification: XNotification) {} + open func onRecieveNotification(_ notification: NENotification) {} func loadData(fetch: Bool = false, _ filters: Set? = nil, completion: @escaping (NSError?, Int) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function) weak var weakSelf = self getContactList(fetch, filters) { contacts, error in if let users = contacts { - NELog.infoLog("contact loadData", desc: "contact data:\(contacts)") + NELog.infoLog("contact loadData", desc: "contact data:\(users)") weakSelf?.contacts = users weakSelf?.indexs = self.getIndexs(contactSections: users) if let headSection = weakSelf?.headerSection(headerItem: weakSelf?.contactHeaders) { @@ -59,10 +59,6 @@ public class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { } } - func reLoadData(completion: @escaping (NSError?, Int) -> Void) { - loadData(fetch: true, completion: completion) - } - func getContactList(_ fetch: Bool = false, _ filters: Set? = nil, _ completion: @escaping ([ContactSection]?, NSError?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", filters.count: \(filters?.count ?? 0)") var contactList: [ContactSection] = [] @@ -73,7 +69,7 @@ public class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { } contactRepo.getFriendList(fetch, local: local) { friends, error in if var users = friends { - NELog.infoLog("contact bar getFriendList", desc: "friend count:\(friends?.count)") + NELog.infoLog("contact bar getFriendList", desc: "friend count:\(users.count)") weakSelf?.initalDict = [String: [ContactInfo]]() if let filterUsers = filters { users = users.filter { user in @@ -87,16 +83,15 @@ public class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { if users.isEmpty { completion(contactList, nil) return -// return contactList } let digitRegular = NSPredicate(format: "SELF MATCHES %@", "[0-9]") let azRegular = NSPredicate(format: "SELF MATCHES %@", "[A-Z]") var digitList = [ContactInfo]() var specialCharList = [ContactInfo]() - for contact: User in users { + for contact: NEKitUser in users { // get inital of name - var name = contact.alias != nil ? contact.alias : contact.userInfo?.nickName + var name = contact.alias?.isEmpty == false ? contact.alias : contact.userInfo?.nickName if name == nil { name = contact.userId } @@ -158,9 +153,9 @@ public class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { } var infos: [ContactInfo] = [] for item in header { - let user = User() + let user = NEKitUser() user.alias = item.name - let userInfo = UserInfo(nickName: "", avatar: item.imageName) + let userInfo = NEKitUserInfo(nickName: "", avatar: item.imageName) user.userInfo = userInfo let info = ContactInfo() diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift index 3f1f5eb4..3bc5bcf8 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift @@ -8,11 +8,11 @@ import NECoreIMKit import NECoreKit @objcMembers -public class FindFriendViewModel: NSObject { +open class FindFriendViewModel: NSObject { let contactRepo = ContactRepo.shared private let className = "FindFriendViewModel" - func searchFriend(_ text: String, _ completion: @escaping ([User]?, NSError?) -> Void) { + func searchFriend(_ text: String, _ completion: @escaping ([NEKitUser]?, NSError?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", text: \(text.count)") contactRepo.fetchUserInfo(accountList: [text], completion) } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift index cb8e2180..c61d8f08 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift @@ -10,17 +10,6 @@ open class NEBaseContactSelectedCell: NEBaseContactTableViewCell { var sModel: ContactInfo? - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } - override open func commonUI() { super.commonUI() leftConstraint?.constant = 50 @@ -30,7 +19,7 @@ open class NEBaseContactSelectedCell: NEBaseContactTableViewCell { sImage.accessibilityIdentifier = "id.selector" } - override public func setModel(_ model: ContactInfo) { + override open func setModel(_ model: ContactInfo) { super.setModel(model) if model.isSelected == false { sImage.isHighlighted = false diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift index 3fbc0b60..7830549b 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import Foundation +import NEChatKit import NECoreIMKit import NECoreKit import UIKit @@ -63,8 +64,9 @@ open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataPro contentView.addSubview(redAngleView) NSLayoutConstraint.activate([ - redAngleView.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), + redAngleView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), redAngleView.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -10), + redAngleView.heightAnchor.constraint(equalToConstant: 18), ]) } @@ -85,11 +87,15 @@ open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataPro } open func setModel(_ model: ContactInfo) { - guard let user = model.user else { + guard var user = model.user else { return } setConfig() + if let userId = user.userId, let u = ChatUserCache.getUserInfo(userId) { + user = u + } + if model.contactCellType == 1 { NELog.infoLog("contact other cell configData", desc: "\(user.alias), image name:\(user.userInfo?.avatarUrl)") nameLabel.text = "" diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift index a3cb1c48..bb6bd6e7 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift @@ -3,16 +3,17 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECoreIMKit import NECoreKit import UIKit @objcMembers open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UITextFieldDelegate { - typealias ModifyBlock = (_ user: User) -> Void + typealias ModifyBlock = (_ user: NEKitUser) -> Void var completion: ModifyBlock? - var user: User? + var user: NEKitUser? let viewmodel = ContactUserViewModel() let textLimit = 15 lazy var aliasInput: UITextField = { @@ -42,7 +43,7 @@ open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UI // return btn // }() - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() setupUI() } @@ -58,7 +59,7 @@ open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UI view.addSubview(aliasInput) aliasInput.placeholder = localizable("input_noteName") - if let alias = user?.alias { + if let alias = user?.alias, !alias.isEmpty { aliasInput.text = alias } } @@ -82,7 +83,11 @@ open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UI return } - user?.alias = aliasInput.text + if user?.alias != aliasInput.text { + user?.alias = aliasInput.text + NotificationCenter.default.post(name: NENotificationName.updateFriendInfo, object: user) + } + if let u = user { view.makeToastActivity(.center) viewmodel.update(u) { error in @@ -117,7 +122,7 @@ open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UI } */ - public func textFieldChange() { + open func textFieldChange() { guard let _ = aliasInput.markedTextRange else { if let text = aliasInput.text, text.count > textLimit { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift index 01ba9f47..3c3afa2d 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.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 NECoreIMKit import NECoreKit import NIMSDK @@ -11,7 +12,7 @@ import UIKit @objcMembers open class NEBaseContactUserViewController: NEBaseContactViewController, UITableViewDelegate, UITableViewDataSource { - var user: User? + var user: NEKitUser? var uid: String? public var isBlack: Bool = false var className = "ContactUserViewController" @@ -21,7 +22,7 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable var data = [[UserItem]]() public var headerView = NEBaseUserInfoHeaderView() - public init(user: User?) { + public init(user: NEKitUser?) { super.init(nibName: nil, bundle: nil) self.user = user uid = user?.userId @@ -56,7 +57,7 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable weakSelf?.showToast(err.localizedDescription) } else if let u = users?.first { weakSelf?.user = u - NotificationCenter.default.post(name: NotificationName.updateFriendInfo, object: u) + ChatUserCache.updateUserInfo(u) weakSelf?.loadData() } } @@ -233,24 +234,24 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable return cell } - public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { if section == 0 { return 0 } return 6.0 } - public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { let header = UIView() header.backgroundColor = UIColor.clear return header } - public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { 0 } - public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + open func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { nil } @@ -291,7 +292,7 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable remark.completion = { [weak self] u in self?.user = u self?.headerView.setData(user: u) - NotificationCenter.default.post(name: NotificationName.updateFriendInfo, object: u) + ChatUserCache.updateUserInfo(u) } navigationController?.pushViewController(remark, animated: true) @@ -339,25 +340,20 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable } } - func chat(user: User?) { + func chat(user: NEKitUser?) { guard let accid = self.user?.userId else { return } + let session = NIMSession(accid, type: .P2P) Router.shared.use( PushP2pChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session], + parameters: ["nav": navigationController as Any, "session": session, "removeUserVC": true], closure: nil ) - - // 移除当前页面 - if let selfVCIndex = navigationController?.viewControllers.firstIndex(of: self), - selfVCIndex > 0 { - navigationController?.viewControllers.remove(at: selfVCIndex) - } } - func deleteFriendAction(user: User?) { + func deleteFriendAction(user: NEKitUser?) { weak var weakSelf = self if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { weakSelf?.showToast(commonLocalizable("network_error")) @@ -372,13 +368,14 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable if error != nil { self.showToast(error?.localizedDescription ?? "") } else { + ChatUserCache.removeUserInfo(userId) self.navigationController?.popViewController(animated: true) } } } } - open func deleteFriend(user: User?) { + open func deleteFriend(user: NEKitUser?) { let alertTitle = String(format: localizable("delete_title"), user?.showName(true) ?? "") let alertController = UIAlertController( title: alertTitle, diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsSelectedViewController.swift index 41b99150..c29e1b79 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsSelectedViewController.swift @@ -67,14 +67,13 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI var tableViewTopAnchor: NSLayoutConstraint? - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - weak var weakSelf = self - viewModel.loadData(filterUsers) { error, userSectionCount in - weakSelf?.emptyView.isHidden = userSectionCount > 0 - weakSelf?.tableView.reloadData() - weakSelf?.emptyView.isHidden = (weakSelf?.viewModel.contacts.count ?? 0) > 0 - } + init(filterUsers: Set? = nil) { + super.init(nibName: nil, bundle: nil) + self.filterUsers = filterUsers + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } override open func viewDidLoad() { @@ -84,6 +83,12 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI emptyView.settingContent(content: localizable("no_friend")) setupUI() setupNavRightItem() + + weak var weakSelf = self + viewModel.loadData(filterUsers) { error, userSectionCount in + weakSelf?.emptyView.isHidden = userSectionCount > 0 + weakSelf?.tableView.reloadData() + } } open func setupUI() { @@ -176,7 +181,10 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI if let completion = callBack { completion(selectArray) + navigationController?.popViewController(animated: true) + return } + var accids = [String]() var names = [String]() diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsViewController.swift index 25c53333..4f418728 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsViewController.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 NECoreIMKit import NECoreKit import UIKit @@ -13,7 +14,7 @@ public protocol NEBaseContactsViewControllerDelegate { @objcMembers open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, - SystemMessageProviderDelegate, FriendProviderDelegate, TabNavigationViewDelegate { + SystemMessageProviderDelegate, FriendProviderDelegate, TabNavigationViewDelegate, UIGestureRecognizerDelegate { public var delegate: NEBaseContactsViewControllerDelegate? // custom ui cell @@ -51,12 +52,19 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, } override open func viewWillAppear(_ animated: Bool) { - // 刷新数据 - viewModel.reLoadData { [weak self] error, userSectionCount in - self?.emptyView.isHidden = userSectionCount > 0 - if error == nil { - self?.delegate?.onDataLoaded() - self?.tableView.reloadData() + super.viewWillAppear(animated) + + // 通讯录异步进行远端加载 + if IMKitConfigCenter.shared.contactAsyncLoadEnable { + DispatchQueue.main.async { + self.loadData(fetch: true) + } + } + if navigationController?.viewControllers.count ?? 0 > 0 { + if let root = navigationController?.viewControllers[0] as? UIViewController { + if root.isKind(of: NEBaseContactsViewController.self) { + navigationController?.interactivePopGestureRecognizer?.delegate = self + } } } } @@ -66,8 +74,12 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, showTitleBar() commonUI() viewModel.refresh = { [weak self] in - self?.tableView.reloadData() + self?.didRefreshTable() } + + loadData(fetch: true) + + NotificationCenter.default.addObserver(self, selector: #selector(didRefreshTable), name: NENotificationName.updateFriendInfo, object: nil) } deinit { @@ -145,6 +157,16 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, } } + open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let navigationController = navigationController, + navigationController.responds(to: #selector(getter: UINavigationController.interactivePopGestureRecognizer)), + gestureRecognizer == navigationController.interactivePopGestureRecognizer, + navigationController.visibleViewController == navigationController.viewControllers.first { + return false + } + return true + } + // MARK: lazy load public lazy var navigationView: TabNavigationView = { @@ -236,12 +258,12 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, return view }() - open func loadData() { - viewModel.loadData { [weak self] error, userSectionCount in + open func loadData(fetch: Bool = false) { + viewModel.loadData(fetch: fetch) { [weak self] error, userSectionCount in self?.emptyView.isHidden = userSectionCount > 0 if error == nil { self?.delegate?.onDataLoaded() - self?.tableView.reloadData() + self?.didRefreshTable() } } } @@ -360,9 +382,13 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, } } + func didRefreshTable() { + tableView.reloadData() + } + // MARK: SystemMessageProviderDelegate - open func onRecieveNotification(notification: XNotification) { + open func onRecieveNotification(notification: NENotification) { print("onRecieveNotification type:\(notification.type)") if notification.type == .addFriendDirectly { loadData() @@ -372,12 +398,12 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, open func onNotificationUnreadCountChanged(count: Int) { print("unread count:\(count)") viewModel.unreadCount = count - tableView.reloadData() + didRefreshTable() } // MARK: FriendProviderDelegate - open func onFriendChanged(user: User) { + open func onFriendChanged(user: NEKitUser) { print("onFriendChanged:\(user.userId)") loadData() } @@ -387,7 +413,7 @@ open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, loadData() } - open func onUserInfoChanged(user: User) { + open func onUserInfoChanged(user: NEKitUser) { print("onUserInfoChanged:\(user.userId)") loadData() } @@ -404,12 +430,12 @@ extension NEBaseContactsViewController { NEBaseFindFriendViewController() } - @objc public func goToFindFriend() { + @objc open func goToFindFriend() { let findFriendController = getFindFriendViewController() navigationController?.pushViewController(findFriendController, animated: true) } - @objc public func searchContact() { + @objc open func searchContact() { Router.shared.use( SearchContactPageRouter, parameters: ["nav": navigationController as Any], diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift index 51005b01..9ce375e1 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift @@ -85,7 +85,7 @@ open class NEBaseFindFriendViewController: NEBaseContactViewController, UITextFi ]) } - public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + open func textFieldShouldReturn(_ textField: UITextField) -> Bool { guard let text = textField.text else { return false } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift index 8f84d9ee..6878dfae 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift @@ -146,7 +146,7 @@ open class NEBaseUserInfoHeaderView: UIView { updateConstraintsIfNeeded() } - open func setData(user: User?) { + open func setData(user: NEKitUser?) { guard let user = user else { return } diff --git a/NEConversationUIKit/NEConversationUIKit.podspec b/NEConversationUIKit/NEConversationUIKit.podspec index 972776ca..ee0ea46a 100644 --- a/NEConversationUIKit/NEConversationUIKit.podspec +++ b/NEConversationUIKit/NEConversationUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEConversationUIKit' - s.version = '9.6.5' + s.version = '9.7.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings b/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings index 15eea0dc..0fbc65a4 100644 --- a/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings +++ b/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings @@ -31,6 +31,8 @@ "file"="[File Message]"; "internet_phone"="[Audio_Chat]"; "video_chat"="[Video_Chat]"; +"custom"="[Custom Message]"; +"chat_history"="[Chat History]"; "unknown"="[Unknown Message]"; "appName"="CommsEase IM"; @@ -40,3 +42,8 @@ // error toast "network_error"="Network is not available, please check"; + + +"tip"="[tip message]"; +"leave_team"="离开群聊"; +"leave_team_desc"="您已被移除群聊或群聊已解散"; diff --git a/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings index 26c79f20..6369c851 100644 --- a/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -29,8 +29,10 @@ "location"="[地理位置]"; "notification"="[通知消息]"; "file"="[文件消息]"; -"internet_phone"="[音频通话]"; +"internet_phone"="[语音通话]"; "video_chat"="[视频通话]"; +"custom"="[自定义消息]"; +"chat_history"="[聊天记录]"; "unknown"="[未知消息体]"; "appName"="云信IM"; @@ -41,3 +43,7 @@ // error toast "network_error"="当前网络不可用,请检查你的网络设置"; + +"tip"="[提醒消息]"; +"leave_team"="离开群聊"; +"leave_team_desc"="您已被移除群聊或群聊已解散"; diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift new file mode 100644 index 00000000..3350b588 --- /dev/null +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift @@ -0,0 +1,50 @@ +//// 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 +@objcMembers +public class ConversationDeduplicationHelper: NSObject, NIMLoginManagerDelegate { + // 单例变量 + static let instance = ConversationDeduplicationHelper() + // 最多缓存数量,可外部修改 + public var limit = 100 + // 撤回消息记录 + public var revokeMessageIds = Set() + + override private init() { + super.init() + NIMSDK.shared().loginManager.add(self) + } + + deinit { + NIMSDK.shared().loginManager.remove(self) + } + + public func onLogin(_ step: NIMLoginStep) { + if step == .logout { + clearCache() + } + } + + public func onKickout(_ result: NIMLoginKickoutResult) { + clearCache() + } + + public func clearCache() { + revokeMessageIds.removeAll() + } + + // 是否已经保存过此撤回消息,防止重复保存本地撤回记录 + public func isRevokeMessageSaved(messageId: String) -> Bool { + if revokeMessageIds.contains(messageId) { + return true + } + if revokeMessageIds.count > limit { + revokeMessageIds.removeAll() + } + revokeMessageIds.insert(messageId) + return false + } +} diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift index b545372d..0cd25e6b 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift @@ -63,9 +63,14 @@ open class NEBaseConversationListCell: UITableViewCell { open func configData(sessionModel: ConversationListModel?) { guard let conversationModel = sessionModel else { return } + if let userId = conversationModel.userInfo?.userId, + let user = ChatUserCache.getUserInfo(userId) { + conversationModel.userInfo = user + } + if conversationModel.recentSession?.session?.sessionType == .P2P { // p2p head image - if let imageName = conversationModel.userInfo?.userInfo?.avatarUrl { + if let imageName = conversationModel.userInfo?.userInfo?.avatarUrl, !imageName.isEmpty { headImge.setTitle("") headImge.sd_setImage(with: URL(string: imageName), completed: nil) headImge.backgroundColor = .clear @@ -86,7 +91,7 @@ open class NEBaseConversationListCell: UITableViewCell { } else if conversationModel.recentSession?.session?.sessionType == .team { // team head image - if let imageName = conversationModel.teamInfo?.avatarUrl { + if let imageName = conversationModel.teamInfo?.avatarUrl, !imageName.isEmpty { headImge.setTitle("") headImge.sd_setImage(with: URL(string: imageName), completed: nil) headImge.backgroundColor = .clear @@ -119,6 +124,8 @@ open class NEBaseConversationListCell: UITableViewCell { } } subTitle.attributedText = mutaAttri // contentForRecentSession(message: lastMessage) + } else { + subTitle.attributedText = nil } // unRead message count @@ -153,11 +160,15 @@ open class NEBaseConversationListCell: UITableViewCell { if let lastMessage = recentSession.lastMessage { return lastMessage.timestamp } - // 服务端时间戳以毫秒为单位,需要转化 - return recentSession.updateTime / 1000 + + return 0 } func dealTime(time: TimeInterval) -> String { + if time <= 0 { + return "" + } + let targetDate = Date(timeIntervalSince1970: time) let fmt = DateFormatter() diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift index 97e115e9..10be5e15 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift @@ -22,7 +22,7 @@ open class NEBaseConversationSearchCell: TextBaseCell { titleLabel.text = userInfo.showName() subTitleLabel.text = userInfo.userId - if let imageName = userInfo.userInfo?.avatarUrl { + if let imageName = userInfo.userInfo?.avatarUrl, !imageName.isEmpty { headImge.setTitle("") headImge.sd_setImage(with: URL(string: imageName), completed: nil) headImge.backgroundColor = .clear @@ -35,7 +35,7 @@ open class NEBaseConversationSearchCell: TextBaseCell { if let teamInfo = searchModel?.teamInfo { titleLabel.text = teamInfo.getShowName() subTitleLabel.text = nil - if let imageName = teamInfo.avatarUrl { + if let imageName = teamInfo.avatarUrl, !imageName.isEmpty { headImge.setTitle("") headImge.sd_setImage(with: URL(string: imageName), completed: nil) headImge.backgroundColor = .clear @@ -51,10 +51,10 @@ open class NEBaseConversationSearchCell: TextBaseCell { public var searchText: String = "" { didSet { - if let titleText = titleLabel.text, - let range = titleText.findAllIndex(searchText).first { + if let titleText = titleLabel.text { let attributedStr = NSMutableAttributedString(string: titleText) - // range必须要加,参数分别表示从索引几开始取几个字符 + // range 表示从索引几开始取几个字符 + let range = attributedStr.mutableString.range(of: searchText) attributedStr.addAttribute( .foregroundColor, value: getRangeTextColor(), @@ -65,10 +65,11 @@ open class NEBaseConversationSearchCell: TextBaseCell { titleLabelTopAnchor?.isActive = false subTitleLabel.isHidden = true } - if let subTitleText = subTitleLabel.text, - let range = subTitleText.findAllIndex(searchText).first { + + if let subTitleText = subTitleLabel.text { let attributedStr = NSMutableAttributedString(string: subTitleText) - // range必须要加,参数分别表示从索引几开始取几个字符 + // range 表示从索引几开始取几个字符 + let range = attributedStr.mutableString.range(of: searchText) attributedStr.addAttribute( .foregroundColor, value: getRangeTextColor(), diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift index f8a5b7f9..708b4de6 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift @@ -14,7 +14,7 @@ public protocol NEBaseConversationControllerDelegate { } @objcMembers -open class NEBaseConversationController: UIViewController, NIMChatManagerDelegate { +open class NEBaseConversationController: UIViewController, NIMChatManagerDelegate, UIGestureRecognizerDelegate { var className = "NEBaseConversationController" public var deleteBottonBackgroundColor: UIColor = NEConstant.hexRGB(0xA8ABB6) @@ -23,7 +23,7 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat private var bodyBottomViewHeightAnchor: NSLayoutConstraint? public var contentViewTopAnchor: NSLayoutConstraint? public var topConstant: CGFloat = 0 - public var popListController = NEBasePopListViewController() + public var popListView = NEBasePopListView() public var delegate: NEBaseConversationControllerDelegate? @@ -76,6 +76,14 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat self?.contentViewTopAnchor?.constant = 0 } } + + if navigationController?.viewControllers.count ?? 0 > 0 { + if let root = navigationController?.viewControllers[0] as? UIViewController { + if root.isKind(of: NEBaseConversationController.self) { + navigationController?.interactivePopGestureRecognizer?.delegate = self + } + } + } } override open func viewDidLoad() { @@ -85,16 +93,27 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat requestData() initialConfig() NIMSDK.shared().chatManager.add(self) + NotificationCenter.default.addObserver(self, selector: #selector(didRefreshTable), name: NENotificationName.updateFriendInfo, object: nil) } override open func viewWillDisappear(_ animated: Bool) { - popListController.removeSelf() + popListView.removeSelf() } deinit { NIMSDK.shared().chatManager.remove(self) } + open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let navigationController = navigationController, + navigationController.responds(to: #selector(getter: UINavigationController.interactivePopGestureRecognizer)), + gestureRecognizer == navigationController.interactivePopGestureRecognizer, + navigationController.visibleViewController == navigationController.viewControllers.first { + return false + } + return true + } + open func showTitleBar() { if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { navigationView.isHidden = true @@ -366,8 +385,8 @@ extension NEBaseConversationController: TabNavigationViewDelegate { ) } - open func getPopListController() -> NEBasePopListViewController { - NEBasePopListViewController() + open func getPopListView() -> NEBasePopListView { + NEBasePopListView() } open func getPopListItems() -> [PopListItem] { @@ -411,10 +430,10 @@ extension NEBaseConversationController: TabNavigationViewDelegate { } if IMKitClient.instance.getConfigCenter().teamEnable { - popListController.itemDatas = getPopListItems() - popListController.view.frame = CGRect(origin: .zero, size: view.frame.size) - popListController.removeSelf() - view.addSubview(popListController.view) + popListView.itemDatas = getPopListItems() + popListView.frame = CGRect(origin: .zero, size: view.frame.size) + popListView.removeSelf() + view.addSubview(popListView) } else { Router.shared.use( ContactAddFriendRouter, @@ -485,7 +504,11 @@ extension NEBaseConversationController: TabNavigationViewDelegate { guard let msg = notification.message else { return } - saveRevokeMessage(msg) { error in + + if ConversationDeduplicationHelper.instance.isRevokeMessageSaved(messageId: msg.messageId) { + return + } + saveRevokeMessage(msg) { [weak self] error in } } @@ -580,7 +603,7 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour /* @available(iOS 11.0, *) - public func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + open func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { var rowActions = [UIContextualAction]() @@ -692,8 +715,7 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour desc: "✅CALLBACK removeStickTopSession SUCCESS" ) weakSelf?.viewModel.stickTopInfos[session] = nil - weakSelf?.viewModel.sortRecentSession() - weakSelf?.tableView.reloadData() + weakSelf?.reloadTableView() completion(nil, topSessionInfo) } } @@ -711,8 +733,7 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour NELog.infoLog(ModuleName + " " + (weakSelf?.className ?? "ConversationController"), desc: "✅CALLBACK addStickTopSession callback SUCCESS") weakSelf?.viewModel.stickTopInfos[session] = newInfo - weakSelf?.viewModel.sortRecentSession() - weakSelf?.tableView.reloadData() + weakSelf?.reloadTableView() completion(nil, newInfo) } } @@ -771,9 +792,7 @@ extension NEBaseConversationController { extension NEBaseConversationController: ConversationViewModelDelegate { open func didAddRecentSession() { NELog.infoLog("ConversationController", desc: "didAddRecentSession") - emptyView.isHidden = (viewModel.conversationListArray?.count ?? 0) > 0 - viewModel.sortRecentSession() - tableView.reloadData() + reloadTableView() } open func didUpdateRecentSession(index: Int) { @@ -785,9 +804,13 @@ extension NEBaseConversationController: ConversationViewModelDelegate { delegate?.onDataLoaded() } + open func didRefreshTable() { + tableView.reloadData() + } + open func reloadTableView() { emptyView.isHidden = (viewModel.conversationListArray?.count ?? 0) > 0 viewModel.sortRecentSession() - tableView.reloadData() + didRefreshTable() } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationNavigationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationNavigationController.swift index b58564e4..b642412c 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationNavigationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationNavigationController.swift @@ -10,7 +10,7 @@ open class NEBaseConversationNavigationController: UIViewController, UIGestureRe var topConstant: CGFloat = 0 public let navigationView = NENavigationView() - override open var title: String? { + override public var title: String? { get { super.title } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift index b0a596e4..66b23264 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift @@ -172,6 +172,7 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + weak var weakSelf = self if indexPath.section == 0 { let searchModel = viewModel.searchResult?.friend[indexPath.row] if let userId = searchModel?.userInfo?.userId { @@ -186,22 +187,44 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr } else if indexPath.section == 1 { let searchModel = viewModel.searchResult?.contactGroup[indexPath.row] if let teamId = searchModel?.teamInfo?.teamId { - let session = NIMSession(teamId, type: .team) - Router.shared.use( - PushTeamChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any], - closure: nil - ) + TeamRepo.shared.fetchTeamInfo(teamId) { error, teamInfo in + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else { + weakSelf?.showSingleAlert(title: localizable("leave_team"), message: localizable("leave_team_desc")) {} + } + } else { + let session = NIMSession(teamId, type: .team) + Router.shared.use( + PushTeamChatVCRouter, + parameters: ["nav": weakSelf?.navigationController as Any, + "session": session as Any], + closure: nil + ) + } + } } } else { let searchModel = viewModel.searchResult?.seniorGroup[indexPath.row] if let teamId = searchModel?.teamInfo?.teamId { - let session = NIMSession(teamId, type: .team) - Router.shared.use( - PushTeamChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any], - closure: nil - ) + TeamRepo.shared.fetchTeamInfo(teamId) { error, teamInfo in + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else { + weakSelf?.showSingleAlert(title: localizable("leave_team"), message: localizable("leave_team_desc")) {} + } + } else { + let session = NIMSession(teamId, type: .team) + Router.shared.use( + PushTeamChatVCRouter, + parameters: ["nav": weakSelf?.navigationController as Any, + "session": session as Any], + closure: nil + ) + } + } } } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListViewController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListView.swift similarity index 87% rename from NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListViewController.swift rename to NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListView.swift index 09627be4..a956c346 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListViewController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListView.swift @@ -19,7 +19,7 @@ open class PopListItem: NSObject { } @objcMembers -open class NEBasePopListViewController: UIViewController { +open class NEBasePopListView: UIView { public let shadowView = UIView() public var buttonHeight: CGFloat = 32.0 let popView = UIView() @@ -31,27 +31,25 @@ open class NEBasePopListViewController: UIViewController { public var itemDatas = [PopListItem]() { didSet { popViewHeight = CGFloat(itemDatas.count) * 32 + 16 + setupUI() } } - override public func viewDidLoad() { - super.viewDidLoad() + override init(frame: CGRect) { + super.init(frame: frame) if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { - navigationController?.isNavigationBarHidden = false topConstant = 10 } else { topConstant = NEConstant.navigationAndStatusHeight } - setupUI() } - override public func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) - removeSelf() + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") } func setupUI() { - view.backgroundColor = .clear + backgroundColor = .clear shadowView.translatesAutoresizingMaskIntoConstraints = false shadowView.backgroundColor = .clear @@ -60,7 +58,7 @@ open class NEBasePopListViewController: UIViewController { shadowView.layer.shadowColor = UIColor.ne_operationBorderColor.cgColor shadowView.layer.shadowOpacity = 0.25 shadowView.layer.shadowRadius = 7 - view.addSubview(shadowView) + addSubview(shadowView) NSLayoutConstraint.activate([ shadowView.widthAnchor.constraint(equalToConstant: popViewWidth), @@ -79,6 +77,7 @@ open class NEBasePopListViewController: UIViewController { popView.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor), ]) + popView.subviews.forEach { $0.removeFromSuperview() } let offset: CGFloat = 8 for index in 0 ..< itemDatas.count { let item = itemDatas[index] @@ -119,11 +118,11 @@ open class NEBasePopListViewController: UIViewController { removeSelf() } - public func removeSelf() { - view.removeFromSuperview() + open func removeSelf() { + removeFromSuperview() } - override public func touchesBegan(_ touches: Set, with event: UIEvent?) { + override open func touchesBegan(_ touches: Set, with event: UIEvent?) { print("pop list view touchesBegan") removeSelf() } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift index 9e924ebd..15469be7 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift @@ -6,7 +6,7 @@ import NIMSDK import UIKit @objcMembers -public class ConversationSearchViewModel: NSObject { +open class ConversationSearchViewModel: NSObject { let repo = ConversationRepo.shared public var searchResult: ( friend: [ConversationSearchListModel], @@ -23,12 +23,12 @@ public class ConversationSearchViewModel: NSObject { /// - Parameters: /// - searchStr: 搜索的内容 /// - completion: 回调结果 - public func doSearch(searchStr: String, - _ completion: @escaping (NSError?, ( - friend: [ConversationSearchListModel], - contactGroup: [ConversationSearchListModel], - seniorGroup: [ConversationSearchListModel] - )?) -> Void) { + open func doSearch(searchStr: String, + _ completion: @escaping (NSError?, ( + friend: [ConversationSearchListModel], + contactGroup: [ConversationSearchListModel], + seniorGroup: [ConversationSearchListModel] + )?) -> Void) { NELog.infoLog( ModuleName + " " + className, desc: #function + ", searchStr.count:\(searchStr.count)" diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift index 901e7929..9225ca5a 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift @@ -19,7 +19,7 @@ public protocol ConversationViewModelDelegate: NSObjectProtocol { } @objcMembers -public class ConversationViewModel: NSObject, ConversationRepoDelegate, +open class ConversationViewModel: NSObject, ConversationRepoDelegate, NIMConversationManagerDelegate, NIMTeamManagerDelegate, NIMUserManagerDelegate, NIMChatManagerDelegate { public var conversationListArray: [ConversationListModel]? public var stickTopInfos = [NIMSession: NIMStickTopSessionInfo]() @@ -35,21 +35,30 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, super.init() repo.delegate = self repo.addSessionDelegate(delegate: self) - repo.chatProvider.addDelegate(delegate: self) + repo.addChatDelegate(delegate: self) repo.addTeamDelegate(delegate: self) stickTopInfos = repo.getStickTopInfos() NIMSDK.shared().userManager.add(self) NotificationCenter.default.addObserver(self, selector: #selector(atMessageChange), name: Notification.Name(AtMessageChangeNoti), object: nil) } + deinit { + NELog.infoLog(ModuleName + className(), desc: #function) + repo.removeSessionDelegate(delegate: self) + repo.removeChatDelegate(delegate: self) + repo.removeTeamDelegate(delegate: self) + NIMSDK.shared().userManager.remove(self) + NotificationCenter.default.removeObserver(self) + } + func atMessageChange() { NELog.infoLog(className(), desc: "atMessageChange") delegate?.reloadTableView() } - public func fetchServerSessions(option: NIMFetchServerSessionOption, - _ completion: @escaping (NSError?, [ConversationListModel]?) - -> Void) { + open func fetchServerSessions(option: NIMFetchServerSessionOption, + _ completion: @escaping (NSError?, [ConversationListModel]?) + -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function) weak var weakSelf = self repo.getSessionList { error, conversaitonList in @@ -95,11 +104,21 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, NELog.infoLog(ModuleName, desc: "conversationListArray count : \(weakSelf?.conversationListArray?.count ?? 0)") completion(error, weakSelf?.conversationListArray) + + // 拉取好友信息 + ChatUserCache.removeAllUserInfo() + ContactRepo.shared.getFriendList(true, local: false) { friends, error in + if let friends = friends { + friends.forEach { user in + ChatUserCache.updateUserInfo(user) + } + } + } } } } - public func deleteRecentSession(recentSession: NIMRecentSession) { + open func deleteRecentSession(recentSession: NIMRecentSession) { NELog.infoLog( ModuleName + " " + className, desc: #function + ", sessionId:" + (recentSession.session?.sessionId ?? "nil") @@ -113,14 +132,14 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, } } - public func stickTopInfoForSession(session: NIMSession) -> NIMStickTopSessionInfo? { + open func stickTopInfoForSession(session: NIMSession) -> NIMStickTopSessionInfo? { NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + session.sessionId) return repo.getStickTopSessionInfo(session: session) } - public func addStickTopSession(session: NIMSession, - _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) - -> Void) { + open func addStickTopSession(session: NIMSession, + _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) + -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + session.sessionId) let params = NIMAddStickTopSessionParams(session: session) repo.addStickTop(params: params) { error, stickTopSessionInfo in @@ -128,43 +147,35 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, } } - public func removeStickTopSession(params: NIMStickTopSessionInfo, - _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) - -> Void) { + open func removeStickTopSession(params: NIMStickTopSessionInfo, + _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) + -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + params.session.sessionId) repo.removeStickTop(params: params) { error, stickTopSessionInfo in completion(error as NSError?, stickTopSessionInfo) } } - public func loadStickTopSessionInfos(_ completion: + open func loadStickTopSessionInfos(_ completion: @escaping (NSError?, [NIMSession: NIMStickTopSessionInfo]?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function) repo.getStickTopSessionList(completion) } - public func notifyForNewMsg(userId: String?) -> Bool { + open func notifyForNewMsg(userId: String?) -> Bool { NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:" + (userId ?? "nil")) return repo.isNeedNotify(userId: userId) } - public func notifyStateForNewMsg(teamId: String?) -> NIMTeamNotifyState { + open func notifyStateForNewMsg(teamId: String?) -> NIMTeamNotifyState { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (teamId ?? "nil")) return repo.isNeedNotifyForTeam(teamId: teamId) } - deinit { - NELog.infoLog(ModuleName + " " + className, desc: #function) - NIMSDK.shared().userManager.remove(self) - repo.removeSessionDelegate(delegate: self) - repo.removeTeamDelegate(delegate: self) - NotificationCenter.default.removeObserver(self) - } - // MARK: ======================== private method ============================== - public func sortRecentSession() { + open func sortRecentSession() { NELog.infoLog(ModuleName + " " + className, desc: #function) var tempArr = [NIMRecentSession]() var dic = [String: ConversationListModel]() @@ -229,7 +240,7 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, // MARK: ==================== NIMChatManagerDelegate ========================== - public func onRecvMessageReceipts(_ receipts: [NIMMessageReceipt]) { + open func onRecvMessageReceipts(_ receipts: [NIMMessageReceipt]) { receipts.forEach { receipt in if receipt.session?.sessionType == .P2P { if let listArr = conversationListArray { @@ -246,20 +257,27 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, // MARK: ==================== ConversationRepoDelegate ========================== - public func onNotifyAddStickTopSession(_ newInfo: NIMStickTopSessionInfo) { + open func onNotifyAddStickTopSession(_ newInfo: NIMStickTopSessionInfo) { NELog.infoLog(ModuleName + " " + className, desc: #function + ",onNotifyAddStickTopSession sessionId:" + newInfo.session.sessionId) stickTopInfos[newInfo.session] = newInfo + if repo.getRecentSession(newInfo.session) == nil { + repo.addRecentSession(newInfo.session) + } delegate?.reloadTableView() } - public func onNotifyRemoveStickTopSession(_ removedInfo: NIMStickTopSessionInfo) { + open func onNotifyRemoveStickTopSession(_ removedInfo: NIMStickTopSessionInfo) { NELog.infoLog(ModuleName + " " + className, desc: #function + ",onNotifyRemoveStickTopSession sessionId:" + removedInfo.session.sessionId) stickTopInfos[removedInfo.session] = nil delegate?.reloadTableView() } - public func onNotifySyncStickTopSessions(_ response: NIMSyncStickTopSessionResponse) { + open func onNotifySyncStickTopSessions(_ response: NIMSyncStickTopSessionResponse) { loadStickTopSessionInfos { [weak self] error, sessionInfos in + NELog.infoLog( + ModuleName + " " + (self?.className ?? "ConversationViewModel"), + desc: "CALLBACK loadStickTopSessionInfos " + (error?.localizedDescription ?? "no error") + ) if error != nil { if let infos = self?.repo.getStickTopInfos() { self?.stickTopInfos = infos @@ -272,25 +290,50 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, } } - public func didServerSessionUpdated(_ recentSession: NIMRecentSession?) {} + open func didServerSessionUpdated(_ recentSession: NIMRecentSession?) { + NELog.infoLog(ModuleName + " " + className, desc: #function + ",didServerSessionUpdated:" + (recentSession?.session?.sessionId ?? "")) + } // MARK: ====================NIMConversationManagerDelegate===================== - public func didAdd(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { + open func didAdd(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { guard let targetId = recentSession.session?.sessionId else { NELog.errorLog(ModuleName + " " + className, desc: "❌sessionId is nil") return } NELog.infoLog(ModuleName + " " + className, desc: #function + ", did add session targetId:" + targetId) - DispatchQueue.main.async {} + + // 解散、退出群聊 if let object = recentSession.lastMessage?.messageObject as? NIMNotificationObject, object.notificationType == .team { if let content = object.content as? NIMTeamNotificationContent { - if content.operationType == .dismiss || (content.operationType == .leave && content.sourceID == NIMSDK.shared().loginManager.currentAccount()) { + if content.operationType == .dismiss || + (content.operationType == .kick && + content.targetIDs?.contains(IMKitClient.instance.imAccid()) == true) || + (content.operationType == .leave && + IMKitClient.instance.isMySelf(content.sourceID)) { + // 群聊被解散 + // 被踢出群聊 + // 主动退出群聊 NELog.infoLog( ModuleName + " " + className, desc: #function + "didAdd team dismiss or leave noti" + (recentSession.session?.sessionId ?? "nil") ) repo.deleteLocalSession(recentSession: recentSession) + + // 移除置顶 + if let session = recentSession.session { + if let param = stickTopInfos[session] { + removeStickTopSession(params: param) { error, _ in + if let err = error { + NELog.errorLog( + ModuleName + " ConversationViewModel", + desc: "CALLBACK removeStickTopSession failed,error = \(err)" + ) + } + } + } + stickTopInfos.removeValue(forKey: session) + } return } } @@ -310,6 +353,7 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, cacheAddSessionDic[sid] = listModel } listModel.recentSession = recentSession + if recentSession.session?.sessionType == .P2P { repo.fetchUserInfo(accountList: [targetId]) { users, error in if error == nil { @@ -324,21 +368,28 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, } } else if recentSession.session?.sessionType == .team { + if !repo.isMyTeam(teamId: targetId) { + // 自己不在群内,不新增该群聊会话 + return + } + repo.getTeamInfo(teamId: targetId) { error, teamInfo in listModel.teamInfo = teamInfo if let model = weakSelf?.sessionIsExist(listModel) { model.teamInfo = teamInfo weakSelf?.delegate?.didAddRecentSession() } else { - // 会话列表新增一项 - weakSelf?.conversationListArray?.append(listModel) - weakSelf?.delegate?.didAddRecentSession() + if listModel.teamInfo != nil { + // 会话列表新增一项 + weakSelf?.conversationListArray?.append(listModel) + weakSelf?.delegate?.didAddRecentSession() + } } } } } - public func didUpdate(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { + open func didUpdate(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { guard let targetId = recentSession.session?.sessionId else { NELog.errorLog(ModuleName + " " + className, desc: "❌sessionId is nil") return @@ -357,12 +408,34 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, if let object = recentSession.lastMessage?.messageObject as? NIMNotificationObject, object.notificationType == .team { if let content = object.content as? NIMTeamNotificationContent { - if content.operationType == .dismiss || (content.operationType == .leave && content.sourceID == NIMSDK.shared().loginManager.currentAccount()) { + if content.operationType == .dismiss || + (content.operationType == .kick && + content.targetIDs?.contains(IMKitClient.instance.imAccid()) == true) || + (content.operationType == .leave && + IMKitClient.instance.isMySelf(content.sourceID)) { + // 群聊被解散 + // 被踢出群聊 + // 主动退出群聊 NELog.infoLog( ModuleName + " " + className, desc: #function + "didUpdate team dismiss or leave noti: \(targetId)" ) repo.deleteLocalSession(recentSession: recentSession) + + // 移除置顶 + if let session = recentSession.session { + if let param = stickTopInfos[session] { + removeStickTopSession(params: param) { error, _ in + if let err = error { + NELog.errorLog( + ModuleName + " ConversationViewModel", + desc: "CALLBACK removeStickTopSession failed,error = \(err)" + ) + } + } + } + stickTopInfos.removeValue(forKey: session) + } return } } @@ -397,13 +470,16 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, } } weakSelf?.delegate?.reloadTableView() - break + return } } + + // 会话列表中没有该回话则添加 + didAdd(recentSession, totalUnreadCount: totalUnreadCount) } } - public func didRemove(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { + open func didRemove(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { guard let targetId = recentSession.session?.sessionId else { NELog.errorLog(ModuleName + " " + className, desc: "❌sessionId is nil") return @@ -439,15 +515,38 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, delegate?.reloadTableView() } + // 收到多端登录单向删除通知,手动更新会话最后一条消息 + open func onRecvMessagesDeleted(_ messages: [NIMMessage], exts: [String: String]?) { + for message in messages { + guard let session = message.session else { return } + + for listModel in conversationListArray ?? [] { + if session.sessionId == listModel.recentSession?.session?.sessionId { + NELog.infoLog(ModuleName + " " + className, desc: #function + "onRecvMessagesDeleted sessionid: \(session.sessionId) message text: \(message.text ?? "")") + + // 手动查询最后一条消息 + let param = NIMGetMessagesDynamicallyParam() + param.session = session + param.limit = 1 + ChatRepo.shared.getMessagesDynamically(param) { [weak self] _, _, messages in + listModel.recentSession?.lastMessage = messages?.first + self?.delegate?.reloadTableView() + } + break + } + } + } + } + // MARK: ========================NIMUserManagerDelegate========================= - public func onFriendChanged(_ user: NIMUser) { + open func onFriendChanged(_ user: NIMUser) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:" + (user.userId ?? "nil")) if let listArr = conversationListArray { for (i, listModel) in listArr.enumerated() { if listModel.recentSession?.session?.sessionType == .P2P { if listModel.userInfo?.userId == user.userId { - listModel.userInfo = User(user: user) + listModel.userInfo = NEKitUser(user: user) delegate?.didUpdateRecentSession(index: i) break } @@ -458,7 +557,7 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, // MARK: =========================NIMTeamManagerDelegate======================== - public func onTeamUpdated(_ team: NIMTeam) { + open func onTeamUpdated(_ team: NIMTeam) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (team.teamId ?? "nil")) guard let conversationArr = conversationListArray else { return @@ -472,7 +571,7 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, } } - public func onTeamAdded(_ team: NIMTeam) { + open func onTeamAdded(_ team: NIMTeam) { NELog.infoLog(ModuleName + " " + className, desc: #function + "onTeamAdded, teamId:" + (team.teamId ?? "nil")) guard let tid = team.teamId else { return @@ -481,16 +580,15 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, delegate?.didAddRecentSession() } - public func onTeamRemoved(_ team: NIMTeam) { + open func onTeamRemoved(_ team: NIMTeam) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (team.teamId ?? "nil")) // 做删除会话操作(自己退出群聊会触发) guard let conversationArr = conversationListArray else { return } - // Fix sdk bug - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - for (_, listModel) in conversationArr.enumerated() { + DispatchQueue.main.async { + for listModel in conversationArr { if let teamInfo = listModel.teamInfo, teamInfo.teamId == team.teamId { if let recentSession = listModel.recentSession { self.deleteRecentSession(recentSession: recentSession) @@ -501,7 +599,7 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, } } - public func onTeamMemberChanged(_ team: NIMTeam) { + open func onTeamMemberChanged(_ team: NIMTeam) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (team.teamId ?? "nil")) guard let conversationArr = conversationListArray else { return @@ -527,7 +625,7 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, return nil } - public func onMuteListChanged() { + open func onMuteListChanged() { delegate?.reloadTableView() } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift index 3124592c..968b50f9 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift @@ -79,7 +79,7 @@ open class FunConversationController: NEBaseConversationController { searchView.heightAnchor.constraint(equalToConstant: 36), ]) - popListController = FunPopListViewController() + popListView = FunPopListView() tableView.rowHeight = 72 tableView.backgroundColor = .funConversationBackgroundColor diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunPopListViewController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunPopListView.swift similarity index 70% rename from NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunPopListViewController.swift rename to NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunPopListView.swift index 05daba7f..81c091bd 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunPopListViewController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunPopListView.swift @@ -8,7 +8,7 @@ import NECommonKit import UIKit @objcMembers -open class FunPopListViewController: NEBasePopListViewController { +open class FunPopListView: NEBasePopListView { public var triangleView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -20,17 +20,17 @@ open class FunPopListViewController: NEBasePopListViewController { override func setupUI() { super.setupUI() NSLayoutConstraint.activate([ - shadowView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - shadowView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -8), + shadowView.topAnchor.constraint(equalTo: topAnchor, constant: topConstant), + shadowView.rightAnchor.constraint(equalTo: rightAnchor, constant: -8), ]) popView.backgroundColor = UIColor.funConversationPopViewBg - view.insertSubview(triangleView, aboveSubview: shadowView) + insertSubview(triangleView, aboveSubview: shadowView) NSLayoutConstraint.activate([ triangleView.widthAnchor.constraint(equalToConstant: 11), triangleView.heightAnchor.constraint(equalToConstant: 11), - triangleView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -25), + triangleView.rightAnchor.constraint(equalTo: rightAnchor, constant: -25), triangleView.topAnchor.constraint(equalTo: popView.topAnchor, constant: -5), ]) } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift index 99a6d4be..a592f4dd 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift @@ -10,20 +10,20 @@ public let yxAitMsg = "yxAitMsg" public let AtMessageChangeNoti = "at_message_change_noti" @objcMembers -public class AtMessageModel: NSObject { +open class AtMessageModel: NSObject { public var messageId: String? public var messageTime: NSNumber? } @objcMembers -public class AtMEMessageRecord: NSObject { +open class AtMEMessageRecord: NSObject { public var atMessages = [String: NSNumber]() public var lastTime: NSNumber? public var isRead = false } @objcMembers -public class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManagerDelegate { +open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManagerDelegate { public static var instance: NEAtMessageManager? private let workQueue = DispatchQueue(label: "AtMessageWorkQueue") private let lock = NSLock() @@ -45,7 +45,7 @@ public class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManag NEAtMessageManager.instance = NEAtMessageManager() } - public func onLogin(_ step: NIMLoginStep) { + open func onLogin(_ step: NIMLoginStep) { if step == .loginOK { NELog.infoLog(className(), desc: "login ok") currentAccid = NIMSDK.shared().loginManager.currentAccount() @@ -66,7 +66,7 @@ public class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManag } } - public func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { + open func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { guard let msg = notification.message else { return } @@ -86,7 +86,7 @@ public class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManag lock.unlock() } - public func isAtCurrentUser(sessionId: String) -> Bool { + open func isAtCurrentUser(sessionId: String) -> Bool { let dic = getMessageDic() NELog.infoLog(className(), desc: "session id : \(sessionId)") NELog.infoLog(className(), desc: "dic : \(dic)") @@ -96,7 +96,7 @@ public class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManag return false } - public func clearAtRecord(_ sessionId: String) { + open func clearAtRecord(_ sessionId: String) { weak var weakSelf = self workQueue.async { guard let dic = weakSelf?.getMessageDic() else { @@ -111,7 +111,7 @@ public class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManag } } - public func filterAtMessage(messages: [NIMMessage]) { + open func filterAtMessage(messages: [NIMMessage]) { NELog.infoLog(className(), desc: "at manager filterAtMessage : \(messages.count)") weak var weakSelf = self workQueue.async { @@ -124,14 +124,14 @@ public class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManag } } - public func removeRevokeAtMessage(messages: [NIMMessage]) { + open func removeRevokeAtMessage(messages: [NIMMessage]) { weak var weakSelf = self workQueue.async { weakSelf?.removeRevokeAtMessageInWorkqueue(messages: messages) } } - public func startFilterRoamingMessagesTask() { + open func startFilterRoamingMessagesTask() { weak var weakSelf = self workQueue.async { weakSelf?.startFilterRoamingMessagesTaskInWorkqueue() @@ -347,7 +347,7 @@ public class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManag } } - public func onRecvMessages(_ messages: [NIMMessage]) { + open func onRecvMessages(_ messages: [NIMMessage]) { filterAtMessage(messages: messages) } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift index 7fa791db..889f9713 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift @@ -56,7 +56,7 @@ open class ConversationController: NEBaseConversationController { override open func setupSubviews() { super.setupSubviews() - popListController = PopListViewController() + popListView = PopListView() tableView.rowHeight = 62 tableView.backgroundColor = .white diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/PopListViewController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/PopListView.swift similarity index 61% rename from NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/PopListViewController.swift rename to NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/PopListView.swift index 6e3e326f..8bb0d252 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/PopListViewController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/PopListView.swift @@ -8,12 +8,12 @@ import NECommonKit import UIKit @objcMembers -open class PopListViewController: NEBasePopListViewController { +open class PopListView: NEBasePopListView { override func setupUI() { super.setupUI() NSLayoutConstraint.activate([ - shadowView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant - 10), - shadowView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + shadowView.topAnchor.constraint(equalTo: topAnchor, constant: topConstant - 10), + shadowView.rightAnchor.constraint(equalTo: rightAnchor, constant: -20), ]) popView.backgroundColor = NEConstant.hexRGB(0xFFFFFF) diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift index a8727ada..847b40ef 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift @@ -5,17 +5,19 @@ import Foundation import NIMSDK -public class NEMessageUtil { +open class NEMessageUtil { /// last message /// - Parameter message: message /// - Returns: result - public class func messageContent(message: NIMMessage) -> String { + open class func messageContent(message: NIMMessage) -> String { var text = "" switch message.messageType { case .text: if let messageText = message.text { text = messageText } + case .tip: + return localizable("tip") case .audio: text = localizable("voice") case .image: @@ -28,10 +30,8 @@ public class NEMessageUtil { text = localizable("notification") case .file: text = localizable("file") - case .tip: - if let messageText = message.text { - text = messageText - } + case .custom: + text = contentOfCustomMessage(message: message) case .rtcCallRecord: let record = message.messageObject as? NIMRtcCallRecordObject text = (record?.callType == .audio) ? localizable("internet_phone") : @@ -42,4 +42,22 @@ public class NEMessageUtil { return text } + + /// 返回自定义消息的外显文案 + static func contentOfCustomMessage(message: NIMMessage?) -> String { + if message?.messageType == .custom, + let object = message?.messageObject as? NIMCustomObject, + let custom = object.attachment as? NECustomAttachment { + if custom.customType == customMultiForwardType { + return localizable("chat_history") + } + if custom.customType == customRichTextType { + if let data = NECustomAttachment.dataOfCustomMessage(message: message), + let title = data["title"] as? String { + return title + } + } + } + return localizable("unknown") + } } diff --git a/NEMapKit/NEMapKit.podspec b/NEMapKit/NEMapKit.podspec index a6315e08..021417ed 100644 --- a/NEMapKit/NEMapKit.podspec +++ b/NEMapKit/NEMapKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEMapKit' - s.version = '9.6.5' + s.version = '9.7.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NERtcCallUIKit/NERtcCallUIKit.podspec b/NERtcCallUIKit/NERtcCallUIKit.podspec index 72278cec..3bef760f 100644 --- a/NERtcCallUIKit/NERtcCallUIKit.podspec +++ b/NERtcCallUIKit/NERtcCallUIKit.podspec @@ -17,45 +17,11 @@ Pod::Spec.new do |s| s.source = { :http => "" } s.source_files = 'NERtcCallUIKit/Classes/**/*' s.resource = 'NERtcCallUIKit/Assets/**/*' - - s.subspec 'NOS' do |nos| - nos.dependency 'NERtcCallKit/NOS','2.2.0' - nos.dependency 'SDWebImage' - nos.dependency 'NECoreKit','9.6.5' - nos.dependency 'NECommonKit' - nos.dependency 'NECommonUIKit' - nos.dependency 'NERtcSDK' - end - - s.subspec 'NOS_Special' do |nos| - nos.dependency 'NERtcCallKit/NOS_Special', "2.2.0" - nos.dependency 'SDWebImage' - nos.dependency 'NECoreKit','9.6.5' - nos.dependency 'NECommonKit' - nos.dependency 'NECommonUIKit' - nos.dependency 'NERtcSDK' - - end - - s.subspec 'FCS' do |fcs| - fcs.dependency 'NERtcCallKit/FCS','2.2.0' - fcs.dependency 'SDWebImage' - fcs.dependency 'NECoreKit','9.6.5' - fcs.dependency 'NECommonKit' - fcs.dependency 'NECommonUIKit' - fcs.dependency 'NERtcSDK' - - end - - s.subspec 'FCS_Special' do |fcs| - fcs.dependency 'NERtcCallKit/FCS_Special', "2.2.0" - fcs.dependency 'SDWebImage' - fcs.dependency 'NECoreKit','9.6.5' - fcs.dependency 'NECommonKit' - fcs.dependency 'NECommonUIKit' - fcs.dependency 'NERtcSDK' - - end - s.default_subspecs = 'NOS' + s.dependency 'NERtcCallKit/NOS_Special','2.2.0' + s.dependency 'SDWebImage' + s.dependency 'NECoreKit' + s.dependency 'NECommonKit' + s.dependency 'NECommonUIKit' + s.dependency 'NERtcSDK' end diff --git a/NETeamUIKit/NETeamUIKit.podspec b/NETeamUIKit/NETeamUIKit.podspec index 8dfe0b70..c0f5c3d1 100644 --- a/NETeamUIKit/NETeamUIKit.podspec +++ b/NETeamUIKit/NETeamUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NETeamUIKit' - s.version = '9.6.5' + s.version = '9.7.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_select.imageset/Contents.json b/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_select.imageset/Contents.json new file mode 100644 index 00000000..ea2e4827 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_select.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "clicked_we@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "clicked_we@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_select.imageset/clicked_we@2x.png b/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_select.imageset/clicked_we@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..f69ac91380fc592530790707e299c2d0b2b96d11 GIT binary patch literal 1304 zcmV+z1?T#SP)zGN4NiatPb}&D&wHg!_oJiy9=wXTyBzAJ6h-H)(xpaQ@xfQ{Q8{J6`FR<~5 z9ZvVQ8DIXNVZ;I0pa`i}*Fu{=I&!TJiQIx+8KvK+CkMWgQoHqBFrCp2)a;0 zps>J?8c)#iIrY8e#b!8p`!qbvFIo?vWQ#sCn4}c(v0VBK0;E{1!PwkIxb@&0%P|}= z7BNyI{#nk)AV8i8Z?P21JhGczC`63SlzR)39)o}=C-(l|zrvmQAIgr`b%+UxvK|8B zNbTFOYwJrT=YN0vCw%hbImhvcx+XB7g`%_e5VX9|Tymbpo|^fLj(@oLFIdlEO>@x` zwv(gm+PV|`z+sESS25niD#we4-7Lk&UVeXNcBUVE4HxfD*oPkrvG3FzEIH2(#%ADF zEW=tI-#GSduQNSLZl7*C=@jS{JgZh>nX$Pq$kBcQudof5edV?i#$I0Vc6-rp(v7j* zT5*=d-kJX%yuw&7$8LETC;^GdWD~Q-Go(hFCPL5}wLecCwnD8)DyyR9wEVD>;IWbO!IdZ(Lmw{L4@wySsVtHYx z4Du=$5esn`amed%7R`a*F2geNgNOM?PzeylxH=jq5;uG9fseNitK8P>lIwz8>cD6X zH&#|UGU$Hr0gGaBKRpR+e5|loew|;j0CQd9W=|R*xct}+5-@xDz(}L<-JDC{@=>=D zBM4XvVz_-Il~;nV-CI?j;* O0000sGdY}gd^k6-ZU_xu7 zhpvs$jVXa%+L(w-jRzAoRh!sDO*+JCn)r`H3nJ`1f4{f8Fx}nRc{4jhXW(-v3p)$@ zzWe(>@0GzS(3$Sn0RC$g5NqUSMl%`H7(g{Xr4?Li2!L5Ggkka+J{A|_U>cw3Cx3u| zVGt0V={{UI+rx#~BeILG1OwmcvVuK){2UZgXNxuZCNQm}yfAuC<925fj;ZEX%AfAPI;fucZ7#F7lbOE;* zAjEL$*^^1|fNLQ_V+A%a2BY(69h3U#tgF|#xtL;s5U~k-Q4@q445$g+jjJdN@nX8S z$rNk?igSNhaV3V55F~;SEFGeFpad>%mNB8L6l9JNPA;7-^X;(}g0cgN0MUsF<-j1nI3d?$gRKyhB4KTaAfZf`Ta(@M#dJ>f_FzIj z4QmkWeD{`W+=_(A#|2nJ)?kH#=R8Z)6&742TqCxoT%r&Po~2ymHjBtv!!=kBpjB9l ztpbaP4Z^zNcB@#nh;?NRc`Q~DU-@bDtf8U09@-w(;en0!!yo_t4dd6&gK_h^`Y$k- zdhp*NdblElfL4eQPj7ny4mZ5C{C(}++o7dqCyZSF0KWV4C#BzkrT0=Yqt}!#M94m=d2r--ERni@tM3_S>c>B_OD8^sl7QD6C#7z#h5Mo~4aS#LK zLuIca5#I7B0>PN6alGNGK}b1-De@6al}@~m?l9w~kSc%ytL0%G!*ytFup5geC(c|Q z#{%j{s07l0DoYJKP;q%%1`=UkU7O=|aUP; z{ezXSZhRx-g5Qu32KWrQ$veA_DcAf?{X?Fao4?5n5da3KVWyi`P_Cvj)(NtjY&E#W z;X1YcfdVoWOhG^za2#(N;1Vih>5Vx-VjSN!AT6Zy^XjEDiEztBGvx-XEN5an_dot5 zO5&WbEl?##!;gmh>Y;kWJ!P*=@B}=y^)V^?6^kXg6~SxOsDcpu^Vp-^E!(ihY`oaq%mp`nybXLSjJ#(TiH=$x5 zL4{C}Gw|(kr6B5rAN_+ea?fUdX34>q-K^9BW&@jdF?{SFl)%pOT=r`YKine^Wdxcd zP&{yh-~EFE(k)@-%!)M-MLTA? zrus%j_MV{cP!OdGvj)p*sxVcWw?L5AaN>aVM@18=yLfzPK`eF$G0UVNdhtf^m7)Vi zp*0XigPA1lEn@5dtW}sOd7Po-gz8g$Y?AqyPQwC~2r0evF$UXF9k~bwkiZX>ts=$( zuF396TOr6r1k3D&07iLt0J&4Y+!+qXP3ckHnV*NoaDpY8osKIwPO)iMJMC_FJKGEc z`JMSHXEv0^`6LFqNi#&tO$^nu8iewF4OxP-j1CbHLr{>%nBe9Fv8x+AB*sScw=kzx zN|6gL&vOx;k2`jzdwX!Q6`Fn96b~|32zos-?djepAq$hNtN7lrx`nKOcHCETZrrm) zSjlZ)9>Ry+u$qMBNOU0Ewq literal 0 HcmV?d00001 diff --git a/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_user_empty.imageset/Contents.json b/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_user_empty.imageset/Contents.json new file mode 100644 index 00000000..3331ca37 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_user_empty.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "fun_user_empty@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "fun_user_empty@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_user_empty.imageset/fun_user_empty@2x.png b/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_user_empty.imageset/fun_user_empty@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..54211c5a115cbf2ca7cf5c8db56b905b12f24f76 GIT binary patch literal 9965 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 literal 0 HcmV?d00001 diff --git a/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_user_empty.imageset/fun_user_empty@3x.png b/NETeamUIKit/NETeamUIKit/Assets/FunTeamUIKit.xcassets/fun_user_empty.imageset/fun_user_empty@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..37ff7f7072e6e478fc139d6b85732fcf571f19a3 GIT binary patch literal 15670 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 literal 0 HcmV?d00001 diff --git a/NETeamUIKit/NETeamUIKit/Assets/NEBaseTeamUIKit.xcassets/common/search.imageset/Frame@3x.png b/NETeamUIKit/NETeamUIKit/Assets/NEBaseTeamUIKit.xcassets/common/search.imageset/Frame@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..a4c9a4c537781f3820ef8029496d12d29939a607 GIT binary patch literal 1132 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 WXR 70 { + cellModel.rowHeight = 78 + } else { + cellModel.rowHeight = 56 + } + } + } + } + + override open func getFooterView() -> UIView? { + let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + footer.addSubview(button) + button.backgroundColor = .white + button.clipsToBounds = true + button.setTitleColor(NEConstant.hexRGB(0xE6605C), for: .normal) + button.titleLabel?.font = NEConstant.defaultTextFont(16.0) + button.setTitle(localizable("transfer_owner"), for: .normal) + button.addTarget(self, action: #selector(transferOwner), for: .touchUpInside) + NSLayoutConstraint.activate([ + button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 0), + button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: 0), + button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), + button.heightAnchor.constraint(equalToConstant: 56), + ]) + return footer + } + + override open func didManagerClick() { + let controller = FunTeamManagerListController() + controller.teamId = viewmodel.teamInfoModel?.team?.teamId + navigationController?.pushViewController(controller, animated: true) + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift new file mode 100644 index 00000000..62c25c82 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift @@ -0,0 +1,94 @@ +//// 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 FunTeamManagerListController: NEBaseTeamManagerListController { + override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + // key 值 为 tableview section 对应的值 + cellClassDic = [0: FunTeamArrowSettingCell.self, 1: FunTeamManagerMemberCell.self] + } + + lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView(imageName: "fun_user_empty", content: localizable("no_manager_member"), frame: CGRect.zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + return view + }() + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + view.addSubview(emptyView) + NSLayoutConstraint.activate([ + emptyView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + emptyView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -100), + emptyView.widthAnchor.constraint(equalToConstant: 122), + emptyView.heightAnchor.constraint(equalToConstant: 91), + ]) + sortAndReloadData() + } + + /* + // 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. + } + */ + + override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.section == 0 { + let cell = tableView.dequeueReusableCell(withIdentifier: "\(indexPath.section)", for: indexPath) as! FunTeamArrowSettingCell + cell.titleLabel.text = localizable("add_manager") + return cell + } + let cell = tableView.dequeueReusableCell(withIdentifier: "\(indexPath.section)", for: indexPath) as! FunTeamManagerMemberCell + cell.delegate = self + cell.index = indexPath.row + cell.configure(viewmodel.managers[indexPath.row]) + if let type = viewmodel.currentMember?.type, type == .manager { + cell.removeBtn.isHidden = true + cell.removeLabel.isHidden = true + } + return cell + } + + override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if indexPath.section == 0 { + return 46 + } else if indexPath.section == 1 { + return 64 + } + return 0 + } + + override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if indexPath.section == 0 { + let selectController = FunTeamMemberSelectController() + selectController.teamId = teamId + selectController.selectMemberBlock = { [weak self] datas in + self?.didAddManagers(datas) + } + navigationController?.pushViewController(selectController, animated: true) + } else if indexPath.section == 1 { + super.tableView(tableView, didSelectRowAt: indexPath) + } + } + + override open func sortAndReloadData() { + super.sortAndReloadData() + emptyView.isHidden = viewmodel.managers.count > 0 + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift new file mode 100644 index 00000000..02a31097 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift @@ -0,0 +1,45 @@ +//// 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 FunTeamMemberSelectController: NEBaseTeamMemberSelectController { + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + cellClassDic[0] = FunTeamMemberSelectCell.self + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + navigationView.moreButton.setTitleColor(.funTeamThemeColor, for: .normal) + // Do any additional setup after loading the view. + emptyView.setEmptyImage(image: coreLoader.loadImage("fun_user_empty")) + } + + override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "\(indexPath.section)", for: indexPath) as! FunTeamMemberSelectCell + let member = viewmodel.showDatas[indexPath.row] + cell.configureMember(member) + return cell + } + + 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/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift index 9723510b..1976eeaa 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift @@ -4,6 +4,8 @@ import NECommonKit import UIKit + +@objcMembers open class FunTeamMembersController: NEBaseTeamMembersController { let searchGrayBackView: UIView = { let view = UIView() @@ -36,7 +38,29 @@ open class FunTeamMembersController: NEBaseTeamMembersController { ) as? FunTeamMemberCell { if let model = getRealModel(indexPath.row) { cell.configure(model) - cell.ownerLabel.isHidden = !isOwner(model.nimUser?.userId) + var isShowRemove = false + if isOwner(model.nimUser?.userId) { + cell.ownerLabel.isHidden = false + cell.ownerLabel.text = localizable("team_owner") + cell.setOwnerStyle() + } else if model.teamMember?.type == .manager { + cell.ownerLabel.isHidden = false + cell.ownerLabel.text = localizable("team_manager") + cell.setManagerStyle() + if isOwner(IMKitClient.instance.imAccid()) { + isShowRemove = true + } + } else { + if isOwner(IMKitClient.instance.imAccid()) || viewmodel.currentMember?.type == .manager { + isShowRemove = true + } + cell.ownerLabel.isHidden = true + } + cell.index = indexPath.row + cell.delegate = self + cell.configure(model) + cell.removeBtn.isHidden = !isShowRemove + cell.removeLabel.isHidden = !isShowRemove } if isLastRow(indexPath.row) { cell.dividerLine.isHidden = true @@ -58,7 +82,7 @@ open class FunTeamMembersController: NEBaseTeamMembersController { return true } } - if let originDatas = datas, originDatas.count - 1 == index { + if viewmodel.datas.count - 1 == index { return true } return false diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift index 8a2ec325..f9fe667e 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift @@ -68,7 +68,7 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { teamHeader.widthAnchor.constraint(equalToConstant: 50), teamHeader.heightAnchor.constraint(equalToConstant: 50), ]) - if let url = viewmodel.teamInfoModel?.team?.avatarUrl { + if let url = viewmodel.teamInfoModel?.team?.avatarUrl, !url.isEmpty { print("icon url : ", url) teamHeader.sd_setImage(with: URL(string: url), completed: nil) } else { @@ -164,7 +164,7 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { addBtn.addTarget(self, action: #selector(addUser), for: .touchUpInside) if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, - let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, inviteMode == .manager { + let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, let member = viewmodel.memberInTeam, inviteMode == .manager, member.type != .manager { addBtnWidth?.constant = 0 addBtn.isHidden = true } @@ -229,8 +229,22 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { override open func checkoutAddShowOrHide() { if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, inviteMode == .manager { - return + if let member = viewmodel.memberInTeam, member.type == .manager { + addBtn.isHidden = false + addBtnWidth?.constant = 36.0 + addBtnLeftMargin?.constant = 16 + checkMemberCountLimit() + } else { + addBtn.isHidden = true + addBtnWidth?.constant = 0 + addBtnLeftMargin?.constant = 0 + } + } else { + checkMemberCountLimit() } + } + + func checkMemberCountLimit() { if viewmodel.teamInfoModel?.team?.level == viewmodel.teamInfoModel?.team?.memberNumber { addBtn.isHidden = true addBtnWidth?.constant = 0 @@ -296,6 +310,13 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { ) } + override open func didClickTeamManage() { + let manageTeam = FunTeamManageController() + manageTeam.managerUsers = getManaterUsers() + manageTeam.viewmodel.teamInfoModel = viewmodel.teamInfoModel + navigationController?.pushViewController(manageTeam, animated: true) + } + override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { if let cell = collectionView.dequeueReusableCell( diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamUIColor.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamUIColor.swift index 365403d1..21e9376a 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamUIColor.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamUIColor.swift @@ -15,4 +15,9 @@ extension UIColor { static let funTeamBackgroundColor = UIColor(hexString: "#EDEDED") static let funTeamLineBorderColor = UIColor(hexString: "#E5E5E5") static let funTeamHistoryCellTitleTextColor = UIColor(hexString: "#737373") + static let funTeamRemoveLabelColor = UIColor(hexString: "#505D75") + + static let funTeamMangerLabelTextColor = UIColor(hexString: "#EA8339") + static let funTeamMangerLabelBackColor = UIColor(hexString: "#F2C46B", 0.1) + static let funTeamManagerLabelBorderColor = UIColor(hexString: "#F2C46B") } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NEBaseTeamRouter.swift b/NETeamUIKit/NETeamUIKit/Classes/NEBaseTeamRouter.swift index 112c400e..60f68364 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NEBaseTeamRouter.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NEBaseTeamRouter.swift @@ -3,12 +3,13 @@ // found in the LICENSE file. import Foundation +import NECommonKit import NECoreIMKit import NECoreKit import NIMSDK @objcMembers -public class TeamRouter: NSObject { +open class TeamRouter: NSObject { public static let repo = TeamRepo.shared public static var iconUrls = ["https://s.netease.im/safe/ABg8YjWQWvcqO6sAAAAAAAAAAAA?_im_url=1", "https://s.netease.im/safe/ABg8YjmQWvcqO6sAAAAAAAABAAA?_im_url=1", @@ -42,6 +43,12 @@ public class TeamRouter: NSObject { option.beInviteMode = .noAuth option.updateInfoMode = .all option.updateClientCustomMode = .all + var disucssFlag = [String: Any]() + disucssFlag[discussTeamKey] = true + let jsonString = NECommonUtil.getJSONStringFromDictionary(disucssFlag) + if jsonString.count > 0 { + option.clientCustomInfo = jsonString + } repo.createAdvanceTeam(accids, option) { error, teamid, failedIds in var result = [String: Any]() @@ -52,19 +59,6 @@ public class TeamRouter: NSObject { result["code"] = 0 result["msg"] = "ok" result["teamId"] = teamid - repo.sendCreateAdavanceNoti( - teamid ?? "", - localizable("create_senior_team_noti") - ) { error in - print("send noti message : ", error as Any) - } - if let tid = teamid { - repo.updateTeamCustomInfo(discussTeamKey, tid) { error in - if let err = error { - print(#function + "error: \(err.localizedDescription)") - } - } - } } Router.shared.use(TeamCreateDiscussResult, parameters: result, closure: nil) } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamManagerMemberCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamManagerMemberCell.swift new file mode 100644 index 00000000..2aac954a --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamManagerMemberCell.swift @@ -0,0 +1,13 @@ +//// 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 + +class TeamManagerMemberCell: TeamMemberCell { + override func setupUI() { + super.setupUI() + ownerLabel.isHidden = true + nameLabelRightMargin?.constant = -65 + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamMemberCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamMemberCell.swift index 0736b0c4..e1d10a91 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamMemberCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamMemberCell.swift @@ -5,4 +5,28 @@ import UIKit @objcMembers -open class TeamMemberCell: NEBaseTeamMemberCell {} +open class TeamMemberCell: NEBaseTeamMemberCell { + override open func setupUI() { + super.setupUI() + contentView.addSubview(removeLabel) + NSLayoutConstraint.activate([ + removeLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + removeLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + removeLabel.heightAnchor.constraint(equalToConstant: 22), + removeLabel.widthAnchor.constraint(equalToConstant: 40), + ]) + removeLabel.textColor = .ne_redText + removeLabel.clipsToBounds = true + removeLabel.layer.cornerRadius = 4.0 + removeLabel.layer.borderColor = UIColor.ne_redColor.cgColor + removeLabel.layer.borderWidth = 1.0 + removeLabel.font = UIFont.systemFont(ofSize: 12.0) + + ownerLabel.backgroundColor = .normalTeamOwnerBgColor + ownerLabel.textColor = .normalTeamOwnerColor + ownerLabel.layer.borderColor = UIColor.normalTeamOwnerBorderColor.cgColor + ownerLabel.layer.cornerRadius = 11.0 + + setupRemoveButton() + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamMemberSelectCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamMemberSelectCell.swift new file mode 100644 index 00000000..f07479ee --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamMemberSelectCell.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 UIKit + +@objcMembers +open class TeamMemberSelectCell: NEBaseTeamMemberSelectCell { + override open func setupUI() { + super.setupUI() + headerView.layer.cornerRadius = 18 + checkImageView.highlightedImage = coreLoader.loadImage("select") + NSLayoutConstraint.activate([ + checkImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + checkImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 18), + checkImageView.widthAnchor.constraint(equalToConstant: 18), + checkImageView.heightAnchor.constraint(equalToConstant: 18), + ]) + + NSLayoutConstraint.activate([ + headerView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + headerView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 52), + headerView.widthAnchor.constraint(equalToConstant: 36), + headerView.heightAnchor.constraint(equalToConstant: 36), + ]) + + NSLayoutConstraint.activate([ + nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + nameLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 102), + nameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -18), + ]) + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.swift new file mode 100644 index 00000000..f4a52175 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.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 UIKit + +@objcMembers +open class TeamSettingLabelArrowCell: TeamArrowSettingCell { + let arrowLabel: UILabel = { + let q = UILabel() + q.translatesAutoresizingMaskIntoConstraints = false + q.textColor = .ne_lightText + q.font = UIFont.systemFont(ofSize: 16) + q.textAlignment = .right + return q + }() + + override open func setupUI() { + super.setupUI() + contentView.addSubview(arrowLabel) + NSLayoutConstraint.activate([ + arrowLabel.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), + arrowLabel.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -4), + ]) + } + + override open func configure(_ anyModel: Any) { + super.configure(anyModel) + if let model = anyModel as? SettingCellLabelArrowModel { + arrowLabel.text = model.arrowLabelText + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManageController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManageController.swift new file mode 100644 index 00000000..9685464b --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManageController.swift @@ -0,0 +1,55 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NIMSDK +import UIKit + +@objcMembers +open class TeamManageController: NEBaseTeamManageController { + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + navigationView.backgroundColor = .ne_lightBackgroundColor + navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor + cellClassDic = [ + SettingCellType.SettingArrowCell.rawValue: TeamSettingLabelArrowCell.self, + SettingCellType.SettingSwitchCell.rawValue: TeamSettingSwitchCell.self, + SettingCellType.SettingSelectCell.rawValue: TeamSettingSelectCell.self, + ] + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open func viewDidLoad() { + super.viewDidLoad() + } + + override open func getFooterView() -> UIView? { + let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + footer.addSubview(button) + button.backgroundColor = .white + button.clipsToBounds = true + button.setTitleColor(NEConstant.hexRGB(0xE6605C), for: .normal) + button.titleLabel?.font = NEConstant.defaultTextFont(16.0) + button.setTitle(localizable("transfer_owner"), for: .normal) + button.addTarget(self, action: #selector(transferOwner), for: .touchUpInside) + button.layer.cornerRadius = 8.0 + NSLayoutConstraint.activate([ + button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 20), + button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: -20), + button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), + button.heightAnchor.constraint(equalToConstant: 40), + ]) + return footer + } + + override open func didManagerClick() { + let controller = TeamManagerListController() + controller.teamId = viewmodel.teamInfoModel?.team?.teamId + navigationController?.pushViewController(controller, animated: true) + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift new file mode 100644 index 00000000..7f367af2 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift @@ -0,0 +1,95 @@ +//// 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 TeamManagerListController: NEBaseTeamManagerListController { + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + // key 值 为 tableview section 对应的值 + cellClassDic = [0: TeamArrowSettingCell.self, 1: TeamManagerMemberCell.self] + } + + public lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView(imageName: "user_empty", content: localizable("no_manager_member"), frame: CGRect.zero) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + return view + }() + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + navigationView.backgroundColor = .white + + // Do any additional setup after loading the view. + view.addSubview(emptyView) + NSLayoutConstraint.activate([ + emptyView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + emptyView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -100), + emptyView.widthAnchor.constraint(equalToConstant: 122), + emptyView.heightAnchor.constraint(equalToConstant: 91), + ]) + sortAndReloadData() + } + + /* + // 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. + } + */ + + override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.section == 0 { + let cell = tableView.dequeueReusableCell(withIdentifier: "\(indexPath.section)", for: indexPath) as! TeamArrowSettingCell + cell.titleLabel.text = localizable("add_manager") + return cell + } + let cell = tableView.dequeueReusableCell(withIdentifier: "\(indexPath.section)", for: indexPath) as! TeamManagerMemberCell + cell.delegate = self + cell.index = indexPath.row + cell.configure(viewmodel.managers[indexPath.row]) + if let type = viewmodel.currentMember?.type, type == .manager { + cell.removeBtn.isHidden = true + cell.removeLabel.isHidden = true + } + return cell + } + + override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if indexPath.section == 0 { + return 46 + } else if indexPath.section == 1 { + return 52 + } + return 0 + } + + override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if indexPath.section == 0 { + let selectController = TeamMemberSelectController() + selectController.teamId = teamId + selectController.selectMemberBlock = { [weak self] datas in + self?.didAddManagers(datas) + } + navigationController?.pushViewController(selectController, animated: true) + } else if indexPath.section == 1 { + super.tableView(tableView, didSelectRowAt: indexPath) + } + } + + override open func sortAndReloadData() { + super.sortAndReloadData() + emptyView.isHidden = viewmodel.managers.count > 0 + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.swift new file mode 100644 index 00000000..0333e5a6 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.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 TeamMemberSelectController: NEBaseTeamMemberSelectController { + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + cellClassDic[0] = TeamMemberSelectCell.self + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + navigationView.backgroundColor = .white + navigationView.moreButton.setTitleColor(.normalTeamBlueColor, for: .normal) + } + + override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "\(indexPath.section)", for: indexPath) as! TeamMemberSelectCell + let member = viewmodel.showDatas[indexPath.row] + cell.configureMember(member) + return cell + } + + override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 52.0 + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.swift index dd7540b2..0e5c509e 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.swift @@ -20,8 +20,29 @@ open class TeamMembersController: NEBaseTeamMembersController { for: indexPath ) as? TeamMemberCell { if let model = getRealModel(indexPath.row) { + var isShowRemove = false + if isOwner(model.nimUser?.userId) { + cell.ownerLabel.isHidden = false + cell.ownerLabel.text = localizable("team_owner") + cell.ownerWidth?.constant = 40 + } else if model.teamMember?.type == .manager { + cell.ownerLabel.isHidden = false + cell.ownerLabel.text = localizable("team_manager") + cell.ownerWidth?.constant = 52 + if isOwner(IMKitClient.instance.imAccid()) { + isShowRemove = true + } + } else { + if isOwner(IMKitClient.instance.imAccid()) || viewmodel.currentMember?.type == .manager { + isShowRemove = true + } + cell.ownerLabel.isHidden = true + } + cell.index = indexPath.row + cell.delegate = self cell.configure(model) - cell.ownerLabel.isHidden = !isOwner(model.nimUser?.userId) + cell.removeBtn.isHidden = !isShowRemove + cell.removeLabel.isHidden = !isShowRemove } return cell } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift index d07b4f68..e60ab70d 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift @@ -55,7 +55,7 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { teamHeader.widthAnchor.constraint(equalToConstant: 42), teamHeader.heightAnchor.constraint(equalToConstant: 42), ]) - if let url = viewmodel.teamInfoModel?.team?.avatarUrl { + if let url = viewmodel.teamInfoModel?.team?.avatarUrl, !url.isEmpty { print("icon url : ", url) teamHeader.sd_setImage(with: URL(string: url), completed: nil) } else { @@ -152,7 +152,7 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { addBtn.addTarget(self, action: #selector(addUser), for: .touchUpInside) if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, - let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, inviteMode == .manager { + let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, let member = viewmodel.memberInTeam, inviteMode == .manager, member.type != .manager { addBtnWidth?.constant = 0 addBtn.isHidden = true } @@ -176,8 +176,22 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { override open func checkoutAddShowOrHide() { if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, inviteMode == .manager { - return + if let member = viewmodel.memberInTeam, member.type == .manager { + addBtn.isHidden = false + addBtnWidth?.constant = 36.0 + addBtnLeftMargin?.constant = 16 + checkMemberCountLimit() + } else { + addBtn.isHidden = true + addBtnWidth?.constant = 0 + addBtnLeftMargin?.constant = 0 + } + } else { + checkMemberCountLimit() } + } + + func checkMemberCountLimit() { if viewmodel.teamInfoModel?.team?.level == viewmodel.teamInfoModel?.team?.memberNumber { addBtn.isHidden = true addBtnWidth?.constant = 0 @@ -257,6 +271,13 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { ) } + override open func didClickTeamManage() { + let manageTeam = TeamManageController() + manageTeam.managerUsers = getManaterUsers() + manageTeam.viewmodel.teamInfoModel = viewmodel.teamInfoModel + navigationController?.pushViewController(manageTeam, animated: true) + } + override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { if let cell = collectionView.dequeueReusableCell( @@ -264,6 +285,9 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { for: indexPath ) as? TeamUserCell { if let user = viewmodel.teamInfoModel?.users[indexPath.row] { + if let userId = user.nimUser?.userId, let nimUser = ChatUserCache.getUserInfo(userId) { + user.nimUser = nimUser + } cell.user = user } return cell diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamUIColor.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamUIColor.swift new file mode 100644 index 00000000..cd8d8691 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamUIColor.swift @@ -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. + +import Foundation + +extension UIColor { + static let normalTeamBlueColor = UIColor(hexString: "#337EFF") + static let normalTeamOwnerColor = UIColor(hexString: "#656A72") + static let normalTeamOwnerBgColor = UIColor(hexString: "#F7F7F7") + static let normalTeamOwnerBorderColor = UIColor(hexString: "#D6D8DB") +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift new file mode 100644 index 00000000..8a25f8c0 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift @@ -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. + +import NEChatKit +import UIKit + +@objcMembers +open class NESelectTeamMember: NSObject { + var isSelected: Bool = false + var member: TeamMemberInfoModel? +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingCellLabelArrowModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingCellLabelArrowModel.swift new file mode 100644 index 00000000..41a7a442 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingCellLabelArrowModel.swift @@ -0,0 +1,9 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +open class SettingCellLabelArrowModel: SettingCellModel { + var arrowLabelText = "" +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingCellModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingCellModel.swift index b901d018..a458cb26 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingCellModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingCellModel.swift @@ -17,7 +17,7 @@ public enum SettingCellType: Int { } @objcMembers -public class SettingCellModel: NSObject { +open class SettingCellModel: NSObject { public typealias SwitchChangeCompletion = (Bool) -> Void public typealias CellClick = () -> Void public var cellName: String? diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift index 87eb5d52..608f1dff 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift @@ -6,7 +6,7 @@ import Foundation @objcMembers -public class SettingSectionModel: NSObject { +open class SettingSectionModel: NSObject { public var cellModels = [SettingCellModel]() override public init() {} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift index 49214611..22af08ea 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift @@ -105,24 +105,17 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField // MARK: private method func searchTextFieldChange(textfield: SearchTextField) { - if textfield.text?.count == 0 { + guard let searchText = textfield.text else { + return + } + if searchText.count <= 0 { viewmodel.searchResultInfos?.removeAll() emptyView.isHidden = true tableView.reloadData() - } - } - - // MARK: UITextFieldDelegate - - open func textFieldShouldReturn(_ textField: UITextField) -> Bool { - guard let searchText = textField.text else { - return false - } - if searchText.count <= 0 { - return false + return } guard let session = teamSession else { - return false + return } weak var weakSelf = self searchStr = searchText @@ -147,8 +140,6 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField ) } } - - return true } // MARK: UITableViewDelegate, UITableViewDataSource diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManageController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManageController.swift new file mode 100644 index 00000000..be2a7f90 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManageController.swift @@ -0,0 +1,386 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIMKit +import NIMSDK +import UIKit + +@objcMembers +open class NEBaseTeamManageController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource, TeamManageViewModelDelegate { + public let viewmodel = TeamManageViewModel() + + public var managerUsers = [TeamMemberInfoModel]() + + public var cellClassDic = [Int: NEBaseTeamSettingCell.Type]() + + public lazy var contentTable: UITableView = { + let table = UITableView() + table.translatesAutoresizingMaskIntoConstraints = false + table.backgroundColor = .clear + table.dataSource = self + table.delegate = self + table.separatorColor = .clear + table.separatorStyle = .none + table.sectionHeaderHeight = 12.0 + table + .tableFooterView = + UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) + if #available(iOS 15.0, *) { + table.sectionHeaderTopPadding = 0.0 + } + return table + }() + + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + title = localizable("manage_team") + viewmodel.managerUsers = managerUsers + viewmodel.delegate = self + view.backgroundColor = .ne_lightBackgroundColor + view.addSubview(contentTable) + + NSLayoutConstraint.activate([ + contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in + contentTable.register(value, forCellReuseIdentifier: "\(key)") + } + if let tid = viewmodel.teamInfoModel?.team?.teamId { + viewmodel.getTeamInfo(tid) { [weak self] error in + self?.reloadSectionData() + self?.contentTable.reloadData() + } + } + } + + open func reloadSectionData() {} + + // MARK: UITableViewDataSource, UITableViewDelegate + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if viewmodel.sectionData.count > section { + let model = viewmodel.sectionData[section] + return model.cellModels.count + } + return 0 + } + + open func numberOfSections(in tableView: UITableView) -> Int { + viewmodel.sectionData.count + } + + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] + if let cell = tableView.dequeueReusableCell( + withIdentifier: "\(model.type)", + for: indexPath + ) as? NEBaseTeamSettingCell { + cell.configure(model) + return cell + } + return UITableViewCell() + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] + if let block = model.cellClick { + block() + } + } + + open func tableView(_ tableView: UITableView, + heightForRowAt indexPath: IndexPath) -> CGFloat { + let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] + return model.rowHeight + } + + open func tableView(_ tableView: UITableView, + heightForHeaderInSection section: Int) -> CGFloat { + if viewmodel.sectionData.count > section { + let model = viewmodel.sectionData[section] + if model.cellModels.count > 0 { + return 12.0 + } + } + return 0 + } + + open func tableView(_ tableView: UITableView, + viewForHeaderInSection section: Int) -> UIView? { + let header = UIView() + header.backgroundColor = .ne_lightBackgroundColor + return header + } + + open func tableView(_ tableView: UITableView, + heightForFooterInSection section: Int) -> CGFloat { + if section == viewmodel.sectionData.count - 1 { + return 12.0 + } + return 0 + } + + open func getFooterView() -> UIView? { + nil + } + + open func transferOwner() {} + + func updateTeamInfoAllAction(_ model: SettingCellModel) { + weak var weakSelf = self + view.makeToastActivity(.center) + viewmodel.repo + .updateTeamInfoPrivilege(.all, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in + NELog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .all + model.subTitle = localizable("team_all") + weakSelf?.contentTable.reloadData() + } + } + } + + open func didUpdateTeamInfoClick(_ model: SettingCellModel) { + weak var weakSelf = self + + let actionSheetController = UIAlertController( + title: nil, + message: nil, + preferredStyle: .actionSheet + ) + + let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in + print("Cancel") + } + cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + actionSheetController.addAction(cancelActionButton) + + let all = UIAlertAction(title: localizable("team_all"), style: .default) { _ in + weakSelf?.updateTeamInfoAllAction(model) + } + all.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + all.accessibilityIdentifier = "id.teamAllMember" + actionSheetController.addAction(all) + + let manager = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { _ in + weakSelf?.updateTeamInfoOwnerAction(model) + } + manager.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + manager.accessibilityIdentifier = "id.teamOwner" + actionSheetController.addAction(manager) + + actionSheetController.fixIpadAction() + + navigationController?.present(actionSheetController, animated: true, completion: nil) + } + + func updateTeamInfoOwnerAction(_ model: SettingCellModel) { + weak var weakSelf = self + view.makeToastActivity(.center) + viewmodel.repo + .updateTeamInfoPrivilege(.manager, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in + NELog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .manager + model.subTitle = localizable("team_owner_and_manager") + weakSelf?.contentTable.reloadData() + } + } + } + + func updateInviteModeOwnerAction(_ model: SettingCellModel) { + weak var weakSelf = self + view.makeToastActivity(.center) + viewmodel.repo.updateInviteMode(.manager, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in + NELog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .manager + model.subTitle = localizable("team_owner_and_manager") + weakSelf?.contentTable.reloadData() + } + } + } + + func updateInviteModeAllAction(_ model: SettingCellModel) { + weak var weakSelf = self + view.makeToastActivity(.center) + viewmodel.repo.updateInviteMode(.all, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in + NELog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .all + model.subTitle = localizable("team_all") + weakSelf?.contentTable.reloadData() + } + } + } + + open func didChangeInviteModeClick(_ model: SettingCellModel) { + weak var weakSelf = self + + let actionSheetController = UIAlertController( + title: nil, + message: nil, + preferredStyle: .actionSheet + ) + + let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in + print("Cancel") + } + cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + actionSheetController.addAction(cancelActionButton) + + let allActionButton = UIAlertAction(title: localizable("team_all"), style: .default) { _ in + weakSelf?.updateInviteModeAllAction(model) + } + + allActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + allActionButton.accessibilityIdentifier = "id.teamAllMember" + actionSheetController.addAction(allActionButton) + + let ownerActionButton = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { _ in + weakSelf?.updateInviteModeOwnerAction(model) + } + ownerActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + ownerActionButton.accessibilityIdentifier = "id.teamOwner" + actionSheetController.addAction(ownerActionButton) + + actionSheetController.fixIpadAction() + navigationController?.present(actionSheetController, animated: true, completion: nil) + } + + open func didAtPermissionClick(_ model: SettingCellModel) { + weak var weakSelf = self + + let actionSheetController = UIAlertController( + title: nil, + message: nil, + preferredStyle: .actionSheet + ) + + let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in + print("Cancel") + } + cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + actionSheetController.addAction(cancelActionButton) + + let all = UIAlertAction(title: localizable("team_all"), style: .default) { _ in + weakSelf?.viewmodel.updateTeamAtPermission(false) { error in + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewmodel.sendTipNoti(false) { error in + } + model.subTitle = localizable("team_all") + weakSelf?.contentTable.reloadData() + } + } + } + all.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + all.accessibilityIdentifier = "id.teamAllMember" + actionSheetController.addAction(all) + actionSheetController.fixIpadAction() + + let manager = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { _ in + weakSelf?.viewmodel.updateTeamAtPermission(true) { error in + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewmodel.sendTipNoti(true) { error in + } + model.subTitle = localizable("team_owner_and_manager") + weakSelf?.contentTable.reloadData() + } + } + } + manager.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + manager.accessibilityIdentifier = "id.teamOwner" + actionSheetController.addAction(manager) + + navigationController?.present(actionSheetController, animated: true, completion: nil) + } + + open func didManagerClick() {} + + open func didRefreshData() { + reloadSectionData() + contentTable.reloadData() + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift new file mode 100644 index 00000000..b804e0d6 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift @@ -0,0 +1,173 @@ +//// 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 NEBaseTeamManagerListController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource, TeamMemberCellDelegate, TeamManagerListViewModelDelegate { + public var teamId: String? + + let viewmodel = TeamManagerListViewModel() + + public lazy var contentTable: UITableView = { + let table = UITableView() + table.translatesAutoresizingMaskIntoConstraints = false + table.backgroundColor = .clear + table.dataSource = self + table.delegate = self + table.separatorColor = .clear + table.separatorStyle = .none + table.keyboardDismissMode = .onDrag + table.sectionHeaderHeight = 12.0 + table + .tableFooterView = + UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) + if #available(iOS 15.0, *) { + table.sectionHeaderTopPadding = 0.0 + } + return table + }() + + public var cellClassDic = [Int: UITableViewCell.Type]() // key 值为 table section 值 + + override open func viewDidLoad() { + super.viewDidLoad() + + title = localizable("group_manager") + + viewmodel.teamId = teamId + + viewmodel.delegate = self + + if let tid = teamId { + viewmodel.getCurrentMember(tid) + viewmodel.getManagerDatas(tid) { [weak self] error in + if let err = error { + self?.view.makeToast(err.localizedDescription) + } else { + self?.sortAndReloadData() + } + } + } + view.addSubview(contentTable) + NSLayoutConstraint.activate([ + contentTable.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), + contentTable.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), + contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0), + ]) + + cellClassDic.forEach { (key: Int, value: UITableViewCell.Type) in + contentTable.register(value, forCellReuseIdentifier: "\(key)") + } + } + + open func numberOfSections(in tableView: UITableView) -> Int { + 2 + } + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0 { + return 1 + } + return viewmodel.managers.count + } + + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + UITableViewCell() + } + + open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 0 + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + if indexPath.section == 1 { + let model = viewmodel.managers[indexPath.row] + if let user = model.nimUser { + if IMKitClient.instance.isMySelf(user.userId) { + Router.shared.use( + MeSettingRouter, + parameters: ["nav": navigationController as Any], + closure: nil + ) + } else { + if let uid = user.userId { + Router.shared.use( + ContactUserInfoPageRouter, + parameters: ["nav": navigationController as Any, "uid": uid], + closure: nil + ) + } + } + } + } + } + + open func didAddManagers(_ managers: [TeamMemberInfoModel]) { + if let tid = teamId { + var uids = [String]() + managers.forEach { member in + if let uid = member.nimUser?.userId { + uids.append(uid) + } + } + viewmodel.addTeamManager(tid, uids) { [weak self] error in + if let err = error { + self?.view.makeToast(err.localizedDescription) + } else { + self?.viewmodel.managers.insert(contentsOf: managers, at: 0) + self?.sortAndReloadData() + } + } + } + } + + func didClickRemoveButton(_ model: TeamMemberInfoModel?, _ index: Int) { + print("did click remove button") + weak var weakSelf = self + // let content = String(format: localizable("confirm_delete_text"), model?.atNameInTeam() ?? "") + localizable("question_mark") + showAlert(title: localizable("remove_manager_title"), message: localizable("remove_manager_tip")) { + if let tid = weakSelf?.teamId, let uid = model?.nimUser?.userId { + weakSelf?.viewmodel.removeTeamManager(tid, [uid]) { error in + if let err = error { + weakSelf?.view.makeToast(err.localizedDescription) + } else { + if weakSelf?.viewmodel.managers.count ?? 0 > index { + weakSelf?.viewmodel.managers.remove(at: index) + weakSelf?.sortAndReloadData() + } + } + } + } + } + } + + open func getFilters() -> Set { + var filters = Set() + viewmodel.managers.forEach { model in + if let uid = model.nimUser?.userId { + filters.insert(uid) + } + } + return filters + } + + open func sortAndReloadData() { + // 数据源根据时间排序 + viewmodel.managers.sort { model1, model2 -> Bool in + if let time1 = model1.teamMember?.createTime, let time2 = model2.teamMember?.createTime { + return time2 > time1 + } + return false + } + contentTable.reloadData() + } + + open func didNeedReloadData() { + sortAndReloadData() + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift new file mode 100644 index 00000000..86d629ae --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift @@ -0,0 +1,266 @@ +//// 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 UIKit + +public typealias NESelectTeamMemberBlock = ([TeamMemberInfoModel]) -> Void + +@objcMembers +open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate, TeamMemberSelectViewModelDelegate { + public var selectMemberBlock: NESelectTeamMemberBlock? + + let viewmodel = TeamMemberSelectViewModel() + + var teamId: String? + + public var cellClassDic = [Int: UITableViewCell.Type]() // key 值为 table section 值 + + public let searchInput = UITextField() + + public var selectCountLimit = 10 + + public lazy var contentTable: UITableView = { + let table = UITableView() + table.translatesAutoresizingMaskIntoConstraints = false + table.backgroundColor = .clear + table.dataSource = self + table.delegate = self + table.separatorColor = .clear + table.separatorStyle = .none + table.sectionHeaderHeight = 12.0 + table.keyboardDismissMode = .onDrag + table + .tableFooterView = + UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) + if #available(iOS 15.0, *) { + table.sectionHeaderTopPadding = 0.0 + } + return table + }() + + public lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView( + imageName: "user_empty", + content: localizable("member_select_no_member"), + frame: CGRect.zero + ) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + return view + + }() + + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + viewmodel.delegate = self + setupUI() + if let tid = teamId { + viewmodel.getTeamInfo(tid) { [weak self] error in + if let err = error { + self?.view.makeToast(err.localizedDescription) + } else { + self?.contentTable.reloadData() + } + } + } + } + + open func setupUI() { + title = localizable("team_member_select") + view.backgroundColor = .white + view.addSubview(contentTable) + + let searchBack = UIView() + view.addSubview(searchBack) + searchBack.backgroundColor = UIColor(hexString: "F2F4F5") + searchBack.translatesAutoresizingMaskIntoConstraints = false + searchBack.clipsToBounds = true + searchBack.layer.cornerRadius = 4.0 + NSLayoutConstraint.activate([ + searchBack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + searchBack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + searchBack.topAnchor.constraint(equalTo: view.topAnchor, constant: 13 + topConstant), + searchBack.heightAnchor.constraint(equalToConstant: 32), + ]) + + let searchImage = UIImageView() + searchBack.addSubview(searchImage) + searchImage.image = coreLoader.loadImage("search") + searchImage.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + searchImage.centerYAnchor.constraint(equalTo: searchBack.centerYAnchor), + searchImage.leftAnchor.constraint(equalTo: searchBack.leftAnchor, constant: 18), + searchImage.widthAnchor.constraint(equalToConstant: 13), + searchImage.heightAnchor.constraint(equalToConstant: 13), + ]) + + searchBack.addSubview(searchInput) + searchInput.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + searchInput.leftAnchor.constraint(equalTo: searchImage.rightAnchor, constant: 5), + searchInput.rightAnchor.constraint(equalTo: searchBack.rightAnchor, constant: -18), + searchInput.topAnchor.constraint(equalTo: searchBack.topAnchor), + searchInput.bottomAnchor.constraint(equalTo: searchBack.bottomAnchor), + ]) + searchInput.textColor = UIColor(hexString: "333333") + searchInput.placeholder = localizable("search_member") + searchInput.font = UIFont.systemFont(ofSize: 14.0) + searchInput.returnKeyType = .search + searchInput.delegate = self + searchInput.clearButtonMode = .always + if let clearButton = searchInput.value(forKey: "_clearButton") as? UIButton { + clearButton.accessibilityIdentifier = "id.clear" + } + searchInput.accessibilityIdentifier = "id.addFriendAccount" + +// NotificationCenter.default.addObserver( +// self, +// selector: #selector(textFieldChange), +// name: UITextField.textDidChangeNotification, +// object: nil +// ) + + NSLayoutConstraint.activate([ + contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTable.topAnchor.constraint(equalTo: searchBack.bottomAnchor), + contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + cellClassDic.forEach { (key: Int, value: UITableViewCell.Type) in + contentTable.register(value, forCellReuseIdentifier: "\(key)") + } + + view.addSubview(emptyView) + NSLayoutConstraint.activate([ + emptyView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + emptyView.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -100), + emptyView.widthAnchor.constraint(equalToConstant: 122), + emptyView.heightAnchor.constraint(equalToConstant: 91), + ]) + + navigationView.moreButton.isHidden = false + navigationView.moreButton.setImage(nil, for: .normal) + navigationView.moreButton.addTarget(self, action: #selector(didClickSure), for: .touchUpInside) + didChangeSelectMember() + } + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if viewmodel.showDatas.count <= 0 { + emptyView.isHidden = false + } else { + emptyView.isHidden = true + } + return viewmodel.showDatas.count + } + + open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + UITableViewCell() + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let model = viewmodel.showDatas[indexPath.row] + guard let cell = tableView.cellForRow(at: indexPath) as? NEBaseTeamMemberSelectCell else { + return + } + if let member = model.member, let accid = member.teamMember?.userId { + if viewmodel.selectDic[accid] != nil { + viewmodel.selectDic[accid] = nil + model.isSelected = false + cell.checkImageView.isHighlighted = false + } else { + if viewmodel.selectDic.count >= selectCountLimit { + let toastString = String(format: localizable("select_limit_tip"), selectCountLimit) + view.makeToast(toastString) + return + } + viewmodel.selectDic[accid] = member + model.isSelected = true + cell.checkImageView.isHighlighted = true + } + didChangeSelectMember() + } + } + + open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 0 + } + + open func textFieldShouldReturn(_ textField: UITextField) -> Bool { + guard let text = textField.text else { + return false + } + if text.count <= 0 { + return false + } + return true + } + + open func textFieldShouldClear(_ textField: UITextField) -> Bool { + viewmodel.showDatas = viewmodel.datas + contentTable.reloadData() + return true + } + + open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + let finalString = (textField.text! as NSString).replacingCharacters(in: range, with: string) + if string.count <= 0 { + if finalString.count <= 0 { + viewmodel.showDatas = viewmodel.datas + contentTable.reloadData() + } else { + viewmodel.showDatas = viewmodel.searchAllData(finalString) + contentTable.reloadData() + } + } else { + viewmodel.showDatas = viewmodel.searchAllData(finalString) + contentTable.reloadData() + } + return true + } + + func didChangeSelectMember() { + if viewmodel.selectDic.count > 0 { + let title = localizable("member_select_sure") + "(\(viewmodel.selectDic.count))" + navigationView.moreButton.setTitle(title, for: .normal) + } else { + navigationView.moreButton.setTitle(localizable("member_select_sure"), for: .normal) + } + } + + open func didNeedRefresh() { + contentTable.reloadData() + didChangeSelectMember() + } + + open func didClickSure() { + print("sure click") + + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + view.makeToast(localizable("network_error")) + return + } + + if viewmodel.selectDic.count + viewmodel.managerSet.count > selectCountLimit { + view.makeToast(localizable("max_managers_tip")) + return + } + + if viewmodel.selectDic.count <= 0 { + view.makeToast(localizable("member_empty_tip")) + return + } + + var retArray = [TeamMemberInfoModel]() + viewmodel.selectDic.forEach { (key: String, value: TeamMemberInfoModel) in + retArray.append(value) + } + if let block = selectMemberBlock { + block(retArray) + } + navigationController?.popViewController(animated: true) + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift index 109bc7d9..48862110 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift @@ -3,15 +3,22 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit +import NECommonUIKit +import NECoreIMKit import NECoreKit import UIKit @objcMembers open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegate, - UITableViewDataSource { + UITableViewDataSource, TeamMemberCellDelegate, TeamMembersViewModelDelegate { public var teamId: String? - public var datas: [TeamMemberInfoModel]? + public var memberDatas: [TeamMemberInfoModel]? { + didSet { + viewmodel.setShowDatas(memberDatas) + } + } public var ownerId: String? @@ -21,6 +28,8 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat public let back = UIView() + let viewmodel = TeamMembersViewModel() + public lazy var searchTextField: UITextField = { let field = UITextField() field.translatesAutoresizingMaskIntoConstraints = false @@ -71,31 +80,30 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat fatalError("init(coder:) has not been implemented") } - override open func viewWillAppear(_ animated: Bool) { - TeamRepo.shared.fetchTeamInfo(teamId ?? "") { [weak self] error, teamModel in - if let err = error as? NSError { - if err.code == 803 || err.code == 1 { - self?.showToast(localizable("team_not_exist")) - } else { - self?.showToast(err.localizedDescription) - } - } else { - self?.datas = teamModel?.users - self?.contentTable.reloadData() - } - } - } - override open func viewDidLoad() { super.viewDidLoad() + addObserver() + viewmodel.delegate = self let team = TeamProvider.shared.getTeam(teamId: teamId ?? "") ownerId = team?.owner if team?.isDisscuss() == false { isSenior = true } + if let tid = team?.teamId { + viewmodel.getMemberInfo(tid) + } setupUI() + + TeamRepo.shared.fetchTeamInfo(teamId ?? "") { [weak self] error, teamModel in + if error != nil { + self?.emptyView.isHidden = false + } else { + self?.viewmodel.setShowDatas(teamModel?.users) + self?.didNeedRefreshUI() + } + } } open func setupUI() { @@ -149,28 +157,21 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat NSLayoutConstraint.activate([ emptyView.leftAnchor.constraint(equalTo: contentTable.leftAnchor), emptyView.rightAnchor.constraint(equalTo: contentTable.rightAnchor), - emptyView.topAnchor.constraint(equalTo: contentTable.topAnchor), + emptyView.topAnchor.constraint(equalTo: contentTable.topAnchor, constant: 50), emptyView.bottomAnchor.constraint(equalTo: contentTable.bottomAnchor), ]) + } + func addObserver() { NotificationCenter.default.addObserver( self, selector: #selector(textChange), name: UITextField.textDidChangeNotification, object: nil ) + NotificationCenter.default.addObserver(self, selector: #selector(didNeedRefreshUI), name: NENotificationName.updateFriendInfo, object: nil) } - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ - func isOwner(_ userId: String?) -> Bool { if isSenior == false { return false @@ -184,7 +185,7 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat func textChange() { searchDatas.removeAll() if let text = searchTextField.text, text.count > 0 { - datas?.forEach { model in + viewmodel.datas.forEach { model in if let uid = model.nimUser?.userId, uid.contains(text) { searchDatas.append(model) } else if let nick = model.nimUser?.userInfo?.nickName, nick.contains(text) { @@ -199,14 +200,14 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat } else { emptyView.isHidden = true } - contentTable.reloadData() + didNeedRefreshUI() } func getRealModel(_ index: Int) -> TeamMemberInfoModel? { if let text = searchTextField.text, text.count > 0 { return searchDatas[index] } - return datas?[index] + return viewmodel.datas[index] } deinit { @@ -219,7 +220,7 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat if let text = searchTextField.text, text.count > 0 { return searchDatas.count } - return datas?.count ?? 0 + return viewmodel.datas.count } open func tableView(_ tableView: UITableView, @@ -261,4 +262,45 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat } } } + + func didClickRemoveButton(_ model: TeamMemberInfoModel?, _ index: Int) { + print("did click remove button") + weak var weakSelf = self + + // 注释掉的暂时留存,后续可能更改提示语 let content = String(format: localizable("confirm_delete_text"), model?.atNameInTeam() ?? "") + localizable("question_mark") + + showAlert(title: localizable("remove_manager_title"), message: localizable("remove_member_tip")) { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.view.makeToast(commonLocalizable("network_error")) + return + } + + if let tid = weakSelf?.teamId, let uid = model?.nimUser?.userId { + weakSelf?.viewmodel.removeTeamMember(tid, [uid]) { error in + if let err = error { + if err.code == noPermissionCode { + weakSelf?.view.makeToast(localizable("no_permission_tip")) + } else { + weakSelf?.view.makeToast(localizable("remove_failed")) + } + } else { + if let text = weakSelf?.searchTextField.text, text.count > 0 { + weakSelf?.searchDatas.remove(at: index) + weakSelf?.viewmodel.removeModel(model) + weakSelf?.didNeedRefreshUI() + } else { + weakSelf?.viewmodel.removeModel(model) + weakSelf?.didNeedRefreshUI() + } + } + } + } + } + } + + // 查找移除数据在 data 中的位置 + + func didNeedRefreshUI() { + contentTable.reloadData() + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift index 89f9416d..d6a5097c 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift @@ -10,17 +10,6 @@ open class NEBaseHistoryMessageCell: UITableViewCell { public var searchText: String? public var rangeTextColor = UIColor.ne_blueText - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupSubviews() @@ -44,21 +33,20 @@ open class NEBaseHistoryMessageCell: UITableViewCell { guard let searchStr = searchText else { return } - if let range = resultText.findAllIndex(searchStr).first { - let attributedStr = NSMutableAttributedString(string: resultText) - // range必须要加,参数分别表示从索引几开始取几个字符 - attributedStr.addAttribute( - .foregroundColor, - value: rangeTextColor, - range: range - ) - subTitle.attributedText = attributedStr - } + let attributedStr = NSMutableAttributedString(string: resultText) + // range 表示从索引几开始取几个字符 + let range = attributedStr.mutableString.range(of: searchStr) + attributedStr.addAttribute( + .foregroundColor, + value: rangeTextColor, + range: range + ) + subTitle.attributedText = attributedStr title.text = message?.name timeLabel.text = message?.time - if let imageName = message?.avatar { + if let imageName = message?.avatar, !imageName.isEmpty { headImge.setTitle("") headImge.sd_setImage(with: URL(string: imageName), completed: nil) } else { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift index 78ed7443..1a036a8f 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift @@ -17,7 +17,7 @@ open class NEBaseTeamArrowSettingCell: NEBaseTeamSettingCell { super.init(coder: coder) } - override public func configure(_ anyModel: Any) { + override open func configure(_ anyModel: Any) { super.configure(anyModel) } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamAvatarViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamAvatarViewController.swift index cd3fb504..2a93848c 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamAvatarViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamAvatarViewController.swift @@ -21,6 +21,8 @@ open class NEBaseTeamAvatarViewController: NEBaseViewController, UICollectionVie public let tag = UILabel() public var iconUrls = TeamRouter.iconUrls + public var viewmodel = TeamAvatarViewModel() + public lazy var headerView: NEUserHeaderView = { let header = NEUserHeaderView(frame: .zero) header.translatesAutoresizingMaskIntoConstraints = false @@ -51,6 +53,7 @@ open class NEBaseTeamAvatarViewController: NEBaseViewController, UICollectionVie override open func viewDidLoad() { super.viewDidLoad() + viewmodel.getCurrentUserTeamMember(team?.teamId) setupUI() } @@ -74,7 +77,7 @@ open class NEBaseTeamAvatarViewController: NEBaseViewController, UICollectionVie headerView.heightAnchor.constraint(equalToConstant: 80.0), headerView.widthAnchor.constraint(equalToConstant: 80.0), ]) - if let url = team?.avatarUrl { + if let url = team?.avatarUrl, !url.isEmpty { headerView.sd_setImage(with: URL(string: url), completed: nil) headerUrl = url } @@ -127,6 +130,9 @@ open class NEBaseTeamAvatarViewController: NEBaseViewController, UICollectionVie if let mode = team?.updateInfoMode, mode == .all { return true } + if let member = viewmodel.currentTeamMember, member.type == .manager { + return true + } return false } @@ -147,10 +153,10 @@ open class NEBaseTeamAvatarViewController: NEBaseViewController, UICollectionVie NELog.infoLog(ModuleName + " " + self.className(), desc: #function + "CALLBACK " + (error?.localizedDescription ?? "no error")) weakSelf?.view.hideToastActivity() if let err = error as? NSError { - if err.code == 408 { + if err.code == noNetworkCode { weakSelf?.showToast(commonLocalizable("network_error")) } else { - weakSelf?.showToast(err.localizedDescription) + weakSelf?.showToast(localizable("failed_operation")) } } else { weakSelf?.team?.avatarUrl = weakSelf?.headerUrl diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamInfoViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamInfoViewController.swift index 2b552fe5..f7f8ee64 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamInfoViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamInfoViewController.swift @@ -42,11 +42,11 @@ open class NEBaseTeamInfoViewController: NEBaseViewController, UITableViewDelega } else { title = localizable("group_info") } - viewmodel.getData(team) } override open func viewDidLoad() { super.viewDidLoad() + viewmodel.getData(team) setupUI() } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamIntroduceViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamIntroduceViewController.swift index 619cc68d..eafa4c6e 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamIntroduceViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamIntroduceViewController.swift @@ -9,15 +9,13 @@ import UIKit @objcMembers open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDelegate { -// typealias SaveCompletion = () -> Void -// -// var block: SaveCompletion? - public var team: NIMTeam? public let textLimit = 100 public let repo = TeamRepo.shared public let backView = UIView() + public let viewmodel = TeamIntroduceViewModel() + public lazy var textView: UITextView = { let text = UITextView() text.translatesAutoresizingMaskIntoConstraints = false @@ -50,6 +48,7 @@ open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDe override open func viewDidLoad() { super.viewDidLoad() + viewmodel.getCurrentUserTeamMember(team?.teamId) setupUI() } @@ -93,6 +92,9 @@ open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDe if let mode = team?.updateInfoMode, mode == .all { return true } + if let member = viewmodel.currentTeamMember, member.type == .manager { + return true + } return false } @@ -113,8 +115,12 @@ open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDe desc: "CALLBACK updateTeamIntroduce " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error { - weakSelf?.showToast(err.localizedDescription) + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } } else { weakSelf?.team?.intro = text weakSelf?.navigationController?.popViewController(animated: true) diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift index 733b2d03..59cbf140 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift @@ -6,8 +6,22 @@ import NECommonUIKit import UIKit +protocol TeamMemberCellDelegate: NSObject { + func didClickRemoveButton(_ model: TeamMemberInfoModel?, _ index: Int) +} + @objcMembers open class NEBaseTeamMemberCell: UITableViewCell { + var currentModel: TeamMemberInfoModel? + + weak var delegate: TeamMemberCellDelegate? + + public var ownerWidth: NSLayoutConstraint? + + public var nameLabelRightMargin: NSLayoutConstraint? + + var index = 0 + lazy var headerView: NEUserHeaderView = { let header = NEUserHeaderView(frame: .zero) header.titleLabel.font = NEConstant.defaultTextFont(14) @@ -15,7 +29,6 @@ open class NEBaseTeamMemberCell: UITableViewCell { header.layer.cornerRadius = 21 header.clipsToBounds = true header.translatesAutoresizingMaskIntoConstraints = false - header.accessibilityIdentifier = "id.avatar" return header }() @@ -44,16 +57,20 @@ open class NEBaseTeamMemberCell: UITableViewCell { return label }() - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) + lazy var removeLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = localizable("team_member_remove") + label.font = UIFont.systemFont(ofSize: 14.0) + label.textAlignment = .center + return label + }() - // Configure the view for the selected state - } + lazy var removeBtn: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -74,25 +91,30 @@ open class NEBaseTeamMemberCell: UITableViewCell { headerView.heightAnchor.constraint(equalToConstant: 42), ]) - contentView.addSubview(ownerLabel) - NSLayoutConstraint.activate([ - ownerLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - ownerLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - ownerLabel.heightAnchor.constraint(equalToConstant: 25.0), - ownerLabel.widthAnchor.constraint(equalToConstant: 48.0), - ]) - + nameLabelRightMargin = nameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -116) contentView.addSubview(nameLabel) NSLayoutConstraint.activate([ nameLabel.leftAnchor.constraint(equalTo: headerView.rightAnchor, constant: 14.0), nameLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - nameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -70), + nameLabelRightMargin!, + ]) + + ownerWidth = ownerLabel.widthAnchor.constraint(equalToConstant: 48.0) + contentView.addSubview(ownerLabel) + NSLayoutConstraint.activate([ + ownerLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -70), + ownerLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + ownerLabel.heightAnchor.constraint(equalToConstant: 22.0), + ownerWidth!, ]) } func configure(_ model: TeamMemberInfoModel) { -// ownerLabel.isHidden = !isOwner(model.nimUser?.userId) - if let url = model.nimUser?.userInfo?.avatarUrl { + if let userId = model.nimUser?.userId, let user = ChatUserCache.getUserInfo(userId) { + model.nimUser = user + } + currentModel = model + if let url = model.nimUser?.userInfo?.avatarUrl, !url.isEmpty { headerView.sd_setImage(with: URL(string: url), completed: nil) headerView.setTitle("") } else { @@ -102,4 +124,19 @@ open class NEBaseTeamMemberCell: UITableViewCell { } nameLabel.text = model.atNameInTeam() } + + func setupRemoveButton() { + contentView.addSubview(removeBtn) + NSLayoutConstraint.activate([ + removeBtn.topAnchor.constraint(equalTo: contentView.topAnchor), + removeBtn.rightAnchor.constraint(equalTo: contentView.rightAnchor), + removeBtn.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + removeBtn.widthAnchor.constraint(equalToConstant: 100), + ]) + removeBtn.addTarget(self, action: #selector(didClickRemove), for: .touchUpInside) + } + + func didClickRemove() { + delegate?.didClickRemoveButton(currentModel, index) + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift new file mode 100644 index 00000000..b473143d --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift @@ -0,0 +1,66 @@ +//// 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 NEBaseTeamMemberSelectCell: UITableViewCell { + // check box image + public lazy var checkImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFit + imageView.image = coreLoader.loadImage("unselect") + return imageView + }() + + lazy var headerView: NEUserHeaderView = { + let header = NEUserHeaderView(frame: .zero) + header.titleLabel.font = NEConstant.defaultTextFont(14) + header.titleLabel.textColor = UIColor.white + header.clipsToBounds = true + header.translatesAutoresizingMaskIntoConstraints = false + header.accessibilityIdentifier = "id.avatar" + return header + }() + + lazy var nameLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = NEConstant.defaultTextFont(16.0) + label.textColor = .ne_darkText + label.accessibilityIdentifier = "id.userName" + return label + }() + + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + setupUI() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + open func setupUI() { + contentView.addSubview(headerView) + contentView.addSubview(nameLabel) + contentView.addSubview(checkImageView) + } + + open func configureMember(_ model: NESelectTeamMember?) { + checkImageView.isHighlighted = model?.isSelected ?? false + if let url = model?.member?.nimUser?.userInfo?.avatarUrl, !url.isEmpty { + headerView.sd_setImage(with: URL(string: url), completed: nil) + headerView.setTitle("") + } else { + headerView.image = nil + headerView.setTitle(model?.member?.showNickInTeam() ?? "") + headerView.backgroundColor = UIColor.colorWithString(string: model?.member?.nimUser?.userId) + } + nameLabel.text = model?.member?.atNameInTeam() + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamNameViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamNameViewController.swift index 62ae95f7..237e5d71 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamNameViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamNameViewController.swift @@ -10,7 +10,6 @@ import UIKit @objcMembers open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegate { public var team: NIMTeam? -// var user: NIMUser? public var type = ChangeType.TeamName public var teamMember: NIMTeamMember? public var repo = TeamRepo.shared @@ -18,6 +17,8 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat public let backView = UIView() + let viewModel = TeamNameViewModel() + public lazy var countLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -49,6 +50,7 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat override open func viewDidLoad() { super.viewDidLoad() + viewModel.getCurrentUserTeamMember(team?.teamId) setupUI() } @@ -117,6 +119,9 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat if let mode = team?.updateInfoMode, mode == .all { return true } + if let member = viewModel.currentTeamMember, member.type == .manager { + return true + } return false } @@ -136,7 +141,7 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat open func saveName() { guard let tid = team?.teamId else { - showToast(localizable("team_not_exist")) + showToast(localizable("failed_operation")) return } @@ -163,7 +168,7 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat repo.updateTeamName(tid, n) { error in weakSelf?.view.hideToastActivity() if let err = error { - weakSelf?.showToast(err.localizedDescription) + weakSelf?.showToast(localizable("failed_operation")) } else { weakSelf?.team?.teamName = n weakSelf?.navigationController?.popViewController(animated: true) @@ -176,7 +181,7 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat weakSelf?.view.hideToastActivity() if let err = error { - weakSelf?.showToast(err.localizedDescription) + weakSelf?.showToast(localizable("failed_operation")) } else { weakSelf?.navigationController?.popViewController(animated: true) } @@ -204,7 +209,7 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat // MARK: UITextViewDelegate - public func textViewDidChange(_ textView: UITextView) { + open func textViewDidChange(_ textView: UITextView) { if let _ = textView.markedTextRange { return } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift index 623a13cf..ec8fcff6 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift @@ -46,18 +46,7 @@ open class NEBaseTeamSettingCell: CornerCell { super.init(coder: coder) } - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } - - public func configure(_ anyModel: Any) { + open func configure(_ anyModel: Any) { if let m = anyModel as? SettingCellModel { model = m cornerType = m.cornerType diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift index 698da2d7..4668b0fd 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift @@ -12,7 +12,6 @@ open class NEBaseTeamSettingHeaderCell: NEBaseTeamSettingCell { let header = NEUserHeaderView(frame: .zero) header.translatesAutoresizingMaskIntoConstraints = false header.clipsToBounds = true - header.accessibilityIdentifier = "id.avatar" return header }() @@ -27,9 +26,9 @@ open class NEBaseTeamSettingHeaderCell: NEBaseTeamSettingCell { setupUI() } - override public func configure(_ anyModel: Any) { + override open func configure(_ anyModel: Any) { super.configure(anyModel) - if let url = model?.headerUrl { + if let url = model?.headerUrl, !url.isEmpty { headerView.sd_setImage(with: URL(string: url), completed: nil) headerView.setTitle("") } else { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift index a09f5922..a35e0c9d 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift @@ -26,7 +26,7 @@ open class NEBaseTeamSettingSelectCell: NEBaseTeamSettingCell { super.init(coder: coder) } - override public func configure(_ anyModel: Any) { + override open func configure(_ anyModel: Any) { super.configure(anyModel) subTitleLabel.text = model?.subTitle } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSwitchCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSwitchCell.swift index 8adf331b..a0a677c0 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSwitchCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSwitchCell.swift @@ -24,7 +24,7 @@ open class NEBaseTeamSettingSwitchCell: NEBaseTeamSettingCell { super.init(coder: coder) } - override public func configure(_ anyModel: Any) { + override open func configure(_ anyModel: Any) { super.configure(anyModel) if let open = model?.switchOpen { tSwitch.isOn = open @@ -37,7 +37,7 @@ open class NEBaseTeamSettingSwitchCell: NEBaseTeamSettingCell { tSwitch.addTarget(self, action: #selector(switchChange(_:)), for: .touchUpInside) } - public func switchChange(_ s: UISwitch) { + open func switchChange(_ s: UISwitch) { if let block = model?.swichChange { block(s.isOn) } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingViewController.swift index 193d82d5..75b580fc 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingViewController.swift @@ -51,7 +51,6 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi imageView.translatesAutoresizingMaskIntoConstraints = false imageView.clipsToBounds = true imageView.titleLabel.font = NEConstant.defaultTextFont(16.0) - imageView.accessibilityIdentifier = "id.avatar" return imageView }() @@ -97,7 +96,7 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) if let model = viewmodel.teamInfoModel { - if let url = model.team?.avatarUrl { + if let url = model.team?.avatarUrl, !url.isEmpty { teamHeader.sd_setImage(with: URL(string: url)) } if let name = model.team?.teamName { @@ -118,8 +117,12 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi ModuleName + " " + self.className, desc: "CALLBACK getTeamInfo " + (error?.localizedDescription ?? "no error") ) - if let err = error { - weakSelf?.showToast(err.localizedDescription) + if let err = error as? NSError { + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else { + weakSelf?.showToast(localizable("team_not_exist")) + } } else { if let type = weakSelf?.viewmodel.teamInfoModel?.team?.type { if type == .normal { @@ -133,21 +136,30 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } } } + if let type = weakSelf?.teamSettingType { + weakSelf?.viewmodel.teamSettingType = type + } weakSelf?.reloadSectionData() weakSelf?.contentTable.tableHeaderView = weakSelf?.getHeaderView() weakSelf?.contentTable.tableFooterView = weakSelf?.getFooterView() weakSelf?.contentTable.reloadData() - weakSelf?.userinfoCollection.reloadData() + weakSelf?.didRefreshUserinfoCollection() weakSelf?.checkoutAddShowOrHide() } } } // Do any additional setup after loading the view. setupUI() + + NotificationCenter.default.addObserver(self, selector: #selector(didRefreshUserinfoCollection), name: NENotificationName.updateFriendInfo, object: nil) } open func reloadSectionData() {} + open func didRefreshUserinfoCollection() { + userinfoCollection.reloadData() + } + open func setupUI() { view.backgroundColor = .ne_lightBackgroundColor view.addSubview(contentTable) @@ -241,13 +253,9 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi viewmodel.transferTeamOwner { error in weakSelf?.view.hideToastActivity() if let err = error as? NSError { - if err.code == 408 { - weakSelf?.showToast(commonLocalizable("network_error")) - } else { - weakSelf?.showToast(err.localizedDescription) - } + weakSelf?.didError(err) } else { - weakSelf?.navigationController?.popViewController(animated: true) + NotificationCenter.default.post(name: NotificationName.popGroupChatVC, object: nil) } } return @@ -372,20 +380,26 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi func didAddUserAndRefreshUI(_ accids: [String], _ tid: String) { weak var weakSelf = self view.makeToastActivity(.center) - viewmodel.repo.inviteUser(accids, tid, nil, nil) { error, members in + viewmodel.inviterUsers(accids, tid) { error, members in if let err = error { weakSelf?.view.hideToastActivity() - weakSelf?.showToast(err.localizedDescription) + if err.code == noNetworkCode { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } } else { print("add users success : ", members as Any) if let ms = members, let model = weakSelf?.viewmodel.teamInfoModel { weakSelf?.viewmodel.repo.splitGroupMember(ms, model) { error, team in weakSelf?.view.hideToastActivity() - if let e = error { - weakSelf?.showToast(e.localizedDescription) + if let err = error as? NSError { + weakSelf?.didError(err) } else { weakSelf?.refreshMemberCount() - weakSelf?.userinfoCollection.reloadData() + weakSelf?.didRefreshUserinfoCollection() weakSelf?.checkoutAddShowOrHide() } } @@ -405,8 +419,8 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi desc: "CALLBACK inviteUser " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error { - weakSelf?.showToast(err.localizedDescription) + if let err = error as? NSError { + weakSelf?.didError(err) } else { weakSelf?.showToast(localizable("invite_has_send")) } @@ -424,29 +438,22 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi ) weakSelf?.view.hideToastActivity() if let err = error as? NSError { - if err.code == 408 { - weakSelf?.showToast(commonLocalizable("network_error")) - } else { - weakSelf?.showToast(err.localizedDescription) - } + weakSelf?.didError(err) } else { - weakSelf?.navigationController?.popViewController(animated: true) + NotificationCenter.default.post(name: NotificationName.popGroupChatVC, object: nil) } } } } func refreshMemberCount() { - if let count = viewmodel.teamInfoModel?.users.count { + if let count = viewmodel.teamInfoModel?.team?.memberNumber { memberCountLabel.text = "\(count)" } } func leaveTeam() { if let tid = teamId { - // 需要先于 SDK 回调进行通知 - NotificationCenter.default.post(name: NotificationName.leaveTeamBySelf, object: true) - view.makeToastActivity(.center) viewmodel.quitTeam(tid) { [weak self] error in NELog.infoLog( @@ -455,27 +462,18 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi ) self?.view.hideToastActivity() if let err = error as? NSError { - // 退出群聊失败则需要重置通知 - NotificationCenter.default.post(name: NotificationName.leaveTeamBySelf, object: false) - if err.code == 803 { - self?.navigationController?.popViewController(animated: true) - } else if err.code == 408 { - self?.showToast(commonLocalizable("network_error")) - } else { - self?.showToast(err.localizedDescription) - } + self?.didError(err) } else { - // 会话列表中移除该群聊 - let session = NIMSession(tid, type: .team) - if let stickInfo = self?.viewmodel.getTopSessionInfo(session) { - self?.viewmodel.removeStickTop(params: stickInfo) { err, _ in - NELog.infoLog( - ModuleName + " " + (self?.className ?? "TeamSettingViewController"), - desc: "CALLBACK removeStickTop " + (error?.localizedDescription ?? "no error") - ) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { + self?.viewmodel.getTeamInfo(tid) { _ in + if self?.viewmodel.teamInfoModel?.team?.memberNumber == 1, + self?.viewmodel.isOwner() == true { + self?.dismissTeam() + } else { + NotificationCenter.default.post(name: NotificationName.popGroupChatVC, object: nil) + } } } - self?.navigationController?.popViewController(animated: true) } } } @@ -489,20 +487,22 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } func didError(_ error: NSError) { - if error.code == 408 { + if error.code == noNetworkCode { showToast(commonLocalizable("network_error")) } else { - showToast(error.localizedDescription) + showToast(localizable("failed_operation")) } } func didNeedRefreshUI() { contentTable.reloadData() refreshMemberCount() - userinfoCollection.reloadData() + didRefreshUserinfoCollection() checkoutAddShowOrHide() } + open func didClickTeamManage() {} + open func checkoutAddShowOrHide() {} func updateInviteModeOwnerAction(_ model: SettingCellModel) { @@ -515,11 +515,7 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi ) weakSelf?.view.hideToastActivity() if let err = error as? NSError { - if err.code == 408 { - weakSelf?.showToast(commonLocalizable("network_error")) - } else { - weakSelf?.showToast(err.localizedDescription) - } + weakSelf?.didError(err) } else { weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .manager model.subTitle = localizable("team_owner") @@ -538,11 +534,7 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi ) weakSelf?.view.hideToastActivity() if let err = error as? NSError { - if err.code == 408 { - weakSelf?.showToast(commonLocalizable("network_error")) - } else { - weakSelf?.showToast(err.localizedDescription) - } + weakSelf?.didError(err) } else { weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .all model.subTitle = localizable("team_all") @@ -595,11 +587,7 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi ) weakSelf?.view.hideToastActivity() if let err = error as? NSError { - if err.code == 408 { - weakSelf?.showToast(commonLocalizable("network_error")) - } else { - weakSelf?.showToast(err.localizedDescription) - } + weakSelf?.didError(err) } else { weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .manager model.subTitle = localizable("team_owner") @@ -619,11 +607,7 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi ) weakSelf?.view.hideToastActivity() if let err = error as? NSError { - if err.code == 408 { - weakSelf?.showToast(commonLocalizable("network_error")) - } else { - weakSelf?.showToast(err.localizedDescription) - } + weakSelf?.didError(err) } else { weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .all model.subTitle = localizable("team_all") @@ -667,4 +651,14 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func didClickChangeNick() {} open func didClickHistoryMessage() {} + + open func getManaterUsers() -> [TeamMemberInfoModel] { + var members = [TeamMemberInfoModel]() + viewmodel.teamInfoModel?.users.forEach { model in + if model.teamMember?.type == .manager { + members.append(model) + } + } + return members + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift index f1855812..15bbe475 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift @@ -16,7 +16,10 @@ open class NEBaseTeamUserCell: UICollectionViewCell { if let name = user?.showNickInTeam() { userHeader.setTitle(name) } - if let url = user?.nimUser?.userInfo?.avatarUrl { + if let userId = user?.nimUser?.userId, let nimUser = ChatUserCache.getUserInfo(userId) { + user?.nimUser = nimUser + } + if let url = user?.nimUser?.userInfo?.avatarUrl, !url.isEmpty { userHeader.sd_setImage(with: URL(string: url), completed: nil) userHeader.setTitle("") } else if let id = user?.nimUser?.userId { @@ -31,7 +34,6 @@ open class NEBaseTeamUserCell: UICollectionViewCell { header.translatesAutoresizingMaskIntoConstraints = false header.titleLabel.font = NEConstant.defaultTextFont(11.0) header.clipsToBounds = true - header.accessibilityIdentifier = "id.avatar" return header }() diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift index 289a9d22..22af44a5 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift @@ -7,18 +7,7 @@ import UIKit @objcMembers open class TeamSettingRightCustomCell: TeamSettingSubtitleCell { - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } - - override public func configure(_ anyModel: Any) { + override open func configure(_ anyModel: Any) { super.configure(anyModel) if let icon = model?.rightCustomViewIcon, icon.count > 0 { customRightView.isHidden = false @@ -54,7 +43,7 @@ open class TeamSettingRightCustomCell: TeamSettingSubtitleCell { return btn }() - public func customRightViewClick() { + open func customRightViewClick() { if let block = model?.customViewClick { block() } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift index c041c1d4..115bd1b5 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift @@ -9,17 +9,6 @@ import UIKit open class TeamSettingSubtitleCell: NEBaseTeamSettingCell { public var titleWidthAnchor: NSLayoutConstraint? - override public func awakeFromNib() { - super.awakeFromNib() - // Initialization code - } - - override public func setSelected(_ selected: Bool, animated: Bool) { - super.setSelected(selected, animated: animated) - - // Configure the view for the selected state - } - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none @@ -56,7 +45,7 @@ open class TeamSettingSubtitleCell: NEBaseTeamSettingCell { ]) } - override public func configure(_ anyModel: Any) { + override open func configure(_ anyModel: Any) { super.configure(anyModel) if let m = anyModel as? SettingCellModel { titleWidthAnchor?.constant = m.titleWidth diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.swift new file mode 100644 index 00000000..2073ab57 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.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 NEChatKit +import NIMSDK +import UIKit + +@objcMembers +open class TeamAvatarViewModel: NSObject { + let repo = ChatRepo.shared + var currentTeamMember: NIMTeamMember? + + func getCurrentUserTeamMember(_ teamId: String?) { + if let tid = teamId { + let currentUserAccid = IMKitClient.instance.imAccid() + currentTeamMember = repo.getTeamMemberList(userId: currentUserAccid, teamId: tid) + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift index 9c03c911..d3c5a532 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift @@ -7,7 +7,7 @@ import NECoreIMKit import NIMSDK @objcMembers -public class TeamInfoViewModel: NSObject { +open class TeamInfoViewModel: NSObject { var cellDatas = [SettingCellModel]() func getData(_ team: NIMTeam?) { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.swift new file mode 100644 index 00000000..381db61d --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.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 NEChatKit +import NIMSDK +import UIKit + +@objc +@objcMembers +open class TeamIntroduceViewModel: NSObject { + let repo = ChatRepo.shared + var currentTeamMember: NIMTeamMember? + + func getCurrentUserTeamMember(_ teamId: String?) { + if let tid = teamId { + let currentUserAccid = IMKitClient.instance.imAccid() + currentTeamMember = repo.getTeamMemberList(userId: currentUserAccid, teamId: tid) + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManageViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManageViewModel.swift new file mode 100644 index 00000000..ba330f3b --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManageViewModel.swift @@ -0,0 +1,230 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECommonKit +import NIMSDK +import UIKit + +public protocol TeamManageViewModelDelegate: NSObjectProtocol { + func didChangeInviteModeClick(_ model: SettingCellModel) + func didUpdateTeamInfoClick(_ model: SettingCellModel) + func didAtPermissionClick(_ model: SettingCellModel) + func didManagerClick() + func didRefreshData() +} + +@objc +@objcMembers +open class TeamManageViewModel: NSObject, NIMTeamManagerDelegate { + let repo = TeamRepo.shared + + let chatRepo = ChatRepo.shared + + public var teamInfoModel: TeamInfoModel? + + var sectionData = [SettingSectionModel]() + + public var managerUsers = [TeamMemberInfoModel]() + + var isRequestData = false + + weak var delegate: TeamManageViewModelDelegate? + + override public init() { + super.init() + NIMSDK.shared().teamManager.add(self) + } + + open func getSectionData() { + sectionData.removeAll() + if teamInfoModel?.team?.owner == IMKitClient.instance.imAccid() { + sectionData.append(getTopSection()) + } + sectionData.append(getMidSection()) + } + + open func getTeamInfo(_ tid: String, _ completion: @escaping (Error?) -> Void) { + if isRequestData == true { + return + } + weak var weakSelf = self + isRequestData = true + repo.fetchTeamInfo(tid) { error, teamInfo in + weakSelf?.isRequestData = false + if error == nil { + weakSelf?.managerUsers.removeAll() + } + teamInfo?.users.forEach { model in + if model.teamMember?.type == .manager { + weakSelf?.managerUsers.append(model) + } + } + weakSelf?.teamInfoModel = teamInfo + weakSelf?.getSectionData() + completion(error) + } + } + + open func getTopSection() -> SettingSectionModel { + NELog.infoLog(ModuleName + " " + className(), desc: #function) + weak var weakSelf = self + let model = SettingSectionModel() + let manager = SettingCellLabelArrowModel() + manager.cellName = localizable("manage_manger") + manager.type = SettingCellType.SettingArrowCell.rawValue + manager.rowHeight = 56 + manager.arrowLabelText = "\(managerUsers.count)" + model.cellModels.append(contentsOf: [manager]) + model.setCornerType() + manager.cellClick = { + weakSelf?.delegate?.didManagerClick() + } + return model + } + + open func getMidSection() -> SettingSectionModel { + NELog.infoLog(ModuleName + " " + className(), desc: #function) + weak var weakSelf = self + let model = SettingSectionModel() + + let editTeam = SettingCellModel() + editTeam.cellName = localizable("who_edit_team_info") + editTeam.type = SettingCellType.SettingSelectCell.rawValue + editTeam.rowHeight = 73 + + if let updateMode = teamInfoModel?.team?.updateInfoMode, updateMode == .all { + editTeam.subTitle = localizable("team_all") + } else { + editTeam.subTitle = localizable("team_owner_and_manager") + } + + editTeam.cellClick = { + weakSelf?.delegate?.didUpdateTeamInfoClick(editTeam) + } + + let invitePermission = SettingCellModel() + invitePermission.cellName = localizable("who_edit_user_info") + invitePermission.type = SettingCellType.SettingSelectCell.rawValue + invitePermission.rowHeight = 73 + if let inviteMode = teamInfoModel?.team?.inviteMode, inviteMode == .all { + invitePermission.subTitle = localizable("team_all") + } else { + invitePermission.subTitle = localizable("team_owner_and_manager") + } + + invitePermission.cellClick = { + weakSelf?.delegate?.didChangeInviteModeClick(invitePermission) + } + + let atAll = SettingCellModel() + atAll.cellName = localizable("who_at_all") + atAll.type = SettingCellType.SettingSelectCell.rawValue + atAll.rowHeight = 73 + atAll.subTitle = localizable("team_owner_and_manager") + atAll.subTitle = getTeamAtPermissionValue() + + atAll.cellClick = { + weakSelf?.delegate?.didAtPermissionClick(atAll) + } + + model.cellModels.append(contentsOf: [editTeam, invitePermission, atAll]) + model.setCornerType() + + return model + } + + open func updateTeamAtPermission(_ isManager: Bool, _ completion: @escaping (Error?) -> Void) { + let value = isManager == true ? allowAtManagerValue : allowAtAllValue + guard let tid = teamInfoModel?.team?.teamId else { + return + } + let latestTeam = repo.getTeam(tid) + if let custom = latestTeam?.clientCustomInfo { + if var dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { + dic[keyAllowAtAll] = value + let info = NECommonUtil.getJSONStringFromDictionary(dic) + repo.updateTeamCustomInfo(info, tid) { error in + completion(error) + } + } + } else { + var dic = [String: Any]() + dic[keyAllowAtAll] = value + let info = NECommonUtil.getJSONStringFromDictionary(dic) + repo.updateTeamCustomInfo(info, tid) { error in + completion(error) + } + } + } + + func getTeamAtPermissionValue() -> String { + if let custom = teamInfoModel?.team?.clientCustomInfo { + if let dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { + if let value = dic[keyAllowAtAll] as? String { + if value == allowAtManagerValue { + return localizable("team_owner_and_manager") + } + } + } + } + return localizable("team_all") + } + + open func onTeamMemberChanged(_ team: NIMTeam) { + updateTeamInfo(team) + } + + open func onTeamUpdated(_ team: NIMTeam) { + updateTeamInfo(team) + } + + func sendTipNoti(_ isManagerPermission: Bool, _ completion: @escaping (Error?) -> Void) { + guard let teamId = teamInfoModel?.team?.teamId else { + return + } + let session = NIMSession(teamId, type: .team) + let tipContent = isManagerPermission ? localizable("team_tip_noti_at_manager") : localizable("team_tip_noti_at_all") + let tipMessage = NIMMessage() + let object = NIMTipObject(attach: nil, callbackExt: nil) + tipMessage.messageObject = object + tipMessage.text = tipContent + let setting = NIMMessageSetting() + setting.shouldBeCounted = false + setting.apnsEnabled = false + tipMessage.setting = setting + chatRepo.sendMessage(message: tipMessage, session: session) { error in + completion(error) + } + } + + private func updateTeamInfo(_ team: NIMTeam) { + guard let tid = teamInfoModel?.team?.teamId else { + return + } + + if isRequestData == true { + return + } + isRequestData = true + repo.fetchTeamInfo(tid) { [weak self] error, info in + if error == nil, info != nil { + self?.teamInfoModel = info + self?.managerUsers.removeAll() + info?.users.forEach { userInfo in + if userInfo.teamMember?.type == .manager { + self?.managerUsers.append(userInfo) + } + } + + self?.sectionData.removeAll() + self?.getSectionData() + self?.delegate?.didRefreshData() + + print("onTeamMemberChanged managers count : ", self?.managerUsers.count as Any) + } + self?.isRequestData = false + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift new file mode 100644 index 00000000..d768b721 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift @@ -0,0 +1,90 @@ +//// 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 + +public protocol TeamManagerListViewModelDelegate: NSObject { + func didNeedReloadData() +} + +open class TeamManagerListViewModel: NSObject, NIMTeamManagerDelegate { + let repo = TeamRepo.shared + + public var currentMember: NIMTeamMember? + + public var managers = [TeamMemberInfoModel]() + + weak var delegate: TeamManagerListViewModelDelegate? + + public var teamId: String? + + override public init() { + super.init() + NIMSDK.shared().teamManager.add(self) + NotificationCenter.default.addObserver(self, selector: #selector(refreshData), name: NENotificationName.updateFriendInfo, object: nil) + } + + deinit { + NIMSDK.shared().teamManager.remove(self) + } + + open func getManagerDatas(_ tid: String, _ completion: @escaping (Error?) -> Void) { + repo.fetchTeamInfo(tid) { [weak self] error, teamInfo in + if error == nil { + self?.managers.removeAll() + teamInfo?.users.forEach { model in + if model.teamMember?.type == .manager { + self?.managers.append(model) + } + } + completion(nil) + } else { + completion(error) + } + } + } + + open func addTeamManager(_ teamId: String, _ uids: [String], _ completion: @escaping (Error?) -> Void) { + repo.addTeamManagers(teamId, uids) { error in + completion(error) + } + } + + open func removeTeamManager(_ teamId: String, _ uids: [String], _ completion: @escaping (Error?) -> Void) { + repo.removeTeamManagers(teamId, uids) { error in + completion(error) + } + } + + open func getCurrentMember(_ teamId: String) { + currentMember = repo.getMemberInfo(IMKitClient.instance.imAccid(), teamId) + } + + @objc func refreshData() { + guard let tid = teamId else { + return + } + getManagerDatas(tid) { [weak self] error in + if error == nil { + self?.delegate?.didNeedReloadData() + } + } + } + + public func onTeamMemberChanged(_ team: NIMTeam) { + guard let tid = teamId else { + return + } + if tid != team.teamId { + return + } + getManagerDatas(tid) { [weak self] error in + if error != nil { + self?.delegate?.didNeedReloadData() + } + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift new file mode 100644 index 00000000..7b863baa --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift @@ -0,0 +1,135 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIMKit +import NIMSDK +import UIKit + +public protocol TeamMemberSelectViewModelDelegate: NSObject { + func didNeedRefresh() +} + +class TeamMemberSelectViewModel: NSObject, NIMTeamManagerDelegate { + let repo = TeamRepo.shared + + var datas = [NESelectTeamMember]() + + var showDatas = [NESelectTeamMember]() + + var teamInfoModel: TeamInfoModel? + + weak var delegate: TeamMemberSelectViewModelDelegate? + + var selectDic = [String: TeamMemberInfoModel]() // key 值为用户 id + + var isRequest = false + + var managerSet = Set() + + override init() { + super.init() + NIMSDK.shared().teamManager.add(self) + } + + deinit { + NIMSDK.shared().teamManager.remove(self) + } + + func getTeamInfo(_ teamId: String, _ completion: @escaping (Error?) -> Void) { + if isRequest == true { + return + } + weak var weakSelf = self + isRequest = true + repo.fetchTeamInfo(teamId) { error, teamInfo in + weakSelf?.isRequest = false + if error == nil { + weakSelf?.datas.removeAll() + weakSelf?.showDatas.removeAll() + } + weakSelf?.teamInfoModel = teamInfo + weakSelf?.getData() + completion(error) + } + } + + func getData() { + var temFilters = Set() + selectDic.forEach { (key: String, value: TeamMemberInfoModel) in + temFilters.insert(key) + } + managerSet.removeAll() + + teamInfoModel?.users.forEach { [weak self] userModel in + if let uid = userModel.nimUser?.userId { + temFilters.remove(uid) + if uid == IMKitClient.instance.imAccid() { + return + } + if uid == self?.teamInfoModel?.team?.owner { + return + } + if userModel.teamMember?.type == .manager { + self?.managerSet.insert(uid) + self?.selectDic.removeValue(forKey: uid) + return + } + } + let selectMember = NESelectTeamMember() + selectMember.member = userModel + self?.datas.append(selectMember) + self?.showDatas.append(selectMember) + } + temFilters.forEach { uid in + selectDic.removeValue(forKey: uid) + } + datas.forEach { member in + if let accid = member.member?.nimUser?.userId { + if selectDic.contains(where: { (key: String, value: TeamMemberInfoModel) in + key == accid + }) { + member.isSelected = true + } + } + } + } + + func searchAllData(_ searchText: String) -> [NESelectTeamMember] { + let result = datas.filter { findContainStr(searchText, $0) } + return result + } + + func searchShowData(_ searchText: String) -> [NESelectTeamMember] { + let result = showDatas.filter { findContainStr(searchText, $0) } + return result + } + + func findContainStr(_ text: String, _ selectModel: NESelectTeamMember) -> Bool { + if let uid = selectModel.member?.nimUser?.userId, uid.contains(text) { + return true + } else if let nick = selectModel.member?.nimUser?.userInfo?.nickName, nick.contains(text) { + return true + } else if let alias = selectModel.member?.nimUser?.alias, alias.contains(text) { + return true + } else if let tNick = selectModel.member?.teamMember?.nickname, tNick.contains(text) { + return true + } + return false + } + + func onTeamMemberChanged(_ team: NIMTeam) { + guard let tid = teamInfoModel?.team?.teamId else { + return + } + if tid != team.teamId { + return + } + getTeamInfo(tid) { [weak self] error in + if error == nil { + self?.delegate?.didNeedRefresh() + } + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift new file mode 100644 index 00000000..055f439f --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift @@ -0,0 +1,82 @@ +//// 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 + +protocol TeamMembersViewModelDelegate: NSObject { + func didNeedRefreshUI() +} + +class TeamMembersViewModel: NSObject { + weak var delegate: TeamMembersViewModelDelegate? + + var datas = [TeamMemberInfoModel]() + + let repo = TeamRepo.shared + public var currentMember: NIMTeamMember? + + func getMemberInfo(_ teamId: String) { + currentMember = repo.getMemberInfo(IMKitClient.instance.imAccid(), teamId) + } + + func removeTeamMember(_ teamdId: String, _ uids: [String], _ completion: @escaping (NSError?) -> Void) { + repo.removeMembers(teamdId, uids) { error in + completion(error as NSError?) + } + } + + func setShowDatas(_ memberDatas: [TeamMemberInfoModel]?) { + var owner: TeamMemberInfoModel? + var managers = [TeamMemberInfoModel]() + var normalMembers = [TeamMemberInfoModel]() + + memberDatas?.forEach { model in + if model.teamMember?.type == .owner { + owner = model + } else if model.teamMember?.type == .manager { + managers.append(model) + } else { + normalMembers.append(model) + } + } + + datas.removeAll() + if let findOwner = owner { + datas.append(findOwner) + } + // managers 根据 时间排序 排序 + managers.sort { model1, model2 in + if let time1 = model1.teamMember?.createTime, let time2 = model2.teamMember?.createTime { + return time2 > time1 + } + return false + } + // normalMembers 根据 时间排序 排序 + normalMembers.sort { model1, model2 in + if let time1 = model1.teamMember?.createTime, let time2 = model2.teamMember?.createTime { + return time2 > time1 + } + return false + } + datas.append(contentsOf: managers) + datas.append(contentsOf: normalMembers) + delegate?.didNeedRefreshUI() + } + + func removeModel(_ model: TeamMemberInfoModel?) { + guard let rmModel = model else { + return + } + datas.removeAll(where: { model in + if let rmUid = rmModel.nimUser?.userId, let uid = model.nimUser?.userId { + if rmUid == uid { + return true + } + } + return false + }) + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.swift new file mode 100644 index 00000000..bf92a566 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.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 NEChatKit +import NIMSDK +import UIKit + +@objc +@objcMembers +open class TeamNameViewModel: NSObject { + let repo = ChatRepo.shared + var currentTeamMember: NIMTeamMember? + + func getCurrentUserTeamMember(_ teamId: String?) { + if let tid = teamId { + let currentUserAccid = IMKitClient.instance.imAccid() + currentTeamMember = repo.getTeamMemberList(userId: currentUserAccid, teamId: tid) + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift index 9f850d6d..7ecec906 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift @@ -15,10 +15,11 @@ protocol TeamSettingViewModelDelegate: NSObjectProtocol { func didNeedRefreshUI() func didError(_ error: NSError) func didClickMark() + func didClickTeamManage() } @objcMembers -public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { +open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { public var sectionData = [SettingSectionModel]() public var searchResultInfos: [HistoryMessageModel]? @@ -33,6 +34,8 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { private let className = "TeamSettingViewModel" + public var teamSettingType: TeamSettingType = .Discuss + override public init() { super.init() NIMSDK.shared().teamManager.add(self) @@ -52,7 +55,6 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return } sectionData.append(getThreeSection()) - sectionData.append(getFourSection()) } } @@ -110,7 +112,7 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { } } } else { -// weakSelf?.repo.updateNoti(.none, tid) + // weakSelf?.repo.updateNoti(.none, tid) weakSelf?.repo.setTeamNotify(.none, tid) { error in if let err = error as? NSError { weakSelf?.delegate?.didNeedRefreshUI() @@ -137,9 +139,14 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { if let tid = weakSelf?.teamInfoModel?.team?.teamId { let session = NIMSession(tid, type: .team) if isOpen { + // 不存在最近会话的置顶,先创建最近会话 + if weakSelf?.getRecenterSession() == nil { + NELog.infoLog(weakSelf?.className() ?? "", desc: #function + "addRecentetSession") + weakSelf?.addRecentetSession() + } let params = NIMAddStickTopSessionParams(session: session) weakSelf?.repo.addStickTop(params: params) { error, info in - print("add stick : ", error as Any) + NELog.infoLog(weakSelf?.className() ?? "", desc: #function + "addStickTop error : \(error?.localizedDescription ?? "") ") if let err = error { weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) @@ -150,7 +157,7 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { } else { if let info = weakSelf?.repo.getTopSessionInfo(session) { weakSelf?.repo.removeStickTop(params: info) { error, info in - print("remote stick : ", error as Any) + NELog.infoLog(weakSelf?.className() ?? "", desc: #function + "removeStickTop error : \(error?.localizedDescription ?? "") ") if let err = error { weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) @@ -163,12 +170,21 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { } } - model.cellModels.append(contentsOf: [ - mark, // 标记 - history, // 历史记录 - remind, // 开启消息提醒 - setTop, // 聊天置顶 - ]) + let nick = SettingCellModel() + nick.cellName = localizable("team_nick") + nick.type = SettingCellType.SettingArrowCell.rawValue + nick.cellClick = { + weakSelf?.delegate?.didClickChangeNick() + } + + model.cellModels.append(mark) + model.cellModels.append(history) + model.cellModels.append(remind) + model.cellModels.append(setTop) + if isNormalTeam() == false { + model.cellModels.append(nick) + } + model.setCornerType() return model } @@ -177,14 +193,7 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { private func getThreeSection() -> SettingSectionModel { NELog.infoLog(ModuleName + " " + className, desc: #function) let model = SettingSectionModel() - weak var weakSelf = self - let nick = SettingCellModel() - nick.cellName = localizable("team_nick") - nick.type = SettingCellType.SettingArrowCell.rawValue - nick.cellClick = { - weakSelf?.delegate?.didClickChangeNick() - } let forbiddenWords = SettingCellModel() forbiddenWords.cellName = localizable("team_no_speak") @@ -208,10 +217,21 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { } } - model.cellModels.append(nick) + // 群管理 + let teamManager = SettingCellModel() + teamManager.cellName = localizable("manage_team") + teamManager.type = SettingCellType.SettingArrowCell.rawValue + teamManager.cellClick = { + weakSelf?.delegate?.didClickTeamManage() + } + if isOwner() { model.cellModels.append(forbiddenWords) } + + if isOwner() || isManager() { + model.cellModels.append(teamManager) + } model.setCornerType() return model } @@ -249,45 +269,6 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { modifyPermission.cellClick = { weakSelf?.delegate?.didUpdateTeamInfoClick(modifyPermission) } - // 产品策略调整,暂时移除该功能 - /* - let agreePermission = SettingCellModel() - agreePermission.cellName = localizable("agree") - agreePermission.type = SettingCellType.SettingSwitchCell.rawValue - if let inviteMode = teamInfoModel?.team?.beInviteMode, inviteMode == .needAuth { - agreePermission.switchOpen = true - } - agreePermission.swichChange = { isOpen in - if let tid = weakSelf?.teamInfoModel?.team?.teamId { - if isOpen == true { - weakSelf?.repo.updateBeInviteMode(.needAuth, tid) { error in - print("join mode : ", error as Any) - if let err = error { - agreePermission.switchOpen = false - weakSelf?.delegate?.didNeedRefreshUI() - weakSelf?.delegate?.didError(err) - } else { - weakSelf?.teamInfoModel?.team?.joinMode = .needAuth - agreePermission.switchOpen = true - } - } - - } else { - weakSelf?.repo.updateBeInviteMode(.noAuth, tid) { error in - print("join mode : ", error as Any) - if let err = error { - agreePermission.switchOpen = true - weakSelf?.delegate?.didNeedRefreshUI() - weakSelf?.delegate?.didError(err) - } else { - weakSelf?.teamInfoModel?.team?.joinMode = .noAuth - agreePermission.switchOpen = false - } - } - } - } - } - */ if isOwner() { model.cellModels.append(contentsOf: [invitePermission, modifyPermission]) @@ -302,12 +283,6 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { weak var weakSelf = self repo.fetchTeamInfo(teamId) { error, teamInfo in weakSelf?.teamInfoModel = teamInfo - print("get team info user: ", teamInfo?.users as Any) - teamInfo?.users.forEach { member in - print("team meber accid : ", member.nimUser?.userId as Any) - } - print("get team info team: ", teamInfo?.team as Any) - print("get team info error: ", error as Any) if error == nil { weakSelf?.getData() weakSelf?.getCurrentMember(IMKitClient.instance.imAccid(), teamId) @@ -321,19 +296,19 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { repo.dismissTeam(teamId, completion) } - public func quitTeam(_ teamId: String, _ completion: @escaping (Error?) -> Void) { + open func quitTeam(_ teamId: String, _ completion: @escaping (Error?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") repo.quitTeam(teamId, completion) } - public func getTopSessionInfo(_ session: NIMSession) -> NIMStickTopSessionInfo { + open func getTopSessionInfo(_ session: NIMSession) -> NIMStickTopSessionInfo { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(session.sessionId)") return repo.getTopSessionInfo(session) } - public func removeStickTop(params: NIMStickTopSessionInfo, - _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) - -> Void) { + open func removeStickTop(params: NIMStickTopSessionInfo, + _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) + -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(params.session.sessionId)") repo.removeStickTop(params: params, completion) } @@ -349,6 +324,7 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { func isOwner() -> Bool { NELog.infoLog(ModuleName + " " + className, desc: #function) + if let accid = teamInfoModel?.team?.owner { if IMKitClient.instance.isMySelf(accid) { return true @@ -357,9 +333,21 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return false } + func isManager() -> Bool { + if let tid = teamInfoModel?.team?.teamId, let currentTeamMebmer = repo.getMemberInfo(IMKitClient.instance.imAccid(), tid) { + if currentTeamMebmer.type == .manager { + return true + } + } + return false + } + private func sampleMemberId(arr: [TeamMemberInfoModel], owner: String) -> String? { var index = arc4random_uniform(UInt32(arr.count)) while arr[Int(index)].teamMember?.userId == owner { + if arr.count == 1 { + return owner + } index = arc4random_uniform(UInt32(arr.count)) } return arr[Int(index)].teamMember?.userId @@ -384,33 +372,41 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { userId = sampleOwnerId } + if userId == NIMSDK.shared().loginManager.currentAccount() { + dismissTeam(teamId, completion) + return + } + NIMSDK.shared().teamManager.transferManager(withTeam: teamId, newOwnerId: userId, isLeave: true) { error in completion(error) } } - public func updateInfoMode(_ mode: NIMTeamUpdateInfoMode, _ teamId: String, - _ completion: @escaping (Error?) -> Void) { + open func updateInfoMode(_ mode: NIMTeamUpdateInfoMode, _ teamId: String, + _ completion: @escaping (Error?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", mode:\(mode.rawValue)") repo.updateTeamInfoPrivilege(mode, teamId, completion) } - public func updateInviteMode(_ mode: NIMTeamInviteMode, _ teamId: String, - _ completion: @escaping (Error?) -> Void) { + open func updateInviteMode(_ mode: NIMTeamInviteMode, _ teamId: String, + _ completion: @escaping (Error?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", mode:\(mode.rawValue)") repo.updateInviteMode(mode, teamId, completion) } - public func isNormalTeam() -> Bool { + open func isNormalTeam() -> Bool { NELog.infoLog(ModuleName + " " + className, desc: #function) if let type = teamInfoModel?.team?.type, type == .normal { return true } + if teamInfoModel?.team?.clientCustomInfo?.contains(discussTeamKey) == true { + return true + } return false } - public func searchMessages(_ session: NIMSession, option: NIMMessageSearchOption, - _ completion: @escaping (NSError?, [HistoryMessageModel]?) -> Void) { + open func searchMessages(_ session: NIMSession, option: NIMMessageSearchOption, + _ completion: @escaping (NSError?, [HistoryMessageModel]?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", session:\(session.sessionId)") weak var weakSelf = self repo.searchMessages(session, option: option) { error, messages in @@ -423,9 +419,8 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { } } - public func onTeamMemberRemoved(_ team: NIMTeam, withMembers memberIDs: [String]?) { + open func onTeamMemberRemoved(_ team: NIMTeam, withMembers memberIDs: [String]?) { if let accids = memberIDs { - weak var weakSelf = self accids.forEach { accid in if let users = teamInfoModel?.users { for (i, m) in users.enumerated() { @@ -439,9 +434,41 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { } } - public func onTeamMemberChanged(_ team: NIMTeam) {} + public func onTeamMemberChanged(_ team: NIMTeam) { + if let tid = teamInfoModel?.team?.teamId, tid != team.teamId { + return + } + teamInfoModel?.team = team + getCurrentMember(IMKitClient.instance.imAccid(), teamInfoModel?.team?.teamId) + getData() + delegate?.didNeedRefreshUI() + } - public func onTeamUpdated(_ team: NIMTeam) { + open func onTeamUpdated(_ team: NIMTeam) { + if let teamId = teamInfoModel?.team?.teamId, teamId != team.teamId { + return + } teamInfoModel?.team = team } + + open func inviterUsers(_ accids: [String], _ tid: String, _ completion: @escaping (NSError?, [NIMTeamMember]?) -> Void) { + repo.inviteUser(accids, tid, nil, nil) { error, members in + completion(error as NSError?, members) + } + } + + public func addRecentetSession() { + if let tid = teamInfoModel?.team?.teamId { + let currentSession = NIMSession(tid, type: .team) + repo.addRecentSession(currentSession) + } + } + + public func getRecenterSession() -> NIMRecentSession? { + if let tid = teamInfoModel?.team?.teamId { + let currentSession = NIMSession(tid, type: .team) + return repo.getRecentSession(currentSession) + } + return nil + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift b/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift index 0ca9ea84..04f5cdb1 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift @@ -21,4 +21,5 @@ public var inviteNumberLimit: Int = 200 enum NotificationName { static let leaveTeamBySelf = Notification.Name(rawValue: "team.leaveTeamBySelf") + static let popGroupChatVC = Notification.Name(rawValue: "team.popGroupChatVC") } diff --git a/Podfile b/Podfile index a5edbe7a..30148591 100644 --- a/Podfile +++ b/Podfile @@ -10,38 +10,37 @@ target 'app' do pod 'YXLogin', '1.0.0' #可选UI库 -# pod 'NEContactUIKit', '9.6.5' -# pod 'NEConversationUIKit', '9.6.5' -# pod 'NEChatUIKit', '9.6.5' -# pod 'NETeamUIKit', '9.6.5' + pod 'NEContactUIKit', '9.7.0' + pod 'NEConversationUIKit', '9.7.0' + pod 'NEChatUIKit', '9.7.0' + pod 'NETeamUIKit', '9.7.0' #可选Kit库(和UIKit对应) - pod 'NEChatKit', '9.6.5' + pod 'NEChatKit', '9.7.0' #基础kit库 - pod 'NIMSDK_LITE','9.14.0' - pod 'NECommonUIKit', '9.6.5' - pod 'NECommonKit', '9.6.4' - pod 'NECoreIMKit', '9.6.5' - pod 'NECoreKit', '9.6.5' + pod 'NECommonUIKit', '9.6.6' + pod 'NECommonKit', '9.6.6' + pod 'NECoreIMKit', '9.6.7' + pod 'NECoreKit', '9.6.6' #扩展库 -# pod 'NEMapKit', '9.6.5' +# pod 'NEMapKit', '9.7.0' #呼叫组件,音视频通话能力,需要开通 音视频2.0,可选,聊天一面会根据依赖初始化自动显示音视频通话入口 - pod 'NIMSDK_LITE','9.14.0' + pod 'NIMSDK_LITE','9.14.2' pod 'NERtcCallKit/NOS_Special', '2.2.0' -# pod 'NERtcCallUIKit/NOS_Special', '2.2.0' + pod 'NERtcCallUIKit/NOS_Special', '2.2.0' pod 'NERtcSDK', '5.5.2' # # 如果需要查看UI部分源码请注释掉以上在线依赖,打开下面的本地依赖 - 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 'NEMapKit', :path => 'NEMapKit/NEMapKit.podspec' - pod 'NERtcCallUIKit', :path => 'NERtcCallUIKit/NERtcCallUIKit.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 'NEMapKit', :path => 'NEMapKit/NEMapKit.podspec' +# pod 'NERtcCallUIKit', :path => 'NERtcCallUIKit/NERtcCallUIKit.podspec' end diff --git a/app/Custom/CustomAttachment.swift b/app/Custom/CustomAttachment.swift index 602ed073..58cdac24 100644 --- a/app/Custom/CustomAttachment.swift +++ b/app/Custom/CustomAttachment.swift @@ -1,4 +1,3 @@ - // 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. @@ -6,21 +5,19 @@ import NEChatUIKit import NIMSDK import UIKit -public class CustomAttachment: NSObject, NIMCustomAttachment, NECustomAttachmentProtocol { - public var customType: Int = 0 - - public var cellHeight: CGFloat = 0 - - public var type = 0 - +public class CustomAttachment: NECustomAttachment { public var goodsName = "name" public var goodsURL = "url" - public func encode() -> String { - let info = ["goodsName": goodsName, "goodsURL": goodsURL, "type": type] as [String: Any] + override public func encode() -> String { + // 自定义序列化方法之前必须调用父类的序列化方法 + let neContent = super.encode() + var info: [String: Any] = getDictionaryFromJSONString(neContent) as? [String: Any] ?? [:] + info["goodsName"] = goodsName + info["goodsURL"] = goodsURL - let jsonData = try? JSONSerialization.data(withJSONObject: info, options: .prettyPrinted) + let jsonData = try? JSONSerialization.data(withJSONObject: info, options: []) var content = "" if let data = jsonData { content = String(data: data, encoding: .utf8) ?? "" @@ -29,41 +26,18 @@ public class CustomAttachment: NSObject, NIMCustomAttachment, NECustomAttachment } } -public class CustomAttachmentDecoder: NSObject, NIMCustomAttachmentCoding { - public func decodeAttachment(_ content: String?) -> NIMCustomAttachment? { - var attachment: NIMCustomAttachment? - let data = content?.data(using: .utf8) - guard let dataInfo = data else { - return attachment +public class CustomAttachmentDecoder: NECustomAttachmentDecoder { + override public func decodeCustomMessage(info: [String: Any]) -> CustomAttachment { + // 自定义反序列化方法之前必须调用父类的反序列化方法 + let neCustomAttachment = super.decodeCustomMessage(info: info) + let customAttachment = CustomAttachment(customType: neCustomAttachment.customType, + cellHeight: neCustomAttachment.cellHeight, + data: neCustomAttachment.data) + if customAttachment.customType == 20 { + customAttachment.cellHeight = 50 } - - let infoDict = try? JSONSerialization.jsonObject( - with: dataInfo, - options: .mutableContainers - ) - let infoResult = infoDict as? [String: Any] - let type = infoResult?["type"] as? Int - - switch type { - case 0: - attachment = - decodeCustomMessage(info: infoDict as? [String: Any] ?? [String(): String()]) - default: - print("test") - } - - return attachment - } - - func decodeCustomMessage(info: [String: Any]) -> CustomAttachment { - let customAttachment = CustomAttachment() customAttachment.goodsName = info["goodsName"] as? String ?? "" customAttachment.goodsURL = info["goodsURL"] as? String ?? "" - if let type = info["type"] as? Int { - customAttachment.type = type - } - customAttachment.customType = 20 - customAttachment.cellHeight = 50 return customAttachment } diff --git a/app/Custom/CustomChatCell.swift b/app/Custom/CustomChatCell.swift index e21c5024..0625c66f 100644 --- a/app/Custom/CustomChatCell.swift +++ b/app/Custom/CustomChatCell.swift @@ -36,7 +36,7 @@ class CustomChatCell: NEChatBaseCell { testLabel.textColor = UIColor.black } - override func setModel(_ model: MessageContentModel) { + override func setModel(_ model: MessageContentModel, _ isSend: Bool) { print("this is custom message") testLabel.text = "this is custom message" } diff --git a/app/Custom/CustomContactTableViewCell.swift b/app/Custom/CustomContactTableViewCell.swift index cb95f899..e93c4a63 100644 --- a/app/Custom/CustomContactTableViewCell.swift +++ b/app/Custom/CustomContactTableViewCell.swift @@ -29,6 +29,7 @@ public class CustomContactTableViewCell: ContactTableViewCell { fatalError("init(coder:) has not been implemented") } + // 根据数据模型设置 cell 内容 override public func setModel(_ model: ContactInfo) { super.setModel(model) onlineView.isHidden = false diff --git a/app/Custom/CustomContactsViewController.swift b/app/Custom/CustomContactsViewController.swift index ca603bb1..2d0dbad6 100644 --- a/app/Custom/CustomContactsViewController.swift +++ b/app/Custom/CustomContactsViewController.swift @@ -144,6 +144,7 @@ public class CustomContactsViewController: ContactsViewController, NEBaseContact bodyBottomViewHeight = 0 } + // 父类加载完数据后会调用此方法,可在此对数据进行二次处理 public func onDataLoaded() { viewModel.contacts[1].contacts.forEach { info in info.contactCellType = ContactCellType.ContactCutom.rawValue diff --git a/app/Custom/CustomConversationController.swift b/app/Custom/CustomConversationController.swift index bb2867bf..8a302134 100644 --- a/app/Custom/CustomConversationController.swift +++ b/app/Custom/CustomConversationController.swift @@ -117,9 +117,9 @@ open class CustomConversationController: ConversationController, NEBaseConversat } /// 会话列表点击事件 - NEKitConversationConfig.shared.ui.itemClick = { model, indexPath in - self.showToast((model?.userInfo?.showName(true) ?? model?.teamInfo?.getShowName()) ?? "会话列表点击事件") - } +// NEKitConversationConfig.shared.ui.itemClick = { model, indexPath in +// self.showToast((model?.userInfo?.showName(true) ?? model?.teamInfo?.getShowName()) ?? "会话列表点击事件") +// } /* 布局自定义 @@ -148,6 +148,14 @@ open class CustomConversationController: ConversationController, NEBaseConversat // 实现协议(重写tabbar点击事件) navigationView.delegate = self + // 自定义会话列表标题、图标、间距 + navigationView.brandBtn.setTitle("消息", for: .normal) + navigationView.brandBtn.setImage(nil, for: .normal) + navigationView.brandBtn.layoutButtonImage(style: .left, space: 0) + + // 自定义添加按钮图标 + navigationView.addBtn.setImage(UIImage.ne_imageNamed(name: "noNeed_notify"), for: .normal) + // 顶部bodyTopView中添加自定义view(需要设置bodyTopView的高度) customTopView.btn.setTitle("通过重写方式添加", for: .normal) bodyTopView.addSubview(customTopView) diff --git a/app/Custom/CustomConversationListCell.swift b/app/Custom/CustomConversationListCell.swift index afa64a91..de8d11e9 100644 --- a/app/Custom/CustomConversationListCell.swift +++ b/app/Custom/CustomConversationListCell.swift @@ -7,6 +7,7 @@ import NEConversationUIKit import UIKit open class CustomConversationListCell: ConversationListCell { + // 新增 UI 元素,用于展示在线状态 private lazy var onlineView: UIImageView = { let notify = UIImageView() notify.translatesAutoresizingMaskIntoConstraints = false @@ -16,6 +17,8 @@ open class CustomConversationListCell: ConversationListCell { override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) + + // 头像右下角 contentView.addSubview(onlineView) NSLayoutConstraint.activate([ onlineView.rightAnchor.constraint(equalTo: headImge.rightAnchor), @@ -29,8 +32,9 @@ open class CustomConversationListCell: ConversationListCell { fatalError("init(coder:) has not been implemented") } + // 此方法用于数据和 UI 的绑定,可在此处在数据展示前对数据进行处理 override open func configData(sessionModel: ConversationListModel?) { super.configData(sessionModel: sessionModel) -// subTitle.text = "[自定义类型文案]" +// subTitle.text = "[自定义类型文案]" } } diff --git a/app/Custom/CustomP2PChatViewController.swift b/app/Custom/CustomP2PChatViewController.swift index 0e4c435b..91302645 100644 --- a/app/Custom/CustomP2PChatViewController.swift +++ b/app/Custom/CustomP2PChatViewController.swift @@ -6,9 +6,10 @@ import NEChatUIKit import NIMSDK import UIKit class CustomP2PChatViewController: P2PChatViewController { + let customMessageType = 20 override func viewDidLoad() { - // 自定义消息以及外部扩展 覆盖cell UI 样式示例 -// customMessage() + // 自定义消息cell绑定需要放在 super.viewDidLoad() 之前 + NEChatUIKitClient.instance.regsiterCustomCell(["\(customMessageType)": CustomChatCell.self]) // 通过配置项实现自定义 customByConfig() @@ -17,6 +18,9 @@ class CustomP2PChatViewController: P2PChatViewController { // customByOverread() super.viewDidLoad() + + // 自定义消息以及外部扩展 覆盖cell UI 样式示例 + customMessage() } /// 通过配置项实现 UI 自定义 @@ -200,7 +204,6 @@ class CustomP2PChatViewController: P2PChatViewController { func customMessage() { // 注册自定义消息的解析器 NIMCustomObject.registerCustomDecoder(CustomAttachmentDecoder()) - NEChatUIKitClient.instance.regsiterCustomCell(["20": CustomChatCell.self]) // 测试自定义消息发送按钮 let testBtn = UIButton() @@ -269,9 +272,8 @@ class CustomP2PChatViewController: P2PChatViewController { } @objc func sendCustomButton() { - let attachment = CustomAttachment() - attachment.customType = 20 - attachment.cellHeight = 50 + let data = ["type": customMessageType] + let attachment = CustomAttachment(customType: customMessageType, cellHeight: 50, data: data) let message = NIMMessage() let object = NIMCustomObject() object.attachment = attachment diff --git a/app/Main/AppDelegate.swift b/app/Main/AppDelegate.swift index d87ceaa6..8668a71b 100644 --- a/app/Main/AppDelegate.swift +++ b/app/Main/AppDelegate.swift @@ -176,7 +176,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return } let anchor = param["anchor"] as? NIMMessage - var p2pChatVC = P2PChatViewController(session: session, anchor: anchor) + let p2pChatVC = P2PChatViewController(session: session, anchor: anchor) + for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { nav?.viewControllers[i] = p2pChatVC @@ -184,6 +185,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return } } + + if let remove = param["removeUserVC"] as? Bool, remove { + nav?.viewControllers.removeLast() + } + nav?.pushViewController(p2pChatVC, animated: true) } } else { @@ -194,7 +200,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return } let anchor = param["anchor"] as? NIMMessage - var p2pChatVC = FunP2PChatViewController(session: session, anchor: anchor) + let p2pChatVC = FunP2PChatViewController(session: session, anchor: anchor) + for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { nav?.viewControllers[i] = p2pChatVC @@ -202,6 +209,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return } } + + if let remove = param["removeUserVC"] as? Bool, remove { + nav?.viewControllers.removeLast() + } + nav?.pushViewController(p2pChatVC, animated: true) } } diff --git a/app/Mine/Controller/MeViewController.swift b/app/Mine/Controller/MeViewController.swift index 65bea16f..24e36faa 100644 --- a/app/Mine/Controller/MeViewController.swift +++ b/app/Mine/Controller/MeViewController.swift @@ -10,7 +10,7 @@ import NIMSDK import UIKit import YXLogin -class MeViewController: UIViewController { +class MeViewController: UIViewController, UIGestureRecognizerDelegate { private let mineData = [ [NSLocalizedString("setting", comment: ""): "mine_setting"], [NSLocalizedString("about_yunxin", comment: ""): "about_yunxin"], @@ -22,7 +22,6 @@ class MeViewController: UIViewController { let view = UIView(frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .white - view.accessibilityIdentifier = "id.avatar" return view }() @@ -61,6 +60,13 @@ class MeViewController: UIViewController { navigationController?.setNavigationBarHidden(true, animated: false) updateUserInfo() super.viewWillAppear(animated) + if navigationController?.viewControllers.count ?? 0 > 0 { + if let root = navigationController?.viewControllers[0] as? UIViewController { + if root.isKind(of: MeViewController.self) { + navigationController?.interactivePopGestureRecognizer?.delegate = self + } + } + } } func setupSubviews() { @@ -233,4 +239,14 @@ extension MeViewController: UITableViewDelegate, UITableViewDataSource { navigationController?.pushViewController(ctrl, animated: true) } else if indexPath.row == 2 {} } + + public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + if let navigationController = navigationController, + navigationController.responds(to: #selector(getter: UINavigationController.interactivePopGestureRecognizer)), + gestureRecognizer == navigationController.interactivePopGestureRecognizer, + navigationController.visibleViewController == navigationController.viewControllers.first { + return false + } + return true + } } diff --git a/app/Mine/Controller/NELoginViewController.swift b/app/Mine/Controller/NELoginViewController.swift index 9dd4ae8a..4b298a55 100644 --- a/app/Mine/Controller/NELoginViewController.swift +++ b/app/Mine/Controller/NELoginViewController.swift @@ -3,11 +3,11 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import UIKit +import NEChatUIKit import NECommonKit -import YXLogin import NECoreIMKit -import NEChatUIKit +import UIKit +import YXLogin public class NELoginViewController: UIViewController { // 登录成功 @@ -156,6 +156,7 @@ public class NELoginViewController: UIViewController { label.text = NSLocalizedString("appName", comment: "") label.font = UIFont.systemFont(ofSize: 24.0) label.textColor = UIColor(hexString: "333333") + label.accessibilityIdentifier = "id.appYunxin" return label }() @@ -168,6 +169,7 @@ public class NELoginViewController: UIViewController { btn.titleLabel?.font = UIFont.systemFont(ofSize: 15.0) btn.setTitle(NSLocalizedString("register_login", comment: ""), for: .normal) btn.addTarget(self, action: #selector(loginBtnClick), for: .touchUpInside) + btn.accessibilityIdentifier = "id.loginButton" return btn }() @@ -178,7 +180,7 @@ public class NELoginViewController: UIViewController { btn.titleLabel?.font = UIFont.systemFont(ofSize: 12.0) btn.setTitle(NSLocalizedString("email_login", comment: ""), for: .normal) btn.addTarget(self, action: #selector(emailLoginBtnClick), for: .touchUpInside) - + btn.accessibilityIdentifier = "id.emailLogin" return btn }() @@ -196,7 +198,7 @@ public class NELoginViewController: UIViewController { btn.titleLabel?.font = UIFont.systemFont(ofSize: 12.0) btn.setTitle(NSLocalizedString("node_select", comment: ""), for: .normal) btn.addTarget(self, action: #selector(nodeBtnClick), for: .touchUpInside) - + btn.accessibilityIdentifier = "id.serverConfig" return btn }() } diff --git a/app/Mine/Controller/PersonInfoViewController.swift b/app/Mine/Controller/PersonInfoViewController.swift index 8e79511a..62cb08d9 100644 --- a/app/Mine/Controller/PersonInfoViewController.swift +++ b/app/Mine/Controller/PersonInfoViewController.swift @@ -110,7 +110,7 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, if let t = time { weakSelf?.viewModel.updateBirthday(birthDay: t) { error in if error != nil { - if error?.code == 408 { + if error?.code == noNetworkCode { weakSelf?.showToast(commonLocalizable("network_error")) } else { weakSelf?.showToast(NSLocalizedString("setting_birthday_failure", comment: "")) @@ -240,7 +240,7 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, sex = value == 0 ? .male : .female weakSelf?.viewModel.updateSex(sex: sex) { error in if error != nil { - if error?.code == 408 { + if error?.code == noNetworkCode { weakSelf?.showToast(commonLocalizable("network_error")) } else { weakSelf?.showToast(NSLocalizedString("change_gender_failure", comment: "")) diff --git a/app/Mine/ViewModel/PersonInfoViewModel.swift b/app/Mine/ViewModel/PersonInfoViewModel.swift index c213c7ab..8a7e54c9 100644 --- a/app/Mine/ViewModel/PersonInfoViewModel.swift +++ b/app/Mine/ViewModel/PersonInfoViewModel.swift @@ -24,7 +24,7 @@ public class PersonInfoViewModel: NSObject { public let friendProvider = FriendProvider.shared public let userProvider = UserInfoProvider.shared - private var userInfo: User? + private var userInfo: NEKitUser? weak var delegate: PersonInfoViewModelDelegate? func getData() { From b72679bfdf1f01741de3a48023bf93de85f96cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=AF=97=E6=96=87?= Date: Thu, 25 Jan 2024 17:03:25 +0800 Subject: [PATCH 4/4] rm appKey --- IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/AppKey.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/AppKey.h b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/AppKey.h index 93201753..f5538b15 100644 --- a/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/AppKey.h +++ b/IMUIKitOC/IMUIKitOCExample/IMUIKitOCExample/AppKey.h @@ -7,6 +7,6 @@ #define AppKey_h /// IM key -static NSString *const AppKey = @"3e215d27b6a6a9e27dad7ef36dd5b65c"; +static NSString *const AppKey = @"<#appKey#>"; #endif /* AppKey_h */