diff --git a/NEChatUIKit/NEChatUIKit.podspec b/NEChatUIKit/NEChatUIKit.podspec index 34d6e4d9..e3d49a42 100644 --- a/NEChatUIKit/NEChatUIKit.podspec +++ b/NEChatUIKit/NEChatUIKit.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| # s.name = 'NEChatUIKit' - s.version = '9.7.0' + s.version = '10.1.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_top_message_Image.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_top_message_Image.imageset/Contents.json new file mode 100644 index 00000000..bff6a9d8 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_top_message_Image.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "fun_top_message_Image@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "fun_top_message_Image@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_top_message_Image.imageset/fun_top_message_Image@2x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_top_message_Image.imageset/fun_top_message_Image@2x.png new file mode 100644 index 00000000..d3885c3e Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_top_message_Image.imageset/fun_top_message_Image@2x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_top_message_Image.imageset/fun_top_message_Image@3x.png b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_top_message_Image.imageset/fun_top_message_Image@3x.png new file mode 100644 index 00000000..0347ba7b Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/FunChatUIKit.xcassets/Chat/fun_top_message_Image.imageset/fun_top_message_Image@3x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/top_close.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/top_close.imageset/Contents.json new file mode 100644 index 00000000..6e91021d --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/top_close.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "top_close@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "top_close@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/top_close.imageset/top_close@2x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/top_close.imageset/top_close@2x.png new file mode 100644 index 00000000..533e2799 Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/top_close.imageset/top_close@2x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/top_close.imageset/top_close@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/top_close.imageset/top_close@3x.png new file mode 100644 index 00000000..44e931dc Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Chat/top_close.imageset/top_close@3x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/Contents.json new file mode 100644 index 00000000..6cca0ae0 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_default 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_default.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/location_default 1.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/location_default 1.png new file mode 100644 index 00000000..3a8d7cb7 Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/location_default 1.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/location_default.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/location_default.png new file mode 100644 index 00000000..3a8d7cb7 Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/chat_map_default.imageset/location_default.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/Contents.json new file mode 100644 index 00000000..c5dbccb1 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "map_placeholder_image@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "map_placeholder_image@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/map_placeholder_image@2x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/map_placeholder_image@2x.png new file mode 100644 index 00000000..dfaf8844 Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/map_placeholder_image@2x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/map_placeholder_image@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/map_placeholder_image@3x.png new file mode 100644 index 00000000..2543b28a Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/Map/map_placeholder_image.imageset/map_placeholder_image@3x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_collect.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_collect.imageset/Contents.json new file mode 100644 index 00000000..d3a3c616 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_collect.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "chat_collection@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "chat_collection@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_collect.imageset/chat_collection@2x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_collect.imageset/chat_collection@2x.png new file mode 100644 index 00000000..44b2ce98 Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_collect.imageset/chat_collection@2x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_collect.imageset/chat_collection@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_collect.imageset/chat_collection@3x.png new file mode 100644 index 00000000..2258553f Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/NEBaseChatUIKit.xcassets/operation/op_collect.imageset/chat_collection@3x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@2x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@2x.png index a1be3fd4..5dddc8a0 100644 Binary files a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@2x.png and b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@2x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@3x.png index 7b4b6a0c..d877bc03 100644 Binary files a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@3x.png and b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/multiForward_message_send.imageset/merge_message_send@3x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/top_message_Image.imageset/Contents.json b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/top_message_Image.imageset/Contents.json new file mode 100644 index 00000000..68462277 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/top_message_Image.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "top_message_Image@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "top_message_Image@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/top_message_Image.imageset/top_message_Image@2x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/top_message_Image.imageset/top_message_Image@2x.png new file mode 100644 index 00000000..302898a6 Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/top_message_Image.imageset/top_message_Image@2x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/top_message_Image.imageset/top_message_Image@3x.png b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/top_message_Image.imageset/top_message_Image@3x.png new file mode 100644 index 00000000..49149383 Binary files /dev/null and b/NEChatUIKit/NEChatUIKit/Assets/NormalChatUIKit.xcassets/Chat/top_message_Image.imageset/top_message_Image@3x.png differ diff --git a/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings b/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings index fcfad433..921ac574 100644 --- a/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings +++ b/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings @@ -17,7 +17,6 @@ "record_too_short"="Time is too short"; "no_microphone_permission"="no micro permission"; "choose"="select"; -"take_photo"="camera"; "select_from_album"="album"; "select_from_icloud"="icloud"; "editing"="typing"; @@ -51,9 +50,11 @@ "team_join_mode" = "join mode"; "team_be_invited_mode" = "invite mode"; "team_be_invited_permission" = "Invite Permission"; -"team_be_invited_author" = "是否需要被邀请者同意权限"; -"team_update_info_permission" = "update info permission"; +"team_be_invited_author" = "BeInvite Permission"; +"team_update_info_permission" = "Update info permission"; "team_update_client_custom"="Update Permission"; +"team_at_permission"="@All Permission update to "; +"team_top_permission"="Top message permission update to "; "team_custom_info" = "custom info"; "not_mute" = "unmute"; @@ -75,11 +76,15 @@ "pin_text_P2P"="pinned this message"; "pin_text_team"="pinned this message"; +"top_message"="top message"; +"untop_message"="untop message"; + "chat_setting"="chat setting"; "session_set_top"="sticky to top"; "message_remind"="open notification"; "open_soon"="Not open"; +"no_permission_tip"="No permission"; //MARK: operation "operation_copy"="copy"; @@ -87,8 +92,11 @@ "operation_forward"="forward"; "operation_pin"="pin"; "operation_cancel_pin"="unpin"; +"operation_delete_collection"="delete collection"; "operation_select"="multi-select"; -"operation_collection"="favorite"; +"operation_collection"="collection"; +"operation_top"="top"; +"operation_untop"="untop"; "operation_delete"="delete"; "operation_recall"="recall"; "withdrew_message" = "withdrew this message"; @@ -98,7 +106,6 @@ //MARK: toast -"copy_success"="Copy successfully"; "collection_success"="Favorite"; "no_permession"="No Permission"; "msg_reply"="reply"; @@ -123,6 +130,9 @@ "send_to"="send to"; "send"="send"; "confirm"="yes"; +"session_record"="session record"; + +"collection_message"=" collection message"; "mdhm"="MM.dd HH:mm"; @@ -163,12 +173,14 @@ "pin_jump_detail"="jump to details"; "cancel_pin_success"="Unpin"; "no_pin_message"="No pin message"; +"no_collection_message"="No collection message"; "you_were_mentioned"="[You were mentioned]"; // MARK: error toast "failed_operation"="Failed Operation"; "team_not_exist"="team not exist"; "unpin_failed"="Unpin failed"; +"pin_limit_exceeded"="The number of pin has reached the upper limit"; "fun_hold_to_talk"="Hold to talk"; @@ -194,6 +206,7 @@ "multiForward_open_failed"="Failed to obtain information"; "file_check_failed"="File verification failed"; "per_item_forward_limit"="Forward one by one to limit %d messages"; +"selete_messages_limit"="Delete limits %d messages"; "multiForward_forward_limit"="Combined forwarding limits %d messages"; "exception_description"="Exception description"; @@ -201,3 +214,8 @@ "null_message_not_support"="Empty messages are not allowed to be sent"; "black_list_tip"="Opposite has blocked you and the message cannot be sent."; +"disable_stranger_call"="Cannot make audio or video calls to someone who is not in your friends list."; + +"chat_collection_p2p_tip"="From Chat with %@"; + +"chat_collection_team_tip"="From %@ "; diff --git a/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings index 0d699799..b71f29af 100644 --- a/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -17,7 +17,6 @@ "record_too_short"="录音时间太短"; "no_microphone_permission"="没有麦克风权限"; "choose"="请选择"; -"take_photo"="拍照"; "select_from_album"="从相册选择"; "select_from_icloud"="从iCloud选择"; "editing"="对方正在输入中..."; @@ -54,6 +53,8 @@ "team_be_invited_author" = "是否需要被邀请者同意权限"; "team_update_info_permission" = "群资料修改权限"; "team_update_client_custom"="更新客户端自定义字段的权限"; +"team_at_permission"="@所有人权限更新为"; +"team_top_permission"="置顶消息权限更新为"; "team_custom_info" = "自定义扩展字段"; "not_mute" = "解除禁言"; @@ -73,10 +74,13 @@ "team_all_no_mute"="群禁言已关闭"; "pin_text_P2P"="标记了这条信息"; "pin_text_team"="标记了这条信息"; +"top_message"="置顶了一条消息"; +"untop_message"="移除了置顶消息"; "chat_setting"="聊天设置"; "session_set_top"="聊天置顶"; "message_remind"="开启消息提醒"; "open_soon"="暂未开放"; +"no_permission_tip"="暂无操作权限"; //MARK: operation "operation_copy"="复制"; @@ -84,8 +88,11 @@ "operation_forward"="转发"; "operation_pin"="标记"; "operation_cancel_pin"="取消标记"; +"operation_delete_collection"="删除收藏"; "operation_select"="多选"; "operation_collection"="收藏"; +"operation_top"="置顶"; +"operation_untop"="移除置顶"; "operation_delete"="删除"; "operation_recall"="撤回"; "withdrew_message" = "撤回了一条消息"; @@ -95,7 +102,6 @@ //MARK: toast -"copy_success"="复制成功"; "collection_success"="已收藏"; "no_permession"="暂无权限"; "msg_reply"="回复"; @@ -120,6 +126,9 @@ "send_to"="发送给"; "send"="发送"; "confirm"="确定"; +"session_record"="的会话记录"; + +"collection_message"="的收藏消息"; "mdhm"="MM月dd日 HH:mm"; @@ -161,12 +170,14 @@ "pin_jump_detail"="跳转查看"; "cancel_pin_success"="已取消标记"; "no_pin_message"="暂无标记消息"; +"no_collection_message"="暂无收藏消息"; "you_were_mentioned"="[有人@我]"; // MARK: error toast "failed_operation"="操作失败"; "team_not_exist"="群组不存在"; "unpin_failed"="取消标记失败"; +"pin_limit_exceeded"="已超出 pin 数量上限"; "fun_hold_to_talk"="按住 说话"; @@ -192,6 +203,7 @@ "multiForward_open_failed"="信息获取失败"; "file_check_failed"="文件校验失败"; "per_item_forward_limit"="逐条转发限制%d条消息"; +"selete_messages_limit"="批量删除限制%d条消息"; "multiForward_forward_limit"="合并转发限制%d条消息"; "exception_description"="异常说明"; @@ -199,3 +211,8 @@ "null_message_not_support"="不支持发送空消息"; "black_list_tip"="对方已将您拉黑,消息无法发送"; +"disable_stranger_call"="非好友暂不支持音视频通话"; + +"chat_collection_p2p_tip"="来自于 与%@的会话"; + +"chat_collection_team_tip"="来自于 %@"; diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCenterTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCenterTextCell.swift index f6b587af..6da85cc3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCenterTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCenterTextCell.swift @@ -36,6 +36,6 @@ open class ChatCenterTextCell: ChatCornerCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCornerCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCornerCell.swift index f238c1c0..9b29aa67 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCornerCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatCornerCell.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import NECommonUIKit + // this cell has rounding corner style import UIKit diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatHeaderView.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatHeaderView.swift index 4f52d551..10b02a91 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatHeaderView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatHeaderView.swift @@ -15,14 +15,6 @@ open class ChatHeaderView: UIView { return label }() - /* - // Only override draw() if you perform custom drawing. - // An empty implementation adversely affects performance during animation. - override func draw(_ rect: CGRect) { - // Drawing code - } - */ - override init(frame: CGRect) { super.init(frame: frame) setupUI() diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift index 2ce65364..f146518b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatImageTextCell.swift @@ -7,10 +7,44 @@ import UIKit @objcMembers open class ChatImageTextCell: ChatStateCell { + public lazy var avatarImageView: UIImageView = { + let avatarView = UIImageView() + avatarView.translatesAutoresizingMaskIntoConstraints = false + avatarView.clipsToBounds = true + avatarView.backgroundColor = .ne_defautAvatarColor + return avatarView + }() + + public lazy var shortNameLabel: UILabel = { + let nameLabel = UILabel() + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.textColor = .white + nameLabel.textAlignment = .center + nameLabel.font = UIFont.systemFont(ofSize: 14.0) + return nameLabel + }() + + public lazy var nameLabel: UILabel = { + let label = UILabel() + label.textAlignment = .left + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 14.0) + label.textColor = .ne_darkText + return label + }() + var circleView = UIImageView() + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - // circle view + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupUI() { + super.setupUI() circleView.translatesAutoresizingMaskIntoConstraints = false circleView.layer.cornerRadius = 16 circleView.clipsToBounds = true @@ -22,7 +56,7 @@ open class ChatImageTextCell: ChatStateCell { circleView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 40), circleView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) -// short name label + // short name label contentView.addSubview(shortNameLabel) NSLayoutConstraint.activate([ shortNameLabel.widthAnchor.constraint(equalTo: circleView.widthAnchor), @@ -30,14 +64,14 @@ open class ChatImageTextCell: ChatStateCell { shortNameLabel.leftAnchor.constraint(equalTo: circleView.leftAnchor), shortNameLabel.topAnchor.constraint(equalTo: circleView.topAnchor), ]) -// name label + // name label contentView.addSubview(nameLabel) NSLayoutConstraint.activate([ nameLabel.leftAnchor.constraint(equalTo: circleView.rightAnchor, constant: 12), nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor), nameLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) -// line + // line let line = UIView() line.backgroundColor = .ne_greyLine line.translatesAutoresizingMaskIntoConstraints = false @@ -50,36 +84,6 @@ open class ChatImageTextCell: ChatStateCell { ]) } - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - public lazy var avatarImage: UIImageView = { - let avatar = UIImageView() - avatar.translatesAutoresizingMaskIntoConstraints = false - avatar.clipsToBounds = true - avatar.backgroundColor = .ne_defautAvatarColor - return avatar - }() - - public lazy var shortNameLabel: UILabel = { - let name = UILabel() - name.translatesAutoresizingMaskIntoConstraints = false - name.textColor = .white - name.textAlignment = .center - name.font = UIFont.systemFont(ofSize: 14.0) - return name - }() - - public lazy var nameLabel: UILabel = { - let label = UILabel() - label.textAlignment = .left - label.translatesAutoresizingMaskIntoConstraints = false - label.font = UIFont.systemFont(ofSize: 14.0) - label.textColor = .ne_darkText - return label - }() - open func setup(accid: String?, nickName: String?) { let name = nickName?.count ?? 0 > 0 ? nickName : accid nameLabel.text = name diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift index e0998732..5ac4eac6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatSectionView.swift @@ -14,7 +14,7 @@ open class ChatSectionView: UITableViewHeaderFooterView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift index 84f90b78..60ae2245 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatStateCell.swift @@ -15,7 +15,7 @@ public enum RightStyle: Int { @objcMembers open class ChatStateCell: ChatCornerCell { private var style: RightStyle = .none - public var rightImage = UIImageView() + public var rightImageView = UIImageView() var rightImageMargin: NSLayoutConstraint? public var rightStyle: RightStyle { get { @@ -25,33 +25,38 @@ open class ChatStateCell: ChatCornerCell { style = newValue switch style { case .none: - rightImage.image = nil + rightImageView.image = nil case .indicate: - rightImage.image = UIImage.ne_imageNamed(name: "arrowRight") + rightImageView.image = UIImage.ne_imageNamed(name: "arrowRight") case .delete: - rightImage.image = UIImage.ne_imageNamed(name: "delete") + rightImageView.image = UIImage.ne_imageNamed(name: "delete") } } } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - rightImage.contentMode = .center - rightImage.translatesAutoresizingMaskIntoConstraints = false - contentView.addSubview(rightImage) - rightImageMargin = rightImage.rightAnchor.constraint( + setupUI() + } + + /// UI 初始化 + open func setupUI() { + rightImageView.contentMode = .center + rightImageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(rightImageView) + rightImageMargin = rightImageView.rightAnchor.constraint( equalTo: contentView.rightAnchor, constant: -36 ) rightImageMargin?.isActive = true NSLayoutConstraint.activate([ - rightImage.widthAnchor.constraint(equalToConstant: 20), - rightImage.heightAnchor.constraint(equalToConstant: 20), - rightImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + rightImageView.widthAnchor.constraint(equalToConstant: 20), + rightImageView.heightAnchor.constraint(equalToConstant: 20), + rightImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift index 1acc67a0..d76003c2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextArrowCell.swift @@ -13,6 +13,6 @@ open class ChatTextArrowCell: ChatTextCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift index d8fa170d..f602321b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatTextCell.swift @@ -17,7 +17,10 @@ open class ChatTextCell: ChatStateCell { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) + } + override open func setupUI() { + super.setupUI() titleLabel.font = UIFont.systemFont(ofSize: 16) titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.textColor = .ne_darkText @@ -64,6 +67,6 @@ open class ChatTextCell: ChatStateCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift index e9b21bc2..39dfcac2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatUnfoldCell.swift @@ -7,11 +7,11 @@ import UIKit @objcMembers open class ChatUnfoldCell: ChatCornerCell { - lazy var arrowImage: UIImageView = { - let arrow = UIImageView() - arrow.translatesAutoresizingMaskIntoConstraints = false - arrow.image = UIImage.ne_imageNamed(name: "arrowDown") - return arrow + lazy var arrowImageView: UIImageView = { + let arrowImageView = UIImageView() + arrowImageView.translatesAutoresizingMaskIntoConstraints = false + arrowImageView.image = UIImage.ne_imageNamed(name: "arrowDown") + return arrowImageView }() lazy var contentLabel: UILabel = { @@ -38,18 +38,18 @@ open class ChatUnfoldCell: ChatCornerCell { contentLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), ]) - contentView.addSubview(arrowImage) + contentView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - arrowImage.leftAnchor.constraint(equalTo: contentLabel.rightAnchor, constant: 5), - arrowImage.centerYAnchor.constraint(equalTo: contentLabel.centerYAnchor), + arrowImageView.leftAnchor.constraint(equalTo: contentLabel.rightAnchor, constant: 5), + arrowImageView.centerYAnchor.constraint(equalTo: contentLabel.centerYAnchor), ]) } func changeToArrowUp() { - arrowImage.image = UIImage.ne_imageNamed(name: "arrowUp") + arrowImageView.image = UIImage.ne_imageNamed(name: "arrowUp") } func changeToArrowDown() { - arrowImage.image = UIImage.ne_imageNamed(name: "arrowDown") + arrowImageView.image = UIImage.ne_imageNamed(name: "arrowDown") } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift index 42f8e49d..6ae1366d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift @@ -14,10 +14,10 @@ open class NEChatBaseCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } - open func uploadProgress(byRight: Bool, _ progress: Float) { + open func uploadProgress(byRight: Bool, _ progress: UInt) { fatalError("override in sub class") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/ChatBaseViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/NEChatBaseViewController.swift similarity index 96% rename from NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/ChatBaseViewController.swift rename to NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/NEChatBaseViewController.swift index d91547c9..1861dee8 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/ChatBaseViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseViewController/NEChatBaseViewController.swift @@ -8,7 +8,7 @@ import NECoreKit import UIKit @objcMembers -open class ChatBaseViewController: UIViewController, UIGestureRecognizerDelegate { +open class NEChatBaseViewController: UIViewController, UIGestureRecognizerDelegate { var topConstant: CGFloat = 0 public let navigationView = NENavigationView() diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift index bc51c77a..6c734fd2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift @@ -8,7 +8,7 @@ import MJRefresh import NEChatKit import NECommonKit import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import Photos @@ -16,29 +16,32 @@ import UIKit import WebKit @objcMembers -open class ChatViewController: ChatBaseViewController, UINavigationControllerDelegate, - ChatInputViewDelegate, ChatViewModelDelegate, NIMMediaManagerDelegate, - MessageOperationViewDelegate, UITableViewDataSource, - UITableViewDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate, CLLocationManagerDelegate, UITextViewDelegate, ChatInputMultilineDelegate { - private let tag = "ChatViewController" +open class ChatViewController: NEChatBaseViewController, UINavigationControllerDelegate, UITableViewDataSource, UITableViewDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate, NIMMediaManagerDelegate, CLLocationManagerDelegate, UITextViewDelegate, ChatInputViewDelegate, ChatInputMultilineDelegate, ChatViewModelDelegate, MessageOperationViewDelegate, NEContactListener { private let kCallKitDismissNoti = "kCallKitDismissNoti" private let kCallKitShowNoti = "kCallKitShowNoti" public var titleContent = "" + var audioPlayer: AVAudioPlayer? // 仅用于语音消息的播放 - public var viewmodel: ChatViewModel + public var viewModel: ChatViewModel = .init() let interactionController = UIDocumentInteractionController() private lazy var manager = CLLocationManager() private var playingCell: ChatAudioCellProtocol? private var playingModel: MessageAudioModel? + private var anchorCell: NEBaseChatMessageCell? // 锚点跳转时的 cell(视频、文件) + private var anchorModel: MessageVideoModel? // 锚点跳转时的 model(视频、文件) private var timer: Timer? private var isFile: Bool? // 是否以文件形式发送 public var isCurrentPage = true public var isMute = false // 是否禁言 private var isMutilSelect = false // 是否多选模式 + private var isUploadingData = false // 是否正在加载数据(上拉) + private var hasFirstLoadData = false // 是否完成第一次加载数据 + private var uploadHasNoMore = false // 上拉无更多数据 + private var networkBroken = false // 网络断开标志 public var operationCellFilter: [OperationType]? // 消息长按菜单全局过滤列表 public var cellRegisterDic = [String: UITableViewCell.Type]() - private var needMarkReadMsgs = [NIMMessage]() + private var needMarkReadMsgs = [V2NIMMessage]() private var atUsers = [NSRange]() var replyView = ReplyView() @@ -76,17 +79,164 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel public var bottomViewTopAnchor: NSLayoutConstraint? public var bottomViewHeightAnchor: NSLayoutConstraint? - public init(session: NIMSession) { - viewmodel = ChatViewModel(session: session, anchor: nil) + public lazy var bodyTopView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.clear + return view + }() + + public lazy var bodyView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.clear + view.addSubview(brokenNetworkView) + view.addSubview(contentView) + + NSLayoutConstraint.activate([ + brokenNetworkView.topAnchor.constraint(equalTo: view.topAnchor), + brokenNetworkView.leftAnchor.constraint(equalTo: view.leftAnchor), + brokenNetworkView.rightAnchor.constraint(equalTo: view.rightAnchor), + brokenNetworkView.heightAnchor.constraint(equalToConstant: brokenNetworkViewHeight), + ]) + + contentViewTopAnchor = contentView.topAnchor.constraint(equalTo: view.topAnchor) + contentViewTopAnchor?.isActive = true + NSLayoutConstraint.activate([ + contentView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + return view + }() + + public lazy var brokenNetworkView: NEBrokenNetworkView = { + let view = NEBrokenNetworkView() + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + return view + }() + + public lazy var contentView: UIView = { + let contentView = UIView() + contentView.translatesAutoresizingMaskIntoConstraints = false + contentView.backgroundColor = UIColor.clear + contentView.addSubview(tableView) + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: contentView.topAnchor), + tableView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + tableView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + tableView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + + return contentView + }() + + 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.mj_header = MJRefreshNormalHeader( + refreshingTarget: self, + refreshingAction: #selector(loadMoreData) + ) + tableView.keyboardDismissMode = .onDrag + + if #available(iOS 11.0, *) { + tableView.estimatedRowHeight = 0 + tableView.estimatedSectionHeaderHeight = 0 + tableView.estimatedSectionFooterHeight = 0 + } + return tableView + }() + + public lazy var bodyBottomView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.clear + return view + }() + + public lazy var bottomView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.clear + + view.addSubview(chatInputView) + NSLayoutConstraint.activate([ + chatInputView.leftAnchor.constraint(equalTo: view.leftAnchor), + chatInputView.rightAnchor.constraint(equalTo: view.rightAnchor), + 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 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 + }() + + /// 消息置顶视图 + public lazy var topMessageView: TopMessageView = { + let topMessageView = TopMessageView() + topMessageView.translatesAutoresizingMaskIntoConstraints = false + topMessageView.delegate = self + return topMessageView + }() + + public init(conversationId: String) { super.init(nibName: nil, bundle: nil) NEKeyboardManager.shared.enable = false NEKeyboardManager.shared.enableAutoToolbar = false + NIMSDK.shared().mediaManager.add(self) + ContactRepo.shared.addContactListener(self) + IMKitClient.instance.addLoginListener(self) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) + } + + deinit { + NEALog.infoLog(className(), desc: "deinit") + viewModel.clearUnreadCount() + cleanDelegate() + } + + func cleanDelegate() { + NIMSDK.shared().mediaManager.remove(self) + ContactRepo.shared.removeContactListener(self) + IMKitClient.instance.removeLoginListener(self) + viewModel.delegate = nil } override open func viewWillAppear(_ animated: Bool) { @@ -95,8 +245,6 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel NEKeyboardManager.shared.shouldResignOnTouchOutside = false isCurrentPage = true markNeedReadMsg() - getSessionInfo(session: viewmodel.session) - clearAtRemind() NEChatDetectNetworkTool.shareInstance.netWorkReachability { [weak self] status in if status == .notReachable { @@ -111,13 +259,20 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel override open func viewDidLoad() { super.viewDidLoad() - viewmodel.delegate = self + viewModel.delegate = self commonUI() addObseve() weak var weakSelf = self - viewmodel.fetchPinMessage { + getSessionInfo(sessionId: viewModel.sessionId) { weakSelf?.loadData() } + Router.shared.register(NERouterUrl.LocationSearchResult) { result in + if let model = ChatLocaitonModel.yx_model(with: result) { + weakSelf?.viewModel.sendLocationMessage(model: model) { error in + weakSelf?.showErrorToast(error) + } + } + } } override open func viewWillDisappear(_ animated: Bool) { @@ -126,11 +281,10 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel NEKeyboardManager.shared.shouldResignOnTouchOutside = true isCurrentPage = false operationView?.removeFromSuperview() - if NIMSDK.shared().mediaManager.isPlaying() { - NIMSDK.shared().mediaManager.stopPlay() + if audioPlayer?.isPlaying == true { + audioPlayer?.stop() } - clearAtRemind() chatInputView.textView.resignFirstResponder() chatInputView.titleField.resignFirstResponder() } @@ -140,6 +294,14 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel stopPlay() } + override open func didMove(toParent parent: UIViewController?) { + super.didMove(toParent: parent) + if parent == nil { + let param = ["sessionId": viewModel.conversationId] + Router.shared.use("ClearAtMessageRemind", parameters: param, closure: nil) + } + } + open func setMoreButton() { if NEKitChatConfig.shared.ui.messageProperties.showTitleBarRightIcon { let image = NEKitChatConfig.shared.ui.messageProperties.titleBarRightRes ?? UIImage.ne_imageNamed(name: "three_point") @@ -151,7 +313,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func commonUI() { - title = viewmodel.session.sessionId + title = viewModel.sessionId navigationView.titleBarBottomLine.isHidden = false setMoreButton() setMutilSelectBottomView() @@ -205,11 +367,11 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel forCellReuseIdentifier: "\(NEBaseChatMessageCell.self)" ) - NEChatUIKitClient.instance.getRegisterCustomCell().forEach { (key: String, value: UITableViewCell.Type) in + for (key, value) in NEChatUIKitClient.instance.getRegisterCustomCell() { cellRegisterDic[key] = value } - cellRegisterDic.forEach { (key: String, value: UITableViewCell.Type) in + for (key, value) in cellRegisterDic { tableView.register(value, forCellReuseIdentifier: key) } @@ -220,9 +382,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - // MARK: 子类可重写方法 + // MARK: - 子类可重写方法 - public func onTeamMemberChange(team: NIMTeam) {} + public func onTeamMemberChange(team: V2NIMTeam) {} override open func backEvent() { super.backEvent() @@ -230,12 +392,20 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } // load data的时候会调用 - open func getSessionInfo(session: NIMSession) {} + open func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { + if NEFriendUserCache.shared.getFriendInfo(IMKitClient.instance.account()) == nil { + ContactRepo.shared.getUserList(accountIds: [IMKitClient.instance.account()]) { users, error in + completion() + } + } else { + completion() + } + } /// 点击头像回调 /// - Parameter model: cell模型 open func didTapHeadPortrait(model: MessageContentModel?) { - if let isOut = model?.message?.isOutgoingMsg, isOut { + if let isOut = model?.message?.isSelf, isOut { Router.shared.use( MeSettingRouter, parameters: ["nav": navigationController as Any], @@ -243,7 +413,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ) return } - if let uid = model?.message?.from { + if let uid = model?.message?.senderId { Router.shared.use( ContactUserInfoPageRouter, parameters: ["nav": navigationController as Any, "uid": uid], @@ -278,7 +448,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel operationView?.removeFromSuperview() // get operations - guard let items = viewmodel.avalibleOperationsForMessage(model) else { + guard let items = viewModel.avalibleOperationsForMessage(model) else { return } @@ -300,7 +470,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // 供用户自定义 items setOperationItems(items: &filterItems, model: model) - viewmodel.operationModel = model + viewModel.operationModel = model guard let index = tableView.indexPath(for: cell) else { return } DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: DispatchWorkItem(block: { [self] in // size @@ -319,7 +489,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } var frameX = 0.0 if let msg = model?.message, - msg.isOutgoingMsg { + msg.isSelf { frameX = kScreenWidth - w } var frame = CGRect(x: frameX, y: operationY, width: w, height: h) @@ -336,126 +506,6 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel })) } - // MARK: lazy Method - - public lazy var bodyTopView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.clear - return view - }() - - public lazy var bodyView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.clear - view.addSubview(brokenNetworkView) - view.addSubview(contentView) - - NSLayoutConstraint.activate([ - brokenNetworkView.topAnchor.constraint(equalTo: view.topAnchor), - brokenNetworkView.leftAnchor.constraint(equalTo: view.leftAnchor), - brokenNetworkView.rightAnchor.constraint(equalTo: view.rightAnchor), - brokenNetworkView.heightAnchor.constraint(equalToConstant: brokenNetworkViewHeight), - ]) - - contentViewTopAnchor = contentView.topAnchor.constraint(equalTo: view.topAnchor) - contentViewTopAnchor?.isActive = true - NSLayoutConstraint.activate([ - contentView.leftAnchor.constraint(equalTo: view.leftAnchor), - contentView.rightAnchor.constraint(equalTo: view.rightAnchor), - contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - return view - }() - - public lazy var brokenNetworkView: NEBrokenNetworkView = { - let view = NEBrokenNetworkView() - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - return view - }() - - public lazy var contentView: UIView = { - let content = UIView() - content.translatesAutoresizingMaskIntoConstraints = false - content.backgroundColor = UIColor.clear - content.addSubview(tableView) - - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: content.topAnchor), - tableView.leftAnchor.constraint(equalTo: content.leftAnchor), - tableView.rightAnchor.constraint(equalTo: content.rightAnchor), - tableView.bottomAnchor.constraint(equalTo: content.bottomAnchor), - ]) - - return content - }() - - 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.mj_header = MJRefreshNormalHeader( - refreshingTarget: self, - refreshingAction: #selector(loadMoreData) - ) - tableView.keyboardDismissMode = .onDrag - return tableView - }() - - public lazy var bodyBottomView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.clear - return view - }() - - public lazy var bottomView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.clear - - view.addSubview(chatInputView) - NSLayoutConstraint.activate([ - chatInputView.leftAnchor.constraint(equalTo: view.leftAnchor), - chatInputView.rightAnchor.constraint(equalTo: view.rightAnchor), - 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 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 open func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, @@ -513,20 +563,10 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ) } - deinit { - NELog.infoLog(className(), desc: "deinit") - cleanDelegate() - } - - func cleanDelegate() { - NIMSDK.shared().mediaManager.remove(self) - viewmodel.delegate = nil - } - - // MARK: objc 方法 + // MARK: - objc 方法 func getUserSettingViewController() -> NEBaseUserSettingViewController { - UserSettingViewController(userId: viewmodel.session.sessionId) + UserSettingViewController(userId: viewModel.sessionId) } /// 设置按钮点击事件 @@ -542,14 +582,14 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return } - if viewmodel.session.sessionType == .team { + if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) == .CONVERSATION_TYPE_TEAM { Router.shared.use( TeamSettingViewRouter, parameters: ["nav": navigationController as Any, - "teamid": viewmodel.session.sessionId], + "teamid": viewModel.sessionId as Any], closure: nil ) - } else if viewmodel.session.sessionType == .P2P { + } else if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) == .CONVERSATION_TYPE_P2P { let userSetting = getUserSettingViewController() navigationController?.pushViewController(userSetting, animated: true) } @@ -569,40 +609,48 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel chatInputView.textView.resignFirstResponder() chatInputView.titleField.resignFirstResponder() } else { - layoutInputView(offset: 0) + if tap.location(in: view).y < kScreenHeight - bottomExanpndHeight { + layoutInputView(offset: 0) + } } } } - // MARK: private 方法 + // MARK: - private 方法 func loadData() { weak var weakSelf = self - viewmodel.queryRoamMsgHasMoreTime_v2 { error, historyEnd, newEnd, index in - NELog.infoLog( - ModuleName + " " + self.tag, - desc: #function + "CALLBACK queryRoamMsgHasMoreTime_v2 " + (error?.localizedDescription ?? "no error") + // 多端登录清空未读数 + viewModel.clearUnreadCount() + + viewModel.loadData { error, historyEnd, newEnd, index in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK loadData " + (error?.localizedDescription ?? "no error") ) - if let ms = weakSelf?.viewmodel.messages, ms.count > 0 { - weakSelf?.didRefreshTable() - if weakSelf?.viewmodel.isHistoryChat == true { + if let ms = weakSelf?.viewModel.messages, ms.count > 0 { + weakSelf?.tableViewReload() + weakSelf?.hasFirstLoadData = true + if weakSelf?.viewModel.isHistoryChat == true, + let num = weakSelf?.tableView.numberOfRows(inSection: 0), + index < num, index >= 0 { let indexPath = IndexPath(row: index, section: 0) weakSelf?.tableView.scrollToRow(at: indexPath, at: .middle, animated: false) + weakSelf?.anchorCell = weakSelf?.tableView.cellForRow(at: indexPath) as? NEBaseChatMessageCell + weakSelf?.anchorModel = weakSelf?.viewModel.messages[index] as? MessageVideoModel if newEnd > 0 { weakSelf?.addBottomLoadMore() } } else { - if let tempArray = weakSelf?.viewmodel.messages, tempArray.count > 0 { - weakSelf?.tableView.scrollToRow( - at: IndexPath(row: tempArray.count - 1, section: 0), - at: .bottom, - animated: false - ) + weakSelf?.removeBottomLoadMore() + if let last = weakSelf?.tableView.numberOfRows(inSection: 0) { + let indexPath = IndexPath(row: last - 1, section: 0) + weakSelf?.tableView.scrollToRow(at: indexPath, at: .bottom, animated: false) } + weakSelf?.removeBottomLoadMore() } - } else if let err = error { weakSelf?.showErrorToast(err) } @@ -612,14 +660,14 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel func loadMoreData() { weak var weakSelf = self - viewmodel.dropDownRemoteRefresh { error, count, messages in - NELog.infoLog( - ModuleName + " " + self.tag, + viewModel.dropDownRemoteRefresh { error, count, messages in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK dropDownRemoteRefresh " + (error?.localizedDescription ?? "no error") ) weakSelf?.tableView.reloadData() - if count > 0 { + if count > 0, let num = weakSelf?.tableView.numberOfRows(inSection: 0), count <= num { weakSelf?.tableView.scrollToRow( at: IndexPath(row: count - 1, section: 0), at: .top, @@ -634,38 +682,29 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel func loadCloserToNowData() { weak var weakSelf = self - viewmodel.pullRemoteRefresh { error, count, datas in - NELog.infoLog( - ModuleName + " " + self.tag, + viewModel.pullRemoteRefresh { error, count, datas in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK pullRemoteRefresh " + (error?.localizedDescription ?? "no error") ) if count <= 0 { weakSelf?.removeBottomLoadMore() } else { weakSelf?.tableView.mj_footer?.endRefreshing() - weakSelf?.didRefreshTable() + weakSelf?.tableViewReload() } } } func addObseve() { - NotificationCenter.default.addObserver(self, selector: #selector(didRefreshTable), name: NENotificationName.updateFriendInfo, object: nil) - NotificationCenter.default.addObserver(self, - selector: #selector(keyBoardWillShow(_:)), - name: UIResponder.keyboardWillShowNotification, - object: nil) - - NotificationCenter.default.addObserver(self, - selector: #selector(keyBoardWillHide(_:)), - name: UIResponder.keyboardWillHideNotification, - object: nil) - - NotificationCenter.default.addObserver(self, selector: #selector(didShowCallView), name: Notification.Name(kCallKitShowNoti), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyBoardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(appEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(appEnterForegournd), name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didShowCallView), name: Notification.Name(kCallKitShowNoti), object: nil) + let tap = UITapGestureRecognizer(target: self, action: #selector(viewTap)) tap.delegate = self tap.cancelsTouchesInView = false @@ -677,27 +716,33 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func addBottomLoadMore() { - tableView.mj_footer = MJRefreshBackNormalFooter( + let footer = MJRefreshAutoFooter( refreshingTarget: self, refreshingAction: #selector(loadCloserToNowData) ) + footer.triggerAutomaticallyRefreshPercent = -20 + tableView.mj_footer = footer } open func removeBottomLoadMore() { tableView.mj_footer?.endRefreshingWithNoMoreData() tableView.mj_footer = nil - viewmodel.isHistoryChat = false // 转为普通聊天页面 + viewModel.isHistoryChat = false // 转为普通聊天页面 } func markNeedReadMsg() { if isCurrentPage, needMarkReadMsgs.count > 0 { - viewmodel.markRead(messages: needMarkReadMsgs) { error in - NELog.infoLog( - ModuleName + " " + self.tag, + viewModel.markRead(messages: needMarkReadMsgs) { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK markRead " + (error?.localizedDescription ?? "no error") ) + + self?.viewModel.clearUnreadCount() + if error == nil { + self?.needMarkReadMsgs = [V2NIMMessage]() + } } - needMarkReadMsgs = [NIMMessage]() } } @@ -710,7 +755,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel markNeedReadMsg() } - // MARK: 键盘通知相关操作 + // MARK: - 键盘通知相关操作 open func keyBoardWillShow(_ notification: Notification) { if !isCurrentPage { @@ -735,18 +780,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel animationDuration = keyboardAnimationDuration } - layoutInputViewWithAnimation(offset: keyboardRect.size.height, animationDuration) - weak var weakSelf = self - UIView.animate(withDuration: 0.25, animations: { - weakSelf?.view.layoutIfNeeded() - }) - - // 键盘已经弹出 - if oldKeyboardRect == keyboardRect { - return - } - - scrollTableViewToBottom() + // oldKeyboardRect == keyboardRect 说明键盘已经弹出,无需重复滚动 + layoutInputViewWithAnimation(offset: keyboardRect.size.height, animationDuration, oldKeyboardRect != keyboardRect) } open func keyBoardWillHide(_ notification: Notification) { @@ -764,25 +799,27 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } private func scrollTableViewToBottom() { - NELog.infoLog(className(), desc: "self.viewmodel.messages.count\(viewmodel.messages.count)") - NELog.infoLog(className(), desc: "self.tableView.numberOfRows(inSection: 0)\(tableView.numberOfRows(inSection: 0))") - if viewmodel.messages.count > 0 { - weak var weakSelf = self - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: DispatchWorkItem(block: { - if let row = weakSelf?.tableView.numberOfRows(inSection: 0) { - let indexPath = IndexPath(row: row - 1, section: 0) - weakSelf?.tableView.scrollToRow(at: indexPath, at: .bottom, animated: true) - } - })) + NEALog.infoLog(className(), desc: "self.viewModel.messages.count\(viewModel.messages.count)") + NEALog.infoLog(className(), desc: "self.tableView.numberOfRows(inSection: 0)\(tableView.numberOfRows(inSection: 0))") + + if viewModel.isHistoryChat { + dataReload() + return + } + + let row = tableView.numberOfRows(inSection: 0) + if row > 0, row == viewModel.messages.count { + let indexPath = IndexPath(row: row - 1, section: 0) + tableView.scrollToRow(at: indexPath, at: .bottom, animated: true) } } - open func layoutInputView(offset: CGFloat) { - layoutInputViewWithAnimation(offset: offset) + open func layoutInputView(offset: CGFloat, _ scrollToBottom: Bool = false) { + layoutInputViewWithAnimation(offset: offset, 0.1, scrollToBottom) } - open func layoutInputViewWithAnimation(offset: CGFloat, _ animation: CGFloat = 0.1) { - NELog.infoLog(className(), desc: "normal height : \(normalInputHeight) normal offset: \(normalOffset) offset : \(offset)") + open func layoutInputViewWithAnimation(offset: CGFloat, _ animation: CGFloat = 0.1, _ scrollToBottom: Bool = false) { + NEALog.infoLog(className(), desc: "normal height : \(normalInputHeight) normal offset: \(normalOffset) offset : \(offset)") weak var weakSelf = self var topValue = normalInputHeight if chatInputView.chatInpuMode != .multipleReturn { @@ -792,17 +829,22 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel chatInputView.contentSubView?.isHidden = true chatInputView.currentButton?.isSelected = false } - UIView.animate(withDuration: animation, animations: { + + UIView.animate(withDuration: animation) { weakSelf?.bottomViewTopAnchor?.constant = -topValue - offset - }) + if scrollToBottom { + weakSelf?.view.layoutIfNeeded() + weakSelf?.scrollTableViewToBottom() + } + } } - // MARK: ChatInputViewDelegate + // 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)") + NEALog.infoLog(className(), desc: #function + "换行消息: \(title)") var dataDic = [String: Any]() dataDic["title"] = title if let t = text?.trimmingCharacters(in: .whitespacesAndNewlines), !t.isEmpty { @@ -813,33 +855,29 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel attachDic["type"] = customRichTextType attachDic["data"] = dataDic - let attachment = NECustomAttachment(customType: customRichTextType, data: attachDic) - let remoteExt = chatInputView.getRemoteExtension(attribute) + let rawAttachment = getJSONStringFromDictionary(attachDic) + let customMessage = MessageUtils.customMessage(text: text ?? "", rawAttachment: rawAttachment) + if let remoteExt = chatInputView.getAtRemoteExtension(attribute) { + customMessage.serverExtension = getJSONStringFromDictionary(remoteExt) + } - weak var weakSelf = self - if viewmodel.isReplying, let msg = viewmodel.operationModel?.message { - viewmodel.replyMessageWithoutThread(message: - MessageUtils.customMessage(attachment: attachment, - remoteExt: remoteExt, - apnsContent: title), - target: msg) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), - desc: #function + "CALLBACK replyMessage " + (error?.localizedDescription ?? "no error") - ) - if error != nil { - weakSelf?.showErrorToast(error) - } else { - weakSelf?.closeReply(button: nil) - } - self?.chatInputView.titleField.text = nil - self?.chatInputView.textView.text = nil - self?.didSendFinishAndCheckoutInput() + if viewModel.isReplying, let msg = viewModel.operationModel?.message { + viewModel.replyMessageWithoutThread(message: customMessage, + replyMessage: msg) { [weak self] message, error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK replyMessage " + (error?.localizedDescription ?? "no error") + ) + if error != nil { + self?.showErrorToast(error) } + self?.chatInputView.titleField.text = nil + self?.chatInputView.textView.text = nil + self?.didSendFinishAndCheckoutInput() + } + closeReply(button: nil) } else { - viewmodel.sendCustomMessage(attachment: attachment, - remoteExt: remoteExt, - apnsConstent: title) { [weak self] error in + viewModel.sendMessage(message: customMessage) { [weak self] message, error in self?.showErrorToast(error) self?.chatInputView.titleField.text = nil self?.chatInputView.textView.text = nil @@ -855,36 +893,34 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel func sendContentText(text: String?, attribute: NSAttributedString?) { guard let removeSpace = text?.trimmingCharacters(in: .whitespaces), removeSpace.count > 0 else { chatInputView.titleField.text = nil - showToast(chatLocalizable("null_message_not_support")) + view.makeToast(chatLocalizable("null_message_not_support"), position: .center) return } guard let content = text, content.count > 0 else { return } - let remoteExt = chatInputView.getRemoteExtension(attribute) - chatInputView.cleartAtCache() - weak var weakSelf = self - if viewmodel.isReplying, let msg = viewmodel.operationModel?.message { - viewmodel.replyMessageWithoutThread(message: MessageUtils.textMessage(text: content, remoteExt: remoteExt), target: msg) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), + let remoteExt = chatInputView.getAtRemoteExtension(attribute) + chatInputView.clearAtCache() + + if viewModel.isReplying, let msg = viewModel.operationModel?.message { + viewModel.replyMessageWithoutThread(message: MessageUtils.textMessage(text: content, remoteExt: remoteExt), replyMessage: msg) { [weak self] message, error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK replyMessage " + (error?.localizedDescription ?? "no error") ) if error != nil { - weakSelf?.showErrorToast(error) - } else { - weakSelf?.closeReply(button: nil) + self?.showErrorToast(error) } - weakSelf?.didSendFinishAndCheckoutInput() + self?.didSendFinishAndCheckoutInput() } - + closeReply(button: nil) } else { - viewmodel.sendTextMessage(text: content, remoteExt: remoteExt) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), + viewModel.sendTextMessage(text: content, remoteExt: remoteExt) { [weak self] message, error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendTextMessage " + (error?.localizedDescription ?? "no error") ) - weakSelf?.showErrorToast(error) + self?.showErrorToast(error) self?.chatInputView.titleField.text = nil self?.chatInputView.textView.text = nil self?.didSendFinishAndCheckoutInput() @@ -944,44 +980,54 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func showRtcCallAction() { - var param = [String: AnyObject]() - param["remoteUserAccid"] = viewmodel.session.sessionId as AnyObject - param["currentUserAccid"] = NIMSDK.shared().loginManager.currentAccount() as AnyObject - param["remoteShowName"] = titleContent as AnyObject - if let user = viewmodel.repo.getUserInfo(userId: viewmodel.session.sessionId), let avatar = user.userInfo?.avatarUrl { - param["remoteAvatar"] = avatar as AnyObject + let videoCallAction = UIAlertAction(title: chatLocalizable("video_call"), style: .default) { [weak self] _ in + self?.useToCallViewRouter(2) } - let videoCallAction = UIAlertAction(title: chatLocalizable("video_call"), style: .default) { _ in - param["type"] = NSNumber(integerLiteral: 2) as AnyObject - Router.shared.use(CallViewRouter, parameters: param) - } - let audioCallAction = UIAlertAction(title: chatLocalizable("audio_call"), style: .default) { _ in - param["type"] = NSNumber(integerLiteral: 1) as AnyObject - Router.shared.use(CallViewRouter, parameters: param) + let audioCallAction = UIAlertAction(title: chatLocalizable("audio_call"), style: .default) { [weak self] _ in + self?.useToCallViewRouter(1) } - let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), - style: .cancel) { action in + + let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), style: .cancel) { _ in } + showActionSheet([videoCallAction, audioCallAction, cancelAction]) } - func didToSearchLocationView() { - let ctrl = NEDetailMapController(type: .search) - navigationController?.pushViewController(ctrl, animated: true) - weak var weakSelf = self - ctrl.completion = { model in - NELog.infoLog(self.className(), desc: "position : \(model.yx_modelToJSONString() ?? "")") - weakSelf?.viewmodel.sendLocationMessage(model) { error in - weakSelf?.showErrorToast(error) - } + /// 跳转音视频呼叫页面 + /// - Parameter type: 呼叫类型,1 - 音频;2 - 视频 + func useToCallViewRouter(_ type: Int) { + // 校验配置项 + if !IMKitConfigCenter.shared.strangerCallEnable, + !NEFriendUserCache.shared.isFriend(viewModel.sessionId) { + viewModel.insertTipMessage(chatLocalizable("disable_stranger_call"), viewModel.conversationId) + return + } + + var param = [String: Any]() + param["remoteUserAccid"] = viewModel.sessionId + param["currentUserAccid"] = IMKitClient.instance.account() + param["remoteShowName"] = titleContent + param["type"] = NSNumber(integerLiteral: type) + + if let user = NEFriendUserCache.shared.getFriendInfo(viewModel.sessionId) ?? ChatUserCache.shared.getUserInfo(viewModel.sessionId) { + param["remoteAvatar"] = user.user?.avatar } + + Router.shared.use(CallViewRouter, parameters: param) + } + + func didToSearchLocationView() { + var params = [String: Any]() + params["type"] = NEMapType.search.rawValue + params["nav"] = navigationController + Router.shared.use(NERouterUrl.LocationVCRouter, parameters: params) } open func textChanged(text: String) -> Bool { if text == "@" { // 做p2p类型判断 - if viewmodel.session.sessionType == .P2P { + if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) == .CONVERSATION_TYPE_P2P { return true } else { DispatchQueue.main.async { @@ -1048,7 +1094,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func textFieldDidEndEditing(_ text: String?) { - viewmodel.sendInputTypingEndState() + checkAndSendTypingState(endEdit: true) } open func textFieldDidBeginEditing(_ text: String?) { @@ -1059,31 +1105,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel checkAndSendTypingState() } - func checkAndSendTypingState() { - if chatInputView.chatInpuMode == .normal { - if let content = chatInputView.textView.text, content.count > 0 { - viewmodel.sendInputTypingState() - } else { - viewmodel.sendInputTypingEndState() - } - } else { - var title = "" - var content = "" - - if let titleText = chatInputView.titleField.text { - title = titleText - } - - if let contentText = chatInputView.textView.text { - content = contentText - } - if title.count <= 0, content.count <= 0 { - viewmodel.sendInputTypingEndState() - } else { - viewmodel.sendInputTypingState() - } - } - } + /// 检查并发送正在输入状态 + /// - Parameter endEdit: 是否停止输入 + open func checkAndSendTypingState(endEdit: Bool = false) {} open func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { textView.typingAttributes = [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)] @@ -1095,26 +1119,23 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel if index == 2 || button?.isSelected == true { if index == 0 { // 语音 - layoutInputView(offset: bottomExanpndHeight) - scrollTableViewToBottom() + layoutInputView(offset: bottomExanpndHeight, true) } else if index == 1 { // emoji - layoutInputView(offset: bottomExanpndHeight) - scrollTableViewToBottom() + layoutInputView(offset: bottomExanpndHeight, true) } else if index == 2 { // 相册 isFile = false goPhotoAlbumWithVideo(self) { [weak self] in - if NIMSDK.shared().mediaManager.isPlaying() { - NIMSDK.shared().mediaManager.stopPlay() - self?.playingCell?.stopAnimation(byRight: self?.playingModel?.message?.isOutgoingMsg ?? true) + if self?.audioPlayer?.isPlaying == true { + self?.audioPlayer?.stop() + self?.playingCell?.stopAnimation(byRight: self?.playingModel?.message?.isSelf ?? true) self?.playingModel?.isPlaying = false } } } else if index == 3 { // 更多 - layoutInputView(offset: bottomExanpndHeight) - scrollTableViewToBottom() + layoutInputView(offset: bottomExanpndHeight, true) } } else { layoutInputView(offset: 0) @@ -1128,7 +1149,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel preferredStyle: .actionSheet ) alert.modalPresentationStyle = .popover - let camera = UIAlertAction(title: chatLocalizable("take_photo"), style: .default) { action in + let camera = UIAlertAction(title: commonLocalizable("take_picture"), style: .default) { action in self.takePhoto() } let photo = UIAlertAction(title: chatLocalizable("select_from_album"), style: .default) { action in @@ -1154,7 +1175,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel imagePickerVC.delegate = self imagePickerVC.allowsEditing = false imagePickerVC.sourceType = .photoLibrary - present(imagePickerVC, animated: true) {} + present(imagePickerVC, animated: true) } open func takePhoto() { @@ -1162,18 +1183,17 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel imagePickerVC.delegate = self imagePickerVC.allowsEditing = false imagePickerVC.sourceType = .camera - present(imagePickerVC, animated: true) {} - } - - open func clearAtRemind() { - let sessionId = viewmodel.session.sessionId - let param = ["sessionId": sessionId] - Router.shared.use("ClearAtMessageRemind", parameters: param, closure: nil) + present(imagePickerVC, animated: true) } open func sendMediaMessage(didFinishPickingMediaWithInfo info: [UIImagePickerController .InfoKey: Any]) { var imageName = "IMG_0001" + var imageWidth: Int32 = 0 + var imageHeight: Int32 = 0 + var videoDuration: Int32 = 0 + + // 获取展示名称 if isFile == true, let imgUrl = info[.referenceURL] as? URL { let fetchRes = PHAsset.fetchAssets(withALAssetURLs: [imgUrl], options: nil) @@ -1181,18 +1201,42 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel if let fileName = asset?.value(forKey: "filename") as? String { imageName = fileName } - } + } + + // 获取图片宽高、视频时长 + // phAsset 不一定有 + if #available(iOS 11.0, *) { + if let phAsset = info[.phAsset] as? PHAsset { + imageWidth = Int32(phAsset.pixelWidth) + imageHeight = Int32(phAsset.pixelHeight) + videoDuration = Int32(phAsset.duration * 1000) + } + } + + // video + if let videoUrl = info[.mediaURL] as? URL { + print("image picker video : url", videoUrl) + + // 获取视频宽高、时长 + let asset = AVURLAsset(url: videoUrl) + videoDuration = Int32(asset.duration.seconds * 1000) + + let track = asset.tracks(withMediaType: .video).first + if let track = track { + let size = track.naturalSize + let transform = track.preferredTransform + let correctedSize = size.applying(transform) + imageWidth = Int32(abs(correctedSize.width)) + imageHeight = Int32(abs(correctedSize.height)) + } - if let url = info[.mediaURL] as? URL { - // video - print("image picker video : url", url) weak var weakSelf = self if isFile == true { - copyFileToSend(url: url, displayName: imageName) + copyFileToSend(url: videoUrl, displayName: imageName) } else { - viewmodel.sendVideoMessage(url: url) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ChatViewController"), + viewModel.sendVideoMessage(url: videoUrl, name: imageName, width: imageWidth, height: imageHeight, duration: videoDuration) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendVideoMessage " + (error?.localizedDescription ?? "no error") ) weakSelf?.showErrorToast(error) @@ -1201,85 +1245,110 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return } - guard let image = info[.originalImage] as? UIImage else { - showToast(chatLocalizable("image_is_nil")) - return - } + if #available(iOS 11.0, *) { + var imageUrl = info[.imageURL] as? URL + var image = info[.originalImage] as? UIImage + image = image?.fixOrientation() - if isFile == true, - let imgData = image.pngData() { - let imgSize_MB = Double(imgData.count) / 1e6 - NELog.infoLog(ModuleName + " " + tag, desc: #function + "imgSize_MB: \(imgSize_MB) MB") - if imgSize_MB > NEKitChatConfig.shared.ui.fileSizeLimit { - showToast(String(format: chatLocalizable("fileSize_over_limit"), "\(NEKitChatConfig.shared.ui.fileSizeLimit)")) - } else { - viewmodel.sendFileMessage(data: imgData, displayName: imageName) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), - desc: #function + "CALLBACK sendFileMessage" + (error?.localizedDescription ?? "no error") - ) - if error != nil { - self?.view.makeToast(error!.localizedDescription) + // 获取图片宽度 + if let width = image?.size.width { + imageWidth = Int32(width) + } + + // 获取图片高度度 + if let height = image?.size.height { + imageHeight = Int32(height) + } + + let pngImage = image?.pngData() + var needDelete = false + + // 无url则临时保存到本地,发送成功后删除临时文件 + if imageUrl == nil { + if let data = pngImage, let path = NEPathUtils.getDirectoryForDocuments(dir: "\(imkitDir)image/") { + let url = URL(fileURLWithPath: path + "\(imageName).png") + do { + try data.write(to: url) + imageUrl = url + needDelete = true + } catch { + showToast(chatLocalizable("image_is_nil")) } } } - } else { - if let url = info[.referenceURL] as? URL { - if url.absoluteString.hasSuffix("ext=GIF") == true { - // GIF 需要特殊处理 - let imageAsset: PHAsset? - if #available(iOS 11.0, *) { - imageAsset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset - } else { - imageAsset = PHAsset.fetchAssets(withALAssetURLs: [url], options: nil).firstObject - } - let options = PHImageRequestOptions() - options.version = .current - guard let asset = imageAsset else { - return + + guard let imageUrl = imageUrl else { + showToast(chatLocalizable("image_is_nil")) + return + } + + if isFile == true { + let imgSize_MB = Double(pngImage?.count ?? 0) / 1e6 + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "imgSize_MB: \(imgSize_MB) MB") + if imgSize_MB > NEKitChatConfig.shared.ui.fileSizeLimit { + showToast(String(format: chatLocalizable("fileSize_over_limit"), "\(NEKitChatConfig.shared.ui.fileSizeLimit)")) + } else { + viewModel.sendFileMessage(filePath: imageUrl.relativePath, displayName: imageName) { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK sendFileMessage" + (error?.localizedDescription ?? "no error") + ) + self?.showErrorToast(error) } - weak var weakSelf = self - PHImageManager.default().requestImageData(for: asset, options: options) { imageData, dataUTI, orientation, info in - if let data = imageData { - let tempDirectoryURL = FileManager.default.temporaryDirectory - let uniqueString = UUID().uuidString - let temUrl = tempDirectoryURL.appendingPathComponent(uniqueString + ".gif") - print("tem url path : ", temUrl.path) - do { - try data.write(to: temUrl) - DispatchQueue.main.async { - weakSelf?.viewmodel.sendImageMessage(path: temUrl.path) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ChatViewController"), - desc: #function + "CALLBACK sendImageMessage " + (error?.localizedDescription ?? "no error") - ) - if error != nil { - weakSelf?.view.makeToast(error?.localizedDescription) + } + } else { + if let url = info[.referenceURL] as? URL { + if url.absoluteString.hasSuffix("ext=GIF") == true { + // GIF 需要特殊处理 + let imageAsset = info[UIImagePickerController.InfoKey.phAsset] as? PHAsset + let options = PHImageRequestOptions() + options.version = .current + guard let asset = imageAsset else { + return + } + weak var weakSelf = self + PHImageManager.default().requestImageData(for: asset, options: options) { imageData, dataUTI, orientation, info in + if let data = imageData { + let tempDirectoryURL = FileManager.default.temporaryDirectory + let uniqueString = UUID().uuidString + let temUrl = tempDirectoryURL.appendingPathComponent(uniqueString + ".gif") + print("tem url path : ", temUrl.path) + do { + try data.write(to: temUrl) + DispatchQueue.main.async { + weakSelf?.viewModel.sendImageMessage(path: temUrl.path, name: imageName, width: imageWidth, height: imageHeight) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK sendImageMessage " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.showErrorToast(error) } } + } catch { + NEALog.infoLog(ModuleName, desc: #function + "write tem gif data error : \(error.localizedDescription)") } - } catch { - NELog.infoLog(ModuleName, desc: #function + "write tem gif data error : \(error.localizedDescription)") } } + return } - return } - } - viewmodel.sendImageMessage(image: image) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), - desc: #function + "CALLBACK sendImageMessage " + (error?.localizedDescription ?? "no error") - ) - if error != nil { - self?.view.makeToast(error?.localizedDescription) + viewModel.sendImageMessage(path: imageUrl.relativePath, name: imageName, width: imageWidth, height: imageHeight) { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK sendImageMessage " + (error?.localizedDescription ?? "no error") + ) + self?.showErrorToast(error) + // 删除临时保存的图片 + if needDelete { + try? FileManager.default.removeItem(at: imageUrl) + } } } } } - // MARK: UIImagePickerControllerDelegate + // MARK: - UIImagePickerControllerDelegate open func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController @@ -1294,21 +1363,25 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel picker.dismiss(animated: true) } - // MARK: UIDocumentPickerDelegate + // MARK: - UIDocumentPickerDelegate + /// 拷贝文件到沙盒,用于发送 + /// - Parameters: + /// - url: 原始路径 + /// - displayName: 显示名称 func copyFileToSend(url: URL, displayName: String) { let desPath = NSTemporaryDirectory() + "\(url.lastPathComponent)" let dirUrl = URL(fileURLWithPath: desPath) if !FileManager.default.fileExists(atPath: desPath) { - NELog.infoLog(ModuleName + " " + tag, desc: #function + "file not exist") + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "file not exist") do { try FileManager.default.copyItem(at: url, to: dirUrl) } catch { - NELog.errorLog(ModuleName + " " + tag, desc: #function + "copyItem [\(desPath)] ERROR: \(error)") + NEALog.errorLog(ModuleName + " " + ChatViewController.className(), desc: #function + "copyItem [\(desPath)] ERROR: \(error)") } } if FileManager.default.fileExists(atPath: desPath) { - NELog.infoLog(ModuleName + " " + tag, desc: #function + "fileExists") + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "fileExists") do { let fileAttributes = try FileManager.default.attributesOfItem(atPath: desPath) if let size_B = fileAttributes[FileAttributeKey.size] as? Double { @@ -1317,19 +1390,17 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel showToast(String(format: chatLocalizable("fileSize_over_limit"), "\(NEKitChatConfig.shared.ui.fileSizeLimit)")) try? FileManager.default.removeItem(atPath: desPath) } else { - viewmodel.sendFileMessage(filePath: desPath, displayName: displayName) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), + viewModel.sendFileMessage(filePath: desPath, displayName: displayName) { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendFileMessage " + (error?.localizedDescription ?? "no error") ) - if error != nil { - self?.view.makeToast(error!.localizedDescription) - } + self?.showErrorToast(error) } } } } catch { - NELog.errorLog(ModuleName + " " + tag, desc: #function + "get file size error: \(error)") + NEALog.errorLog(ModuleName + " " + ChatViewController.className(), desc: #function + "get file size error: \(error)") } } } @@ -1348,11 +1419,11 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // 停止安全访问权限 url.stopAccessingSecurityScopedResource() } else { - NELog.errorLog(ModuleName + " " + tag, desc: #function + "fileUrlAuthozied FAILED") + NEALog.errorLog(ModuleName + " " + ChatViewController.className(), desc: #function + "fileUrlAuthozied FAILED") } } - // MARK: UIDocumentInteractionControllerDelegate + // MARK: - UIDocumentInteractionControllerDelegate open func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController { self @@ -1362,30 +1433,74 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel controller.dismiss(animated: true) } - // MARK: ChatViewModelDelegate + // MARK: - NEContactListener - open func didLeaveTeam() { - weak var weakSelf = self - showSingleAlert(message: chatLocalizable("team_has_quit")) { - weakSelf?.navigationController?.popViewController(animated: true) + /// 好友(用户)信息变更回调 + /// - Parameter accountId: 用户 id + func onUserOrFriendInfoChanged(_ accountId: String) { + let sessionId = viewModel.sessionId + + if accountId == sessionId { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: DispatchWorkItem(block: { [weak self] in + let showName = ChatTeamCache.shared.getShowName(sessionId) + self?.titleContent = showName + self?.title = showName + })) } } - open func didDismissTeam() { - weak var weakSelf = self - showSingleAlert(message: chatLocalizable("team_has_been_removed")) { - weakSelf?.navigationController?.popViewController(animated: true) + /// 用户信息变更回调 + /// - Parameter users: 用户列表 + public func onUserProfileChanged(_ users: [V2NIMUser]) { + for user in users { + guard let accountId = user.accountId else { + return + } + + if !NEFriendUserCache.shared.isFriend(accountId) { + ChatUserCache.shared.updateUserInfo(user) + } + + onUserOrFriendInfoChanged(accountId) } } - open func onRecvMessages(_ messages: [NIMMessage]) { + /// 好友信息变更回调 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + guard let accountId = friendInfo.accountId else { + return + } + onUserOrFriendInfoChanged(accountId) + } + + // MARK: - ChatviewModelDelegate + + /// 本端即将发送消息状态回调,此时消息还未发送,可对消息进行修改或者拦截发送 + /// 来源: 发送消息, 插入消息 + /// - Parameter message: 消息 + /// - Parameter completion: 是否继续发送消息 + public func readySendMessage(_ message: V2NIMMessage, _ completion: @escaping (Bool) -> Void) { + if let block = NEKitChatConfig.shared.ui.onSendMessage { + completion(block(message, self)) + } else { + completion(true) + } + } + + /// 收到消息 + /// - Parameter messages: 消息列表 + open func onRecvMessages(_ messages: [V2NIMMessage], _ indexs: [IndexPath]) { operationView?.removeFromSuperview() - insertRows() + insertRows(indexs) + + // 如果当前页面是活跃状态,发送已读回执 if isCurrentPage, UIApplication.shared.applicationState == .active { - viewmodel.markRead(messages: messages) { error in - NELog.infoLog( - ModuleName + " " + self.tag, + // 发送已读回执 + viewModel.markRead(messages: messages) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK markRead " + (error?.localizedDescription ?? "no error") ) } @@ -1394,110 +1509,157 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - open func willSend(_ message: NIMMessage) { - insertRows() + /// 消息即将发送 + /// - Parameter message: 消息 + open func sending(_ message: V2NIMMessage, _ index: IndexPath) { + insertRows([index]) } - open func send(_ message: NIMMessage, progress: Float) {} + /// 消息发送成功 + /// - Parameter message: 消息 + public func sendSuccess(_ message: V2NIMMessage, _ index: IndexPath) { + tableViewReloadIndexs([index]) + } - open func send(_ message: NIMMessage, didCompleteWithError error: Error?) { - if indexPathsWithMessags([message]).count > 0 { - tableViewReloadIndexs(indexPathsWithMessags([message])) - } + public func onLoadMoreWithMessage(_ indexs: [IndexPath]) { + tableViewReloadIndexs(indexs) } - private func indexPathsWithMessags(_ messages: [NIMMessage]) -> [IndexPath] { - var indexPaths = [IndexPath]() - for messageModel in messages { - for (i, model) in viewmodel.messages.enumerated() { - if model.message?.messageId == messageModel.messageId { - indexPaths.append(IndexPath(row: i, section: 0)) + open func onDeleteMessage(_ messages: [V2NIMMessage], deleteIndexs: [IndexPath], reloadIndex: [IndexPath]) { + if deleteIndexs.isEmpty { + return + } + + operationView?.removeFromSuperview() + tableViewReloadIndexs(reloadIndex) { [weak self] in + for index in reloadIndex { + if let numberOfRows = self?.tableView.numberOfRows(inSection: 0), index.row == numberOfRows - 1 { + self?.scrollTableViewToBottom() } } } - return indexPaths - } - open func onDeleteMessage(_ message: NIMMessage, atIndexs: [IndexPath], reloadIndex: [IndexPath]) { - if atIndexs.isEmpty { - return + for message in messages { + viewModel.messages.removeAll { $0.message?.messageClientId == message.messageClientId } + + // 刷新回复弹窗 + if message.messageClientId == viewModel.operationModel?.message?.messageClientId { + replyView.textLabel.attributedText = nil + replyView.textLabel.text = chatLocalizable("message_not_found") + replyView.layoutIfNeeded() + } + } + tableViewDeleteIndexs(deleteIndexs) + + // 如果消息为空(加载的消息全部被删除),则拉取更多数据 + if viewModel.messages.isEmpty { + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: DispatchWorkItem(block: { [weak self] in + self?.loadMoreData() + })) } - viewmodel.selectedMessages.removeAll(where: { $0.messageId == message.messageId }) - operationView?.removeFromSuperview() - tableViewDeleteIndexs(atIndexs) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in - self?.tableViewReloadIndexs(reloadIndex) - })) } - open func updateDownloadProgress(_ message: NIMMessage, atIndex: IndexPath, progress: Float) { - tableViewUpdateDownload(atIndex) + open func onResendSuccess(_ fromIndex: IndexPath, _ toIndexPath: IndexPath) { + tableView.moveRow(at: fromIndex, to: toIndexPath) + tableView.reloadRows(at: [toIndexPath], with: .automatic) + tableView.scrollToRow(at: toIndexPath, at: .bottom, animated: true) } - open func onRevokeMessage(_ message: NIMMessage, atIndexs: [IndexPath]) { + open func onRevokeMessage(_ message: V2NIMMessage, atIndexs: [IndexPath]) { if atIndexs.isEmpty { return } - viewmodel.selectedMessages.removeAll(where: { $0.messageId == message.messageId }) + viewModel.selectedMessages.removeAll(where: { $0.messageClientId == message.messageClientId }) operationView?.removeFromSuperview() - NELog.infoLog(className(), desc: "on revoke message at indexs \(atIndexs)") - tableViewReloadIndexs(atIndexs) - } + NEALog.infoLog(className(), desc: "on revoke message at indexs \(atIndexs)") + tableViewReloadIndexs(atIndexs) { [weak self] in + for index in atIndexs { + if let numberOfRows = self?.tableView.numberOfRows(inSection: 0), index.row == numberOfRows - 1 { + self?.scrollTableViewToBottom() + } + } + } - open func onAddMessagePin(_ message: NIMMessage, atIndexs: [IndexPath]) { - tableViewReloadIndexs(atIndexs) + // 刷新回复弹窗 + if message.messageClientId == viewModel.operationModel?.message?.messageClientId { + replyView.textLabel.attributedText = nil + replyView.textLabel.text = chatLocalizable("message_not_found") + replyView.layoutIfNeeded() + } } - open func onRemoveMessagePin(_ message: NIMMessage, atIndexs: [IndexPath]) { - tableViewReloadIndexs(atIndexs) + public func onMessagePinStatusChange(_ message: V2NIMMessage?, atIndexs: [IndexPath]) { + tableViewReloadIndexs(atIndexs) { [weak self] in + for index in atIndexs { + if let numberOfRows = self?.tableView.numberOfRows(inSection: 0), index.row == numberOfRows - 1 { + self?.scrollTableViewToBottom() + } + } + } } open func tableViewDeleteIndexs(_ indexs: [IndexPath]) { - tableView.beginUpdates() - tableView.deleteRows(at: indexs, with: .none) - tableView.endUpdates() + tableView.deleteData(indexs) } - open func tableViewReloadIndexs(_ indexs: [IndexPath]) { - weak var weakSelf = self - if #available(iOS 11.0, *) { - tableView.performBatchUpdates { - weakSelf?.tableView.reloadRows(at: indexs, with: .none) - } - } else { - tableView.beginUpdates() - tableView.reloadRows(at: indexs, with: .none) - tableView.endUpdates() + open func tableViewReloadIndexs(_ indexs: [IndexPath], _ completion: (() -> Void)? = nil) { + if isUploadingData { + return } - indexs.forEach { index in - if index.row == tableView.numberOfRows(inSection: 0) - 1 { - tableView.scrollToRow(at: index, at: .bottom, animated: true) - } + let indexs = indexs.filter { index in + index.row >= 0 && index.row < tableView.numberOfRows(inSection: 0) + } + + if indexs.isEmpty { + return + } + + tableView.reloadData(indexs) { _ in + completion?() } } - open func didReadedMessageIndexs() { - didRefreshTable() + open func tableViewReload() { + tableView.reloadData() } - open func tableViewUpdateDownload(_ index: IndexPath) { - if #available(iOS 11.0, *) { - tableView.performBatchUpdates { - tableView.reloadRows(at: [index], with: .none) - } + open func dataReload() { + if viewModel.isHistoryChat { + viewModel.anchor = nil + loadData() + } + } + + /// 设置置顶消息展示文案,如果传入nil则移除置顶视图 + /// - Parameter name: 置顶消息发送者昵称 + /// - Parameter content: 置顶消息内容文案 + /// - Parameter url: 置顶消息 图片缩略图/视频首帧 地址 + /// - Parameter isVideo: 置顶消息是否是视频消息 + /// - Parameter hideClose: 是否隐藏移除置顶按钮 + public func setTopValue(name: String?, content: String?, url: String?, isVideo: Bool, hideClose: Bool) { + if let content = content { + contentView.addSubview(topMessageView) + NSLayoutConstraint.activate([ + topMessageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: chat_content_margin), + topMessageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: chat_content_margin), + topMessageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -chat_content_margin), + topMessageView.heightAnchor.constraint(equalToConstant: 40), + ]) + topMessageView.setTopContent(name: name, content: content, url: url, isVideo: isVideo, hideClose: hideClose) } else { - tableView.beginUpdates() - tableView.reloadRows(at: [index], with: .none) - tableView.endUpdates() + topMessageView.removeFromSuperview() } } - open func didRefreshTable() { - getSessionInfo(session: viewmodel.session) - tableView.reloadData() + /// 更新置顶消息中的发送者昵称 + /// - Parameter name: 发送者昵称 + public func updateTopName(name: String?) { + topMessageView.updateTopName(name: name) } + /// 多选消息数量发生改变 + /// - Parameter count: 选中的消息数量 open func selectedMessagesChanged(_ count: Int) { mutilSelectBottomView.setEnable(count > 0) } @@ -1534,36 +1696,51 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - // MARK: audio play + // MARK: - audio play - func startPlaying(audioMessage: NIMMessage?, isSend: Bool) { - guard let message = audioMessage, let audio = message.messageObject as? NIMAudioObject else { + func startPlaying(audioMessage: V2NIMMessage?, isSend: Bool) { + guard let message = audioMessage, let audio = message.attachment as? V2NIMMessageAudioAttachment else { return } + playingCell?.startAnimation(byRight: isSend) - if let path = audio.path, FileManager.default.fileExists(atPath: path) { - NELog.infoLog(className(), desc: #function + " play path : " + path) - if viewmodel.getHandSetEnable() == true { - NIMSDK.shared().mediaManager.switch(.receiver) - } else { - NIMSDK.shared().mediaManager.switch(.speaker) + let path = audio.path ?? ChatMessageHelper.createFilePath(message) + if FileManager.default.fileExists(atPath: path) { + NEALog.infoLog(className(), desc: #function + " play path : " + path) + + // 创建一个URL对象,指向音频文件 + let audioURL = URL(fileURLWithPath: path) + + do { + // 设置听筒/扬声器 + let cate: AVAudioSession.Category = viewModel.getHandSetEnable() ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback + try AVAudioSession.sharedInstance().setCategory(cate, options: .duckOthers) + try AVAudioSession.sharedInstance().setActive(true) + + // 检查URL是否有效并尝试加载音频 + audioPlayer = try AVAudioPlayer(contentsOf: audioURL) + audioPlayer?.delegate = self + + // 开始播放 + audioPlayer?.play() + } catch { + // 处理加载音频文件失败的情况 + playingCell?.stopAnimation(byRight: isSend) + print("Error loading audio: \(error.localizedDescription)") } - NIMSDK.shared().mediaManager.play(path) } else { - NELog.infoLog(className(), desc: #function + " audio path is empty, play url : " + (audio.url ?? "")) + NEALog.infoLog(className(), desc: #function + " audio path is empty, play url : " + (audio.url ?? "")) playingCell?.stopAnimation(byRight: isSend) - ChatMessageHelper.downloadAudioFile(message: message) } } private func startPlay(cell: ChatAudioCellProtocol?, model: MessageAudioModel?) { - guard let audio = model?.message?.messageObject as? NIMAudioObject, - let isSend = model?.message?.isOutgoingMsg else { + guard let isSend = model?.message?.isSelf else { return } if playingModel == model { - if NIMSDK.shared().mediaManager.isPlaying() { + if audioPlayer?.isPlaying == true { stopPlay() } else { startPlaying(audioMessage: model?.message, isSend: isSend) @@ -1577,62 +1754,15 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func stopPlay() { - if NIMSDK.shared().mediaManager.isPlaying() { - playingCell?.startAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) - NIMSDK.shared().mediaManager.stopPlay() - } - } - - // private func startPlay() { - // if NIMSDK.shared().mediaManager.isPlaying() { - // self.playingCell?.startAnimation() - // NIMSDK.shared().mediaManager.stopPlay() - // } - // } - - // MARK: NIMMediaManagerDelegate - - // 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 { - showErrorToast(error) - // stop - playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) - playingModel?.isPlaying = false + if audioPlayer?.isPlaying == true { + audioPlayer?.stop() } - } - - open func playAudio(_ filePath: String, didCompletedWithError error: Error?) { - 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?.localizedDescription ?? "")") - showErrorToast(error) - playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) - playingModel?.isPlaying = false - } - open func playAudio(_ filePath: String, progress value: Float) {} - - open func playAudioInterruptionEnd() { - print(#function) - playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) + playingCell?.stopAnimation(byRight: playingModel?.message?.isSelf ?? true) playingModel?.isPlaying = false } - open func playAudioInterruptionBegin() { - print(#function) - // stop play - playingCell?.stopAnimation(byRight: playingModel?.message?.isOutgoingMsg ?? true) - playingModel?.isPlaying = false - } + // MARK: - NIMMediaManagerDelegate // record open func recordAudio(_ filePath: String?, didBeganWithError error: Error?) { @@ -1650,9 +1780,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel print("dur:\(dur)") if dur > 1 { - viewmodel.sendAudioMessage(filePath: fp) { error in - NELog.infoLog( - ModuleName + " " + self.tag, + viewModel.sendAudioMessage(filePath: fp) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK sendAudioMessage " + (error?.localizedDescription ?? "no error") ) self.showErrorToast(error) @@ -1668,54 +1798,39 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func recordAudioProgress(_ currentTime: TimeInterval) {} - open func recordAudioInterruptionBegin() { - print(#function) - } + open func recordAudioInterruptionBegin() {} - // MARK: Private Method + // MARK: - Private Method private func recordDuration(filePath: String) -> Float64 { let avAsset = AVURLAsset(url: URL(fileURLWithPath: filePath)) return CMTimeGetSeconds(avAsset.duration) } - private func insertRows() { + private func insertRows(_ indexs: [IndexPath]) { + if !hasFirstLoadData { + return + } + let oldRows = tableView.numberOfRows(inSection: 0) if oldRows == 0 { tableView.reloadData() return } - if oldRows == viewmodel.messages.count { + if oldRows == viewModel.messages.count { tableView.reloadData() return } - var indexs = [IndexPath]() - for (i, _) in viewmodel.messages.enumerated() { - if i >= oldRows { - indexs.append(IndexPath(row: i, section: 0)) - } - } if !indexs.isEmpty { - if #available(iOS 11.0, *) { - self.tableView.performBatchUpdates { - self.tableView.insertRows(at: indexs, with: .bottom) - } completion: { finished in - self.tableView.scrollToRow( - at: IndexPath(row: self.viewmodel.messages.count - 1, section: 0), + tableView.insertData(indexs) { [weak self] _ in + if let row = self?.tableView.numberOfRows(inSection: 0), row > 0 { + self?.tableView.scrollToRow( + at: IndexPath(row: row - 1, section: 0), at: .bottom, animated: false ) } - } else { - tableView.insertRows(at: indexs, with: .bottom) - DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: DispatchWorkItem(block: { - self.tableView.scrollToRow( - at: IndexPath(row: self.viewmodel.messages.count - 1, section: 0), - at: .bottom, - animated: false - ) - })) } } } @@ -1723,7 +1838,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func addToAtUsers(addText: String, isReply: Bool = false, accid: String, _ isLongPress: Bool = false) { if let font = chatInputView.textView.font { let mutaString = NSMutableAttributedString(attributedString: chatInputView.textView.attributedText) - let atString = NSAttributedString(string: addText, attributes: [NSAttributedString.Key.foregroundColor: UIColor.ne_blueText, NSAttributedString.Key.font: font]) + let atString = NSAttributedString(string: addText, attributes: [NSAttributedString.Key.foregroundColor: UIColor.ne_normalTheme, NSAttributedString.Key.font: font]) var selectRange = NSMakeRange(0, 0) var location = 0 if chatInputView.textView.isFirstResponder == true { @@ -1762,8 +1877,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } + /// 获取@列表视图控制器 - 基类 func getUserSelectVC() -> NEBaseSelectUserViewController { - NEBaseSelectUserViewController(sessionId: viewmodel.session.sessionId, showSelf: false) + NEBaseSelectUserViewController(sessionId: viewModel.sessionId, showSelf: false) } private func showUserSelectVC(text: String) { @@ -1777,8 +1893,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel addText += chatLocalizable("user_select_all") } else { if let m = model { - addText += m.showNameInTeam() - if let uid = m.nimUser?.userId { + addText += m.showNameInTeam() ?? "" + if let uid = m.nimUser?.user?.accountId { accid = uid } } @@ -1792,15 +1908,15 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel private func showErrorToast(_ error: Error?) { if let err = error as? NSError { switch err.code { - case noNetworkCode: + case protocolSendFailed: showToast(commonLocalizable("network_error")) default: - showToast(err.localizedDescription) + print(err.localizedDescription) } } } - // MARK: MessageOperationViewDelegate + // MARK: - MessageOperationViewDelegate open func didSelectedItem(item: OperationItem) { if let popMenuClick = NEKitChatConfig.shared.ui.popMenuClick { @@ -1819,8 +1935,6 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } case .recall: recallMessage() - case .collection: - collectionMessage() case .forward: forwardMessage() case .pin: @@ -1829,6 +1943,12 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel removePinMessage() case .multiSelect: selectMessage() + case .top: + topMessage() + case .untop: + untopMessage() + case .collection: + toCollectMessage() default: customOperation() } @@ -1837,32 +1957,38 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func customOperation() {} open func copyMessage() { - if let model = viewmodel.operationModel as? MessageTextModel { + if let model = viewModel.operationModel as? MessageTextModel { if let text = model.message?.text, !text.isEmpty { UIPasteboard.general.string = text - showToast(chatLocalizable("copy_success")) + showToast(commonLocalizable("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 { + showToast(commonLocalizable("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")) + showToast(commonLocalizable("copy_success")) } } } } open func deleteMessage() { - showAlert(message: chatLocalizable("message_delete_confirm")) { - self.viewmodel.deleteMessage { error in - self.showErrorToast(error) + showAlert(message: chatLocalizable("message_delete_confirm")) { [weak self] in + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + self?.showToast(commonLocalizable("network_error")) + return + } + + self?.viewModel.deleteMessage { error in + self?.showErrorToast(error) } } } open func showReplyMessageView(isReEdit: Bool = false) { - viewmodel.isReplying = true + viewModel.isReplying = true if chatInputView.chatInpuMode != .multipleReturn { view.addSubview(replyView) replyView.closeButton.addTarget(self, action: #selector(closeReply), for: .touchUpInside) @@ -1875,63 +2001,71 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ]) } - if let message = viewmodel.operationModel?.message { + if let message = viewModel.operationModel?.message { if isReEdit { - replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: viewmodel.operationModel?.replyText ?? "", + replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: viewModel.operationModel?.replyText ?? "", font: replyView.textLabel.font, color: replyView.textLabel.textColor) - if let replyMessage = viewmodel.getReplyMessageWithoutThread(message: message) as? MessageContentModel { - viewmodel.operationModel = replyMessage + viewModel.getReplyMessageWithoutThread(message: message) { model in + if let replyMessage = model as? MessageContentModel { + self.viewModel.operationModel = replyMessage + } } } else { var text = chatLocalizable("msg_reply") - if let uid = message.from { - var showName = ChatUserCache.getShowName(userId: uid, teamId: viewmodel.session.sessionId, false) - if viewmodel.session.sessionType != .P2P, - !IMKitClient.instance.isMySelf(uid) { + if let uid = message.senderId { + var showName = ChatTeamCache.shared.getShowName(uid, false) + if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) != .CONVERSATION_TYPE_P2P, + !IMKitClient.instance.isMe(uid) { addToAtUsers(addText: "@" + showName + "", isReply: true, accid: uid) } - let user = viewmodel.getUserInfo(userId: uid) - if let alias = user?.alias, !alias.isEmpty { - showName = alias - } + showName = ChatTeamCache.shared.getShowName(uid) text += " " + showName + text += ": \(ChatMessageHelper.contentOfMessage(message))" + replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, + font: replyView.textLabel.font, + color: replyView.textLabel.textColor) + chatInputView.textView.becomeFirstResponder() } - text += ": \(ChatMessageHelper.contentOfMessage(message))" - replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, - font: replyView.textLabel.font, - color: replyView.textLabel.textColor) - chatInputView.textView.becomeFirstResponder() } } } open func closeReply(button: UIButton?) { replyView.removeFromSuperview() - viewmodel.isReplying = false + viewModel.isReplying = false } + /// 撤回消息 open func recallMessage() { weak var weakSelf = self showAlert(message: chatLocalizable("message_revoke_confirm")) { - if let message = weakSelf?.viewmodel.operationModel?.message { - if message.messageType == .text { - weakSelf?.viewmodel.operationModel?.isRevokedText = true + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + + if let message = weakSelf?.viewModel.operationModel?.message { + if weakSelf?.viewModel.operationModel?.type == .text { + weakSelf?.viewModel.operationModel?.isReedit = true } - if message.messageType == .custom, - let attach = NECustomAttachment.attachmentOfCustomMessage(message: message), attach.customType == customRichTextType { - weakSelf?.viewmodel.operationModel?.isRevokedText = true + if weakSelf?.viewModel.operationModel?.type == .custom, + weakSelf?.viewModel.operationModel?.customType == customRichTextType { + weakSelf?.viewModel.operationModel?.isReedit = true } - let isPin = weakSelf?.viewmodel.operationModel?.isPined ?? false - weakSelf?.viewmodel.revokeMessage(message: message) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? ""), + let isPin = weakSelf?.viewModel.operationModel?.isPined ?? false + weakSelf?.viewModel.revokeMessage(message: message) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK revokeMessage " + (error?.localizedDescription ?? "no error") ) if let err = error as? NSError { - if err.code == 508 { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == ravokableTimeExpired { weakSelf?.showToast(chatLocalizable("ravokable_time_expired")) } else { weakSelf?.showToast(chatLocalizable("ravoked_failed")) @@ -1940,13 +2074,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // 自己撤回成功 & 收到对方撤回 都会走回调方法 onRevokeMessage // 撤回成功的逻辑统一在代理方法中处理 onRevokeMessage if isPin { - weakSelf?.viewmodel.removePinMessage(message) { error, pinItem, value in - } - } - weakSelf?.viewmodel.saveRevokeMessage(message) { error in - print("message id : ", message.messageId) - if let err = error { - NELog.infoLog(weakSelf?.className() ?? "chat view controller", desc: err.localizedDescription) + weakSelf?.viewModel.removePinMessage(message: message) { error, value in } } } @@ -1955,34 +2083,24 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - open func collectionMessage() { - if let message = viewmodel.operationModel?.message { - viewmodel.addColletion(message) { error, info in - NELog.infoLog( - ModuleName + " " + self.tag, - desc: #function + "CALLBACK addColletion " + (error?.localizedDescription ?? "no error") - ) - if error != nil { - self.showErrorToast(error) - } else { - self.showToast(chatLocalizable("collection_success")) - } - } - } - } - + /// 获取转发确认弹窗 open func getForwardAlertController() -> NEBaseForwardAlertViewController { NEBaseForwardAlertViewController() } + /// 添加转发确认弹窗 + /// - Parameters: + /// - items: 转发对象 + /// - type: 转发类型(合并转发/逐条转发/转发) + /// - sureBlock: 确认按钮点击回调 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.forwardType = type forwardAlert.sureBlock = sureBlock + forwardAlert.sessionName = ChatMessageHelper.getSessionName(conversationId: viewModel.conversationId) addChild(forwardAlert) view.addSubview(forwardAlert.view) @@ -1992,159 +2110,177 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel })) } - func forwardMessageToUser(isMultiForward: Bool = false, - depth: Int = 0, - _ sureBlock: (() -> Void)? = nil) { - weak var weakSelf = self - Router.shared.register(ContactSelectedUsersRouter) { param in - var items = [ForwardItem]() - - if let users = param["im_user"] as? [NIMUser] { - users.forEach { user in - let item = ForwardItem() - item.uid = user.userId - item.avatar = user.userInfo?.avatarUrl - item.name = user.getShowName() - items.append(item) - } - - 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 - } - - 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 - Router.shared.use(ContactUserSelectRouter, parameters: param, closure: nil) - } - - 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 { - let item = ForwardItem() - item.avatar = team.avatarUrl - item.name = team.getShowName() - item.uid = team.teamId - - 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?() - } - } - } - } - } - - Router.shared.use( - ContactTeamListRouter, - parameters: ["nav": weakSelf?.navigationController as Any, - "isClickCallBack": true], - closure: nil - ) - } - + /// 长按转发消息 open func forwardMessage() { - if let message = viewmodel.operationModel?.message { - viewmodel.selectedMessages = [message] + if let message = viewModel.operationModel?.message { + viewModel.selectedMessages = [message] didClickSingleForwardButton() } } + /// 标记消息 open func pinMessage() { - guard let optModel = viewmodel.operationModel, !optModel.isPined else { + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + guard let optModel = viewModel.operationModel, !optModel.isPined else { return } if optModel.isRevoked == true { return } if let message = optModel.message { - viewmodel.pinMessage(message) { [weak self] error, pinItem, index in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), + viewModel.addPinMessage(message: message) { [weak self] error, index in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK pinMessage " + (error?.localizedDescription ?? "no error") ) if let err = error as? NSError { - if err.code == noNetworkCode { + if err.code == pinAlreadyExist { + return + } else if err.code == protocolSendFailed { self?.view.makeToast(commonLocalizable("network_error"), position: .center) + } else if err.code == pinLimitExceeded { + self?.view.makeToast(chatLocalizable("pin_limit_exceeded"), position: .center) } else { self?.view.makeToast(error?.localizedDescription, position: .center) } } else { // update UI if index >= 0 { - self?.tableViewReloadIndexs([IndexPath(row: index, section: 0)]) + self?.tableViewReloadIndexs([IndexPath(row: index, section: 0)]) { [weak self] in + if let numberOfRows = self?.tableView.numberOfRows(inSection: 0), index == numberOfRows - 1 { + self?.scrollTableViewToBottom() + } + } } } } } } + /// 取消标记消息 open func removePinMessage() { - guard let optModel = viewmodel.operationModel, optModel.isPined else { + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + guard let optModel = viewModel.operationModel, optModel.isPined else { return } if let message = optModel.message { - viewmodel.removePinMessage(message) { [weak self] error, pinItem, index in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), + viewModel.removePinMessage(message: message) { [weak self] error, index in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK removePinMessage " + (error?.localizedDescription ?? "no error") ) if let err = error as? NSError { - if err.code == 404 { + if err.code == pinNotExist { return - } else if err.code == noNetworkCode { + } else if err.code == protocolSendFailed { self?.view.makeToast(commonLocalizable("network_error"), position: .center) } else { self?.view.makeToast(error?.localizedDescription, position: .center) } } else { - // update UI - if index >= 0 { - self?.tableViewReloadIndexs([IndexPath(row: index, section: 0)]) - } + // update UI + if index >= 0 { + self?.tableViewReloadIndexs([IndexPath(row: index, section: 0)]) { [weak self] in + if let numberOfRows = self?.tableView.numberOfRows(inSection: 0), index == numberOfRows - 1 { + self?.scrollTableViewToBottom() + } + } + } + } + } + } + } + + /// 置顶消息 + open func topMessage() { + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + viewModel.topMessage { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK topMessage " + (error?.localizedDescription ?? "no error") + ) + if let err = error as? NSError { + if err.code == protocolSendFailed { + self?.view.makeToast(commonLocalizable("network_error"), position: .center) + } else if err.code == noPermissionOperationCode { + self?.view.makeToast(chatLocalizable("no_permission_tip"), position: .center) + } else { + self?.view.makeToast(error?.localizedDescription, position: .center) + } + } + } + } + + /// 收藏消息 + open func toCollectMessage() { + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + guard let operationModel = viewModel.operationModel else { + return + } + + viewModel.collectMessage(operationModel, title ?? "") { [weak self] error in + if error != nil { + self?.showToast(chatLocalizable("failed_operation")) + } else { + self?.showToast(chatLocalizable("collection_success")) + } + } + } + + /// 移除置顶消息 + open func untopMessage() { + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + viewModel.untopMessage { [weak self] error in + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK untopMessage " + (error?.localizedDescription ?? "no error") + ) + if let err = error as? NSError { + if err.code == protocolSendFailed { + self?.view.makeToast(commonLocalizable("network_error"), position: .center) + } else if err.code == noPermissionOperationCode { + self?.view.makeToast(chatLocalizable("no_permission_tip"), position: .center) + } else if err.code == failedOperation { + self?.view.makeToast(chatLocalizable("failed_operation"), position: .center) + } else { + self?.view.makeToast(error?.localizedDescription, position: .center) } } } } + /// 多选消息 open func selectMessage() { isMutilSelect = true - if let model = viewmodel.operationModel, let msg = model.message { + if let model = viewModel.operationModel, let msg = model.message { model.isSelected = true - viewmodel.selectedMessages = [msg] + viewModel.selectedMessages = [msg] } navigationView.setMoreButtonTitle(chatLocalizable("cancel")) @@ -2154,66 +2290,61 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel tableView.reloadData() } + /// 取消多选 func cancelMutilSelect() { isMutilSelect = false - viewmodel.selectedMessages.removeAll() - viewmodel.messages.forEach { model in + viewModel.selectedMessages.removeAll() + for model in viewModel.messages { model.isSelected = false } setMoreButton() setInputView(edit: true) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, + execute: DispatchWorkItem(block: { [weak self] in + self?.scrollTableViewToBottom() + })) tableView.reloadData() } - // edit: 是否显示输入框 + /// 设置输入框样式 + /// - Parameter edit: 是否显示输入框 func setInputView(edit: Bool) { - bottomViewTopAnchor?.constant = edit ? -normalInputHeight : -100 chatInputView.isHidden = !edit mutilSelectBottomView.isHidden = edit + bottomViewTopAnchor?.constant = edit ? -normalInputHeight : -100 } - // MARK: UITableViewDataSource, UITableViewDelegate + // MARK: - UITableViewDataSource, UITableViewDelegate open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let count = viewmodel.messages.count - print("numberOfRowsInSection count : ", count) - return count + viewModel.messages.count } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard indexPath.row < viewmodel.messages.count else { return NEBaseChatMessageCell() } - let model = viewmodel.messages[indexPath.row] - var reuseId = "" + guard indexPath.row < viewModel.messages.count else { return NEBaseChatMessageCell() } + let model = viewModel.messages[indexPath.row] + var reuseId = "\(NEBaseChatMessageCell.self)" if model.replyedModel?.isReplay == true, model.isRevoked == false { - if model.replyedModel?.message?.serverID == nil || - model.replyedModel?.message?.serverID.isEmpty == true { - if let message = model.message { - model.replyedModel = viewmodel.getReplyMessageWithoutThread(message: message) - } - } - - if let attch = NECustomAttachment.attachmentOfCustomMessage(message: model.message), - attch.customType == customRichTextType { + if model.customType == customRichTextType { reuseId = "\(MessageType.richText.rawValue)" } else { reuseId = "\(MessageType.reply.rawValue)" } } else { let key = "\(model.type.rawValue)" - if model.type == .custom, - let attch = NECustomAttachment.attachmentOfCustomMessage(message: model.message) { - if attch.customType == customMultiForwardType { + if model.type == .custom { + if model.customType == customMultiForwardType { reuseId = "\(MessageType.multiForward.rawValue)" - } else if attch.customType == customRichTextType { + } else if model.customType == customRichTextType { reuseId = "\(MessageType.richText.rawValue)" - } else if NEChatUIKitClient.instance.getRegisterCustomCell()["\(attch.customType)"] != nil { - reuseId = "\(attch.customType)" + } else if NEChatUIKitClient.instance.getRegisterCustomCell()["\(model.customType)"] != nil { + reuseId = "\(model.customType)" } else { reuseId = "\(NEBaseChatMessageCell.self)" } - } else if model.type == .time || model.type == .notification || model.type == .tip { + } else if model.type == .notification || model.type == .tip { reuseId = "\(MessageType.time.rawValue)" } else if cellRegisterDic[key] != nil { reuseId = key @@ -2225,40 +2356,37 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel let cell = tableView.dequeueReusableCell(withIdentifier: reuseId, for: indexPath) if let c = cell as? NEBaseChatMessageTipCell { if let m = model as? MessageTipsModel { - m.setText() c.setModel(m) } return c } else if let c = cell as? NEBaseChatMessageCell { c.delegate = self - if let m = model as? MessageContentModel { - // 更新好友昵称、头像 - if let from = model.message?.from, - let user = ChatUserCache.getUserInfo(from) { - if let uid = user.userId, - viewmodel.session.sessionType == .team || - viewmodel.session.sessionType == .superTeam { - m.fullName = ChatUserCache.getShowName(userId: uid, teamId: viewmodel.session.sessionId) - m.shortName = ChatUserCache.getShortName(name: user.showName(false) ?? "", length: 2) - } - m.avatar = user.userInfo?.avatarUrl - } - c.setModel(m, m.message?.isOutgoingMsg ?? false) - c.setSelect(m, isMutilSelect) - } + // 语音消息播放状态 if let audioCell = cell as? ChatAudioCellProtocol, let m = model as? MessageAudioModel, - m.message?.messageId == playingModel?.message?.messageId { - if NIMSDK.shared().mediaManager.isPlaying() { + m.message?.messageClientId == playingModel?.message?.messageClientId { + if audioPlayer?.isPlaying == true { playingCell = audioCell - playingCell?.startAnimation(byRight: true) + m.isPlaying = true + } + } + + // 视频、文件下载状态 + if let m = model as? MessageVideoModel, m.message?.messageConfig == anchorModel?.message?.messageConfig { + if anchorModel?.state == .Downalod { + anchorCell = c } } + if let m = model as? MessageContentModel { + c.setModel(m, m.message?.isSelf ?? false) + c.setSelect(m, isMutilSelect) + } + return c } else if let c = cell as? NEChatBaseCell, let m = model as? MessageContentModel { - c.setModel(m, m.message?.isOutgoingMsg ?? false) + c.setModel(m, m.message?.isSelf ?? false) return cell } else { return NEBaseChatMessageCell() @@ -2266,17 +2394,17 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - guard indexPath.row < viewmodel.messages.count else { return } + guard indexPath.row < viewModel.messages.count else { return } if isMutilSelect { - if indexPath.row < viewmodel.messages.count { - let model = viewmodel.messages[indexPath.row] + if indexPath.row < viewModel.messages.count { + let model = viewModel.messages[indexPath.row] if !model.isRevoked, let cell = tableView.cellForRow(at: indexPath) as? NEBaseChatMessageCell { model.isSelected = !model.isSelected - cell.seletedBtn.isSelected = model.isSelected - viewmodel.selectedMessages.removeAll(where: { $0.messageId == model.message?.messageId }) + cell.selectedButton.isSelected = model.isSelected + viewModel.selectedMessages.removeAll(where: { $0.messageClientId == model.message?.messageClientId }) if model.isSelected, let msg = model.message { - viewmodel.selectedMessages.append(msg) + viewModel.selectedMessages.append(msg) } } } @@ -2293,12 +2421,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - guard indexPath.row < viewmodel.messages.count else { return 0 } - let model = viewmodel.messages[indexPath.row] - if let m = model as? MessageTipsModel { - m.commonInit() - } - + guard indexPath.row < viewModel.messages.count else { return 0 } + let model = viewModel.messages[indexPath.row] return model.cellHeight() + chat_content_margin } @@ -2310,7 +2434,57 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel 0 } - // MARK: UIScrollViewDelegate + public func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { + // 无更多消息 + if viewModel.messages.count < viewModel.messagPageNum { + return + } + + // 预加载 + let leaveCount = 10 // 剩余多少行开始预加载 + weak var weakSelf = self + if indexPath.row <= leaveCount, !isUploadingData, !uploadHasNoMore { + // 上拉预加载更多 + if !isUploadingData { + isUploadingData = true + viewModel.dropDownRemoteRefresh { error, count, messages in + if let err = error { + NEALog.errorLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK dropDownRemoteRefresh " + (err.localizedDescription) + ) + } else { + NEALog.infoLog( + ModuleName + " " + ChatViewController.className(), + desc: #function + "CALLBACK dropDownRemoteRefresh " + (error?.localizedDescription ?? "no error") + ) + if count <= 0 { + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + return + } + + // 无更多数据 + weakSelf?.uploadHasNoMore = true + weakSelf?.tableView.mj_header = nil + } else { + weakSelf?.tableViewReload() + if let num = weakSelf?.tableView.numberOfRows(inSection: 0), indexPath.row + count <= num { + weakSelf?.tableView.scrollToRow( + at: IndexPath(row: indexPath.row + count - 1, section: 0), + at: .top, + animated: false + ) + } + weakSelf?.isUploadingData = false + } + } + } + } + } + } + + // MARK: - UIScrollViewDelegate open func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { operationView?.removeFromSuperview() @@ -2353,13 +2527,14 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // print(error) // } - func getTextViewController(title: String?, body: String?) -> TextViewController { + /// 获取文本详情页视图控制器 + func getTextViewController(title: String?, body: NSAttributedString?) -> TextViewController { let textViewController = TextViewController(title: title, body: body) textViewController.view.backgroundColor = .white return textViewController } - // MARK: OVERRIDE + // MARK: - OVERRIDE open func getMenuView() -> NEBaseChatInputView { NEBaseChatInputView() @@ -2369,6 +2544,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel mutilSelectBottomView.backgroundColor = .ne_backgroundColor } + /// 获取合并转发详情页视图控制器 open func getMultiForwardViewController(_ messageAttachmentUrl: String?, _ messageAttachmentFilePath: String, _ messageAttachmentMD5: String?) -> MultiForwardViewController { @@ -2376,12 +2552,28 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } open func expandMoreAction() { - chatInputView.chatAddMoreView.configData(data: NEChatUIKitClient.instance.getMoreActionData(sessionType: viewmodel.session.sessionType)) + var data = NEChatUIKitClient.instance.getMoreActionData(sessionType: V2NIMConversationIdUtil.conversationType(viewModel.conversationId)) + if NEChatKitClient.instance.delegate == nil { + data = data.filter { item in + if item.type == .location { + return false + } + return true + } + } + chatInputView.chatAddMoreView.configData(data: NEChatUIKitClient.instance.getMoreActionData(sessionType: V2NIMConversationIdUtil.conversationType(viewModel.conversationId))) } open func showTextViewController(_ model: MessageContentModel?) { - let title = NECustomAttachment.titleOfRichText(message: model?.message) - let body = NECustomAttachment.bodyOfRichText(message: model?.message) ?? model?.message?.text + guard let model = model as? MessageTextModel else { return } + + let title = NECustomAttachment.titleOfRichText(model.message?.attachment) + let body = model.attributeStr + + if !(title?.isEmpty == false), !(body?.string.isEmpty == false) { + return + } + let textView = getTextViewController(title: title, body: body) textView.modalPresentationStyle = .fullScreen DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in @@ -2389,169 +2581,320 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel })) } + /// 单击消息 + /// - Parameters: + /// - cell: 消息所在单元格 + /// - model: 消息模型 + /// - replyIndex: 被回复消息的下标 open func didTapMessage(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ replyIndex: Int? = nil) { - if model?.type == .audio { - startPlay(cell: cell as? ChatAudioCellProtocol, model: model as? MessageAudioModel) - } else if model?.type == .image { - if let imageObject = model?.message?.messageObject as? NIMImageObject { - var imageUrl = "" + switch model?.type { + case .audio: + didTapAudioMessage(cell, model) + case .image: + didTapImageMessage(model, replyIndex) + case .video: + didTapVideoMessage(cell, model, replyIndex) + case .file: + didTapFileMessage(cell, model, replyIndex) + case .location: + didTapLocationMessage(model) + case .rtcCallRecord: + didTapCallMessage(model) + case .custom: + didTapCustomMessage(model, replyIndex) + default: + if replyIndex != nil, model?.type == .text || model?.type == .reply { + showTextViewController(model) + } else { + print(#function + "message did tap, type:\(String(describing: model?.type.rawValue))") + } + } + } - if let url = imageObject.url { - imageUrl = url - } else { - if let path = imageObject.path, FileManager.default.fileExists(atPath: path) { - imageUrl = path + /// 单击语音消息 + /// - Parameters: + /// - cell: 消息所在单元格 + /// - model: 消息模型 + /// - replyIndex: 被回复消息的下标 + func didTapAudioMessage(_ cell: UITableViewCell?, _ model: MessageContentModel?) { + guard let audioObject = model?.message?.attachment as? V2NIMMessageAudioAttachment else { + return + } + + let path = audioObject.path ?? ChatMessageHelper.createFilePath(model?.message) + if !FileManager.default.fileExists(atPath: path) { + if let urlString = audioObject.url { + viewModel.downLoad(urlString, path, nil) { [weak self] _, error in + if error == nil { + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK downLoad") + self?.startPlay(cell: cell as? ChatAudioCellProtocol, model: model as? MessageAudioModel) + } else { + self?.showErrorToast(error) } } - 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 { + } else { + startPlay(cell: cell as? ChatAudioCellProtocol, model: model as? MessageAudioModel) + } + } + + /// 单击图片消息 + /// - Parameters: + /// - model: 消息模型 + /// - replyIndex: 被回复消息的下标 + func didTapImageMessage(_ model: MessageContentModel?, _ replyIndex: Int? = nil) { + guard let imageObject = model?.message?.attachment as? V2NIMMessageImageAttachment else { + return + } + + var imageUrl = "" + if let url = imageObject.url { + imageUrl = url + } else { + if let path = imageObject.path, FileManager.default.fileExists(atPath: path) { + imageUrl = path + } + } + + if !imageUrl.isEmpty { + var showImages = ChatMessageHelper.getUrls(messages: viewModel.messages) + + // 如果回复的图片消息未加载,则置于所有加载的图片的最前面 + if !showImages.contains(imageUrl) { + showImages.insert(imageUrl, at: 0) + } + + let showController = PhotoBrowserController(urls: showImages, url: imageUrl) + showController.modalPresentationStyle = .overFullScreen + present(showController, animated: false, completion: nil) + } + } + + /// 单击视频消息 + /// - Parameters: + /// - cell: 消息所在单元格 + /// - model: 消息模型 + /// - replyIndex: 被回复消息的下标 + func didTapVideoMessage(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ replyIndex: Int? = nil) { + guard let object = model?.message?.attachment as? V2NIMMessageVideoAttachment else { + return + } + + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) + if FileManager.default.fileExists(atPath: path) { + // 停止播放语音 stopPlay() - weak var weakSelf = self + + // 设置听筒/扬声器 + let cate: AVAudioSession.Category = viewModel.getHandSetEnable() ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback + try? AVAudioSession.sharedInstance().setCategory(cate, options: .duckOthers) + + let url = URL(fileURLWithPath: path) 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, videoModel.message?.isOutgoingMsg ?? false) + videoPlayer.videoUrl = url + videoPlayer.totalTime = Int(object.duration) + present(videoPlayer, animated: true, completion: nil) + } else { + if let index = replyIndex { + let indexPath = IndexPath(row: index, section: 0) + if tableView.cellForRow(at: indexPath) != nil { + // 消息已加载,直接跳转 + tableView.scrollToRow(at: indexPath, at: .middle, animated: true) + } else { + // 消息未加载,重新加载 + viewModel.anchor = model?.message + loadData() } + } - viewmodel.downLoad(urlString, path) { progress in - NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ChatViewController"), desc: #function + "CALLBACK downLoad: \(progress)") + downloadFile(cell, model, object.url, path) + } + } - videoModel.progress = progress - if progress >= 1.0 { - videoModel.state = .Success - } - videoModel.cell?.uploadProgress(byRight: videoModel.message?.isOutgoingMsg ?? true, progress) - } _: { error in - weakSelf?.showErrorToast(error) + /// 单击文件消息 + /// - Parameters: + /// - cell: 消息所在单元格 + /// - model: 消息模型 + /// - replyIndex: 被回复消息的下标 + func didTapFileMessage(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ replyIndex: Int? = nil) { + guard let object = model?.message?.attachment as? V2NIMMessageFileAttachment else { + return + } + + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) + if !FileManager.default.fileExists(atPath: path) { + if let index = replyIndex { + let indexPath = IndexPath(row: index, section: 0) + if tableView.cellForRow(at: indexPath) != nil { + // 消息已加载,直接跳转 + tableView.scrollToRow(at: indexPath, at: .middle, animated: true) + } else { + // 消息未加载,重新加载 + viewModel.anchor = model?.message + loadData() } } - } else if replyIndex != nil, model?.type == .text || model?.type == .reply { - showTextViewController(model) - } else if model?.type == .location { - if let locationModel = model as? MessageLocationModel, let lat = locationModel.lat, let lng = locationModel.lng { - let mapDetail = NEDetailMapController(type: .detail) - mapDetail.currentPoint = CGPoint(x: lat, y: lng) - mapDetail.locationTitle = locationModel.title - mapDetail.subTitle = locationModel.subTitle - navigationController?.pushViewController(mapDetail, animated: true) - } - } 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, fileModel.message?.isOutgoingMsg ?? false) - } - viewmodel.downLoad(urlString, path) { [weak self] progress in - NELog.infoLog(ModuleName + " " + (self?.tag ?? "ChatViewController"), desc: #function + "downLoad file progress: \(progress)") - var newProgress = progress - if newProgress < 0 { - newProgress = abs(progress) / fileModel.size - } - fileModel.progress = newProgress - if newProgress >= 1.0 { - fileModel.state = .Success - } - fileModel.cell?.uploadProgress(byRight: fileModel.message?.isOutgoingMsg ?? true, newProgress) + downloadFile(cell, model, object.url, path) + } else { + let url = URL(fileURLWithPath: path) + interactionController.url = url + interactionController.delegate = self // UIDocumentInteractionControllerDelegate + if interactionController.presentPreview(animated: true) {} + else { + interactionController.presentOptionsMenu(from: view.bounds, in: view, animated: true) + } + } + } - } _: { [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) - } + /// 单击音视频消息 + /// - Parameter model: 消息模型 + func didTapCallMessage(_ model: MessageContentModel?) { + guard let attachment = model?.message?.attachment as? V2NIMMessageCallAttachment else { + return + } + + useToCallViewRouter(attachment.type) + } + + /// 单击地理位置消息 + /// - Parameters: + /// - model: 消息模型 + func didTapLocationMessage(_ model: MessageContentModel?) { + if let locationModel = model as? MessageLocationModel, + let lat = locationModel.lat, + let lng = locationModel.lng { + var params = [String: Any]() + params["type"] = NEMapType.detail.rawValue + params["nav"] = navigationController + params["lat"] = lat + params["lng"] = lng + params["locationTitle"] = locationModel.title + params["subTitle"] = locationModel.subTitle + Router.shared.use(NERouterUrl.LocationVCRouter, parameters: params) + } + } + + /// 单击自定义消息 + /// - Parameters: + /// - cell: 消息所在单元格 + /// - model: 消息模型 + /// - replyIndex: 被回复消息的下标 + func didTapCustomMessage(_ model: MessageContentModel?, _ replyIndex: Int? = nil) { + guard let customType = model?.customType else { + return + } + + if customType == customRichTextType { + if replyIndex != nil { + showTextViewController(model) + } + } else if customType == customMultiForwardType, + let data = NECustomAttachment.dataOfCustomMessage(model?.message?.attachment) { + let url = data["url"] as? String + let md5 = data["md5"] as? String + guard let fileDirectory = NEPathUtils.getDirectoryForDocuments(dir: "\(imkitDir)file/") else { return } + let fileName = multiForwardFileName + (model?.message?.messageClientId ?? "") + let filePath = fileDirectory + fileName + let multiForwardVC = getMultiForwardViewController(url, filePath, md5) + navigationController?.pushViewController(multiForwardVC, animated: true) + } + } + + /// 下载附件(文件、视频消息) + /// - Parameters: + /// - cell: 当前单元格 + /// - model: 消息模型 + /// - url: 远端下载链接 + /// - path: 本地保存路径 + func downloadFile(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ url: String?, _ path: String) { + // 判断是否是视频或者文件对象 + guard let urlString = url, var fileModel = model as? MessageVideoModel else { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "MessageFileModel not exit") + return + } + + var fileCell = cell as? NEBaseChatMessageCell + + // 判断状态,如果是下载中不能进行预览 + if fileModel.state == .Downalod { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "downLoad state, click ingore") + return + } + + fileCell?.setModel(fileModel, fileModel.message?.isSelf ?? false) + + viewModel.downLoad(urlString, path) { [weak self] progress in + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "downLoad file progress: \(progress)") + if cell == nil { + fileCell = self?.anchorCell + fileModel = self?.anchorModel ?? fileModel } - } else if model?.type == .rtcCallRecord, let object = model?.message?.messageObject as? NIMRtcCallRecordObject { - var param = [String: AnyObject]() - param["remoteUserAccid"] = viewmodel.session.sessionId as AnyObject - param["currentUserAccid"] = NIMSDK.shared().loginManager.currentAccount() as AnyObject - param["remoteShowName"] = titleContent as AnyObject - if let user = viewmodel.repo.getUserInfo(userId: viewmodel.session.sessionId), let avatar = user.userInfo?.avatarUrl { - param["remoteAvatar"] = avatar as AnyObject + + fileModel.state = .Downalod + fileModel.progress = progress + fileCell?.uploadProgress(byRight: fileModel.message?.isSelf ?? true, progress) + + } _: { [weak self] localPath, error in + self?.showErrorToast(error) + if localPath != nil { + fileModel.state = .Success } - if object.callType == .audio { - param["type"] = NSNumber(integerLiteral: 1) as AnyObject - } else { - param["type"] = NSNumber(integerLiteral: 2) as AnyObject + } + } +} + +// MARK: - TopMessageViewDelegate + +extension ChatViewController: TopMessageViewDelegate { + /// 点击置顶消息视图中的关闭按钮 + public func didClickCloseButton() { + viewModel.untopMessage { [weak self] error in + if let err = error { + self?.showErrorToast(err) } - 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) + } + } + + /// 点击置顶消息视图 + public func didTapTopMessageView() { + var index = -1 + for (i, m) in viewModel.messages.enumerated() { + if viewModel.topMessage?.messageClientId == m.message?.messageClientId { + index = i + break } + } + + if index >= 0, index < tableView.numberOfRows(inSection: 0) { + // 消息已加载,直接跳转 + tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) } else { - print(#function + "message did tap, type:\(String(describing: model?.type.rawValue))") + // 消息未加载,重新加载 + viewModel.anchor = viewModel.topMessage + loadData() } } } -// MARK: NEMutilSelectBottomViewDelegate +// MARK: - NEMutilSelectBottomViewDelegate extension ChatViewController: NEMutilSelectBottomViewDelegate { /// 移除不可转发的消息 - /// - Parameters cancelSelect: 是否取消勾选 - func filterSelectedMessage(invalidMessages: [NIMMessage]) { + /// - Parameters invalidMessages: 不可转发的消息 + func filterSelectedMessage(invalidMessages: [V2NIMMessage]) { // 取消勾选 - for msg in viewmodel.selectedMessages { + for msg in viewModel.selectedMessages { if invalidMessages.contains(msg) { - for (row, model) in viewmodel.messages.enumerated() { - if msg.messageId == model.message?.messageId { + for (row, model) in viewModel.messages.enumerated() { + if msg.messageClientId == model.message?.messageClientId { let indexPath = IndexPath(row: row, section: 0) - let model = viewmodel.messages[indexPath.row] + let model = viewModel.messages[indexPath.row] if !model.isRevoked, let cell = tableView.cellForRow(at: indexPath) as? NEBaseChatMessageCell { model.isSelected = !model.isSelected - cell.seletedBtn.isSelected = model.isSelected + cell.selectedButton.isSelected = model.isSelected } } } @@ -2559,31 +2902,34 @@ extension ChatViewController: NEMutilSelectBottomViewDelegate { } // 无论UI上是否取消勾选,都需要移除该消息 - viewmodel.selectedMessages.removeAll(where: { invalidMessages.contains($0) }) + viewModel.selectedMessages.removeAll(where: { invalidMessages.contains($0) }) } - // 合并转发 + /// 点击合并转发 open func didClickMultiForwardButton() { + // 校验网络 if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { showToast(commonLocalizable("network_error")) return } - if viewmodel.selectedMessages.count > customMultiForwardLimitCount { + if viewModel.selectedMessages.count > customMultiForwardLimitCount { showToast(String(format: chatLocalizable("multiForward_forward_limit"), customMultiForwardLimitCount)) return } var depth = 0 - var invalidMessages = [NIMMessage]() - for msg in viewmodel.selectedMessages { - if msg.deliveryState == .failed || msg.isBlackListed { + var invalidMessages = [V2NIMMessage]() + for msg in viewModel.selectedMessages { + if msg.sendingState == .MESSAGE_SENDING_STATE_FAILED +// || msg.isBlackListed + { invalidMessages.append(msg) continue } // 解析消息中的depth - if let data = NECustomAttachment.dataOfCustomMessage(message: msg) { + if let data = NECustomAttachment.dataOfCustomMessage(msg.attachment) { if let dep = data["depth"] as? Int { if dep >= customMultiForwardMaxDepth { invalidMessages.append(msg) @@ -2601,59 +2947,37 @@ extension ChatViewController: NEMutilSelectBottomViewDelegate { 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() + if !viewModel.selectedMessages.isEmpty { + forwardMessages(isMultiForward: true, depth: depth) } } - - let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), style: .cancel) - showActionSheet([teamAction, userAction, cancelAction]) } else { - forwardMessageToUser(isMultiForward: true, depth: depth) { - weakSelf?.cancelMutilSelect() + if !viewModel.selectedMessages.isEmpty { + forwardMessages(isMultiForward: true, depth: depth) } } } - // 逐条转发 + /// 点击逐条转发 open func didClickSingleForwardButton() { + // 校验网络 if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { showToast(commonLocalizable("network_error")) return } - if viewmodel.selectedMessages.count > customSingleForwardLimitCount { + if viewModel.selectedMessages.count > customSingleForwardLimitCount { showToast(String(format: chatLocalizable("per_item_forward_limit"), customSingleForwardLimitCount)) return } - var invalidMessages = [NIMMessage]() - for msg in viewmodel.selectedMessages { - if msg.messageType == .audio || - msg.messageType == .rtcCallRecord || - msg.deliveryState == .failed - || msg.isBlackListed { + var invalidMessages = [V2NIMMessage]() + for msg in viewModel.selectedMessages { + if msg.messageType == .MESSAGE_TYPE_AUDIO || + msg.messageType == .MESSAGE_TYPE_CALL || + msg.sendingState == .MESSAGE_SENDING_STATE_FAILED +// || msg.isBlackListed + { invalidMessages.append(msg) } } @@ -2662,51 +2986,80 @@ extension ChatViewController: NEMutilSelectBottomViewDelegate { showAlert(title: chatLocalizable("exception_description"), message: chatLocalizable("exist_invalid")) { [self] in filterSelectedMessage(invalidMessages: invalidMessages) - if !viewmodel.selectedMessages.isEmpty { - singleForward() + if !viewModel.selectedMessages.isEmpty { + forwardMessages() } } } else { - if !viewmodel.selectedMessages.isEmpty { - singleForward() + if !viewModel.selectedMessages.isEmpty { + forwardMessages() } } } - open func singleForward() { + /// 转发消息 + /// - Parameters: + /// - isMultiForward: 是否是合并转发 + /// - depth: 合并转发层数 + open func forwardMessages(isMultiForward: Bool = false, + depth: Int = 0) { weak var weakSelf = self - if IMKitClient.instance.getConfigCenter().teamEnable { - let userAction = UIAlertAction(title: chatLocalizable("contact_user"), style: .default) { action in - weakSelf?.forwardMessageToUser { - weakSelf?.cancelMutilSelect() - } - } + Router.shared.register(ForwardMultiSelectedRouter) { param in + var items = [ForwardItem]() + + if let conversations = param["conversations"] as? [[String: Any]] { + var conversationIds = [String]() - let teamAction = UIAlertAction(title: chatLocalizable("team"), style: .default) { action in - weakSelf?.forwardMessageToTeam { - weakSelf?.cancelMutilSelect() + for conversation in conversations { + if let conversationId = conversation["conversationId"] as? String { + conversationIds.append(conversationId) + + let item = ForwardItem() + item.conversationId = conversationId + item.name = conversation["name"] as? String + item.avatar = conversation["avatar"] as? String + items.append(item) + } } - } - let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), style: .cancel) - showActionSheet([teamAction, userAction, cancelAction]) - } else { - forwardMessageToUser { - weakSelf?.cancelMutilSelect() + 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 + } + + weakSelf?.viewModel.forwardMessages(conversationIds, isMultiForward, depth, comment) { error in + // 转发失败不展示错误信息 + weakSelf?.cancelMutilSelect() + } + } } } + + Router.shared.use(ForwardMultiSelectRouter, + parameters: ["nav": navigationController as Any, "selctorMode": 0], + closure: nil) } - // 多选删除 + /// 多选删除 open func didClickDeleteButton() { + // 校验网络 if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { showToast(commonLocalizable("network_error")) return } + // 批量删除条数限制 + if viewModel.selectedMessages.count > deleteMessagesLimitCount { + showToast(String(format: chatLocalizable("selete_messages_limit"), deleteMessagesLimitCount)) + return + } + showAlert(message: chatLocalizable("message_delete_confirm")) { [weak self] in - if let messages = self?.viewmodel.selectedMessages { - self?.viewmodel.deleteMessages(messages: messages) { error in + if let messages = self?.viewModel.selectedMessages { + self?.viewModel.deleteMessages(messages: messages) { error in self?.showErrorToast(error) } } @@ -2722,7 +3075,7 @@ extension ChatViewController: ChatBaseCellDelegate { // 非群聊 // 禁言 // 多选 - guard viewmodel.session.sessionType == .team, + guard V2NIMConversationIdUtil.conversationType(viewModel.conversationId) == .CONVERSATION_TYPE_TEAM, !isMute, !isMutilSelect else { return @@ -2731,14 +3084,14 @@ extension ChatViewController: ChatBaseCellDelegate { var addText = "" var accid = "" - if let m = model, let from = m.message?.from { - accid = from - addText += ChatUserCache.getShowName(userId: from, teamId: viewmodel.session.sessionId, false) - } - - addText = "@" + addText + "" + if let m = model, let senderId = m.message?.senderId { + accid = senderId + let name = ChatTeamCache.shared.getShowName(senderId, false) + addText += name + addText = "@" + addText + "" - addToAtUsers(addText: addText, accid: accid, true) + addToAtUsers(addText: addText, accid: accid, true) + } } open func didTapAvatarView(_ cell: UITableViewCell, _ model: MessageContentModel?) { @@ -2749,7 +3102,7 @@ extension ChatViewController: ChatBaseCellDelegate { didTapHeadPortrait(model: model) } - open func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) { + open func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?, _ replyModel: MessageModel?) { if let tapClick = NEKitChatConfig.shared.ui.messageItemClick { tapClick(cell, model) return @@ -2762,26 +3115,22 @@ extension ChatViewController: ChatBaseCellDelegate { return } - var replyId: String? = model?.message?.repliedMessageId - if let yxReplyMsg = model?.message?.remoteExt?[keyReplyMsgKey] as? [String: Any] { - replyId = yxReplyMsg["idClient"] as? String - } - - if let id = replyId, !id.isEmpty { + if var replyModel = replyModel as? MessageContentModel { var index = -1 - var replyModel: MessageModel? - for (i, m) in viewmodel.messages.enumerated() { - if id == m.message?.messageId { + for (i, m) in viewModel.messages.enumerated() { + if replyModel.message?.messageClientId == m.message?.messageClientId { index = i - replyModel = m + if let m = m as? MessageContentModel { + replyModel = m + } break } } let replyCell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) - if let replyContentModel = replyModel as? MessageContentModel { - didTapMessage(replyCell, replyContentModel, index) - } + + didTapMessage(replyCell, replyModel, index) + } else { didTapMessage(cell, model) } @@ -2796,27 +3145,31 @@ extension ChatViewController: ChatBaseCellDelegate { return } - if playingCell?.messageId == model?.message?.messageId { + if playingCell?.messageId == model?.message?.messageClientId { if playingCell?.isPlaying == true { stopPlay() } } + if let m = model, let msg = m.message { - let messages = viewmodel.messages + let messages = viewModel.messages var index = -1 for i in 0 ..< messages.count { if let message = messages[i].message { - if message.messageId == msg.messageId { + if message.messageClientId == msg.messageClientId { index = i break } } } + if index >= 0 { - viewmodel.messages.remove(at: index) - viewmodel.messages.append(m) + viewModel.sendMessage(message: msg) { _, error in + if let err = error { + print("resend message error: \(err.localizedDescription)") + } + } } - viewmodel.resendMessage(message: msg) } } @@ -2829,25 +3182,24 @@ extension ChatViewController: ChatBaseCellDelegate { } if model?.type == .revoke, let message = model?.message, - message.messageType == .text || message.messageType == .custom { - if message.messageType == .custom { - guard let attach = NECustomAttachment.attachmentOfCustomMessage(message: message), - attach.customType == customRichTextType else { + message.messageType == .MESSAGE_TYPE_TEXT || message.messageType == .MESSAGE_TYPE_CUSTOM { + if message.messageType == .MESSAGE_TYPE_CUSTOM { + guard model?.customType == customRichTextType else { return } } - let data = NECustomAttachment.dataOfCustomMessage(message: model?.message) + let data = NECustomAttachment.dataOfCustomMessage(model?.message?.attachment) - let time = message.timestamp + let time = message.createTime let date = Date() let currentTime = date.timeIntervalSince1970 if currentTime - time >= 60 * 2 { showToast(chatLocalizable("editable_time_expired")) - didRefreshTable() + tableViewReload() return } - if message.remoteExt?[keyReplyMsgKey] != nil { - viewmodel.operationModel = model + if let remoteExt = getDictionaryFromJSONString(message.serverExtension ?? ""), remoteExt[keyReplyMsgKey] != nil { + viewModel.operationModel = model showReplyMessageView(isReEdit: true) } else { closeReply(button: nil) @@ -2855,21 +3207,22 @@ extension ChatViewController: ChatBaseCellDelegate { var attributeStr: NSMutableAttributedString? var text = "" - if message.messageType == .text, let txt = message.text { + if message.messageType == .MESSAGE_TYPE_TEXT, let txt = message.text { text = txt - } else if message.messageType == .custom, let body = data?["body"] as? String { + } else if message.messageType == .MESSAGE_TYPE_CUSTOM, let body = data?["body"] as? String { text = body } attributeStr = NSMutableAttributedString(string: text) attributeStr?.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)], range: NSMakeRange(0, text.utf16.count)) - if let remoteExt = message.remoteExt, let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { - dic.forEach { (key: String, value: AnyObject) in + if let remoteExt = getDictionaryFromJSONString(message.serverExtension ?? ""), + let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { + for (key, value) in dic { if let contentDic = value as? [String: AnyObject] { if let array = contentDic[atSegmentsKey] as? [AnyObject] { if let models = NSArray.yx_modelArray(with: MessageAtInfoModel.self, json: array) as? [MessageAtInfoModel] { - models.forEach { model in + for model in models { if var text = contentDic[atTextKey] as? String { if text.last == " " { text = String(text.prefix(text.count - 1)) @@ -2878,7 +3231,7 @@ extension ChatViewController: ChatBaseCellDelegate { } if (attributeStr?.length ?? 0) > model.end { - attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_blueText, range: NSMakeRange(model.start, model.end - model.start)) + attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(model.start, model.end - model.start)) } } } @@ -2892,6 +3245,11 @@ extension ChatViewController: ChatBaseCellDelegate { // 标题填入 chatInputView.titleField.text = title expandButtonDidClick() + } else { + chatInputView.titleField.text = nil + if chatInputView.chatInpuMode != .normal { + didHideMultipleButtonClick() + } } chatInputView.textView.attributedText = attributeStr @@ -2904,26 +3262,32 @@ extension ChatViewController: ChatBaseCellDelegate { return } - if let msg = model?.message, msg.session?.sessionType == .team { - let readVC = getReadView(msg) + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + if let msg = model?.message, msg.conversationType == .CONVERSATION_TYPE_TEAM { + let readVC = getReadView(msg, viewModel.sessionId) navigationController?.pushViewController(readVC, animated: true) } } open func didTapSelectButton(_ cell: UITableViewCell, _ model: MessageContentModel?) { - viewmodel.selectedMessages.removeAll(where: { $0.messageId == model?.message?.messageId }) + viewModel.selectedMessages.removeAll(where: { $0.messageClientId == model?.message?.messageClientId }) if model?.isSelected == true, let msg = model?.message { - viewmodel.selectedMessages.append(msg) + viewModel.selectedMessages.append(msg) } } - open func getReadView(_ message: NIMMessage) -> NEBaseReadViewController { - ReadViewController(message: message) + open func getReadView(_ message: V2NIMMessage, _ teamId: String) -> NEBaseReadViewController { + ReadViewController(message: message, teamId: teamId) } open func loadDataFinish() {} - // MARK: call kit noti + // MARK: - call kit noti open func didShowCallView() { stopPlay() @@ -2931,11 +3295,14 @@ extension ChatViewController: ChatBaseCellDelegate { open func didDismissCallView() {} - // MARK: mutile line delegate + // MARK: - mutile line delegate open func expandButtonDidClick() { chatInputView.textView.resignFirstResponder() - scrollTableViewToBottom() + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, + execute: DispatchWorkItem(block: { [weak self] in + self?.scrollTableViewToBottom() + })) operationView?.removeFromSuperview() } @@ -2959,4 +3326,86 @@ extension ChatViewController: ChatBaseCellDelegate { } open func didHideMultiple() {} + + /// 显示被离开群弹框 + open func showLeaveTeamAlert() { + if IMKitConfigCenter.shared.dismissTeamDeleteConversation == false { + return + } + showSingleAlert(message: chatLocalizable("team_has_been_quit")) { [weak self] in + self?.navigationController?.popViewController(animated: true) + } + } + + /// 显示群被解散弹框 + open func showDismissTeamAlert() { + if IMKitConfigCenter.shared.dismissTeamDeleteConversation == false { + return + } + + showSingleAlert(message: chatLocalizable("team_has_been_removed")) { [weak self] in + self?.navigationController?.popViewController(animated: true) + } + } + + /// 移除弹框 + open func dismissAlert() { + if let alert = navigationController?.presentedViewController, + alert is UIAlertController { + navigationController?.dismiss(animated: true) + } + } +} + +// MARK: - AVAudioPlayerDelegate + +extension ChatViewController: AVAudioPlayerDelegate { + /// 声音播放完成回调 + public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + stopPlay() + } + + /// 声音解码失败回调 + public func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: (any Error)?) { + stopPlay() + } +} + +// MARK: - NEIMKitClientListener + +extension ChatViewController: NEIMKitClientListener { + /// 数据同步回调 + /// - Parameters: + /// - type: 同步的数据类型 + /// - state: 同步状态 + /// - error: 错误信息 + public func onDataSync(_ type: V2NIMDataSyncType, state: V2NIMDataSyncState, error: V2NIMError?) { + if type == .DATA_SYNC_TYPE_MAIN, state == .DATA_SYNC_STATE_COMPLETED { + getSessionInfo(sessionId: viewModel.sessionId) { + print("getSessionInfo again") + } + } + } + + /// 登录连接状态回调 + /// - Parameter status: 连接状态 + public func onConnectStatus(_ status: V2NIMConnectStatus) { + if status == .CONNECT_STATUS_WAITING { + networkBroken = true + for model in viewModel.messages { + model.isPined = false + } + } + + if status == .CONNECT_STATUS_CONNECTED, networkBroken { + networkBroken = false + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: DispatchWorkItem(block: { [weak self] in + // 断网重连后不会重发标记回调,需要手动拉取 + if let models = self?.viewModel.messages { + let messages = models.compactMap(\.message) + self?.viewModel.loadMoreWithMessage(messages) + } + })) + } + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift index bf5be3c6..40f23ed0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/MultiForwardViewController.swift @@ -8,8 +8,8 @@ import NIMSDK import UIKit @objcMembers -open class MultiForwardViewController: ChatBaseViewController, UINavigationControllerDelegate, UITableViewDataSource, UITableViewDelegate, ChatBaseCellDelegate, UIDocumentInteractionControllerDelegate, MultiForwardViewModelDelegate { - public var viewmodel = MultiForwardViewModel() +open class MultiForwardViewController: NEChatBaseViewController, UINavigationControllerDelegate, UITableViewDataSource, UITableViewDelegate, ChatBaseCellDelegate, UIDocumentInteractionControllerDelegate, MultiForwardViewModelDelegate { + public var viewModel = MultiForwardViewModel() private var messageAttachmentUrl: String? private var messageAttachmentFilePath: String = "" private var messageAttachmentMD5: String? @@ -17,6 +17,25 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr public var brokenNetworkViewHeight: CGFloat = 36 let interactionController = UIDocumentInteractionController() + 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 + }() + public init(_ attachmentUrl: String?, _ attachmentFilePath: String, _ attachmentMD5: String?) { @@ -27,7 +46,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewWillAppear(_ animated: Bool) { @@ -48,7 +67,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr override open func viewDidLoad() { super.viewDidLoad() - viewmodel.delegate = self + viewModel.delegate = self commonUI() loadData() } @@ -65,11 +84,11 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr forCellReuseIdentifier: "\(NEBaseChatMessageCell.self)" ) - NEChatUIKitClient.instance.getRegisterCustomCell().forEach { (key: String, value: UITableViewCell.Type) in + for (key, value) in NEChatUIKitClient.instance.getRegisterCustomCell() { cellRegisterDic[key] = value } - cellRegisterDic.forEach { (key: String, value: UITableViewCell.Type) in + for (key, value) in cellRegisterDic { tableView.register(value, forCellReuseIdentifier: key) } @@ -101,7 +120,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr func loadData() { view.makeToastActivity(.center) - viewmodel.loadData(messageAttachmentUrl, + viewModel.loadData(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) { [weak self] error in self?.view.hideToastActivity() @@ -121,7 +140,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr private func showErrorToast(_ error: Error?) { if let err = error as? NSError { switch err.code { - case noNetworkCode, -1009: + case protocolSendFailed, -1009: showToast(commonLocalizable("network_error")) default: showToast(err.localizedDescription) @@ -129,27 +148,6 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr } } - // 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 { @@ -159,14 +157,12 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr // MARK: UITableViewDataSource, UITableViewDelegate open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let count = viewmodel.messages.count - print("numberOfRowsInSection count : ", count) - return count + viewModel.messages.count } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.messages[indexPath.row] + let model = viewModel.messages[indexPath.row] model.inMultiForward = true model.isPined = false var reuseId = "" @@ -176,20 +172,17 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr } else { let key = "\(model.type.rawValue)" if model.type == .custom { - if let attch = NECustomAttachment.attachmentOfCustomMessage(message: model.message) { - if attch.customType == customMultiForwardType { - 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)" - } + let customType = model.customType + if customType == customMultiForwardType { + reuseId = "\(MessageType.multiForward.rawValue)" + } else if customType == customRichTextType { + reuseId = "\(MessageType.richText.rawValue)" + } else if NEChatUIKitClient.instance.getRegisterCustomCell()["\(customType)"] != nil { + reuseId = "\(customType)" } else { reuseId = "\(NEBaseChatMessageCell.self)" } - } else if model.type == .time || model.type == .notification || model.type == .tip { + } else if model.type == .notification || model.type == .tip { reuseId = "\(MessageType.time.rawValue)" } else if cellRegisterDic[key] != nil { reuseId = key @@ -222,7 +215,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - viewmodel.messages[indexPath.row].cellHeight() + chat_content_margin + viewModel.messages[indexPath.row].cellHeight() + chat_content_margin } open func getMultiForwardViewController(_ messageAttachmentUrl: String?, @@ -233,7 +226,7 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr // MARK: ChatBaseCellDelegate - open func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) { + open func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?, _ replyModel: MessageModel?) { if let tapClick = NEKitChatConfig.shared.ui.messageItemClick { tapClick(cell, model) return @@ -244,12 +237,12 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr return } - didTapMessage(cell, model) + didTapMessage(cell, model, nil) } open func didTapMessage(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ replyIndex: Int? = nil) { if model?.type == .image { - if let imageObject = model?.message?.messageObject as? NIMImageObject { + if let imageObject = model?.message?.attachment as? V2NIMMessageImageAttachment { var imageUrl = "" if let url = imageObject.url { @@ -261,112 +254,114 @@ open class MultiForwardViewController: ChatBaseViewController, UINavigationContr } if imageUrl.count > 0 { let showController = PhotoBrowserController( - urls: ChatMessageHelper.getUrls(messages: viewmodel.messages), + urls: ChatMessageHelper.getUrls(messages: viewModel.messages), url: imageUrl ) showController.modalPresentationStyle = .overFullScreen present(showController, animated: false, completion: nil) } } - } else if model?.type == .video, - let object = model?.message?.messageObject as? NIMVideoObject { - weak var weakSelf = self - let videoPlayer = VideoPlayerViewController() - videoPlayer.modalPresentationStyle = .overFullScreen - if let path = object.path, FileManager.default.fileExists(atPath: path) == true { + let object = model?.message?.attachment as? V2NIMMessageVideoAttachment { + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) + if FileManager.default.fileExists(atPath: path) { let url = URL(fileURLWithPath: path) + let videoPlayer = VideoPlayerViewController() + videoPlayer.modalPresentationStyle = .overFullScreen videoPlayer.videoUrl = url - videoPlayer.totalTime = object.duration - print("video url : ", videoPlayer.videoUrl as Any) + videoPlayer.totalTime = Int(object.duration) present(videoPlayer, animated: true, completion: nil) - } else if let urlString = object.url, let path = object.path, - let videoModel = model as? MessageVideoModel { - print("fetch message attachment") - if let index = replyIndex, index >= 0 { + } else { + if let index = replyIndex, index >= 0, + index < tableView.numberOfRows(inSection: 0) { tableView.scrollToRow(at: IndexPath(row: index, section: 0), at: .middle, animated: true) } - videoModel.state = .Downalod - if let videoCell = cell as? NEBaseChatMessageCell { - videoCell.setModel(videoModel, false) - } - - viewmodel.downLoad(urlString, path) { progress in - NELog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: #function + "CALLBACK downLoad: \(progress)") - - videoModel.progress = progress - if progress >= 1.0 { - videoModel.state = .Success - } - videoModel.cell?.uploadProgress(byRight: false, progress) - } _: { error in - weakSelf?.showErrorToast(error) - } + downloadFile(cell, model, object.url, path) } } else if model?.type == .location { if let locationModel = model as? MessageLocationModel, let lat = locationModel.lat, let lng = locationModel.lng { - let mapDetail = NEDetailMapController(type: .detail) - mapDetail.currentPoint = CGPoint(x: lat, y: lng) - mapDetail.locationTitle = locationModel.title - mapDetail.subTitle = locationModel.subTitle - navigationController?.pushViewController(mapDetail, animated: true) + var params = [String: Any]() + params["type"] = NEMapType.detail.rawValue + params["nav"] = navigationController + params["lat"] = lat + params["lng"] = lng + params["locationTitle"] = locationModel.title + params["subTitle"] = locationModel.subTitle + Router.shared.use(NERouterUrl.LocationVCRouter, parameters: params) } } else if model?.type == .file, - let object = model?.message?.messageObject as? NIMFileObject, - let path = object.path { + let object = model?.message?.attachment as? V2NIMMessageFileAttachment { + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) if !FileManager.default.fileExists(atPath: path) { - if let urlString = object.url, let path = object.path, - let fileModel = model as? MessageFileModel { - if let index = replyIndex, index >= 0 { - tableView.scrollToRow(at: IndexPath(row: index, section: 0), - at: .middle, - animated: true) - } - fileModel.state = .Downalod - if let fileCell = cell as? NEBaseChatMessageCell { - fileCell.setModel(fileModel, false) - } - - viewmodel.downLoad(urlString, path) { [weak self] progress in - NELog.infoLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + "downLoad file progress: \(progress)") - var newProgress = progress - if newProgress < 0 { - newProgress = abs(progress) / fileModel.size - } - fileModel.progress = newProgress - if newProgress >= 1.0 { - fileModel.state = .Success - } - fileModel.cell?.uploadProgress(byRight: false, newProgress) - - } _: { [weak self] error in - self?.showErrorToast(error) - } + if let index = replyIndex, index >= 0, + index < tableView.numberOfRows(inSection: 0) { + tableView.scrollToRow(at: IndexPath(row: index, section: 0), + at: .middle, + animated: true) } + downloadFile(cell, model, object.url, path) } else { let url = URL(fileURLWithPath: path) interactionController.url = url interactionController.delegate = self // UIDocumentInteractionControllerDelegate - if interactionController.presentPreview(animated: true) {} else { + if interactionController.presentPreview(animated: true) {} + else { interactionController.presentOptionsMenu(from: view.bounds, in: view, animated: true) } } - } else if model?.type == .custom, let attach = NECustomAttachment.attachmentOfCustomMessage(message: model?.message) { - if attach.customType == customMultiForwardType, - let data = NECustomAttachment.dataOfCustomMessage(message: model?.message) { + } else if model?.type == .custom { + if model?.customType == customMultiForwardType, + let data = NECustomAttachment.dataOfCustomMessage(model?.message?.attachment) { let url = data["url"] as? String let md5 = data["md5"] as? String - guard let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { return } - let fileName = multiForwardFileName + (model?.message?.messageId ?? "") - let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(fileName)").relativePath + guard let fileDirectory = NEPathUtils.getDirectoryForDocuments(dir: imkitDir) else { return } + let fileName = multiForwardFileName + (model?.message?.messageClientId ?? "") + let filePath = fileDirectory + fileName let multiForwardVC = getMultiForwardViewController(url, filePath, md5) navigationController?.pushViewController(multiForwardVC, animated: true) } } else { print(#function + "message did tap, type:\(String(describing: model?.type.rawValue))") } + + /// 下载附件(文件、视频消息) + /// - Parameters: + /// - cell: 当前单元格 + /// - model: 消息模型 + /// - url: 远端下载链接 + /// - path: 本地保存路径 + func downloadFile(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ url: String?, _ path: String) { + // 判断是否是视频或者文件对象 + guard let urlString = url, let fileModel = model as? MessageVideoModel else { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "MessageFileModel not exit") + return + } + + // 判断状态,如果是下载中不能进行预览 + if fileModel.state == .Downalod { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "downLoad state, click ingore") + return + } + + fileModel.state = .Downalod + if let fileCell = cell as? NEBaseChatMessageCell { + fileCell.setModel(fileModel, false) + } + + viewModel.downLoad(urlString, path) { progress in + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "downLoad file progress: \(progress)") + fileModel.progress = progress + fileModel.cell?.uploadProgress(byRight: false, progress) + + } _: { [weak self] localPath, error in + self?.showErrorToast(error) + if localPath != nil { + fileModel.state = .Success + } + } + } } // MARK: ChatBaseCellDelegate ignore protocol diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseCollectionMessageController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseCollectionMessageController.swift new file mode 100644 index 00000000..773f399a --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseCollectionMessageController.swift @@ -0,0 +1,686 @@ +//// 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 MJRefresh +import NEChatKit +import NIMSDK +import UIKit + +@objc +open class NEBaseCollectionMessageController: NEChatBaseViewController, UITableViewDelegate, UITableViewDataSource, CollectionMessageCellDelegate, UIDocumentInteractionControllerDelegate { + var audioPlayer: AVAudioPlayer? // 仅用于语音消息的播放 + + /// 收藏列表 + public lazy var contentTable: 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.mj_footer = MJRefreshBackNormalFooter( +// refreshingTarget: self, +// refreshingAction: #selector(loadMoreData) +// ) + tableView.keyboardDismissMode = .onDrag + return tableView + }() + + /// 收藏列表空占位 + public lazy var collectionEmptyView: NEEmptyDataView = { + let view = NEEmptyDataView( + imageName: "user_empty", + content: chatLocalizable("no_collection_message"), + frame: .zero + ) + view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false + view.isHidden = true + return view + }() + + let viewModel = CollectionMessageViewModel() + + /// 样式注册表 + var cellClassDic: [String: NEBaseCollectionMessageCell.Type] = [:] + /// 收藏列表内容最大宽度 + public var collection_content_maxW = (kScreenWidth - 72) + /// 正在播放的cell + var playingCell: NEBaseCollectionMessageAudioCell? + /// 正在播放数据模型 + var playingModel: MessageAudioModel? + + let interactionController = UIDocumentInteractionController() + + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + stopPlay() + } + + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + setupUI() + loadMoreData() + } + + /// 加载数据 + open func loadMoreData() { + viewModel.loadData { [weak self] error, finished in + if let err = error { + self?.showToast(err.localizedDescription) + self?.contentTable.mj_footer?.endRefreshing() + } else { + if self?.viewModel.collectionDatas.count ?? 0 <= 0 { + self?.collectionEmptyView.isHidden = false + } + self?.contentTable.reloadData() + if finished == false { + self?.contentTable.mj_footer?.endRefreshing() + } else { + self?.contentTable.mj_footer?.endRefreshingWithNoMoreData() + DispatchQueue.main.async { + self?.contentTable.mj_footer = nil + } + } + } + } + } + + /// 在子类中实现注册 + open func getRegisterDic() -> [String: NEBaseCollectionMessageCell.Type] { + cellClassDic + } + + /// UI初始化 + open func setupUI() { + title = chatLocalizable("operation_collection") + navigationView.navTitle.text = chatLocalizable("operation_collection") + navigationView.moreButton.isHidden = true + view.addSubview(contentTable) + NSLayoutConstraint.activate([ + contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight), + contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + view.addSubview(collectionEmptyView) + NSLayoutConstraint.activate([ + collectionEmptyView.topAnchor.constraint(equalTo: contentTable.topAnchor), + collectionEmptyView.leftAnchor.constraint(equalTo: contentTable.leftAnchor), + collectionEmptyView.rightAnchor.constraint(equalTo: contentTable.rightAnchor), + collectionEmptyView.bottomAnchor.constraint(equalTo: contentTable.bottomAnchor), + ]) + cellClassDic = getRegisterDic() + contentTable.register(NEBaseCollectionMessageCell.self, forCellReuseIdentifier: "\(NEBaseCollectionMessageCell.self)") + for (key, value) in cellClassDic { + contentTable.register(value, forCellReuseIdentifier: "\(key)") + } + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + viewModel.collectionDatas.count + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.row >= viewModel.collectionDatas.count { + return UITableViewCell() + } + + let model = viewModel.collectionDatas[indexPath.row] + var reuseId = "\(model.chatmodel.type.rawValue)" + if model.chatmodel.type == .custom { + let customType = model.chatmodel.customType + if customType == customMultiForwardType { + reuseId = "\(MessageType.multiForward.rawValue)" + } else if customType == customRichTextType { + reuseId = "\(MessageType.richText.rawValue)" + } else { + reuseId = "\(NEBaseCollectionMessageTextCell.self)" + } + } else if cellClassDic[reuseId] == nil { + reuseId = "\(NEBaseCollectionMessageTextCell.self)" + } + let cell = tableView.dequeueReusableCell(withIdentifier: reuseId, for: indexPath) as! NEBaseCollectionMessageCell + cell.delegate = self + cell.configureData(model) + return cell + } + + /// 列表行高 + /// - parameter tableView: 列表视图对象 + /// - parameter indexPath: 索引 + /// - returns: 行高 + open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + if indexPath.row >= viewModel.collectionDatas.count { + return 0 + } + + let model = viewModel.collectionDatas[indexPath.row] + if model.chatmodel.type == .custom { + if model.chatmodel.customType == customMultiForwardType { + return model.cellHeight(contenttMaxW: collection_content_maxW) - 30 + } + } + return model.cellHeight(contenttMaxW: collection_content_maxW) + } + + /// 取消收藏消息 + /// - Parameter model: 收藏消息对象 + open func removeCollectionActionClicked(_ model: CollectionMessageModel) { + weak var weakSelf = self + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + guard let collection = model.collection else { + return + } + weakSelf?.viewModel.removeCollection(collection) { error in + if error != nil { + weakSelf?.showToast(chatLocalizable("failed_operation")) + } else { + weakSelf?.viewModel.collectionDatas.removeAll(where: { model in + if model.collection?.collectionId == collection.collectionId { + return true + } + return false + }) + if weakSelf?.viewModel.collectionDatas.count ?? 0 <= 0 { + weakSelf?.collectionEmptyView.isHidden = false + } + weakSelf?.contentTable.reloadData() + } + } + } + + /// 拷贝文本消息 + /// - Parameter model: 收藏对象 + func copyCollectionActionClicked(_ model: CollectionMessageModel) { + guard let text = model.message?.text else { + return + } + + let pasteboard = UIPasteboard.general + pasteboard.string = text + showToast(commonLocalizable("copy_success")) + } + + /// 转发 + /// - Parameter message: 消息 + open func forwardCollectionMessage(_ message: V2NIMMessage, _ senderName: String) { + weak var weakSelf = self + Router.shared.register(ForwardMultiSelectedRouter) { param in + var items = [ForwardItem]() + + if let conversations = param["conversations"] as? [[String: Any]] { + var conversationIds = [String]() + + for conversation in conversations { + if let conversationId = conversation["conversationId"] as? String { + conversationIds.append(conversationId) + + let item = ForwardItem() + item.conversationId = conversationId + item.name = conversation["name"] as? String + item.avatar = conversation["avatar"] as? String + items.append(item) + } + } + + let type = chatLocalizable("operation_forward") + weakSelf?.showForwardAlertController(items: items, type: type, conversationId: message.conversationId, senderName: senderName) { comment in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + + weakSelf?.viewModel.forwardCollectionMessages(message, conversationIds, comment) { result, error, pro in + // 转发失败不展示错误信息 + } + } + } + } + + Router.shared.use(ForwardMultiSelectRouter, + parameters: ["nav": navigationController as Any, "selctorMode": 0], + closure: nil) + } + + /// 显示转发确认弹窗 + /// - Parameters: + /// - items: 转发对象 + /// - type: 转发类型(合并转发/逐条转发/转发) + /// - sureBlock: 确认按钮点击回调 + func showForwardAlertController(items: [ForwardItem], + type: String, + conversationId: String?, + senderName: String?, + _ sureBlock: ((String?) -> Void)? = nil) { + let forwardAlert = getCollectionForwardAlertController() + forwardAlert.setItems(items) + forwardAlert.forwardType = type + forwardAlert.sureBlock = sureBlock + if let name = senderName { + forwardAlert.senderName = name + } + + addChild(forwardAlert) + view.addSubview(forwardAlert.view) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: DispatchWorkItem(block: { + UIApplication.shared.keyWindow?.endEditing(true) + })) + } + + /// 转发消息 + /// - Parameter model: 收藏对象 + func forwardCollectionActionClicked(_ model: CollectionMessageModel) { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + if let message = model.message { + forwardCollectionMessage(message, model.senderName ?? "") + } + } + + /// 弹出底操作悬浮框 + /// - Parameter model: 收藏对象 + open func showActions(_ model: CollectionMessageModel) { + guard let message = model.message else { + return + } + var actions = [UIAlertAction]() + weak var weakSelf = self + let deleteCollectionAction = UIAlertAction(title: chatLocalizable("operation_delete_collection"), style: .default) { _ in + weakSelf?.removeCollectionActionClicked(model) + } + actions.append(deleteCollectionAction) + + if message.messageType == .MESSAGE_TYPE_TEXT { + let copyAction = UIAlertAction(title: chatLocalizable("operation_copy"), style: .default) { _ in + weakSelf?.copyCollectionActionClicked(model) + } + actions.append(copyAction) + } + + if message.messageType != .MESSAGE_TYPE_AUDIO { + let forwardAction = UIAlertAction(title: chatLocalizable("operation_forward"), style: .default) { _ in + weakSelf?.forwardCollectionActionClicked(model) + } + actions.append(forwardAction) + } + + let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), style: .cancel) { _ in } + actions.append(cancelAction) + + showActionSheet(actions) + } + + open func didClickMore(_ model: CollectionMessageModel?) { + if let m = model { + showActions(m) + } + } + + /// 跳转视图显示控件 + /// - Parameter model: 收藏对象 + open func showImageView(_ model: CollectionMessageModel?) { + if let object = model?.message?.attachment as? V2NIMMessageImageAttachment { + var imageUrlString = "" + + if let url = object.url { + imageUrlString = url + + } else { + if let path = object.path, FileManager.default.fileExists(atPath: path) { + imageUrlString = path + } + } + + if imageUrlString.count > 0 { + let showController = PhotoBrowserController(urls: [imageUrlString], url: imageUrlString) + showController.modalPresentationStyle = .overFullScreen + present(showController, animated: false, completion: nil) + } + } + } + + /// 跳转视频播放器 + /// - Parameter model: 收藏对象 + public func showVideoView(_ model: CollectionMessageModel?) { + if let object = model?.message?.attachment as? V2NIMMessageVideoAttachment { + stopPlay() + let player = VideoPlayerViewController() + player.totalTime = Int(object.duration) + player.modalPresentationStyle = .overFullScreen + + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) + if FileManager.default.fileExists(atPath: path) == true { + let url = URL(fileURLWithPath: path) + player.videoUrl = url + present(player, animated: true, completion: nil) + } else if let url = object.url, let remoteUrl = URL(string: url) { + player.videoUrl = remoteUrl + present(player, animated: true, completion: nil) + } + } + } + + /// 跳转地图详情页 + /// - Parameter model: 收藏对象 + public func showMapDetail(_ model: CollectionMessageModel?) { + if let title = model?.message?.text, let locationObject = model?.message?.attachment as? V2NIMMessageLocationAttachment { + let lng = locationObject.longitude + + let subTitle = locationObject.address + + let lat = locationObject.latitude + + var params = [String: Any]() + // 路由参数 + params["nav"] = navigationController + + params["type"] = NEMapType.detail.rawValue + + params["locationTitle"] = title + + params["subTitle"] = subTitle + + params["lat"] = lat + + params["lng"] = lng + + // 调用路由 + Router.shared.use(NERouterUrl.LocationVCRouter, parameters: params) + } + } + + /// 跳转文件查看器 + /// - Parameter model: 收藏对象 + public func showFileDetail(_ model: CollectionMessageModel?) { + if let object = model?.message?.attachment as? V2NIMMessageFileAttachment { + // 判断是否是文件对象 + guard let fileModel = model?.fileModel as? CollectionFileModel else { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "PinMessageFileModel not exit") + return + } + // 判断状态,如果是下载中不能进行预览 + if fileModel.state == .Downalod { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "downLoad state, click ingore") + return + } + + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) + if !FileManager.default.fileExists(atPath: path) { + // 本地文件不存在开始下载 + if let urlString = object.url { + downloadFile(fileModel, urlString, path) + } + } else { + // 有则直接加载 + let url = URL(fileURLWithPath: path) + interactionController.url = url + interactionController.delegate = self + + if interactionController.presentPreview(animated: true) {} + else { + interactionController.presentOptionsMenu(from: view.bounds, in: view, animated: true) + } + } + } + } + + /// 下载文件 + /// - Parameter fileModel: 文件对象 + /// - Parameter urlString: 下载地址 + /// - Parameter path: 保存路径 + open func downloadFile(_ fileModel: CollectionFileModel, _ urlString: String, _ path: String) { + fileModel.state = .Downalod + + // 开始下载 + viewModel.downloadFile(urlString, path) { [weak self] progress in + + NEALog.infoLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + "downLoad file progress: \(progress)") + + // 根据进度设置状态 + fileModel.progress = progress + + // 更新ui进度 + fileModel.cell?.uploadProgress(progress) + } _: { [weak self] localPath, error in + if let err = error { + switch err.code { + case protocolSendFailed: + self?.showToast(commonLocalizable("network_error")) + default: + print(err.localizedDescription) + } + } else if localPath != nil { + fileModel.state = .Success + } + } + } + + /// 跳转文本显示器 + /// - Parameter model: 收藏对象 + open func showTextView(_ model: CollectionMessageModel?) { + guard let message = model?.message, let customType = model?.chatmodel.customType else { + return + } + + if customType == customRichTextType { + showTextViewController(model) + + } else if customType == customMultiForwardType, + let data = NECustomAttachment.dataOfCustomMessage(message.attachment) { + let url = data["url"] as? String + let md5 = data["md5"] as? String + + guard let fileDirectory = NEPathUtils.getDirectoryForDocuments(dir: imkitDir) else { return } + let fileName = multiForwardFileName + (message.messageClientId ?? "") + let filePath = fileDirectory + fileName + let multiForwardVC = getMultiForwardViewController(url, filePath, md5) + navigationController?.pushViewController(multiForwardVC, animated: true) + } + } + + /// 点击内容 + /// - Parameter model: 收藏对象 + /// - Parameter cell: 内容视图显示控件 + open func didClickContent(_ model: CollectionMessageModel?, _ cell: NEBaseCollectionMessageCell) { + NEALog.infoLog(className(), desc: #function + "didClickContent") + + guard let message = model?.message else { + return + } + + if message.messageType == .MESSAGE_TYPE_AUDIO { + didPlay(cell: cell, model: model) + + } else if message.messageType == .MESSAGE_TYPE_IMAGE { + showImageView(model) + + } else if message.messageType == .MESSAGE_TYPE_VIDEO { + showVideoView(model) + + } else if message.messageType == .MESSAGE_TYPE_TEXT { + showTextViewController(model) + + } else if message.messageType == .MESSAGE_TYPE_LOCATION { + showMapDetail(model) + + } else if message.messageType == .MESSAGE_TYPE_FILE { + showFileDetail(model) + + } else if message.messageType == .MESSAGE_TYPE_CUSTOM { + showTextView(model) + } + } + + /// 开始播放 + /// - Parameter cell: 收藏列表视图对象 + /// - Parameter model: 收藏对象 + private func didPlay(cell: NEBaseCollectionMessageCell?, model: CollectionMessageModel?) { + guard let message = model?.message, let audio = message.attachment as? V2NIMMessageAudioAttachment else { + return + } + + let path = audio.path ?? ChatMessageHelper.createFilePath(message) + if !FileManager.default.fileExists(atPath: path) { + if let urlString = audio.url { + viewModel.downloadFile(urlString, path, nil) { [weak self] _, error in + if error == nil { + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK downLoad") + self?.startPlay(cell: cell, model: model) + } else { + self?.showToast(error!.localizedDescription) + } + } + } + } else { + startPlay(cell: cell, model: model) + } + } + + func startPlay(cell: NEBaseCollectionMessageCell?, model: CollectionMessageModel?) { + guard let audioModel = model?.chatmodel as? MessageAudioModel else { + return + } + + if playingModel == audioModel { + if audioPlayer?.isPlaying == true { + stopPlay() + } else { + startPlaying(audioMessage: model?.message) + } + } else { + stopPlay() + if let pCell = cell as? NEBaseCollectionMessageAudioCell { + playingCell = pCell + } + if let audioModel = model?.chatmodel as? MessageAudioModel { + playingModel = audioModel + } + startPlaying(audioMessage: model?.message) + } + } + + /// 开始播放 + /// - Parameter audioMessage: 音频消息对象 + func startPlaying(audioMessage: V2NIMMessage?) { + guard let message = audioMessage, let audio = message.attachment as? V2NIMMessageAudioAttachment else { + return + } + + playingCell?.startPlayAnimation() + + let path = audio.path ?? ChatMessageHelper.createFilePath(message) + if FileManager.default.fileExists(atPath: path) { + NEALog.infoLog(className(), desc: #function + " play path : " + path) + + // 创建一个URL对象,指向音频文件 + let audioURL = URL(fileURLWithPath: path) + + do { + let cate: AVAudioSession.Category = viewModel.getHandSetEnable() ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback + try AVAudioSession.sharedInstance().setCategory(cate, options: .duckOthers) + try AVAudioSession.sharedInstance().setActive(true) + + // 检查URL是否有效并尝试加载音频 + audioPlayer = try AVAudioPlayer(contentsOf: audioURL) + audioPlayer?.delegate = self + + // 开始播放 + audioPlayer?.play() + } catch { + // 处理加载音频文件失败的情况 + playingCell?.stopPlayAnimation() + print("Error loading audio: \(error.localizedDescription)") + } + } else { + NEALog.infoLog(className(), desc: #function + " audio path is empty, play url : " + (audio.url ?? "")) + playingCell?.stopPlayAnimation() + } + } + + func stopPlay() { + if audioPlayer?.isPlaying == true { + audioPlayer?.stop() + } + + playingCell?.stopPlayAnimation() + playingModel?.isPlaying = false + + do { + // 将当前的音频会话设置为非活动状态 + try AVAudioSession.sharedInstance().setActive(false) + } catch { + // 处理设置失败的情况 + print("Error setActive: \(error.localizedDescription)") + } + } + + open func showTextViewController(_ model: CollectionMessageModel?) { + guard let model = model?.chatmodel as? MessageTextModel else { return } + + let title = NECustomAttachment.titleOfRichText(model.message?.attachment) + let body = model.attributeStr + 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: NSAttributedString?) -> 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) + } + + open func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController { + self + } + + open func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { + controller.dismiss(animated: true) + } + + open func getCollectionForwardAlertController() -> NEBaseForwardAlertViewController { + NEBaseForwardAlertViewController() + } +} + +// MARK: - AVAudioPlayerDelegate + +extension NEBaseCollectionMessageController: AVAudioPlayerDelegate { + /// 声音播放完成回调 + public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + stopPlay() + } + + /// 声音解码失败回调 + public func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: (any Error)?) { + stopPlay() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift index f19da8fd..f85deef6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseForwardAlertViewController.swift @@ -3,27 +3,28 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECommonKit import NECommonUIKit import UIKit @objcMembers open class ForwardItem: NSObject { + var conversationId: String? var name: String? - var uid: String? var avatar: String? override public init() {} } @objcMembers -open class NEBaseForwardUserCell: UICollectionViewCell { - lazy var userHeader: NEUserHeaderView = { - let header = NEUserHeaderView(frame: .zero) - header.translatesAutoresizingMaskIntoConstraints = false - header.titleLabel.font = NEConstant.defaultTextFont(11.0) - header.clipsToBounds = true - header.accessibilityIdentifier = "id.forwardHeaderView" - return header +open class NEBaseForwardSessionCell: UICollectionViewCell { + public lazy var sessionHeaderView: NEUserHeaderView = { + let headerView = NEUserHeaderView(frame: .zero) + headerView.translatesAutoresizingMaskIntoConstraints = false + headerView.titleLabel.font = NEConstant.defaultTextFont(11.0) + headerView.clipsToBounds = true + headerView.accessibilityIdentifier = "id.forwardHeaderView" + return headerView }() override public init(frame: CGRect) { @@ -36,12 +37,12 @@ open class NEBaseForwardUserCell: UICollectionViewCell { } func setupUI() { - contentView.addSubview(userHeader) + contentView.addSubview(sessionHeaderView) NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: contentView.leftAnchor), - userHeader.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - userHeader.widthAnchor.constraint(equalToConstant: 32.0), - userHeader.heightAnchor.constraint(equalToConstant: 32.0), + sessionHeaderView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + sessionHeaderView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + sessionHeaderView.widthAnchor.constraint(equalToConstant: 32.0), + sessionHeaderView.heightAnchor.constraint(equalToConstant: 32.0), ]) } } @@ -49,58 +50,92 @@ open class NEBaseForwardUserCell: UICollectionViewCell { @objcMembers open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { - var datas = [ForwardItem]() - + let settingRepo = SettingRepo.shared 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() + /// 转发会话列表 + var forwardSessions = [ForwardItem]() + + /// 转发方式,合并转发 / 逐条转发 / 转发 + var forwardType = chatLocalizable("operation_forward") + + /// 会话名称 + var sessionName = "" + + /// 消息发送者名称 + var senderName = "" + + /// 确定按钮 + lazy var sureButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(sureClick), for: .touchUpInside) + button.setTitle(chatLocalizable("send"), for: .normal) + button.setTitleColor(UIColor.ne_normalTheme, for: .normal) + button.accessibilityIdentifier = "id.forwardSendBtn" + return button + }() + + /// 【发送给】 标签 + lazy var tipLabel: UILabel = { + let tipLabel = UILabel() + tipLabel.translatesAutoresizingMaskIntoConstraints = false + tipLabel.font = NEConstant.defaultTextFont(16.0) + tipLabel.textColor = .ne_darkText + tipLabel.text = chatLocalizable("send_to") + tipLabel.accessibilityIdentifier = "id.forwardTitle" + return tipLabel + }() + public var contentViewCenterYAnchor: NSLayoutConstraint? - lazy var userCollection: UICollectionView = { - let flow = UICollectionViewFlowLayout() - flow.scrollDirection = .horizontal - flow.minimumLineSpacing = 9.5 - flow.minimumInteritemSpacing = 9.5 - let collection = UICollectionView(frame: .zero, collectionViewLayout: flow) - collection.translatesAutoresizingMaskIntoConstraints = false - collection.delegate = self - collection.dataSource = self - collection.backgroundColor = .clear - collection.showsHorizontalScrollIndicator = false - return collection + /// 多个转发的 CollectionView + public lazy var sessionCollectionView: UICollectionView = { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + flowLayout.minimumLineSpacing = 9.5 + flowLayout.minimumInteritemSpacing = 9.5 + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.backgroundColor = .clear + collectionView.showsHorizontalScrollIndicator = false + return collectionView }() - lazy var contentView: UIView = { - let back = UIView() - back.backgroundColor = .white - back.translatesAutoresizingMaskIntoConstraints = false - back.clipsToBounds = true - back.layer.cornerRadius = 8.0 - return back + /// 背景容器 + public lazy var contentView: UIView = { + let backView = UIView() + backView.backgroundColor = .white + backView.translatesAutoresizingMaskIntoConstraints = false + backView.clipsToBounds = true + backView.layer.cornerRadius = 8.0 + return backView }() - lazy var oneUserHead: NEUserHeaderView = { - let header = NEUserHeaderView(frame: .zero) - header.clipsToBounds = true - header.translatesAutoresizingMaskIntoConstraints = false - header.accessibilityIdentifier = "id.forwardHeaderView" - return header + /// 单个转发对象时的 头像 + public lazy var oneSessionHeadView: NEUserHeaderView = { + let headerView = NEUserHeaderView(frame: .zero) + headerView.clipsToBounds = true + headerView.translatesAutoresizingMaskIntoConstraints = false + headerView.accessibilityIdentifier = "id.forwardHeaderView" + return headerView }() - lazy var oneUserName: UILabel = { - let name = UILabel() - name.textColor = .ne_darkText - name.font = NEConstant.defaultTextFont(14.0) - name.translatesAutoresizingMaskIntoConstraints = false - return name + /// 单个转发对象时的 名称 + public lazy var oneSessionNameLabel: UILabel = { + let nameLabel = UILabel() + nameLabel.textColor = .ne_darkText + nameLabel.font = NEConstant.defaultTextFont(14.0) + nameLabel.translatesAutoresizingMaskIntoConstraints = false + return nameLabel }() - lazy var contentText: UILabel = { + /// 转发描述 + public lazy var contentLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(14.0) @@ -111,8 +146,8 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD return label }() - // 留言 - lazy var commentTextFeild: UITextField = { + /// 留言 + public lazy var commentTextFeild: UITextField = { let textFeild = UITextField() textFeild.translatesAutoresizingMaskIntoConstraints = false textFeild.placeholder = chatLocalizable("leave_message") @@ -153,83 +188,91 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD open func setupUI() { view.backgroundColor = NEConstant.hexRGB(0x000000).withAlphaComponent(0.4) + + // 添加背景容器 view.addSubview(contentView) contentViewCenterYAnchor = contentView.centerYAnchor.constraint(equalTo: view.centerYAnchor) + contentViewCenterYAnchor?.isActive = true NSLayoutConstraint.activate([ contentView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - contentViewCenterYAnchor!, contentView.widthAnchor.constraint(equalToConstant: 276), contentView.heightAnchor.constraint(equalToConstant: 250), ]) - tip.translatesAutoresizingMaskIntoConstraints = false - tip.font = NEConstant.defaultTextFont(16.0) - tip.textColor = .ne_darkText - tip.text = chatLocalizable("send_to") - tip.accessibilityIdentifier = "id.forwardTitle" - contentView.addSubview(tip) + // 【发送给】 + contentView.addSubview(tipLabel) NSLayoutConstraint.activate([ - tip.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), - tip.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), - tip.heightAnchor.constraint(equalToConstant: 18.0), + tipLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), + tipLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + tipLabel.heightAnchor.constraint(equalToConstant: 18.0), ]) - contentView.addSubview(oneUserHead) + // 单个转发的头像 + contentView.addSubview(oneSessionHeadView) NSLayoutConstraint.activate([ - oneUserHead.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), - oneUserHead.topAnchor.constraint(equalTo: tip.bottomAnchor, constant: 16), - oneUserHead.widthAnchor.constraint(equalToConstant: 32.0), - oneUserHead.heightAnchor.constraint(equalToConstant: 32.0), + oneSessionHeadView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + oneSessionHeadView.topAnchor.constraint(equalTo: tipLabel.bottomAnchor, constant: 16), + oneSessionHeadView.widthAnchor.constraint(equalToConstant: 32.0), + oneSessionHeadView.heightAnchor.constraint(equalToConstant: 32.0), ]) - contentView.addSubview(oneUserName) + // 单个转发的名称 + contentView.addSubview(oneSessionNameLabel) NSLayoutConstraint.activate([ - oneUserName.leftAnchor.constraint(equalTo: oneUserHead.rightAnchor, constant: 8.0), - oneUserName.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16.0), - oneUserName.centerYAnchor.constraint(equalTo: oneUserHead.centerYAnchor), + oneSessionNameLabel.leftAnchor.constraint(equalTo: oneSessionHeadView.rightAnchor, constant: 8.0), + oneSessionNameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16.0), + oneSessionNameLabel.centerYAnchor.constraint(equalTo: oneSessionHeadView.centerYAnchor), ]) - contentView.addSubview(userCollection) + // 多个转发的 CollectionView + contentView.addSubview(sessionCollectionView) NSLayoutConstraint.activate([ - userCollection.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), - userCollection.rightAnchor.constraint( + sessionCollectionView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), + sessionCollectionView.rightAnchor.constraint( equalTo: contentView.rightAnchor, constant: -16.0 ), - userCollection.heightAnchor.constraint(equalToConstant: 32.0), - userCollection.topAnchor.constraint(equalTo: oneUserHead.topAnchor), + sessionCollectionView.heightAnchor.constraint(equalToConstant: 32.0), + sessionCollectionView.topAnchor.constraint(equalTo: oneSessionHeadView.topAnchor), ]) - let textBack = UIView() - textBack.translatesAutoresizingMaskIntoConstraints = false - textBack.backgroundColor = NEConstant.hexRGB(0xF2F4F5) - textBack.clipsToBounds = true - textBack.layer.cornerRadius = 4.0 - contentView.addSubview(textBack) + // 转发描述的背景 + let textBackView = UIView() + textBackView.translatesAutoresizingMaskIntoConstraints = false + textBackView.backgroundColor = NEConstant.hexRGB(0xF2F4F5) + textBackView.clipsToBounds = true + textBackView.layer.cornerRadius = 4.0 + contentView.addSubview(textBackView) NSLayoutConstraint.activate([ - textBack.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), - textBack.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16.0), - textBack.topAnchor.constraint(equalTo: oneUserHead.bottomAnchor, constant: 12.0), + textBackView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16.0), + textBackView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16.0), + textBackView.topAnchor.constraint(equalTo: oneSessionHeadView.bottomAnchor, constant: 12.0), ]) - textBack.addSubview(contentText) + // 转发描述 + textBackView.addSubview(contentLabel) NSLayoutConstraint.activate([ - contentText.leftAnchor.constraint(equalTo: textBack.leftAnchor, constant: 12), - contentText.rightAnchor.constraint(equalTo: textBack.rightAnchor, constant: -12), - contentText.topAnchor.constraint(equalTo: textBack.topAnchor, constant: 7), - contentText.bottomAnchor.constraint(equalTo: textBack.bottomAnchor, constant: -7), + contentLabel.leftAnchor.constraint(equalTo: textBackView.leftAnchor, constant: 12), + contentLabel.rightAnchor.constraint(equalTo: textBackView.rightAnchor, constant: -12), + contentLabel.topAnchor.constraint(equalTo: textBackView.topAnchor, constant: 7), + contentLabel.bottomAnchor.constraint(equalTo: textBackView.bottomAnchor, constant: -7), ]) - contentText.text = "[\(type)]\(context)的会话记录" + if sessionName.count > 0 { + contentLabel.text = "[\(forwardType)]\(sessionName)\(chatLocalizable("session_record"))" + } else if senderName.count > 0 { + contentLabel.text = "[\(forwardType)]\(senderName)\(chatLocalizable("collection_message"))" + } // 留言 contentView.addSubview(commentTextFeild) NSLayoutConstraint.activate([ - commentTextFeild.leftAnchor.constraint(equalTo: textBack.leftAnchor), - commentTextFeild.rightAnchor.constraint(equalTo: textBack.rightAnchor), - commentTextFeild.topAnchor.constraint(equalTo: textBack.bottomAnchor, constant: 16), + commentTextFeild.leftAnchor.constraint(equalTo: textBackView.leftAnchor), + commentTextFeild.rightAnchor.constraint(equalTo: textBackView.rightAnchor), + commentTextFeild.topAnchor.constraint(equalTo: textBackView.bottomAnchor, constant: 16), commentTextFeild.heightAnchor.constraint(equalToConstant: 32), ]) + // 水平分割线 let verticalLine = UIView() verticalLine.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(verticalLine) @@ -241,6 +284,7 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD verticalLine.topAnchor.constraint(equalTo: commentTextFeild.bottomAnchor, constant: 24.0), ]) + // 竖直分割线 let horizontalLine = UIView() horizontalLine.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(horizontalLine) @@ -252,33 +296,29 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD horizontalLine.bottomAnchor.constraint(equalTo: verticalLine.topAnchor), ]) - let canceBtn = UIButton() - canceBtn.translatesAutoresizingMaskIntoConstraints = false - canceBtn.addTarget(self, action: #selector(cancelClick), for: .touchUpInside) - canceBtn.setTitle(chatLocalizable("cancel"), for: .normal) - canceBtn.setTitleColor(.ne_greyText, for: .normal) - canceBtn.accessibilityIdentifier = "id.forwardCancelBtn" - - sureBtn.translatesAutoresizingMaskIntoConstraints = false - sureBtn.addTarget(self, action: #selector(sureClick), for: .touchUpInside) - sureBtn.setTitle(chatLocalizable("send"), for: .normal) - sureBtn.setTitleColor(.ne_blueText, for: .normal) - sureBtn.accessibilityIdentifier = "id.forwardSendBtn" + // 取消按钮 + let canceButton = UIButton() + canceButton.translatesAutoresizingMaskIntoConstraints = false + canceButton.addTarget(self, action: #selector(cancelClick), for: .touchUpInside) + canceButton.setTitle(chatLocalizable("cancel"), for: .normal) + canceButton.setTitleColor(.ne_greyText, for: .normal) + canceButton.accessibilityIdentifier = "id.forwardCancelBtn" - contentView.addSubview(canceBtn) + contentView.addSubview(canceButton) NSLayoutConstraint.activate([ - canceBtn.leftAnchor.constraint(equalTo: contentView.leftAnchor), - canceBtn.bottomAnchor.constraint(equalTo: verticalLine.bottomAnchor), - canceBtn.topAnchor.constraint(equalTo: horizontalLine.bottomAnchor), - canceBtn.rightAnchor.constraint(equalTo: verticalLine.leftAnchor), + canceButton.leftAnchor.constraint(equalTo: contentView.leftAnchor), + canceButton.bottomAnchor.constraint(equalTo: verticalLine.bottomAnchor), + canceButton.topAnchor.constraint(equalTo: horizontalLine.bottomAnchor), + canceButton.rightAnchor.constraint(equalTo: verticalLine.leftAnchor), ]) - contentView.addSubview(sureBtn) + // 确定按钮 + contentView.addSubview(sureButton) NSLayoutConstraint.activate([ - sureBtn.bottomAnchor.constraint(equalTo: verticalLine.bottomAnchor), - sureBtn.rightAnchor.constraint(equalTo: contentView.rightAnchor), - sureBtn.topAnchor.constraint(equalTo: horizontalLine.bottomAnchor), - sureBtn.leftAnchor.constraint(equalTo: verticalLine.rightAnchor), + sureButton.bottomAnchor.constraint(equalTo: verticalLine.bottomAnchor), + sureButton.rightAnchor.constraint(equalTo: contentView.rightAnchor), + sureButton.topAnchor.constraint(equalTo: horizontalLine.bottomAnchor), + sureButton.leftAnchor.constraint(equalTo: verticalLine.rightAnchor), ]) } @@ -301,32 +341,25 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD } open func setItems(_ items: [ForwardItem]) { - datas.append(contentsOf: items) - if datas.count == 1 { - let item = datas[0] - if let name = item.name { - oneUserHead.setTitle(name) - oneUserName.text = name - } else if let uid = item.uid { - oneUserHead.setTitle(uid) - oneUserName.text = uid - } - if let url = item.avatar, !url.isEmpty { - oneUserHead.sd_setImage(with: URL(string: url), completed: nil) - oneUserHead.titleLabel.text = "" - oneUserHead.backgroundColor = .clear - } else { - oneUserHead.backgroundColor = UIColor.colorWithString(string: item.uid) - oneUserHead.image = nil - } - userCollection.isHidden = true + forwardSessions.append(contentsOf: items) + if forwardSessions.count == 1 { + let item = forwardSessions[0] + oneSessionHeadView.configHeadData(headUrl: item.avatar, + name: item.name ?? "", + uid: item.conversationId ?? "") + oneSessionNameLabel.text = item.name ?? item.conversationId + sessionCollectionView.isHidden = true } else { - oneUserHead.isHidden = true - oneUserName.isHidden = true + oneSessionHeadView.isHidden = true + oneSessionNameLabel.isHidden = true } } func sureClick() { + // 更新最近转发列表 + let recentForwardIdList = forwardSessions.map { $0.conversationId ?? "" } + settingRepo.updateRecentForward(recentForwardIdList) + if let block = sureBlock { block(commentTextFeild.text) } @@ -352,26 +385,16 @@ open class NEBaseForwardAlertViewController: UIViewController, UICollectionViewD removeFromParent() } + // MARK: - UICollectionViewDelegate + open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - datas.count + forwardSessions.count } - open func setCellModel(cell: NEBaseForwardUserCell, indexPath: IndexPath) -> UICollectionViewCell { - let item = datas[indexPath.row] - if let url = item.avatar, !url.isEmpty { - cell.userHeader.sd_setImage(with: URL(string: url), completed: nil) - cell.userHeader.titleLabel.text = "" - cell.userHeader.backgroundColor = .clear - } else { - cell.userHeader.backgroundColor = UIColor.colorWithString(string: item.uid) - cell.userHeader.image = nil - if let name = item.name { - cell.userHeader.setTitle(name) - } else if let uid = item.uid { - cell.userHeader.setTitle(uid) - } - } + open func setCellModel(cell: NEBaseForwardSessionCell, indexPath: IndexPath) -> UICollectionViewCell { + let item = forwardSessions[indexPath.row] + cell.sessionHeaderView.configHeadData(headUrl: item.avatar, name: item.name ?? "", uid: item.conversationId ?? "") return cell } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift index c14e7c20..c001b2a5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBasePinMessageViewController.swift @@ -4,39 +4,35 @@ import NEChatKit import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit let PinMessageDefaultType = 1000 @objcMembers -open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDataSource, UITableViewDelegate, PinMessageViewModelDelegate, PinMessageCellDelegate, UIDocumentInteractionControllerDelegate, NIMMediaManagerDelegate { - let viewmodel = PinMessageViewModel() +open class NEBasePinMessageViewController: NEChatBaseViewController, UITableViewDataSource, UITableViewDelegate, PinMessageViewModelDelegate, PinMessageCellDelegate, UIDocumentInteractionControllerDelegate, NEIMKitClientListener { + let viewModel = PinMessageViewModel() + var audioPlayer: AVAudioPlayer? // 仅用于语音消息的播放 - var session: NIMSession + /// 会话id + var conversationId: String? + /// 样式注册表 var cellClassDic: [String: NEBasePinMessageCell.Type] = [:] - // pin 列表内容最大宽度 + /// pin 列表内容最大宽度 public var pin_content_maxW = (kScreenWidth - 72) - + /// 正在播放的cell var playingCell: NEBasePinMessageAudioCell? var playingModel: MessageAudioModel? - public init(session: NIMSession) { - self.session = session - super.init(nibName: nil, bundle: nil) - NIMSDK.shared().mediaManager.add(self) - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } + /// 网络断开标志 + var networkBroken = false - deinit { - NIMSDK.shared().mediaManager.remove(self) - } + /// 数据拉取标志 + var isLoadingData = false - private lazy var tableView: UITableView = { + /// 内容列表 + public lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) tableView.translatesAutoresizingMaskIntoConstraints = false tableView.separatorStyle = .none @@ -55,16 +51,33 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa frame: .zero ) view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false view.isHidden = true return view }() let interactionController = UIDocumentInteractionController() + /// 初始化 + /// - Parameter conversationId: 会话id + public init(conversationId: String) { + self.conversationId = conversationId + super.init(nibName: nil, bundle: nil) + IMKitClient.instance.addLoginListener(self) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + deinit { + IMKitClient.instance.removeLoginListener(self) + } + override open func viewDidLoad() { super.viewDidLoad() - viewmodel.delegate = self + viewModel.delegate = self setupUI() loadData() } @@ -76,23 +89,29 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa func loadData() { weak var weakSelf = self - viewmodel.getPinitems(session: session) { error in + guard let cid = conversationId else { + return + } + + isLoadingData = true + viewModel.getPinitems(conversationId: cid) { error in + weakSelf?.isLoadingData = false if let err = error as? NSError { - if weakSelf?.session.sessionType == .team, err.code == 414 { + if V2NIMConversationIdUtil.conversationType(weakSelf?.conversationId ?? "") == .CONVERSATION_TYPE_TEAM, err.code == teamNotExistCode { weakSelf?.showToast(chatLocalizable("team_not_exist")) + } else if err.code == protocolTimeout { + weakSelf?.showToast(commonLocalizable("network_error")) } else { weakSelf?.showToast(err.localizedDescription) } } else { - weakSelf?.viewmodel.items.forEach { model in - ChatMessageHelper.downloadAudioFile(message: model.message) - } - weakSelf?.emptyView.isHidden = (weakSelf?.viewmodel.items.count ?? 0) > 0 + weakSelf?.emptyView.isHidden = (weakSelf?.viewModel.items.count ?? 0) > 0 weakSelf?.tableView.reloadData() } } } + /// UI 初始化 func setupUI() { title = chatLocalizable("operation_pin") navigationView.navTitle.text = chatLocalizable("operation_pin") @@ -114,50 +133,56 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa ]) cellClassDic = getRegisterCellDic() tableView.register(NEBasePinMessageTextCell.self, forCellReuseIdentifier: "\(NEBasePinMessageTextCell.self)") - cellClassDic.forEach { (key: String, value: NEBasePinMessageCell.Type) in + for (key, value) in cellClassDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ - + /// 列表点击回调 + /// - Parameter tableView: 列表视图对象 + /// - Parameter indexPath: 点击的索引 open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let item = viewmodel.items[indexPath.row] - if item.message.session?.sessionType == .P2P { - let session = item.message.session + if indexPath.row >= viewModel.items.count { + return + } + + let item = viewModel.items[indexPath.row] + if item.message.conversationType == .CONVERSATION_TYPE_P2P { + let conversationId = item.message.conversationId Router.shared.use( PushP2pChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any, + parameters: ["nav": navigationController as Any, + "conversationId": conversationId as Any, "anchor": item.message], closure: nil ) - } else if item.message.session?.sessionType == .team { - let session = item.message.session + } else if item.message.conversationType == .CONVERSATION_TYPE_TEAM { + let conversationId = item.message.conversationId Router.shared.use( PushTeamChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any, + parameters: ["nav": navigationController as Any, + "conversationId": conversationId as Any, "anchor": item.message], closure: nil ) } } + /// 列表数据绑定 + /// - parameter tableView: 列表视图对象 + /// - parameter indexPath: 索引 open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.items[indexPath.row] + if indexPath.row >= viewModel.items.count { + return UITableViewCell() + } + + let model = viewModel.items[indexPath.row] var reuseId = "\(model.chatmodel.type.rawValue)" - if model.chatmodel.type == .custom, - let attach = NECustomAttachment.attachmentOfCustomMessage(message: model.chatmodel.message) { - if attach.customType == customMultiForwardType { + if model.chatmodel.type == .custom { + let customType = model.chatmodel.customType + if customType == customMultiForwardType { reuseId = "\(MessageType.multiForward.rawValue)" - } else if attach.customType == customRichTextType { + } else if customType == customRichTextType { reuseId = "\(MessageType.richText.rawValue)" } else { reuseId = "\(NEBasePinMessageTextCell.self)" @@ -171,50 +196,60 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa return cell } + /// 列表行数 + /// - parameter tableView: 列表视图对象 + /// - parameter section: 索引 + /// - returns: 行数 open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewmodel.items.count + viewModel.items.count } + /// 列表行高 + /// - parameter tableView: 列表视图对象 + /// - parameter indexPath: 索引 + /// - returns: 行高 open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.items[indexPath.row] - if let attach = NECustomAttachment.attachmentOfCustomMessage(message: model.message) { - if attach.customType == customMultiForwardType { - return model.cellHeight(pinContentMaxW: pin_content_maxW) - 30 - } + if indexPath.row >= viewModel.items.count { + return 0 + } + + let model = viewModel.items[indexPath.row] + if model.chatmodel.customType == customMultiForwardType { + return model.cellHeight(pinContentMaxW: pin_content_maxW) - 30 } return model.cellHeight(pinContentMaxW: pin_content_maxW) } - func cancelPinActionClicked(item: PinMessageModel) { + /// 取消pin消息 + /// - Parameter item: pin消息对象 + open func cancelPinActionClicked(item: NEPinMessageModel) { weak var weakSelf = self if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { weakSelf?.showToast(commonLocalizable("network_error")) return } - weakSelf?.viewmodel.removePinMessage(item.message) { error, model in + weakSelf?.viewModel.removePinMessage(item.message) { error in if let err = error { - // weakSelf?.showToast(err.localizedDescription) + print(err.localizedDescription) } else { - if let index = weakSelf?.viewmodel.items.firstIndex(of: item) { - NotificationCenter.default.post(name: Notification.Name(removePinMessageNoti), object: item.message) - weakSelf?.viewmodel.items.remove(at: index) - weakSelf?.emptyView.isHidden = (weakSelf?.viewmodel.items.count ?? 0) > 0 - weakSelf?.tableView.reloadData() - weakSelf?.showToast(chatLocalizable("cancel_pin_success")) - } + weakSelf?.emptyView.isHidden = (weakSelf?.viewModel.items.count ?? 0) > 0 + weakSelf?.showToast(chatLocalizable("cancel_pin_success")) } } } - func copyActionClicked(item: PinMessageModel) { - weak var weakSelf = self + /// 拷贝文本消息 + /// - Parameter item: pin消息对象 + func copyActionClicked(item: NEPinMessageModel) { let text = item.message.text let pasteboard = UIPasteboard.general pasteboard.string = text - weakSelf?.view.makeToast(chatLocalizable("copy_success"), duration: 2, position: .center) + showToast(commonLocalizable("copy_success")) } - func forwardActionClicked(item: PinMessageModel) { + /// 转发消息 + /// - Parameter item: pin消息对象 + func forwardActionClicked(item: NEPinMessageModel) { weak var weakSelf = self if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { weakSelf?.showToast(commonLocalizable("network_error")) @@ -223,7 +258,9 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa weakSelf?.forwardMessage(item.message) } - open func showAction(item: PinMessageModel) { + /// 弹出底操作悬浮框 + /// - Parameter item: pin消息对象 + open func showAction(item: NEPinMessageModel) { var actions = [UIAlertAction]() weak var weakSelf = self let cancelPinAction = UIAlertAction(title: chatLocalizable("operation_cancel_pin"), style: .default) { _ in @@ -231,14 +268,14 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa } actions.append(cancelPinAction) - if item.message.messageType == .text { + if item.message.messageType == .MESSAGE_TYPE_TEXT { let copyAction = UIAlertAction(title: chatLocalizable("operation_copy"), style: .default) { _ in weakSelf?.copyActionClicked(item: item) } actions.append(copyAction) } - if item.message.messageType != .audio { + if item.message.messageType != .MESSAGE_TYPE_AUDIO { let forwardAction = UIAlertAction(title: chatLocalizable("operation_forward"), style: .default) { _ in weakSelf?.forwardActionClicked(item: item) } @@ -251,252 +288,352 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa showActionSheet(actions) } + /// 获取转发确认弹窗 open func getForwardAlertController() -> NEBaseForwardAlertViewController { NEBaseForwardAlertViewController() } - func forwardMessageToUser(_ message: NIMMessage) { + /// 添加转发确认弹窗 + /// - Parameters: + /// - items: 转发对象 + /// - type: 转发类型(合并转发/逐条转发/转发) + /// - sureBlock: 确认按钮点击回调 + func addForwardAlertController(items: [ForwardItem], + type: String, + _ sureBlock: ((String?) -> Void)? = nil) { + let forwardAlert = getForwardAlertController() + forwardAlert.setItems(items) + forwardAlert.forwardType = type + forwardAlert.sureBlock = sureBlock + if let conversationId = conversationId { + let content = ChatMessageHelper.getSessionName(conversationId: conversationId) + forwardAlert.sessionName = content + } + + addChild(forwardAlert) + view.addSubview(forwardAlert.view) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: DispatchWorkItem(block: { + UIApplication.shared.keyWindow?.endEditing(true) + })) + } + + /// 转发消息 + /// - Parameters: + /// - message: 转发的消息 + open func forwardMessage(_ message: V2NIMMessage) { weak var weakSelf = self - Router.shared.register(ContactSelectedUsersRouter) { param in - print("user setting accids : ", param) + Router.shared.register(ForwardMultiSelectedRouter) { param in var items = [ForwardItem]() - if let users = param["im_user"] as? [NIMUser] { - users.forEach { user in - let item = ForwardItem() - item.uid = user.userId - item.avatar = user.userInfo?.avatarUrl - item.name = user.getShowName() - items.append(item) - } + if let conversations = param["conversations"] as? [[String: Any]] { + var conversationIds = [String]() - let forwardAlert = weakSelf!.getForwardAlertController() - forwardAlert.setItems(items) - if let session = self.viewmodel.session { - forwardAlert.context = ChatMessageHelper.getSessionName(session: session) - } - weakSelf?.addChild(forwardAlert) - weakSelf?.view.addSubview(forwardAlert.view) + for conversation in conversations { + if let conversationId = conversation["conversationId"] as? String { + conversationIds.append(conversationId) - 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) - } - } + let item = ForwardItem() + item.conversationId = conversationId + item.name = conversation["name"] as? String + item.avatar = conversation["avatar"] as? String + items.append(item) } } - } - } - var param = [String: Any]() - param["nav"] = weakSelf?.navigationController as Any - param["limit"] = 6 - Router.shared.use(ContactUserSelectRouter, parameters: param, closure: nil) - } - func forwardMessageToTeam(_ message: NIMMessage) { - weak var weakSelf = self - Router.shared.register(ContactTeamDataRouter) { param in - if let team = param["team"] as? NIMTeam { - let item = ForwardItem() - item.avatar = team.avatarUrl - item.name = team.getShowName() - item.uid = team.teamId - - let forwardAlert = weakSelf!.getForwardAlertController() - forwardAlert.setItems([item]) - if let session = self.viewmodel.session { - forwardAlert.context = ChatMessageHelper.getSessionName(session: session) - } - forwardAlert.sureBlock = { comment in + let type = chatLocalizable("operation_forward") + weakSelf?.addForwardAlertController(items: items, type: type) { 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?.viewModel.forwardMessages(message, conversationIds, comment) { result, error, pro in + // 转发失败不展示错误信息 } } - weakSelf?.addChild(forwardAlert) - weakSelf?.view.addSubview(forwardAlert.view) } } - Router.shared.use( - ContactTeamListRouter, - parameters: ["nav": weakSelf?.navigationController as Any, - "isClickCallBack": true], - closure: nil - ) + Router.shared.use(ForwardMultiSelectRouter, + parameters: ["nav": navigationController as Any, "selctorMode": 0], + closure: nil) } - open func forwardMessage(_ message: NIMMessage) { - if IMKitClient.instance.getConfigCenter().teamEnable { - weak var weakSelf = self - let userAction = UIAlertAction(title: chatLocalizable("contact_user"), - style: .default) { action in - weakSelf?.forwardMessageToUser(message) + open func tableViewReload(needLoad: Bool) { + if needLoad { + loadData() + } + tableView.reloadData() + } + + /// 刷新数据 + /// - Parameter model: 标记数据模型 + public func refreshModel(_ model: NEPinMessageModel) { + var index = -1 + for (i, item) in viewModel.items.enumerated() { + if item == model { + viewModel.items[i] = model + index = i + break } + } - let teamAction = UIAlertAction(title: chatLocalizable("team"), style: .default) { action in - weakSelf?.forwardMessageToTeam(message) - } + if index < 0 || index >= tableView.numberOfRows(inSection: 0) { + return + } + tableViewReload([IndexPath(row: index, section: 0)]) + } - let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), - style: .cancel) { action in - } + public func tableViewReload(_ indexs: [IndexPath]) { + tableView.reloadData(indexs) + } - showActionSheet([teamAction, userAction, cancelAction]) - } else { - forwardMessageToUser(message) + public func tableViewDelete(_ indexs: [IndexPath]) { + if isLoadingData { + return } - } - // MARK: PinMessageViewModelDelegate + let indexs = indexs.filter { $0.row >= 0 && $0.row < viewModel.items.count } - open func didNeedRefreshUI() { - loadData() + if !indexs.isEmpty { + tableView.deleteData(indexs) + emptyView.isHidden = viewModel.items.count > 0 + } } - // MARK: PinMessageCellDelegate - - open func didClickMore(_ model: PinMessageModel?) { + open func didClickMore(_ model: NEPinMessageModel?) { if let item = model { showAction(item: item) } } - open func didClickContent(_ model: PinMessageModel?, _ cell: NEBasePinMessageCell) { - NELog.infoLog(className(), desc: #function + "didClickContent") + /// 跳转视图显示控件 + /// - Parameter model: 标记对象 + open func toImageView(_ model: NEPinMessageModel?) { + if let object = model?.message.attachment as? V2NIMMessageImageAttachment { + var imageUrlString = "" - if model?.message.messageType == .audio { - startPlay(cell: cell, model: model) - } else if model?.message.messageType == .image { - if let imageObject = model?.message.messageObject as? NIMImageObject { - var imageUrl = "" + if let url = object.url { + imageUrlString = url - if let url = imageObject.url { - imageUrl = url - } else { - if let path = imageObject.path, FileManager.default.fileExists(atPath: path) { - imageUrl = path - } - } - if imageUrl.count > 0 { - let showController = PhotoBrowserController( - urls: [imageUrl], - url: imageUrl - ) - showController.modalPresentationStyle = .overFullScreen - present(showController, animated: false, completion: nil) + } else { + if let path = object.path, FileManager.default.fileExists(atPath: path) { + imageUrlString = path } } - } else if model?.message.messageType == .video, - let object = model?.message.messageObject as? NIMVideoObject { + + if imageUrlString.count > 0 { + let showController = PhotoBrowserController(urls: [imageUrlString], url: imageUrlString) + showController.modalPresentationStyle = .overFullScreen + present(showController, animated: false, completion: nil) + } + } + } + + /// 跳转视频播放器 + /// - Parameter model: 标记对象 + public func toVideoView(_ model: NEPinMessageModel?) { + if let object = model?.message.attachment as? V2NIMMessageVideoAttachment { stopPlay() - let videoPlayer = VideoPlayerViewController() - videoPlayer.modalPresentationStyle = .overFullScreen - videoPlayer.totalTime = object.duration + let player = VideoPlayerViewController() + player.totalTime = Int(object.duration) + player.modalPresentationStyle = .overFullScreen - if let path = object.path, FileManager.default.fileExists(atPath: path) == true { + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) + if FileManager.default.fileExists(atPath: path) == true { let url = URL(fileURLWithPath: path) - videoPlayer.videoUrl = url - present(videoPlayer, animated: true, completion: nil) - + player.videoUrl = url + present(player, animated: true, completion: nil) } else if let url = object.url, let remoteUrl = URL(string: url) { - videoPlayer.videoUrl = remoteUrl - present(videoPlayer, animated: true, completion: nil) + player.videoUrl = remoteUrl + present(player, animated: true, completion: nil) } + } + } - } else if model?.message.messageType == .text { - showTextViewController(model) - } else if model?.message.messageType == .location, let title = model?.message.text, - let locationObject = model?.message.messageObject as? NIMLocationObject { - let lat = locationObject.latitude + /// 跳转地图详情页 + /// - Parameter model: 标记对象 + public func toMapDetail(_ model: NEPinMessageModel?) { + if let title = model?.message.text, let locationObject = model?.message.attachment as? V2NIMMessageLocationAttachment { let lng = locationObject.longitude - let subTitle = locationObject.title - - let mapDetail = NEDetailMapController(type: .detail) - mapDetail.currentPoint = CGPoint(x: lat, y: lng) - mapDetail.locationTitle = title - mapDetail.subTitle = subTitle - navigationController?.pushViewController(mapDetail, animated: true) - } else if model?.message.messageType == .file, - let object = model?.message.messageObject as? NIMFileObject, - let path = object.path { + + let subTitle = locationObject.address + + let lat = locationObject.latitude + + var params = [String: Any]() + // 路由参数 + params["nav"] = navigationController + + params["type"] = NEMapType.detail.rawValue + + params["locationTitle"] = title + + params["subTitle"] = subTitle + + params["lat"] = lat + + params["lng"] = lng + + // 调用路由 + Router.shared.use(NERouterUrl.LocationVCRouter, parameters: params) + } + } + + /// 跳转文件查看器 + /// - Parameter model: 标记对象 + public func toFileDetail(_ model: NEPinMessageModel?) { + if let object = model?.message.attachment as? V2NIMMessageFileAttachment { + // 判断是否是文件对象 guard let fileModel = model?.pinFileModel as? PinMessageFileModel else { - NELog.infoLog(ModuleName + " " + className(), desc: #function + "PinMessageFileModel not exit") + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "PinMessageFileModel not exit") return } - + // 判断状态,如果是下载中不能进行预览 if fileModel.state == .Downalod { - NELog.infoLog(ModuleName + " " + className(), desc: #function + "downLoad state, click ingore") + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "downLoad state, click ingore") return } + + let path = object.path ?? ChatMessageHelper.createFilePath(model?.message) 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 - } + // 本地文件不存在开始下载 + if let urlString = object.url { + downloadFile(fileModel, urlString, path) } } else { + // 有则直接加载 let url = URL(fileURLWithPath: path) interactionController.url = url interactionController.delegate = self + if interactionController.presentPreview(animated: true) {} else { interactionController.presentOptionsMenu(from: view.bounds, in: view, animated: true) } } - } else if model?.message.messageType == .custom, let attach = NECustomAttachment.attachmentOfCustomMessage(message: model?.message) { - if attach.customType == customRichTextType { - 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) + } + } + + /// 下载文件 + /// - Parameter fileModel: 文件对象 + /// - Parameter urlString: 下载地址 + /// - Parameter path: 保存路径 + open func downloadFile(_ fileModel: PinMessageFileModel, _ urlString: String, _ path: String) { + fileModel.state = .Downalod + + // 开始下载 + viewModel.downLoad(urlString, path) { [weak self] progress in + + NEALog.infoLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + "downLoad file progress: \(progress)") + + // 根据进度设置状态 + fileModel.progress = progress + + if progress >= 100 { + fileModel.state = .Success + } + // 更新ui进度 + fileModel.cell?.uploadProgress(progress: progress) + } _: { [weak self] localPath, error in + if let err = error { + switch err.code { + case protocolSendFailed: + self?.showToast(commonLocalizable("network_error")) + default: + print(err.localizedDescription) + } + } else if localPath != nil { + fileModel.state = .Success } } } - private func startPlay(cell: NEBasePinMessageCell?, model: PinMessageModel?) { + /// 跳转文本显示器 + /// - Parameter model: 标记对象 + open func toTextViewShow(_ model: NEPinMessageModel?) { + let customType = model?.chatmodel.customType + if customType == customRichTextType { + showTextViewController(model) + + } else if customType == customMultiForwardType, + let data = NECustomAttachment.dataOfCustomMessage(model?.message.attachment) { + let url = data["url"] as? String + let md5 = data["md5"] as? String + + guard let fileDirectory = NEPathUtils.getDirectoryForDocuments(dir: imkitDir) else { return } + let fileName = multiForwardFileName + (model?.message.messageClientId ?? "") + let filePath = fileDirectory + fileName + let multiForwardVC = getMultiForwardViewController(url, filePath, md5) + navigationController?.pushViewController(multiForwardVC, animated: true) + } + } + + /// 点击内容 + /// - Parameter model: 标记对象 + /// - Parameter cell: 内容视图显示控件 + open func didClickContent(_ model: NEPinMessageModel?, _ cell: NEBasePinMessageCell) { + NEALog.infoLog(className(), desc: #function + "didClickContent") + + if model?.message.messageType == .MESSAGE_TYPE_AUDIO { + didPlay(cell: cell, model: model) + + } else if model?.message.messageType == .MESSAGE_TYPE_IMAGE { + toImageView(model) + + } else if model?.message.messageType == .MESSAGE_TYPE_VIDEO { + toVideoView(model) + + } else if model?.message.messageType == .MESSAGE_TYPE_TEXT { + showTextViewController(model) + + } else if model?.message.messageType == .MESSAGE_TYPE_LOCATION { + toMapDetail(model) + + } else if model?.message.messageType == .MESSAGE_TYPE_FILE { + toFileDetail(model) + + } else if model?.message.messageType == .MESSAGE_TYPE_CUSTOM { + toTextViewShow(model) + } + } + + /// 点击开始播放 + /// - Parameter cell: 标记列表视图对象 + /// - Parameter model: 标记对象 + private func didPlay(cell: NEBasePinMessageCell?, model: NEPinMessageModel?) { + guard let message = model?.message, let audio = message.attachment as? V2NIMMessageAudioAttachment else { + return + } + + let path = audio.path ?? ChatMessageHelper.createFilePath(message) + if !FileManager.default.fileExists(atPath: path) { + if let urlString = audio.url { + viewModel.downLoad(urlString, path, nil) { [weak self] _, error in + if error == nil { + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK downLoad") + self?.startPlay(cell: cell, model: model) + } else { + self?.showToast(error!.localizedDescription) + } + } + } + } else { + startPlay(cell: cell, model: model) + } + } + + /// 开始播放 + /// - Parameter cell: 标记列表视图对象 + /// - Parameter model: 标记对象 + private func startPlay(cell: NEBasePinMessageCell?, model: NEPinMessageModel?) { guard let audioModel = model?.chatmodel as? MessageAudioModel else { return } if playingModel == audioModel { - if NIMSDK.shared().mediaManager.isPlaying() { + if audioPlayer?.isPlaying == true { stopPlay() } else { startPlaying(audioMessage: model?.message) @@ -513,86 +650,70 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa } } - func startPlaying(audioMessage: NIMMessage?) { - guard let message = audioMessage, let audio = message.messageObject as? NIMAudioObject else { + /// 开始播放 + /// - Parameter audioMessage: 音频消息对象 + func startPlaying(audioMessage: V2NIMMessage?) { + guard let message = audioMessage, let audio = message.attachment as? V2NIMMessageAudioAttachment else { return } + playingCell?.startAnimation() - if let path = audio.path, FileManager.default.fileExists(atPath: path) == true { - NELog.infoLog(className(), desc: #function + " play path : " + path) - if viewmodel.getHandSetEnable() == true { - NIMSDK.shared().mediaManager.switch(.receiver) - } else { - NIMSDK.shared().mediaManager.switch(.speaker) - } - 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() - } - } + let path = audio.path ?? ChatMessageHelper.createFilePath(message) + if FileManager.default.fileExists(atPath: path) { + NEALog.infoLog(className(), desc: #function + " play path : " + path) + + // 创建一个URL对象,指向音频文件 + let audioURL = URL(fileURLWithPath: path) - // 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) + do { + let cate: AVAudioSession.Category = viewModel.getHandSetEnable() ? AVAudioSession.Category.playAndRecord : AVAudioSession.Category.playback + try AVAudioSession.sharedInstance().setCategory(cate, options: .duckOthers) + try AVAudioSession.sharedInstance().setActive(true) + + // 检查URL是否有效并尝试加载音频 + audioPlayer = try AVAudioPlayer(contentsOf: audioURL) + audioPlayer?.delegate = self + + // 开始播放 + audioPlayer?.play() + } catch { + // 处理加载音频文件失败的情况 + playingCell?.stopAnimation() + print("Error loading audio: \(error.localizedDescription)") } + } else { + NEALog.infoLog(className(), desc: #function + " audio path is empty, play url : " + (audio.url ?? "")) 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 ?? "") + func stopPlay() { + if audioPlayer?.isPlaying == true { + audioPlayer?.stop() } - // 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 + do { + // 将当前的音频会话设置为非活动状态 + try AVAudioSession.sharedInstance().setActive(false) + } catch { + // 处理设置失败的情况 + print("Error setActive: \(error.localizedDescription)") + } } 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 + open func showTextViewController(_ model: NEPinMessageModel?) { + guard let model = model?.chatmodel as? MessageTextModel else { return } + + let title = NECustomAttachment.titleOfRichText(model.message?.attachment) + let body = model.attributeStr let textView = getTextViewController(title: title, body: body) textView.modalPresentationStyle = .fullScreen DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in @@ -600,7 +721,7 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa })) } - func getTextViewController(title: String?, body: String?) -> TextViewController { + func getTextViewController(title: String?, body: NSAttributedString?) -> TextViewController { let textViewController = TextViewController(title: title, body: body) textViewController.view.backgroundColor = .white return textViewController @@ -612,8 +733,6 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa MultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) } - // MARK: UIDocumentInteractionControllerDelegate - open func documentInteractionControllerViewControllerForPreview(_ controller: UIDocumentInteractionController) -> UIViewController { self } @@ -621,4 +740,34 @@ open class NEBasePinMessageViewController: ChatBaseViewController, UITableViewDa open func documentPickerWasCancelled(_ controller: UIDocumentPickerViewController) { controller.dismiss(animated: true) } + + // MARK: - NEIMKitClientListener + + public func onConnectStatus(_ status: V2NIMConnectStatus) { + if status == .CONNECT_STATUS_WAITING { + networkBroken = true + } + + if status == .CONNECT_STATUS_CONNECTED, networkBroken { + networkBroken = false + DispatchQueue.main.asyncAfter(deadline: .now() + 1, execute: DispatchWorkItem(block: { [weak self] in + // 断网重连后不会重发标记回调,需要手动拉取 + self?.loadData() + })) + } + } +} + +// MARK: - AVAudioPlayerDelegate + +extension NEBasePinMessageViewController: AVAudioPlayerDelegate { + /// 声音播放完成回调 + public func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { + stopPlay() + } + + /// 声音解码失败回调 + public func audioPlayerDecodeErrorDidOccur(_ player: AVAudioPlayer, error: (any Error)?) { + stopPlay() + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift index ff999a40..85ced6a5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseReadViewController.swift @@ -3,56 +3,113 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit +/// 消息已读未读页面 - 基类 @objcMembers -open class NEBaseReadViewController: ChatBaseViewController, UIScrollViewDelegate, UITableViewDelegate, +open class NEBaseReadViewController: NEChatBaseViewController, UITableViewDelegate, UITableViewDataSource { - public var read: Bool = true - public var line: UIView = .init() - public var lineLeftCons: NSLayoutConstraint? - public var readTableView = UITableView(frame: .zero, style: .plain) - public var readUsers = [NEKitUser]() - public var unReadUsers = [NEKitUser]() - public let readButton = UIButton(type: .custom) - public let unreadButton = UIButton(type: .custom) - private var message: NIMMessage - init(message: NIMMessage) { + private let viewModel = ReadViewModel() + private var message: V2NIMMessage + private var teamId: String + + /// 已读/未读 按钮下方横线的左侧布局约束 + public var bottonBottomLineLeftAnchor: NSLayoutConstraint? + /// 已读/未读 按钮下方横线 + lazy var bottonBottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + /// 已读 tableView + lazy var readTableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.delegate = self + tableView.dataSource = self + tableView.sectionHeaderHeight = 0 + tableView.sectionFooterHeight = 0 + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + return tableView + }() + + /// 已读 tableView + lazy var unreadTableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.delegate = self + tableView.dataSource = self + tableView.sectionHeaderHeight = 0 + tableView.sectionFooterHeight = 0 + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.isHidden = true + return tableView + }() + + /// 已读按钮 + lazy var readButton: UIButton = { + let button = UIButton(type: .custom) + button.titleLabel?.font = UIFont.systemFont(ofSize: 14) + button.setTitle(chatLocalizable("read"), for: .normal) + button.setTitleColor(UIColor.ne_darkText, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(readButtonEvent), for: .touchUpInside) + button.accessibilityIdentifier = "id.tabHasRead" + return button + }() + + /// 未读按钮 + lazy var unreadButton: UIButton = { + let button = UIButton(type: .custom) + button.titleLabel?.font = UIFont.systemFont(ofSize: 14) + button.setTitle(chatLocalizable("unread"), for: .normal) + button.setTitleColor(UIColor.ne_darkText, for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(unreadButtonEvent), for: .touchUpInside) + button.accessibilityIdentifier = "id.tabUnRead" + return button + }() + + /// 空视图 + public lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView( + imageName: "emptyView", + content: chatLocalizable("message_all_unread"), + frame: .zero + ) + view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false + self.view.addSubview(view) + return view + }() + + init(message: V2NIMMessage, teamId: String) { self.message = message + self.teamId = teamId super.init(nibName: nil, bundle: nil) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + message = V2NIMMessage() + teamId = "" + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() commonUI() - loadData(message: message) + loadData() } open func commonUI() { title = chatLocalizable("message_read") navigationView.moreButton.isHidden = true - readButton.titleLabel?.font = UIFont.systemFont(ofSize: 14) - readButton.setTitle(chatLocalizable("read"), for: .normal) - readButton.setTitleColor(UIColor.ne_darkText, for: .normal) - readButton.translatesAutoresizingMaskIntoConstraints = false - readButton.addTarget(self, action: #selector(readButtonEvent), for: .touchUpInside) - 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(readButton) NSLayoutConstraint.activate([ readButton.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), @@ -69,155 +126,137 @@ open class NEBaseReadViewController: ChatBaseViewController, UIScrollViewDelegat unreadButton.heightAnchor.constraint(equalToConstant: 48), ]) - line.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(line) - lineLeftCons = line.leadingAnchor.constraint(equalTo: view.leadingAnchor) + view.addSubview(bottonBottomLine) + bottonBottomLineLeftAnchor = bottonBottomLine.leadingAnchor.constraint(equalTo: view.leadingAnchor) NSLayoutConstraint.activate([ - line.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 0), - line.heightAnchor.constraint(equalToConstant: 1), - line.widthAnchor.constraint(equalTo: readButton.widthAnchor), - lineLeftCons!, + bottonBottomLine.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 0), + bottonBottomLine.heightAnchor.constraint(equalToConstant: 1), + bottonBottomLine.widthAnchor.constraint(equalTo: readButton.widthAnchor), + bottonBottomLineLeftAnchor!, ]) - view.addSubview(emptyView) + view.addSubview(readTableView) if #available(iOS 11.0, *) { NSLayoutConstraint.activate([ - emptyView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), - emptyView.leftAnchor.constraint(equalTo: self.view.leftAnchor, constant: 0), - emptyView.rightAnchor.constraint(equalTo: self.view.rightAnchor, constant: 0), - emptyView.bottomAnchor.constraint( - equalTo: self.view.safeAreaLayoutGuide.bottomAnchor, - constant: 0 - ), + readTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), + readTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + readTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + readTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), ]) } else { NSLayoutConstraint.activate([ - emptyView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), - emptyView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), - emptyView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), - emptyView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0), + readTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), + readTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + readTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + readTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } - readTableView.delegate = self - readTableView.dataSource = self - readTableView.sectionHeaderHeight = 0 - readTableView.sectionFooterHeight = 0 - readTableView.translatesAutoresizingMaskIntoConstraints = false - readTableView.separatorStyle = .none - readTableView.tableFooterView = UIView() - view.addSubview(readTableView) + view.addSubview(unreadTableView) if #available(iOS 11.0, *) { NSLayoutConstraint.activate([ - readTableView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), - readTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), - readTableView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), - readTableView.bottomAnchor - .constraint(equalTo: self.view.safeAreaLayoutGuide.bottomAnchor), + unreadTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), + unreadTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + unreadTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + unreadTableView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), ]) } else { - // Fallback on earlier versions NSLayoutConstraint.activate([ - readTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), - readTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), - readTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - readTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + unreadTableView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), + unreadTableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + unreadTableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + unreadTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } + + view.addSubview(emptyView) + if #available(iOS 11.0, *) { + NSLayoutConstraint.activate([ + emptyView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), + emptyView.leftAnchor.constraint(equalTo: view.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: view.rightAnchor), + emptyView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + ]) + } else { + NSLayoutConstraint.activate([ + emptyView.topAnchor.constraint(equalTo: readButton.bottomAnchor, constant: 1), + emptyView.leftAnchor.constraint(equalTo: view.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: view.rightAnchor), + emptyView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + } + + /// 加载数据 + open func loadData() { + viewModel.getTeamMessageReceiptDetail(message, teamId) { [weak self] error in + if let err = error as? NSError { + if err.code == protocolSendFailed { + self?.showToast(commonLocalizable("network_error")) + } else { + self?.showToast(err.localizedDescription) + } + } else { + self?.readButton.setTitle("已读 (" + "\(self?.viewModel.readUsers.count ?? 0)" + ")", for: .normal) + self?.unreadButton.setTitle("未读 (" + "\(self?.viewModel.unReadUsers.count ?? 0)" + ")", for: .normal) + self?.readTableView.reloadData() + self?.emptyView.isHidden = self?.viewModel.readUsers.isEmpty == false + } + } } + /// 已读按钮点击事件 + /// - Parameter button: 按钮 open func readButtonEvent(button: UIButton) { - if read { + if readTableView.isHidden == false { return } - read = true - lineLeftCons?.constant = 0 + + bottonBottomLineLeftAnchor?.constant = 0 UIView.animate(withDuration: 0.5) { self.view.layoutIfNeeded() } - if readUsers.count == 0 { - readTableView.isHidden = true - emptyView.isHidden = false - } else { - readTableView.isHidden = false - emptyView.isHidden = true - readTableView.reloadData() - } + + readTableView.reloadData() + readTableView.isHidden = false + unreadTableView.isHidden = true + emptyView.isHidden = !viewModel.readUsers.isEmpty } + /// 未读按钮点击事件 + /// - Parameter button: 按钮 open func unreadButtonEvent(button: UIButton) { - if !read { + if unreadTableView.isHidden == false { return } - read = false - lineLeftCons?.constant = button.width + + bottonBottomLineLeftAnchor?.constant = button.width UIView.animate(withDuration: 0.5) { self.view.layoutIfNeeded() } - if unReadUsers.count == 0 { - readTableView.isHidden = true - emptyView.isHidden = false - } else { - readTableView.isHidden = false - emptyView.isHidden = true - readTableView.reloadData() - } - } - func loadData(message: NIMMessage) { - NIMSDK.shared().chatManager.queryMessageReceiptDetail(message) { anError, receiptInfo in - print("anError:\(anError) receiptInfo:\(receiptInfo)") - if let error = anError as? NSError { - if error.code == noNetworkCode { - self.showToast(commonLocalizable("network_error")) - } else { - self.showToast(error.localizedDescription) - } - return - } - - for userId in receiptInfo?.readUserIds ?? [] { - if let uId = userId as? String, - let user = UserInfoProvider.shared.getUserInfo(userId: uId) { - self.readUsers.append(user) - } - } - - for userId in receiptInfo?.unreadUserIds ?? [] { - if let uId = userId as? String, - let user = UserInfoProvider.shared.getUserInfo(userId: uId) { - self.unReadUsers.append(user) - } - } - self.readButton.setTitle("已读 (" + "\(self.readUsers.count)" + ")", for: .normal) - self.unreadButton.setTitle("未读 (" + "\(self.unReadUsers.count)" + ")", for: .normal) - self.readTableView.reloadData() - - if self.read, self.readUsers.count == 0 { - self.readTableView.isHidden = true - self.emptyView.isHidden = false - } else { - self.readTableView.isHidden = false - self.emptyView.isHidden = true - } - } + unreadTableView.reloadData() + unreadTableView.isHidden = false + readTableView.isHidden = true + emptyView.isHidden = !viewModel.unReadUsers.isEmpty } + // MARK: - UITableViewDelegate, UITableViewDataSource + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if read { - return readUsers.count + if tableView == readTableView { + return viewModel.readUsers.count } else { - return unReadUsers.count + return viewModel.unReadUsers.count } } - func cellSetModel(cell: UserBaseTableViewCell, indexPath: IndexPath) -> UITableViewCell { - if read { - let model = readUsers[indexPath.row] + func cellSetModel(tableView: UITableView, cell: UserBaseTableViewCell, indexPath: IndexPath) -> UITableViewCell { + if tableView == readTableView { + let model = viewModel.readUsers[indexPath.row] cell.setModel(model) - } else { - let model = unReadUsers[indexPath.row] + let model = viewModel.unReadUsers[indexPath.row] cell.setModel(model) } return cell @@ -229,17 +268,6 @@ open class NEBaseReadViewController: ChatBaseViewController, UIScrollViewDelegat withIdentifier: "\(UserBaseTableViewCell.self)", for: indexPath ) as! UserBaseTableViewCell - return cellSetModel(cell: cell, indexPath: indexPath) + return cellSetModel(tableView: tableView, cell: cell, indexPath: indexPath) } - - public lazy var emptyView: NEEmptyDataView = { - let view = NEEmptyDataView( - imageName: "emptyView", - content: chatLocalizable("message_all_unread"), - frame: .zero - ) - view.translatesAutoresizingMaskIntoConstraints = false - self.view.addSubview(view) - return view - }() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift index afbec002..09052f37 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseSelectUserViewController.swift @@ -4,20 +4,21 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import UIKit -public typealias DidSelectedAtRow = (_ index: Int, _ model: ChatTeamMemberInfoModel?) -> Void +public typealias DidSelectedAtRow = (_ index: Int, _ model: NETeamMemberInfoModel?) -> Void @objcMembers -open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDelegate, +open class NEBaseSelectUserViewController: NEChatBaseViewController, UITableViewDelegate, UITableViewDataSource { public var tableView = UITableView(frame: .zero, style: .plain) public var sessionId: String public var viewModel = TeamMemberSelectVM() public var selectedBlock: DidSelectedAtRow? - var teamInfo: ChatTeamInfoModel? - private var showSelf = true // 是否展示自己 + var teamInfo: NETeamInfoModel? + //// 是否展示自己 + private var showSelf = true var className = "SelectUserViewController" var isShowAtAll = true @@ -28,7 +29,9 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + sessionId = "" + showSelf = true + super.init(coder: coder) } override open func viewDidLoad() { @@ -39,28 +42,29 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe loadData() } + /// UI 内容初始化以及布局 func commonUI() { - let btn = UIButton(type: .custom) - btn.translatesAutoresizingMaskIntoConstraints = false - btn.accessibilityIdentifier = "id.arrowDown" - btn.setImage(UIImage.ne_imageNamed(name: "arrowDown"), for: .normal) - btn.addTarget(self, action: #selector(btnEvent), for: .touchUpInside) - view.addSubview(btn) + let button = UIButton(type: .custom) + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityIdentifier = "id.arrowDown" + button.setImage(UIImage.ne_imageNamed(name: "arrowDown"), for: .normal) + button.addTarget(self, action: #selector(btnEvent), for: .touchUpInside) + view.addSubview(button) if #available(iOS 11.0, *) { NSLayoutConstraint.activate([ - btn.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 16), - btn.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), - btn.widthAnchor.constraint(equalToConstant: 50), - btn.heightAnchor.constraint(equalToConstant: 50), + button.leadingAnchor.constraint(equalTo: self.view.leadingAnchor, constant: 16), + button.topAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.topAnchor), + button.widthAnchor.constraint(equalToConstant: 50), + button.heightAnchor.constraint(equalToConstant: 50), ]) } else { // Fallback on earlier versions NSLayoutConstraint.activate([ - btn.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), - btn.topAnchor.constraint(equalTo: view.topAnchor), - btn.widthAnchor.constraint(equalToConstant: 50), - btn.heightAnchor.constraint(equalToConstant: 50), + button.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + button.topAnchor.constraint(equalTo: view.topAnchor), + button.widthAnchor.constraint(equalToConstant: 50), + button.heightAnchor.constraint(equalToConstant: 50), ]) } @@ -78,6 +82,7 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe label.heightAnchor.constraint(equalToConstant: 50), ]) + /// 内容列表 tableView.delegate = self tableView.dataSource = self tableView.sectionHeaderHeight = 0 @@ -107,8 +112,8 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe } func loadData() { - viewModel.fetchTeamMembers(sessionId: sessionId) { [weak self] error, team in - NELog.infoLog( + viewModel.fetchTeamMembers(sessionId) { [weak self] error, team in + NEALog.infoLog( ModuleName + " " + (self?.className ?? "SelectUserViewController"), desc: "CALLBACK fetchTeamMembers " + (error?.localizedDescription ?? "no error") ) @@ -119,32 +124,33 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe // 人员选择页面移除自己 var selfIndex = -1 - if !(self?.showSelf ?? true), - let users = team?.users { + if !(self?.showSelf ?? true), let users = team?.users { for (index, user) in users.enumerated() { - if let u = team?.users[index].nimUser { - ChatUserCache.updateUserInfo(u) - } - if user.nimUser?.userId == IMKitLoginManager.instance.currentAccount() { - if user.teamMember?.type != .manager, let custom = team?.team?.clientCustomInfo, custom.count > 0, let json = getDictionaryFromJSONString(custom), let atValue = json[keyAllowAtAll] as? String, atValue == allowAtManagerValue { + if user.nimUser?.user?.accountId == IMKitClient.instance.account() { + if user.teamMember?.memberRole == .TEAM_MEMBER_ROLE_NORMAL, + let custom = team?.team?.serverExtension, custom.count > 0, + let json = getDictionaryFromJSONString(custom), + let atValue = json[keyAllowAtAll] as? String, atValue == allowAtManagerValue { self?.isShowAtAll = false } selfIndex = index } } - team?.users.remove(at: selfIndex) + if selfIndex >= 0 { + team?.users.remove(at: selfIndex) + } } // 根据身份+进群时间正序排序 if let users = team?.users { - var owner: ChatTeamMemberInfoModel? // 群主 - var managers = [ChatTeamMemberInfoModel]() // 管理员 - var normals = [ChatTeamMemberInfoModel]() // 普通成员 + var owner: NETeamMemberInfoModel? // 群主 + var managers = [NETeamMemberInfoModel]() // 管理员 + var normals = [NETeamMemberInfoModel]() // 普通成员 for user in users { - if user.teamMember?.type == .owner { + if user.teamMember?.memberRole == .TEAM_MEMBER_ROLE_OWNER { owner = user - } else if user.teamMember?.type == .manager { + } else if user.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { managers.append(user) } else { normals.append(user) @@ -152,11 +158,11 @@ open class NEBaseSelectUserViewController: ChatBaseViewController, UITableViewDe } managers.sort(by: { m1, m2 in - (m1.teamMember?.createTime ?? 0) < (m2.teamMember?.createTime ?? 0) + (m1.teamMember?.joinTime ?? 0) < (m2.teamMember?.joinTime ?? 0) }) normals.sort(by: { m1, m2 in - (m1.teamMember?.createTime ?? 0) < (m2.teamMember?.createTime ?? 0) + (m1.teamMember?.joinTime ?? 0) < (m2.teamMember?.joinTime ?? 0) }) if let owner = owner { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift index b9fc7f57..a932f27d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEBaseUserSettingViewController.swift @@ -3,18 +3,19 @@ // 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 @objcMembers -open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingViewModelDelegate, +open class NEBaseUserSettingViewController: NEChatBaseViewController, UserSettingViewModelDelegate, UITableViewDataSource, UITableViewDelegate { public var userId: String? - let viewmodel = UserSettingViewModel() + let viewModel = UserSettingViewModel() - lazy var userHeader: NEUserHeaderView = { + public lazy var userHeaderView: NEUserHeaderView = { let imageView = NEUserHeaderView(frame: .zero) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.clipsToBounds = true @@ -23,7 +24,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV return imageView }() - lazy var addBtn: ExpandButton = { + public lazy var addButton: ExpandButton = { let button = ExpandButton() button.translatesAutoresizingMaskIntoConstraints = false button.setImage(coreLoader.loadImage("setting_add"), for: .normal) @@ -31,7 +32,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV return button }() - lazy var nameLabel: UILabel = { + public lazy var nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(12.0) @@ -41,22 +42,22 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV return label }() - lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - table + public lazy var contentTable: UITableView = { + let contentTable = UITableView() + contentTable.translatesAutoresizingMaskIntoConstraints = false + contentTable.backgroundColor = .clear + contentTable.dataSource = self + contentTable.delegate = self + contentTable.separatorColor = .clear + contentTable.separatorStyle = .none + contentTable.sectionHeaderHeight = 12.0 + contentTable .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + contentTable.sectionHeaderTopPadding = 0.0 } - return table + return contentTable }() public var cellClassDic = [Int: NEBaseUserSettingCell.Type]() @@ -67,20 +68,27 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() - viewmodel.delegate = self + viewModel.delegate = self if let uid = userId { - viewmodel.getUserSettingModel(uid) - contentTable.tableHeaderView = headerView() - contentTable.reloadData() + viewModel.getConversation(uid) { [weak self] error in + self?.viewModel.getUserSettingModel(uid) { [weak self] in + self?.contentTable.tableHeaderView = self?.headerView() + self?.didLoadData() + self?.contentTable.reloadData() + } + } } setupUI() } + /// 渲染数据开始,在子类中使用 + open func didLoadData() {} + func setupUI() { view.backgroundColor = .ne_lightBackgroundColor title = chatLocalizable("chat_setting") @@ -95,7 +103,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseUserSettingCell.Type) in + for (key, value) in cellClassDic { contentTable.register(value, forCellReuseIdentifier: "\(key)") } if let pan = navigationController?.interactivePopGestureRecognizer { @@ -104,80 +112,80 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV } open func headerView() -> UIView { - let header = UIView(frame: CGRect(x: 0, y: 0, width: view.width, height: 110)) - header.backgroundColor = .clear - let cornerBack = UIView() - cornerBack.layer.cornerRadius = 8.0 - cornerBack.backgroundColor = .white - cornerBack.translatesAutoresizingMaskIntoConstraints = false - header.addSubview(cornerBack) + let headerView = UIView(frame: CGRect(x: 0, y: 0, width: view.width, height: 110)) + headerView.backgroundColor = .clear + let cornerBackView = UIView() + cornerBackView.layer.cornerRadius = 8.0 + cornerBackView.backgroundColor = .white + cornerBackView.translatesAutoresizingMaskIntoConstraints = false + headerView.addSubview(cornerBackView) NSLayoutConstraint.activate([ - cornerBack.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -12), - cornerBack.leftAnchor.constraint(equalTo: header.leftAnchor, constant: 20), - cornerBack.widthAnchor.constraint(equalToConstant: kScreenWidth - 40), - cornerBack.heightAnchor.constraint(equalToConstant: 86.0), + cornerBackView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -12), + cornerBackView.leftAnchor.constraint(equalTo: headerView.leftAnchor, constant: 20), + cornerBackView.widthAnchor.constraint(equalToConstant: kScreenWidth - 40), + cornerBackView.heightAnchor.constraint(equalToConstant: 86.0), ]) - cornerBack.addSubview(userHeader) - let tap = UITapGestureRecognizer() - userHeader.addGestureRecognizer(tap) - tap.numberOfTapsRequired = 1 - tap.numberOfTouchesRequired = 1 - - if let url = viewmodel.userInfo?.userInfo?.avatarUrl, !url.isEmpty { - userHeader.sd_setImage(with: URL(string: url), completed: nil) - userHeader.setTitle("") - userHeader.backgroundColor = .clear - } else if let name = viewmodel.userInfo?.showName(false) { - userHeader.sd_setImage(with: nil) - userHeader.setTitle(name) - userHeader.backgroundColor = UIColor.colorWithString(string: viewmodel.userInfo?.userId) + cornerBackView.addSubview(userHeaderView) + let tapGesture = UITapGestureRecognizer() + userHeaderView.addGestureRecognizer(tapGesture) + tapGesture.numberOfTapsRequired = 1 + tapGesture.numberOfTouchesRequired = 1 + + if let url = viewModel.userInfo?.user?.avatar, !url.isEmpty { + userHeaderView.sd_setImage(with: URL(string: url), completed: nil) + userHeaderView.setTitle("") + userHeaderView.backgroundColor = .clear + } else if let name = viewModel.userInfo?.showName() { + userHeaderView.sd_setImage(with: nil) + userHeaderView.setTitle(name) + userHeaderView.backgroundColor = UIColor.colorWithString(string: viewModel.userInfo?.user?.accountId) } - nameLabel.text = viewmodel.userInfo?.showName() - cornerBack.addSubview(nameLabel) - if IMKitClient.instance.getConfigCenter().teamEnable { + nameLabel.text = viewModel.userInfo?.showName() + cornerBackView.addSubview(nameLabel) + if IMKitConfigCenter.shared.teamEnable { NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: cornerBack.leftAnchor, constant: 16), - userHeader.topAnchor.constraint(equalTo: cornerBack.topAnchor, constant: 12), - userHeader.widthAnchor.constraint(equalToConstant: 42), - userHeader.heightAnchor.constraint(equalToConstant: 42), + userHeaderView.leftAnchor.constraint(equalTo: cornerBackView.leftAnchor, constant: 16), + userHeaderView.topAnchor.constraint(equalTo: cornerBackView.topAnchor, constant: 12), + userHeaderView.widthAnchor.constraint(equalToConstant: 42), + userHeaderView.heightAnchor.constraint(equalToConstant: 42), ]) nameLabel.font = NEConstant.defaultTextFont(12) nameLabel.textAlignment = .center NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: userHeader.leftAnchor, constant: -12.0), - nameLabel.rightAnchor.constraint(equalTo: userHeader.rightAnchor, constant: 12.0), - nameLabel.topAnchor.constraint(equalTo: userHeader.bottomAnchor, constant: 6.0), + nameLabel.leftAnchor.constraint(equalTo: userHeaderView.leftAnchor, constant: -12.0), + nameLabel.rightAnchor.constraint(equalTo: userHeaderView.rightAnchor, constant: 12.0), + nameLabel.topAnchor.constraint(equalTo: userHeaderView.bottomAnchor, constant: 6.0), ]) - cornerBack.addSubview(addBtn) - addBtn.addTarget(self, action: #selector(createDiscuss), for: .touchUpInside) + cornerBackView.addSubview(addButton) + addButton.addTarget(self, action: #selector(createDiscuss), for: .touchUpInside) NSLayoutConstraint.activate([ - addBtn.leftAnchor.constraint(equalTo: userHeader.rightAnchor, constant: 20.0), - addBtn.topAnchor.constraint(equalTo: userHeader.topAnchor), - addBtn.widthAnchor.constraint(equalToConstant: 42.0), - addBtn.heightAnchor.constraint(equalToConstant: 42.0), + addButton.leftAnchor.constraint(equalTo: userHeaderView.rightAnchor, constant: 20.0), + addButton.topAnchor.constraint(equalTo: userHeaderView.topAnchor), + addButton.widthAnchor.constraint(equalToConstant: 42.0), + addButton.heightAnchor.constraint(equalToConstant: 42.0), ]) } else { NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: cornerBack.leftAnchor, constant: 16), - userHeader.centerYAnchor.constraint(equalTo: cornerBack.centerYAnchor), - userHeader.widthAnchor.constraint(equalToConstant: 60), - userHeader.heightAnchor.constraint(equalToConstant: 60), + userHeaderView.leftAnchor.constraint(equalTo: cornerBackView.leftAnchor, constant: 16), + userHeaderView.centerYAnchor.constraint(equalTo: cornerBackView.centerYAnchor), + userHeaderView.widthAnchor.constraint(equalToConstant: 60), + userHeaderView.heightAnchor.constraint(equalToConstant: 60), ]) nameLabel.font = NEConstant.defaultTextFont(16) nameLabel.textAlignment = .left NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: userHeader.rightAnchor, constant: 16.0), - nameLabel.rightAnchor.constraint(equalTo: cornerBack.rightAnchor), - nameLabel.centerYAnchor.constraint(equalTo: userHeader.centerYAnchor), + nameLabel.leftAnchor.constraint(equalTo: userHeaderView.rightAnchor, constant: 16.0), + nameLabel.rightAnchor.constraint(equalTo: cornerBackView.rightAnchor), + nameLabel.centerYAnchor.constraint(equalTo: userHeaderView.centerYAnchor), ]) } - return header + return headerView } open func filterStackViewController() -> [UIViewController]? { @@ -195,7 +203,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV Router.shared.register(ContactSelectedUsersRouter) { param in print("user setting create disscuss : ", param) var convertParam = [String: Any]() - param.forEach { (key: String, value: Any) in + for (key, value) in param { if key == "names", let names = value as? String { convertParam[key] = "\(weakSelf?.nameLabel.text ?? "")、\(names)" } else { @@ -205,7 +213,12 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV weakSelf?.view.makeToastActivity(.center) Router.shared.use(TeamCreateDisuss, parameters: convertParam, closure: nil) } + + // 单聊设置-创建讨论组-人员选择页面不包含自己 var filters = Set() + filters.insert(IMKitClient.instance.account()) + + // 单聊设置-创建讨论组-人员选择页面不包含单聊对方 if let uid = userId { filters.insert(uid) } @@ -215,7 +228,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV parameters: [ "nav": navigationController as Any, "filters": filters, - "limit": 199, + "limit": inviteNumberLimit, "uid": userId ?? "", ], closure: nil @@ -226,7 +239,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV weakSelf?.view.hideToastActivity() if let code = param["code"] as? Int, let teamid = param["teamId"] as? String, code == 0 { - let session = NIMSession(teamid, type: .team) + let conversationId = V2NIMConversationIdUtil.teamConversationId(teamid) DispatchQueue.main.async { if let allControllers = weakSelf?.filterStackViewController() { @@ -234,7 +247,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV Router.shared.use( PushTeamChatVCRouter, parameters: ["nav": weakSelf?.navigationController as Any, - "session": session as Any], + "conversationId": conversationId as Any], closure: nil ) } @@ -246,7 +259,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV } open func showUserInfo() { - if let user = viewmodel.userInfo { + if let user = viewModel.userInfo { Router.shared.use( ContactUserInfoPageRouter, parameters: ["nav": navigationController as Any, "user": user], @@ -256,6 +269,7 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV } func didNeedRefreshUI() { + didLoadData() contentTable.reloadData() } @@ -266,12 +280,12 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV // MARK: UITableViewDataSource, UITableViewDelegate open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewmodel.cellDatas.count + viewModel.cellDatas.count } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] if let cell = tableView.dequeueReusableCell( withIdentifier: "\(model.type)", for: indexPath @@ -282,15 +296,14 @@ open class NEBaseUserSettingViewController: ChatBaseViewController, UserSettingV return UITableViewCell() } - func getPinMessageViewController(session: NIMSession) -> NEBasePinMessageViewController { - NEBasePinMessageViewController(session: session) + func getPinMessageViewController(conversationId: String) -> NEBasePinMessageViewController { + NEBasePinMessageViewController(conversationId: conversationId) } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if indexPath.row == 0 { - if let accid = userId { - let session = NIMSession(accid, type: .P2P) - let pin = getPinMessageViewController(session: session) + if let accid = userId, let conversationId = V2NIMConversationIdUtil.p2pConversationId(accid) { + let pin = getPinMessageViewController(conversationId: conversationId) navigationController?.pushViewController(pin, animated: true) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift index 6b365f5d..6a48dd08 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift @@ -6,20 +6,20 @@ import Foundation import NECommonKit @objcMembers -open class TextViewController: ChatBaseViewController { +open class TextViewController: NEChatBaseViewController { let leftRightMargin: CGFloat = 20 var contentMaxWidth: CGFloat = 0 let titleFont = UIFont.systemFont(ofSize: 24, weight: .semibold) let bodyFont = UIFont.systemFont(ofSize: 24) - lazy var scrollView: UIScrollView = { + public lazy var scrollView: UIScrollView = { let scrollView = UIScrollView() scrollView.isScrollEnabled = true scrollView.translatesAutoresizingMaskIntoConstraints = false return scrollView }() - lazy var textView: UIView = { + public lazy var textView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = .clear @@ -42,7 +42,7 @@ open class TextViewController: ChatBaseViewController { return view }() - lazy var titleLabel: CopyableLabel = { + public lazy var titleLabel: CopyableLabel = { let label = CopyableLabel() label.numberOfLines = 0 label.translatesAutoresizingMaskIntoConstraints = false @@ -52,7 +52,7 @@ open class TextViewController: ChatBaseViewController { return label }() - lazy var bodyLabel: CopyableLabel = { + public lazy var bodyLabel: CopyableLabel = { let label = CopyableLabel() label.numberOfLines = 0 label.translatesAutoresizingMaskIntoConstraints = false @@ -65,24 +65,23 @@ open class TextViewController: ChatBaseViewController { var contentLabelTopAnchor: NSLayoutConstraint? var contentLabelLeftAnchor: NSLayoutConstraint? - init(title: String?, body: String?) { + init(title: String?, body: NSAttributedString?) { super.init(nibName: nil, bundle: nil) contentMaxWidth = kScreenWidth - leftRightMargin * 2 if let title = title { + titleLabel.copyString = title let titleAtt = NEEmotionTool.getAttWithStr(str: title, font: titleFont, CGPoint(x: 0, y: -3)) - titleLabel.copyString = titleAtt.string titleLabel.attributedText = titleAtt } if let body = body { - let bodyAtt = NEEmotionTool.getAttWithStr(str: body, font: bodyFont, CGPoint(x: 0, y: -3)) - bodyLabel.copyString = bodyAtt.string - bodyLabel.attributedText = bodyAtt + bodyLabel.copyString = body.string + bodyLabel.attributedText = body } } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -122,37 +121,32 @@ open class TextViewController: ChatBaseViewController { bodyLabel.preferredMaxLayoutWidth = contentMaxWidth scrollView.addSubview(textView) contentLabelTopAnchor = textView.topAnchor.constraint(equalTo: scrollView.topAnchor) + contentLabelTopAnchor?.isActive = true contentLabelLeftAnchor = textView.leftAnchor.constraint(equalTo: scrollView.leftAnchor, constant: leftRightMargin) + contentLabelLeftAnchor?.isActive = true NSLayoutConstraint.activate([ - contentLabelTopAnchor!, textView.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor), - contentLabelLeftAnchor!, textView.rightAnchor.constraint(equalTo: scrollView.rightAnchor, constant: -leftRightMargin), ]) } // textView 垂直居中 func contentSizeToFit() { - var label = bodyLabel - if let titleLength = titleLabel.attributedText?.length { - if let bodyLength = bodyLabel.attributedText?.length { - label = titleLength > bodyLength ? titleLabel : bodyLabel - } else { - label = titleLabel - } - } + let titleSize = NSAttributedString.getRealSize(titleLabel.attributedText, titleFont, CGSize(width: contentMaxWidth, height: CGFloat.greatestFiniteMagnitude)) + let bodySize = NSAttributedString.getRealSize(bodyLabel.attributedText, bodyFont, CGSize(width: contentMaxWidth, height: CGFloat.greatestFiniteMagnitude)) - let textSize = label.attributedText?.finalSize(bodyFont, CGSize(width: contentMaxWidth, height: CGFloat.greatestFiniteMagnitude)) ?? .zero + let textHeight = titleSize.height + bodySize.height let textViewHeight = kScreenHeight - kNavigationHeight - KStatusBarHeight - if textSize.height <= textViewHeight { - let offsetY = (textViewHeight - textSize.height) / 2 + if textHeight <= textViewHeight { + let offsetY = (textViewHeight - textHeight) / 2 contentLabelTopAnchor?.constant = offsetY } - if textSize.width <= contentMaxWidth { - let offsetX = (kScreenWidth - textSize.width) / 2 + let textWidth = max(titleSize.width, bodySize.width) + if textWidth <= contentMaxWidth { + let offsetX = (kScreenWidth - textWidth) / 2 contentLabelLeftAnchor?.constant = offsetX } - scrollView.contentSize = textSize + scrollView.contentSize = CGSize(width: textWidth, height: textHeight) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift index cc6471f8..0eef48d7 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/EmojiPageView.swift @@ -26,13 +26,24 @@ open class EmojiPageView: UIView { private var pages = [AnyObject]() private let className = "EmojiPageView" + private lazy var scrollView: UIScrollView = { + let scrollView = UIScrollView(frame: self.bounds) + scrollView.autoresizingMask = .flexibleWidth + scrollView.showsVerticalScrollIndicator = false + scrollView.showsHorizontalScrollIndicator = false + scrollView.isPagingEnabled = true + scrollView.delegate = self + scrollView.scrollsToTop = false + return scrollView + }() + override public init(frame: CGRect) { super.init(frame: frame) setupControls() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override public var frame: CGRect { @@ -78,7 +89,7 @@ open class EmojiPageView: UIView { func reloadPage() { // reload时候记录上次位置 // guard let cPage = currentPage else { -// NELog.errorLog(className, desc: "❌currentPage is nil") +// NEALog.errorLog(className, desc: "❌currentPage is nil") // return // } if currentPage >= pages.count { @@ -223,19 +234,6 @@ open class EmojiPageView: UIView { // } } - // MARK: private method - - private lazy var scrollView: UIScrollView = { - let scrollView = UIScrollView(frame: self.bounds) - scrollView.autoresizingMask = .flexibleWidth - scrollView.showsVerticalScrollIndicator = false - scrollView.showsHorizontalScrollIndicator = false - scrollView.isPagingEnabled = true - scrollView.delegate = self - scrollView.scrollsToTop = false - return scrollView - }() - // MARK: 辅助方法 func raisePageIndexChangedDelegate() { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift index 1d6ec145..2aad5db6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonContainerView.swift @@ -3,8 +3,9 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit + @objc public protocol InputEmoticonContainerViewDelegate: NSObjectProtocol { func selectedEmoticon(emoticonID: String, emotCatalogID: String, description: String) func didPressSend(sender: UIButton) @@ -46,7 +47,7 @@ open class InputEmoticonContainerView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setUpSubViews() { @@ -141,7 +142,7 @@ open class InputEmoticonContainerView: UIView { return emotionsCount / layoutCount + 1 } } else { - NELog.errorLog(classTag, desc: "❌count maybe nil") + NEALog.errorLog(classTag, desc: "❌count maybe nil") return 0 } } @@ -183,12 +184,12 @@ extension InputEmoticonContainerView { page: NSInteger) -> UIView { let subView = UIView() guard let layout = emoticon.layout else { - NELog.errorLog(classTag, desc: "layout is nil") + NEALog.errorLog(classTag, desc: "layout is nil") return UIView() } guard let emotions = emoticon.emoticons else { - NELog.errorLog(classTag, desc: "emoticon.emoticons is nil") + NEALog.errorLog(classTag, desc: "emoticon.emoticons is nil") return UIView() } @@ -250,7 +251,7 @@ extension InputEmoticonContainerView { startX: CGFloat, startY: CGFloat, iconWidth: CGFloat, iconHeight: CGFloat, emotion: NIMInputEmoticonCatalog) { guard let layout = emotion.layout else { - NELog.errorLog(classTag, desc: "❌emotion is nill") + NEALog.errorLog(classTag, desc: "❌emotion is nill") return } @@ -262,6 +263,7 @@ extension InputEmoticonContainerView { deleteIcon.setImage(UIImage.ne_imageNamed(name: "emoji_del_normal"), for: .normal) deleteIcon.setImage(UIImage.ne_imageNamed(name: "emoji_del_pressed"), for: .highlighted) deleteIcon.addTarget(self, action: #selector(onIconSelected), for: .touchUpInside) + deleteIcon.accessibilityIdentifier = "id.emojiDelete" let newX = CGFloat(coloumnIndex + 1) * layout.cellWidth + startX let newY = CGFloat(rowIndex) * layout.cellHeight + startY let deleteIconRect = CGRect( @@ -296,7 +298,7 @@ extension InputEmoticonContainerView: EmojiPageViewDelegate, EmojiPageViewDataSo var resultEmotion = NIMInputEmoticonCatalog() guard let totalData = totalCatalogData, let targetView = pageView else { - NELog.errorLog(classTag, desc: "❌totalCatalogData is nil") + NEALog.errorLog(classTag, desc: "❌totalCatalogData is nil") return UIView() } @@ -334,7 +336,7 @@ extension InputEmoticonContainerView: InputEmoticonTabViewDelegate { extension InputEmoticonContainerView: NIMInputEmoticonButtonDelegate { open func selectedEmoticon(emotion: NIMInputEmoticon, catalogID: String) { guard let emotionId = emotion.emoticonID else { - NELog.errorLog(classTag, desc: "❌emoticonID is nil") + NEALog.errorLog(classTag, desc: "❌emoticonID is nil") return } if emotion.type == .unicode { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift index acc9fb49..e18ce932 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/InputEmoticonTabView.swift @@ -3,8 +3,9 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit + @objc public protocol InputEmoticonTabViewDelegate: NSObjectProtocol { @objc optional func tabView(_ tabView: InputEmoticonTabView?, didSelectTabIndex index: Int) } @@ -15,13 +16,24 @@ open class InputEmoticonTabView: UIControl { private var seps = [UIView]() private var className = "InputEmoticonTabView" + public lazy var sendButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(coreLoader.localizable("send"), for: .normal) + button.titleLabel?.textColor = .white + button.backgroundColor = UIColor.ne_normalTheme + button.titleLabel?.font = DefaultTextFont(14) + button.accessibilityIdentifier = "id.emojiSend" + return button + }() + override public init(frame: CGRect) { super.init(frame: frame) setUpSubViews() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setUpSubViews() { @@ -37,36 +49,36 @@ open class InputEmoticonTabView: UIControl { open func selectTabIndex(_ index: Int) { for i in 0 ..< tabs.count { - let btn = tabs[i] - btn.isSelected = i == index + let button = tabs[i] + button.isSelected = i == index } } open func loadCatalogs(_ emoticonCatalogs: [NIMInputEmoticonCatalog]?) { - tabs.forEach { btn in - btn.removeFromSuperview() + for button in tabs { + button.removeFromSuperview() } - seps.forEach { view in + for view in seps { view.removeFromSuperview() } tabs.removeAll() seps.removeAll() guard let catalogs = emoticonCatalogs else { - NELog.errorLog(className, desc: "❌emoticonCatalogs is nil") + NEALog.errorLog(className, desc: "❌emoticonCatalogs is nil") return } - catalogs.forEach { catelog in + for catelog in catalogs { let button = UIButton() button.addTarget(self, action: #selector(onTouchTab), for: .touchUpInside) button.sizeToFit() - self.addSubview(button) + addSubview(button) tabs.append(button) let sep = UIView(frame: CGRect(x: 0, y: 0, width: 0.5, height: 35)) sep.backgroundColor = UIColor.ne_borderColor seps.append(sep) - self.addSubview(sep) + addSubview(sep) } } @@ -76,17 +88,4 @@ open class InputEmoticonTabView: UIControl { delegate?.tabView?(self, didSelectTabIndex: index) } } - - // MARK: lazy method - - public lazy var sendButton: UIButton = { - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(coreLoader.localizable("send"), for: .normal) - 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/NIMInputEmoticonButton.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonButton.swift index 35f25ac0..9a56fab2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonButton.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonButton.swift @@ -3,8 +3,9 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit + public protocol NIMInputEmoticonButtonDelegate: NSObjectProtocol { func selectedEmoticon(emotion: NIMInputEmoticon, catalogID: String) } @@ -18,32 +19,32 @@ open class NIMInputEmoticonButton: UIButton { open class func iconButtonWithData(data: NIMInputEmoticon, catalogID: String, delegate: NIMInputEmoticonButtonDelegate) -> NIMInputEmoticonButton { - let icon = NIMInputEmoticonButton() - icon.addTarget(icon, action: #selector(onIconSelected), for: .touchUpInside) - icon.emotionData = data - icon.catalogID = catalogID - icon.isUserInteractionEnabled = true - icon.isExclusiveTouch = true - icon.contentMode = .scaleToFill - icon.delegate = delegate - icon.accessibilityIdentifier = "id.emoji" - icon.accessibilityValue = data.tag + let iconButton = NIMInputEmoticonButton() + iconButton.addTarget(iconButton, action: #selector(onIconSelected), for: .touchUpInside) + iconButton.emotionData = data + iconButton.catalogID = catalogID + iconButton.isUserInteractionEnabled = true + iconButton.isExclusiveTouch = true + iconButton.contentMode = .scaleToFill + iconButton.delegate = delegate + iconButton.accessibilityIdentifier = "id.emoji" + iconButton.accessibilityValue = data.tag switch data.type { case .unicode: - icon.setTitle(data.unicode, for: .normal) - icon.setTitle(data.unicode, for: .highlighted) - icon.titleLabel?.font = DefaultTextFont(32) + iconButton.setTitle(data.unicode, for: .normal) + iconButton.setTitle(data.unicode, for: .highlighted) + iconButton.titleLabel?.font = DefaultTextFont(32) default: let image = UIImage.ne_bundleImage(name: data.fileName ?? "") - icon.setImage(image, for: .normal) - icon.setImage(image, for: .highlighted) + iconButton.setImage(image, for: .normal) + iconButton.setImage(image, for: .highlighted) } - return icon + return iconButton } @objc func onIconSelected(sender: NIMInputEmoticonButton) { guard let data = emotionData, let id = catalogID else { - NELog.errorLog(classsTag, desc: "emotionData or catalogID maybe nil") + NEALog.errorLog(classsTag, desc: "emotionData or catalogID maybe nil") return } delegate?.selectedEmoticon(emotion: data, catalogID: id) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift index 60c43eee..9fce7a97 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NIMInputEmoticonManager.swift @@ -3,8 +3,9 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit + public enum NIMEmoticonType: NSInteger { case file = 0 case unicode @@ -100,7 +101,7 @@ open class NIMInputEmoticonManager: NSObject { let cataLog = NIMInputEmoticonCatalog() guard let infoDict = info, let emotions = emoticonsArray else { - NELog.errorLog(classTag, desc: "❌info or emoticonsArray is nil") + NEALog.errorLog(classTag, desc: "❌info or emoticonsArray is nil") return cataLog } cataLog.catalogID = infoDict["id"] as? String @@ -111,7 +112,7 @@ open class NIMInputEmoticonManager: NSObject { var id2Emoticons = [String: NIMInputEmoticon]() var resultEmotions = [NIMInputEmoticon]() - emotions.forEach { emoticonDict in + for emoticonDict in emotions { if let dict = (emoticonDict as? NSDictionary) { let emotion = NIMInputEmoticon() emotion.emoticonID = dict["id"] as? String @@ -151,7 +152,7 @@ open class NIMInputEmoticonManager: NSObject { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { - NELog.errorLog(classTag, desc: "❌catalogs is nil") + NEALog.errorLog(classTag, desc: "❌catalogs is nil") return emotion } @@ -171,7 +172,7 @@ open class NIMInputEmoticonManager: NSObject { open func emoticonByID(emoticonID: String) -> NIMInputEmoticon? { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { - NELog.errorLog(classTag, desc: "❌catalogs is nil") + NEALog.errorLog(classTag, desc: "❌catalogs is nil") return emotion } @@ -191,7 +192,7 @@ open class NIMInputEmoticonManager: NSObject { open func emoticonByCatalogID(catalogID: String, emoticonID: String) -> NIMInputEmoticon? { var emotion: NIMInputEmoticon? guard let clogs = catalogs else { - NELog.errorLog(classTag, desc: "❌catalogs is nil") + NEALog.errorLog(classTag, desc: "❌catalogs is nil") return emotion } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.swift index ae841bf6..25000544 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatDeduplicationHelper.swift @@ -3,9 +3,12 @@ // found in the LICENSE file. import Foundation +import NEChatKit +import NECoreIM2Kit import NIMSDK + @objcMembers -public class ChatDeduplicationHelper: NSObject, NIMLoginManagerDelegate { +public class ChatDeduplicationHelper: NSObject, NEIMKitClientListener { // 单例变量 static let instance = ChatDeduplicationHelper() // 最多缓存数量,可外部修改 @@ -14,25 +17,27 @@ public class ChatDeduplicationHelper: NSObject, NIMLoginManagerDelegate { public var blackListMessageIds = Set() // 音频消息记录 public var recordAudioMessagePaths = Set() + // 发送中消息记录 + public var sendingMessageIds = Set() // 撤回消息记录 public var revokeMessageIds = Set() override private init() { super.init() - NIMSDK.shared().loginManager.add(self) + IMKitClient.instance.addLoginListener(self) } deinit { - NIMSDK.shared().loginManager.remove(self) + IMKitClient.instance.removeLoginListener(self) } - public func onLogin(_ step: NIMLoginStep) { - if step == .logout { + public func onLoginStatus(_ status: V2NIMLoginStatus) { + if status == .LOGIN_STATUS_LOGOUT { clearCache() } } - public func onKickout(_ result: NIMLoginKickoutResult) { + public func onKickedOffline(_ detail: V2NIMKickedOfflineDetail) { clearCache() } @@ -40,9 +45,22 @@ public class ChatDeduplicationHelper: NSObject, NIMLoginManagerDelegate { blackListMessageIds.removeAll() recordAudioMessagePaths.removeAll() revokeMessageIds.removeAll() + NEFriendUserCache.shared.removeAllFriendInfo() } // 是否已经发送过对应消息的提示 + public func isMessageSended(messageId: String) -> Bool { + if sendingMessageIds.contains(messageId) { + return true + } + if sendingMessageIds.count > limit { + sendingMessageIds.removeAll() + } + sendingMessageIds.insert(messageId) + return false + } + + // 是否已经发送过黑名单消息的提示 public func isBlackTipSended(messageId: String) -> Bool { if blackListMessageIds.contains(messageId) { return true diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift index 258da275..c14e62cd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatMessageHelper.swift @@ -7,14 +7,20 @@ import CommonCrypto import Foundation import NEChatKit import NECommonKit -import NECoreIMKit +import NECoreIM2Kit +import NECoreKit import NIMSDK @objcMembers public class ChatMessageHelper: NSObject { public static let repo = ChatRepo.shared - // 获取图片合适尺寸 + /// 获取图片合适尺寸 + /// - Parameters: + /// - maxSize: 最大宽高 + /// - size: 图片宽高 + /// - miniWH: 最小宽高 + /// - Returns: 消息列表中展示的尺寸 public class func getSizeWithMaxSize(_ maxSize: CGSize, size: CGSize, miniWH: CGFloat) -> CGSize { var realSize = CGSize.zero @@ -42,12 +48,27 @@ public class ChatMessageHelper: NSObject { return realSize } - public static func getSessionName(session: NIMSession, showAlias: Bool = true) -> String { - session.sessionType == .P2P ? ChatUserCache.getShowName(userId: session.sessionId, teamId: nil, showAlias) : repo.getTeamInfo(teamId: session.sessionId)?.teamName ?? "" + /// 获取会话昵称 + /// - Parameters: + /// - conversationId: 会话 id + /// - showAlias: 是否优先显示备注 + /// - Returns: 会话昵称 + public static func getSessionName(conversationId: String, showAlias: Bool = true) -> String { + guard let sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId) else { + return "" + } + if V2NIMConversationIdUtil.conversationType(conversationId) == .CONVERSATION_TYPE_P2P { + return NEFriendUserCache.shared.getShowName(sessionId) + } else { + return ChatTeamCache.shared.getTeamInfo()?.name ?? "" + } } // MARK: message + /// 获取消息列表单元格注册列表 + /// - Parameter isFun: 是否是娱乐皮肤 + /// - Returns: 单元格注册列表 public static func getChatCellRegisterDic(isFun: Bool) -> [String: UITableViewCell.Type] { [ "\(MessageType.text.rawValue)": @@ -77,6 +98,9 @@ public class ChatMessageHelper: NSObject { ] } + /// 获取标记列表单元格注册列表 + /// - Parameter isFun: 是否是娱乐皮肤 + /// - Returns: 单元格注册列表 public static func getPinCellRegisterDic(isFun: Bool) -> [String: NEBasePinMessageCell.Type] { [ "\(MessageType.text.rawValue)": @@ -100,31 +124,65 @@ public class ChatMessageHelper: NSObject { ] } - public static func modelFromMessage(message: NIMMessage) -> MessageModel { + /// 获取收藏列表单元格注册列表 + /// - Parameter isFun: 是否是娱乐皮肤 + /// - Returns: 单元格注册列表 + public static func getCollectionCellRegisterDic(isFun: Bool) -> [String: NEBaseCollectionMessageCell.Type] { + [ + "\(MessageType.text.rawValue)": + isFun ? FunCollectionMessageTextCell.self : CollectionMessageTextCell.self, + "\(MessageType.image.rawValue)": + isFun ? FunCollectionMessageImageCell.self : CollectionMessageImageCell.self, + "\(MessageType.audio.rawValue)": + isFun ? FunCollectionMessageAudioCell.self : CollectionMessageAudioCell.self, + "\(MessageType.video.rawValue)": + isFun ? FunCollectionMessageVideoCell.self : CollectionMessageVideoCell.self, + "\(MessageType.location.rawValue)": + isFun ? FunCollectionMessageLocationCell.self : CollectionMessageLocationCell.self, + "\(MessageType.file.rawValue)": + isFun ? FunCollectionMessageFileCell.self : CollectionMessageFileCell.self, + "\(MessageType.multiForward.rawValue)": + isFun ? FunCollectionMessageMultiForwardCell.self : CollectionMessageMultiForwardCell.self, + "\(MessageType.richText.rawValue)": + isFun ? FunCollectionMessageRichTextCell.self : CollectionMessageRichTextCell.self, + "\(NEBasePinMessageTextCell.self)": + isFun ? FunCollectionMessageDefaultCell.self : CollectionMessageDefaultCell.self, + ] + } + + /// 构造消息体 + /// - Parameter message: 消息 + /// - Returns: 消息体 + public static func modelFromMessage(message: V2NIMMessage) -> MessageModel { var model: MessageModel switch message.messageType { - case .video: + case .MESSAGE_TYPE_VIDEO: model = MessageVideoModel(message: message) - case .text: + case .MESSAGE_TYPE_TEXT: model = MessageTextModel(message: message) - case .image: + case .MESSAGE_TYPE_IMAGE: model = MessageImageModel(message: message) - case .audio: + case .MESSAGE_TYPE_AUDIO: model = MessageAudioModel(message: message) - case .notification, .tip: + case .MESSAGE_TYPE_NOTIFICATION, .MESSAGE_TYPE_TIP: model = MessageTipsModel(message: message) - case .file: + case .MESSAGE_TYPE_FILE: model = MessageFileModel(message: message) - case .location: + case .MESSAGE_TYPE_LOCATION: model = MessageLocationModel(message: message) - case .rtcCallRecord: + case .MESSAGE_TYPE_CALL: model = MessageCallRecordModel(message: message) - case .custom: - if let attach = NECustomAttachment.attachmentOfCustomMessage(message: message) { - if attach.customType == customRichTextType { + case .MESSAGE_TYPE_CUSTOM: + if let type = NECustomAttachment.typeOfCustomMessage(message.attachment) { + if type == customMultiForwardType { + return MessageCustomModel(message: message, contentHeight: Int(customMultiForwardCellHeight)) + } + if type == customRichTextType { return MessageRichTextModel(message: message) } - return MessageCustomModel(message: message) + + // 注册过的自定义消息类型 + return MessageCustomModel(message: message, contentHeight: Int(customMultiForwardCellHeight)) } fallthrough default: @@ -135,12 +193,85 @@ public class ChatMessageHelper: NSObject { return model } + /// 构造消息体 + /// - Parameters: + /// - message: 消息 + /// - completion: 完成回调 + public static func modelFromMessage(message: V2NIMMessage, _ completion: @escaping (MessageModel) -> Void) { + var model: MessageModel + switch message.messageType { + case .MESSAGE_TYPE_VIDEO: + model = MessageVideoModel(message: message) + completion(model) + case .MESSAGE_TYPE_TEXT: + model = MessageTextModel(message: message) + completion(model) + case .MESSAGE_TYPE_IMAGE: + model = MessageImageModel(message: message) + completion(model) + case .MESSAGE_TYPE_AUDIO: + model = MessageAudioModel(message: message) + completion(model) + case .MESSAGE_TYPE_NOTIFICATION, .MESSAGE_TYPE_TIP: + // 查询通知消息中 targetId 的用户信息 + if message.messageType == .MESSAGE_TYPE_NOTIFICATION, + let attach = message.attachment as? V2NIMMessageNotificationAttachment, + var accIds = attach.targetIds { + if let senderId = message.senderId { + accIds.append(senderId) + } + + if let conversationId = message.conversationId, let tid = V2NIMConversationIdUtil.conversationTargetId(conversationId) { + ChatTeamCache.shared.loadShowName(userIds: accIds, teamId: tid) { + completion(MessageTipsModel(message: message)) + } + } else { + completion(MessageTipsModel(message: message)) + } + } else { + completion(MessageTipsModel(message: message)) + } + case .MESSAGE_TYPE_FILE: + model = MessageFileModel(message: message) + completion(model) + case .MESSAGE_TYPE_LOCATION: + model = MessageLocationModel(message: message) + completion(model) + case .MESSAGE_TYPE_CALL: + model = MessageCallRecordModel(message: message) + completion(model) + case .MESSAGE_TYPE_CUSTOM: + if let type = NECustomAttachment.typeOfCustomMessage(message.attachment) { + if type == customMultiForwardType { + completion(MessageCustomModel(message: message, contentHeight: Int(customMultiForwardCellHeight))) + return + } + if type == customRichTextType { + completion(MessageRichTextModel(message: message)) + return + } + + // 注册过的自定义消息类型 + completion(MessageCustomModel(message: message, contentHeight: Int(customMultiForwardCellHeight))) + return + } + fallthrough + default: + // 未识别的消息类型,默认为文本消息类型,text为未知消息体 + message.text = chatLocalizable("msg_unknown") + model = MessageTextModel(message: message) + completion(model) + } + } + /// 获取消息列表的中所以图片消息的 url + /// - Parameter messages: 消息列表 + /// - Returns: 图片路径列表 public static func getUrls(messages: [MessageModel]) -> [String] { - NELog.infoLog(ModuleName + " " + className(), desc: #function) + NEALog.infoLog(ModuleName + " " + className(), desc: #function) var urls = [String]() - messages.forEach { model in - if model.type == .image, let message = model.message?.messageObject as? NIMImageObject { + for model in messages { + if model.type == .image, let message = model.message?.attachment as? V2NIMMessageImageAttachment { if let url = message.url { urls.append(url) } else { @@ -153,20 +284,23 @@ public class ChatMessageHelper: NSObject { return urls } - // history message insert message at first of messages, send message add last of messages + /// 为消息体添加时间 + /// - Parameters: + /// - model: 消息体 + /// - lastModel: 最后一条消息 static func addTimeMessage(_ model: MessageModel, _ lastModel: MessageModel?) { guard let message = model.message else { - NELog.errorLog(ModuleName + " " + className(), desc: #function + ", model.message is nil") + NEALog.errorLog(ModuleName + " " + className(), desc: #function + ", model.message is nil") return } - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: \(String(describing: message.messageClientId))") if NotificationMessageUtils.isDiscussSeniorTeamNoti(message: message) { return } - let lastTs = lastModel?.message?.timestamp ?? 0.0 - let curTs = message.timestamp + let lastTs = lastModel?.message?.createTime ?? 0.0 + let curTs = message.createTime let dur = curTs - lastTs if (dur / 60) > 5 { let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: curTs)) @@ -174,37 +308,41 @@ public class ChatMessageHelper: NSObject { } } - public static func contentOfMessage(_ message: NIMMessage?) -> String { + /// 获取消息外显文案 + /// - Parameter message: 消息 + /// - Returns: 外显文案 + public static func contentOfMessage(_ message: V2NIMMessage?) -> String { switch message?.messageType { - case .text: + case .MESSAGE_TYPE_TEXT: if let t = message?.text { return t } else { return chatLocalizable("message_not_found") } - case .image: + case .MESSAGE_TYPE_IMAGE: return chatLocalizable("msg_image") - case .audio: + case .MESSAGE_TYPE_AUDIO: return chatLocalizable("msg_audio") - case .video: + case .MESSAGE_TYPE_VIDEO: return chatLocalizable("msg_video") - case .file: + case .MESSAGE_TYPE_FILE: return chatLocalizable("msg_file") - case .location: - return chatLocalizable("msg_location") - case .rtcCallRecord: - if let record = message?.messageObject as? NIMRtcCallRecordObject { - return record.callType == .audio ? chatLocalizable("msg_rtc_audio") : - chatLocalizable("msg_rtc_video") + case .MESSAGE_TYPE_LOCATION: + return chatLocalizable("msg_location") + " \(message?.text ?? "")" + case .MESSAGE_TYPE_CALL: + if let attachment = message?.attachment as? V2NIMMessageCallAttachment { + return attachment.type == 1 ? chatLocalizable("msg_rtc_audio") : chatLocalizable("msg_rtc_video") } return chatLocalizable("msg_rtc_call") - case .custom: - if let content = NECustomAttachment.contentOfRichText(message: message) { + case .MESSAGE_TYPE_CUSTOM: + // 换行消息 + if let content = NECustomAttachment.contentOfRichText(message?.attachment) { return content } - if let attach = NECustomAttachment.attachmentOfCustomMessage(message: message), - attach.customType == customMultiForwardType { + // 合并转发 + if let customType = NECustomAttachment.typeOfCustomMessage(message?.attachment), + customType == customMultiForwardType { return "[\(chatLocalizable("chat_history"))]" } @@ -215,14 +353,21 @@ public class ChatMessageHelper: NSObject { } /// 移除消息扩展字段中的 回复、@ - public static func clearForwardAtMark(_ forwardMessage: NIMMessage) { - forwardMessage.remoteExt?.removeValue(forKey: yxAtMsg) - forwardMessage.remoteExt?.removeValue(forKey: keyReplyMsgKey) - if forwardMessage.remoteExt?.count ?? 0 <= 0 { - forwardMessage.remoteExt = nil + /// - Parameter forwardMessage: 消息 + public static func clearForwardAtMark(_ forwardMessage: V2NIMMessage) { + guard var remoteExt = getDictionaryFromJSONString(forwardMessage.serverExtension ?? "") as? [String: Any] else { return } + remoteExt.removeValue(forKey: yxAtMsg) + remoteExt.removeValue(forKey: keyReplyMsgKey) + if remoteExt.count <= 0 { + remoteExt = [:] } + forwardMessage.serverExtension = getJSONStringFromDictionary(remoteExt) } + /// 构建合并转发消息附件的 header + /// - Parameters: + /// - messageCount: 消息数量 + /// - completion: 完成回调 public static func buildHeader(messageCount: Int) -> String { var dic = [String: Any]() dic["version"] = 0 // 功能版本 @@ -234,58 +379,128 @@ public class ChatMessageHelper: NSObject { return getJSONStringFromDictionary(dic) } - public static func buildBody(messages: [NIMMessage], + /// 构建合并转发消息附件的 body + /// - Parameters: + /// - messages: 消息 + /// - completion: 完成回调 + public static func buildBody(messages: [V2NIMMessage], _ completion: @escaping (String, [[String: Any]]) -> Void) { let enter = "\n" // 分隔符 var body = "" // 序列化结果 var abstracts = [[String: Any]]() // 摘要信息 - let group = DispatchGroup() for (i, msg) in messages.enumerated() { // 移除扩展字段中的 回复、@ 信息 - let remoteExt = msg.remoteExt + let remoteExt = msg.serverExtension clearForwardAtMark(msg) // 保存消息昵称和头像 - if let from = msg.from { - group.enter() - ChatUserCache.getUserInfo(from) { user, error in - if let user = user { - let senderNick = user.showName(false) - if msg.remoteExt != nil { - msg.remoteExt![mergedMessageNickKey] = senderNick - msg.remoteExt![mergedMessageAvatarKey] = user.userInfo?.avatarUrl ?? user.shortName(count: 2) - } else { - msg.remoteExt = [mergedMessageNickKey: senderNick as Any, - mergedMessageAvatarKey: user.userInfo?.avatarUrl as Any] - } - - // 摘要信息 - if i < 3 { - let content = ChatMessageHelper.contentOfMessage(msg) - abstracts.append(["senderNick": senderNick as Any, - "content": content, - "userAccId": from]) - } + if let from = msg.senderId { + let user = NEFriendUserCache.shared.getFriendInfo(from) ?? ChatUserCache.shared.getUserInfo(from) + if let user = user { + let senderNick = user.showName(false) + if var remoteExt = getDictionaryFromJSONString(msg.serverExtension ?? "") as? [String: Any] { + remoteExt[mergedMessageNickKey] = senderNick + remoteExt[mergedMessageAvatarKey] = user.user?.avatar ?? NEFriendUserCache.getShortName(senderNick ?? "") + msg.serverExtension = getJSONStringFromDictionary(remoteExt) + } else { + let remoteExt = [mergedMessageNickKey: senderNick as Any, + mergedMessageAvatarKey: user.user?.avatar as Any] + msg.serverExtension = getJSONStringFromDictionary(remoteExt) } - body.append(enter) - let data = NIMSDK.shared().conversationManager.encodeMessage(toData: msg) - if let stringData = String(data: data, encoding: .utf8) { - body.append(stringData) + + // 摘要信息 + if i < 3 { + let content = ChatMessageHelper.contentOfMessage(msg) + abstracts.append(["senderNick": senderNick as Any, + "content": content, + "userAccId": from]) } - group.leave() + } + if let stringData = V2NIMMessageConverter.messageSerialization(msg) { + body.append(enter + stringData) } } // 恢复扩展字段中的 回复、@ 信息 - msg.remoteExt = remoteExt + msg.serverExtension = remoteExt } - group.notify(queue: .main, work: DispatchWorkItem(block: { - completion(body, abstracts) - })) + completion(body, abstracts) + } + + /// 获取消息的客户端本地扩展信息(转换为[String: Any]) + /// - Parameter message: 消息 + /// - Returns: 客户端本地扩展信息 + public static func getMessageLocalExtension(message: V2NIMMessage) -> [String: Any]? { + guard let localExtension = message.localExtension else { return nil } + + if let localExt = getDictionaryFromJSONString(localExtension) as? [String: Any] { + return localExt + } + return nil + } + + /// 判断消息是否已撤回 + /// - Parameter message: 消息 + /// - Returns: 是否已撤回 + public static func isRevokeMessage(message: V2NIMMessage?) -> Bool { + guard let message = message else { return false } + + if let localExt = getMessageLocalExtension(message: message), + let isRevoke = localExt[revokeLocalMessage] as? Bool, isRevoke == true { + return true + } + return false + } + + /// 获取消息撤回前的内容(用于重新编辑) + /// - Parameter message: 消息 + /// - Returns: 撤回前的内容 + public static func getRevokeMessageContent(message: V2NIMMessage?) -> String? { + guard let message = message else { return nil } + + if let localExt = getMessageLocalExtension(message: message) { + if let content = localExt[revokeLocalMessageContent] as? String { + return content + } + } + return nil } + /// 查找回复信息键值对 + /// - Parameter message: 消息 + /// - Returns: 回复消息的 id + public static func getReplyDictionary(message: V2NIMMessage) -> [String: Any]? { + if let remoteExt = getDictionaryFromJSONString(message.serverExtension ?? ""), + let yxReplyMsg = remoteExt[keyReplyMsgKey] as? [String: Any] { + return yxReplyMsg + } + + return nil + } + + /// 查找回复信息键值对 + /// - Parameter message: 消息 + /// - Returns: 回复消息的 id + public static func createMessageRefer(_ params: [String: Any]?) -> V2NIMMessageRefer { + let refer = V2NIMMessageRefer() + refer.messageClientId = params?["idClient"] as? String + refer.messageServerId = params?["idServer"] as? String + refer.senderId = params?["from"] as? String + refer.createTime = TimeInterval((params?["time"] as? Int ?? 0) / 1000) + if let conversationId = params?["to"] as? String { + refer.conversationId = conversationId + refer.receiverId = V2NIMConversationIdUtil.conversationTargetId(conversationId) + refer.conversationType = V2NIMConversationIdUtil.conversationType(conversationId) + } + + return refer + } + + /// 获取文件 MD5 值 + /// - Parameter fileURL: 文件 URL + /// - Returns: md5 值 public static func getFileChecksum(fileURL: URL) -> String? { // 打开文件,创建文件句柄 let file = FileHandle(forReadingAtPath: fileURL.path) @@ -317,15 +532,35 @@ public class ChatMessageHelper: NSObject { return md5String } - // 检测语音消息是否下载,非漫游的云端消息不会自动下载语音文件,需要手动下载 - public static func downloadAudioFile(message: NIMMessage) { - if message.messageType == .audio { - if let audio = message.messageObject as? NIMAudioObject { - if let path = audio.path, FileManager.default.fileExists(atPath: path) == false { - repo.downloadMessageAttachment(message) { error in - } - } - } + /// 构造消息附件的本地文件路径 + /// - Parameter message: 消息 + /// - Returns: 本地文件路径 + public static func createFilePath(_ message: V2NIMMessage?) -> String { + var path = NEPathUtils.getDirectoryForDocuments(dir: imkitDir) ?? "" + guard let attach = message?.attachment as? V2NIMMessageFileAttachment else { + return path + } + + switch message?.messageType { + case .MESSAGE_TYPE_AUDIO: + path = NEPathUtils.getDirectoryForDocuments(dir: "\(imkitDir)audio/") ?? "" + case .MESSAGE_TYPE_IMAGE: + path = NEPathUtils.getDirectoryForDocuments(dir: "\(imkitDir)image/") ?? "" + case .MESSAGE_TYPE_VIDEO: + path = NEPathUtils.getDirectoryForDocuments(dir: "\(imkitDir)video/") ?? "" + default: + path = NEPathUtils.getDirectoryForDocuments(dir: "\(imkitDir)file/") ?? "" } + + if let messageClientId = message?.messageClientId { + path += messageClientId + } + + // 后缀(例如:.png) + if let ext = attach.ext, ext.count < 5 { + path += ext + } + + return path } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatTeamCache.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatTeamCache.swift new file mode 100644 index 00000000..97ba3a55 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatTeamCache.swift @@ -0,0 +1,129 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import Foundation +import NEChatKit +import NECoreIM2Kit +import NIMSDK + +public class ChatTeamCache: NSObject { + public static let shared = ChatTeamCache() + private var teamInfo: V2NIMTeam? + private var cacheTeamMemberInfoDic = [String: V2NIMTeamMember]() + + override private init() { + super.init() + } + + public func updateTeamInfo(_ team: V2NIMTeam?) { + guard let teamId = team?.teamId else { return } + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: \(teamId)") + teamInfo = team + } + + public func updateTeamMemberInfo(_ teamMember: V2NIMTeamMember?) { + guard let teamMember = teamMember else { + return + } + + let accountId = teamMember.accountId + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:\(accountId)") + cacheTeamMemberInfoDic[accountId] = teamMember + } + + /// 获取缓存的群聊信息 + public func getTeamInfo() -> V2NIMTeam? { + teamInfo + } + + /// 获取缓存的群成员信息 + public func getTeamMemberInfo(accountId: String) -> V2NIMTeamMember? { + cacheTeamMemberInfoDic[accountId] + } + + /// 删除群成员信息缓存 + public func removeTeamMemberInfo(_ accountId: String) { + if let _ = cacheTeamMemberInfoDic[accountId] { + cacheTeamMemberInfoDic.removeValue(forKey: accountId) + } + } + + /// 删除所有信息缓存 + public func removeAllTeamInfo() { + teamInfo = nil + cacheTeamMemberInfoDic.removeAll() + } + + /// 获取缓存群成员名字,team: 备注 > 群昵称 > 昵称 > ID + public func getShowName(_ accountId: String, + _ showAlias: Bool = true) -> String { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", userId: " + accountId) + // 好友缓存 + var fullName = NEFriendUserCache.shared.getShowName(accountId, showAlias) + + // 非好友缓存 + if !NEFriendUserCache.shared.isFriend(accountId) { + fullName = ChatUserCache.shared.getShowName(accountId) + } + + // 群成员缓存 + if let teamMember = cacheTeamMemberInfoDic[accountId] { + if teamMember.accountId == accountId { + if let teamNick = teamMember.teamNick, !teamNick.isEmpty { + fullName = teamNick + } + + if showAlias, + let friend = NEFriendUserCache.shared.getFriendInfo(accountId), + let alias = friend.friend?.alias, + !alias.isEmpty { + fullName = alias + } + + return fullName + } + } + return fullName + } + + // 获取展示的群成员名字, 备注 > 群昵称 > 昵称 > ID + public func loadShowName(userIds: [String], teamId: String, _ completion: @escaping () -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: \(teamId)") + var loadUserIds = Set() // 需要查询用户信息的ID + var loadMemberIds = Set() // 需要查询群成员信息的ID + let group = DispatchGroup() + + for userId in userIds { + if !NEFriendUserCache.shared.isFriend(userId) { + loadUserIds.insert(userId) + } + + if cacheTeamMemberInfoDic[userId] == nil { + loadMemberIds.insert(userId) + } + } + + // 先查询用户信息(陌生人) + if !loadUserIds.isEmpty { + group.enter() + ContactRepo.shared.getUserList(accountIds: Array(loadUserIds)) { users, error in + users?.forEach { ChatUserCache.shared.updateUserInfo($0) } + group.leave() + } + } + + // 再查询群成员信息 + if !loadMemberIds.isEmpty { + group.enter() + TeamRepo.shared.getTeamMemberListByIds(teamId, .TEAM_TYPE_NORMAL, Array(loadMemberIds)) { [weak self] teamMember, error in + teamMember?.forEach { self?.updateTeamMemberInfo($0) } + group.leave() + } + } + + group.notify(queue: .main) { + completion() + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatUserCache.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatUserCache.swift new file mode 100644 index 00000000..3b5786a3 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ChatUserCache.swift @@ -0,0 +1,56 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import Foundation +import NECoreIM2Kit +import NIMSDK + +/// 用户信息缓存,主要缓存非好友用户 +public class ChatUserCache: NSObject { + public static let shared = ChatUserCache() + + // 非好友列表,聊天页面销毁时同步清空 + public var noUserCache = [String: NEUserWithFriend]() + + override private init() { + super.init() + } + + // 添加(更新)非好友信息 + public func updateUserInfo(_ user: V2NIMUser?) { + guard let userId = user?.accountId else { return } + noUserCache[userId]?.user = user + } + + // 添加(更新)非好友信息 + public func updateUserInfo(_ user: NEUserWithFriend?) { + guard let userId = user?.user?.accountId else { return } + noUserCache[userId] = user + } + + /// 获取缓存的非好友信息 + public func getUserInfo(_ accountId: String) -> NEUserWithFriend? { + noUserCache[accountId] + } + + /// 删除非好友信息缓存 + public func removeUserInfo(_ accountId: String) { + if let _ = noUserCache[accountId] { + noUserCache.removeValue(forKey: accountId) + } + } + + /// 删除所有非好友信息缓存 + public func removeAllUserInfo() { + noUserCache.removeAll() + } + + /// 获取缓存用户名字,p2p: 备注 > 昵称 > ID + public func getShowName(_ userId: String, + _ showAlias: Bool = true) -> String { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", userId: " + userId) + let user = getUserInfo(userId) + return user?.showName(showAlias) ?? userId + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift index 1856b5a4..92bdfb7d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift @@ -4,120 +4,83 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NEChatKit +import NECoreIM2Kit import NIMSDK @objcMembers open class MessageUtils: NSObject { - open class func textMessage(text: String) -> NIMMessage { - let message = NIMMessage() - message.setting = messageSetting() - message.text = text - return message - } - - open class func textMessage(text: String, remoteExt: [String: Any]?) -> NIMMessage { - let message = NIMMessage() - message.setting = messageSetting() - message.text = text - if remoteExt?.count ?? 0 > 0 { - message.remoteExt = remoteExt + open class func textMessage(text: String, remoteExt: [String: Any]?) -> V2NIMMessage { + let message = V2NIMMessageCreator.createTextMessage(text) + if let remoteExt = remoteExt { + message.serverExtension = getJSONStringFromDictionary(remoteExt) } return message } - open class func imageMessage(image: UIImage) -> NIMMessage { - imageMessage(imageObject: NIMImageObject(image: image)) + open class func textMessage(text: String) -> V2NIMMessage { + V2NIMMessageCreator.createTextMessage(text) } - open class func imageMessage(path: String) -> NIMMessage { - imageMessage(imageObject: NIMImageObject(filepath: path)) + open class func forwardMessage(message: V2NIMMessage) -> V2NIMMessage { + V2NIMMessageCreator.createForwardMessage(message) } - open class func imageMessage(data: Data, ext: String) -> NIMMessage { - imageMessage(imageObject: NIMImageObject(data: data, extension: ext)) + open class func imageMessage(path: String, + name: String?, + sceneName: String?, + width: Int32, + height: Int32) -> V2NIMMessage { + V2NIMMessageCreator.createImageMessage(path, + name: name, + sceneName: sceneName ?? V2NIMStorageSceneConfig.default_IM().sceneName, + width: width, + height: height) } - open class func imageMessage(imageObject: NIMImageObject) -> NIMMessage { - let message = NIMMessage() - let option = NIMImageOption() - option.compressQuality = 0.8 - imageObject.option = option - message.messageObject = imageObject - message.apnsContent = chatLocalizable("send_picture") - message.setting = messageSetting() - return message + open class func audioMessage(filePath: String, + name: String?, + sceneName: String?, + duration: Int32) -> V2NIMMessage { + V2NIMMessageCreator.createAudioMessage(filePath, name: name, + sceneName: sceneName ?? V2NIMStorageSceneConfig.default_IM().sceneName, + duration: duration) } - open class func audioMessage(filePath: String) -> NIMMessage { - let messageObject = NIMAudioObject(sourcePath: filePath) - let message = NIMMessage() - message.messageObject = messageObject - message.apnsContent = chatLocalizable("send_voice") - message.setting = messageSetting() - return message + open class func videoMessage(filePath: String, + name: String?, + sceneName: String?, + width: Int32, + height: Int32, + duration: Int32) -> V2NIMMessage { + V2NIMMessageCreator.createVideoMessage(filePath, + name: name, + sceneName: sceneName ?? V2NIMStorageSceneConfig.default_IM().sceneName, + duration: duration, + width: width, + height: height) } - open class func videoMessage(filePath: String) -> NIMMessage { - let messageObject = NIMVideoObject(sourcePath: filePath) - let message = NIMMessage() - message.messageObject = messageObject - message.apnsContent = chatLocalizable("send_video") - message.setting = messageSetting() - return message + open class func locationMessage(lat: Double, + lng: Double, + address: String) -> V2NIMMessage { + V2NIMMessageCreator.createLocationMessage(lat, longitude: lng, address: address) } - open class func locationMessage(_ lat: Double, _ lng: Double, _ title: String, _ address: String) -> NIMMessage { - let messageObject = NIMLocationObject(latitude: lat, longitude: lng, title: address) - let message = NIMMessage() - message.messageObject = messageObject - message.text = title - message.apnsContent = chatLocalizable("send_location") - message.setting = messageSetting() - return message + open class func fileMessage(filePath: String, + displayName: String?, + sceneName: String?) -> V2NIMMessage { + V2NIMMessageCreator.createFileMessage(filePath, + name: displayName, + sceneName: sceneName ?? V2NIMStorageSceneConfig.default_IM().sceneName) } - open class func fileMessage(filePath: String, displayName: String?) -> NIMMessage { - let messageObject = NIMFileObject(sourcePath: filePath) - if let dpName = displayName { - messageObject.displayName = dpName - } - let message = NIMMessage() - message.messageObject = messageObject - message.apnsContent = chatLocalizable("send_file") - message.setting = messageSetting() - return message - } - - open class func fileMessage(data: Data, displayName: String?) -> NIMMessage { - let dpName = displayName ?? "" - let pointIndex = dpName.lastIndex(of: ".") ?? dpName.startIndex - let suffix = dpName[dpName.index(after: pointIndex) ..< dpName.endIndex] - let messageObject = NIMFileObject(data: data, extension: String(suffix)) - messageObject.displayName = dpName - let message = NIMMessage() - message.messageObject = messageObject - message.apnsContent = chatLocalizable("send_file") - message.setting = messageSetting() - return message - } - - open class func customMessage(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 customMessage(text: String, + rawAttachment: String) -> V2NIMMessage { + V2NIMMessageCreator.createCustomMessage(text, rawAttachment: rawAttachment) } - open class func messageSetting() -> NIMMessageSetting { - let setting = NIMMessageSetting() - setting.teamReceiptEnabled = SettingProvider.shared.getMessageRead() - return setting + open class func tipMessage(text: String) -> V2NIMMessage { + V2NIMMessageCreator.createTipsMessage(text) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift index 2f8e97d8..5b3404c7 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift @@ -5,7 +5,7 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK @@ -15,179 +15,127 @@ public enum TeamType { } open class NotificationMessageUtils: NSObject { - open class func textForNotification(message: NIMMessage) -> String { - if message.messageType != .notification { + open class func textForNotification(message: V2NIMMessage) -> String { + if message.messageType != .MESSAGE_TYPE_NOTIFICATION { return "" } - if let object = message.messageObject as? NIMNotificationObject { - switch object.notificationType { - case .team: - return textForTeamNotificationMessage(message: message) - case .superTeam: - return "" - case .netCall: - return "" - case .chatroom: - return "" - default: - return "" - } + if message.attachment is V2NIMMessageNotificationAttachment { + let text = textForTeamNotificationMessage(message: message) + return text + } else { + return fromName(message: message) + (message.text ?? "") } - return "" } /// 是否是群通知 - open class func isDiscussSeniorTeamNoti(message: NIMMessage) -> Bool { - if let object = message.messageObject as? NIMNotificationObject, - let _ = object.content as? NIMTeamNotificationContent { + open class func isDiscussSeniorTeamNoti(message: V2NIMMessage) -> Bool { + if message.attachment is V2NIMMessageNotificationAttachment { return true } return false } - open class func isDiscussSeniorTeamUpdateCustomNoti(message: NIMMessage) -> Bool { - if let object = message.messageObject as? NIMNotificationObject { - guard let content = object.content as? NIMTeamNotificationContent else { - return false - } - - // 转移讨论组的通知 - if content.operationType == .transferOwner, - teamType(message: message) == .discussTeam { - return true - } - - if content.operationType != .update { - return false - } - guard let attach = content.attachment as? NIMUpdateTeamInfoAttachment, - let tag = attach.values?.keys.first?.intValue else { - return false - } - - // 18:客户端自定义拓展字段, 19: 服务器自定义拓展字段 - if tag == 18 || tag == 19 { - return true - } - } - return false - } - - open class func isTeamLeaveOrDismiss(message: NIMMessage) -> (isLeave: Bool, isDismiss: Bool) { + open class func isTeamLeaveOrDismiss(message: V2NIMMessage) -> (isLeave: Bool, isDismiss: Bool) { var leave = false var dismiss = false - if let object = message.messageObject as? NIMNotificationObject, object.notificationType == .team { - if let content = object.content as? NIMTeamNotificationContent { - switch content.operationType { - case .leave: - leave = true - - case .dismiss: - dismiss = true - - @unknown default: - break - } + if let content = message.attachment as? V2NIMMessageNotificationAttachment { + switch content.type { + case .MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE: + leave = true + case .MESSAGE_NOTIFICATION_TYPE_TEAM_DISMISS: + dismiss = true + default: + break } } return (leave, dismiss) } - open class func textForTeamNotificationMessage(message: NIMMessage) -> String { + open class func textForTeamNotificationMessage(message: V2NIMMessage) -> String { var text = chatLocalizable("unknown_system_message") - if let object = message.messageObject as? NIMNotificationObject { - if let content = object.content as? NIMTeamNotificationContent { - let fromName = fromName(message: message) - let toNames = toName(message: message) - let toFirstName = toNames.first ?? "" - let teamName = teamName(message: message) - var toNamestext = toNames.first ?? "" - if toNames.count > 1 { - toNamestext = toNames.joined(separator: "、") - } - switch content.operationType { - case .invite: - text = fromName + chatLocalizable("invite") + toNamestext + chatLocalizable("enter") + chatLocalizable("group_chat") - case .dismiss: - text = fromName + chatLocalizable("dissolve") + chatLocalizable("group_chat") - case .kick: - text = fromName + chatLocalizable("kick") + toNamestext + chatLocalizable("out") + chatLocalizable("group_chat") - case .update: - text = textOfUpdateTeam( - fromName: fromName, - teamName: teamName, - content: content - ) - case .leave: - text = fromName + chatLocalizable("leave") + chatLocalizable("group_chat") - case .applyPass: - if fromName == toNamestext { - text = fromName + chatLocalizable("join") + chatLocalizable("group_chat") - } else { - text = fromName + chatLocalizable("pass") + toNamestext - } - - case .transferOwner: - text = fromName + chatLocalizable("transfer") + toFirstName - case .addManager: - text = toNamestext + chatLocalizable("added_manager") - case .removeManager: - text = toFirstName + chatLocalizable("removed_manager") - case .acceptInvitation: - text = fromName + chatLocalizable("accept") + toNamestext - case .mute: - var mute = false - if let atta = content.attachment as? NIMMuteTeamMemberAttachment { - mute = atta.flag - } - if let atta = content.attachment as? NIMMuteSuperTeamMemberAttachment { - mute = atta.flag - } - // text = mute ? chatLocalizable("team_all_mute") : chatLocalizable("team_all_no_mute") - text = "\(toNamestext) \(mute ? chatLocalizable("mute") : chatLocalizable("not_mute"))" - - default: - text = chatLocalizable("unknown_system_message") + if let content = message.attachment as? V2NIMMessageNotificationAttachment { + let fromName = fromName(message: message) + let toNames = toName(message: message) + let toFirstName = toNames.first ?? "" + let teamName = teamName(message: message) + var toNamestext = toNames.first ?? "" + if toNames.count > 1 { + toNamestext = toNames.joined(separator: "、") + } + switch content.type { + case .MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE: + text = fromName + chatLocalizable("invite") + toNamestext + chatLocalizable("enter") + chatLocalizable("group_chat") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_DISMISS: + text = fromName + chatLocalizable("dissolve") + chatLocalizable("group_chat") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_KICK: + text = fromName + chatLocalizable("kick") + toNamestext + chatLocalizable("out") + chatLocalizable("group_chat") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_UPDATE_TINFO: + text = "update team info" + text = textOfUpdateTeam( + fromName: fromName, + teamName: teamName, + content: content + ) + case .MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE: + text = fromName + chatLocalizable("leave") + chatLocalizable("group_chat") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_APPLY_PASS: + if fromName == toNamestext { + text = fromName + chatLocalizable("join") + chatLocalizable("group_chat") + } else { + text = fromName + chatLocalizable("pass") + toNamestext } + + case .MESSAGE_NOTIFICATION_TYPE_TEAM_OWNER_TRANSFER: + text = fromName + chatLocalizable("transfer") + toFirstName + case .MESSAGE_NOTIFICATION_TYPE_TEAM_ADD_MANAGER: + text = toNamestext + chatLocalizable("added_manager") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_REMOVE_MANAGER: + text = toFirstName + chatLocalizable("removed_manager") + case .MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE_ACCEPT: + text = fromName + chatLocalizable("accept") + toNamestext + case .MESSAGE_NOTIFICATION_TYPE_TEAM_BANNED_TEAM_MEMBER: + text = "\(toNamestext) \(content.chatBanned ? chatLocalizable("mute") : chatLocalizable("not_mute"))" + + default: + text = chatLocalizable("unknown_system_message") } + return text + } else { + return text } - return text } - open class func fromName(message: NIMMessage) -> String { - if let object = message.messageObject as? NIMNotificationObject { - if let content = object.content as? NIMTeamNotificationContent { - if content.sourceID == NIMSDK.shared().loginManager.currentAccount() { - return chatLocalizable("You") + " " - } else { - if let sourceId = content.sourceID { - return ChatUserCache.getShowName(userId: sourceId, teamId: message.session?.sessionId) - } - } + open class func fromName(message: V2NIMMessage) -> String { + if let sourceId = message.senderId { + if sourceId == IMKitClient.instance.account() { + return chatLocalizable("You") + " " + } else { + return ChatTeamCache.shared.getShowName(sourceId) } + } else { + return "" } - return "" } - open class func toName(message: NIMMessage) -> [String] { + open class func toName(message: V2NIMMessage) -> [String] { var toNames = [String]() - guard let object = message.messageObject as? NIMNotificationObject, - let content = object.content as? NIMTeamNotificationContent, - let targetIDs = content.targetIDs else { + guard let content = message.attachment as? V2NIMMessageNotificationAttachment, + let targetIDs = content.targetIds else { return toNames } + for targetID in targetIDs { - if targetID == NIMSDK.shared().loginManager.currentAccount() { + if targetID == IMKitClient.instance.account() { toNames.append(chatLocalizable("You") + " ") } else { - toNames - .append(ChatUserCache.getShowName(userId: targetID, teamId: message.session?.sessionId)) + let name = ChatTeamCache.shared.getShowName(targetID) + toNames.append(name) } } return toNames } - open class func teamName(message: NIMMessage) -> String { + open class func teamName(message: V2NIMMessage) -> String { let teamtype = teamType(message: message) switch teamtype { case .advanceTeam: @@ -197,113 +145,120 @@ open class NotificationMessageUtils: NSObject { } } - open class func teamType(message: NIMMessage) -> TeamType { - let team = TeamProvider.shared.getTeam(teamId: message.session?.sessionId ?? "") - if team?.isDisscuss() == true { - return .discussTeam - } else { - return .advanceTeam + open class func teamType(message: V2NIMMessage) -> TeamType { + if let team = ChatTeamCache.shared.getTeamInfo() { + if team.isDisscuss() == true { + return .discussTeam + } else { + return .advanceTeam + } } + return .advanceTeam } private class func textOfUpdateTeam(fromName: String, teamName: String, - content: NIMTeamNotificationContent) -> String { + content: V2NIMMessageNotificationAttachment) -> String { var text = fromName + chatLocalizable("has_updated") + teamName - if let attach = content.attachment as? NIMUpdateTeamInfoAttachment { - if let tag = attach.values { - let string = getShowString(fromName, teamName, tag) - if string.count > 0 { - text = string - } - } - } - if let attach = content.attachment as? NIMMuteTeamMemberAttachment { - if attach.flag == false { - text = teamName + chatLocalizable("team_all_mute") - } else { - text = teamName + chatLocalizable("team_all_no_mute") - } - } - return text - } - private class func getShowString(_ fromName: String, - _ teamName: String, - _ tag: [NSNumber: String]) -> String { - var text = "" + guard let updatedTeamInfo = content.updatedTeamInfo else { return text } // 群名 - if let value = tag[3] { - text = fromName + " " + chatLocalizable("has_updated") + teamName + - chatLocalizable("team_name") + chatLocalizable("to") + "\"" + value + "\"" + if let name = updatedTeamInfo.name { + return fromName + " " + chatLocalizable("has_updated") + teamName + + chatLocalizable("team_name") + chatLocalizable("to") + "\"" + name + "\"" } // 群简介 - if let _ = tag[14] { - text = fromName + " " + chatLocalizable("has_updated") + teamName + + if updatedTeamInfo.intro != nil { + return fromName + " " + chatLocalizable("has_updated") + teamName + chatLocalizable("team_intro") } // 群公告 - if let _ = tag[15] { - text = fromName + " " + chatLocalizable("has_updated") + teamName + + if updatedTeamInfo.announcement != nil { + return fromName + " " + chatLocalizable("has_updated") + teamName + chatLocalizable("team_anouncement") } - // 群验证方式 - if let _ = tag[16] { - text = fromName + " " + chatLocalizable("has_updated") + teamName + - chatLocalizable("team_join_mode") - } - - // 客户端自定义拓展字段 - if let _ = tag[18] { - text = fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_custom_info") - } - - // 服务器自定义拓展字段(SDK 无法直接修改这个字段, 请调用服务器接口) - if let _ = tag[19] { - text = fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_custom_info") - } - // 头像 - if let _ = tag[20] { - text = fromName + " " + chatLocalizable("has_updated") + teamName + + if updatedTeamInfo.avatar != nil { + return fromName + " " + chatLocalizable("has_updated") + teamName + chatLocalizable("team_avatar") } + // 群验证方式 + if updatedTeamInfo.joinMode.rawValue != -1 { + return fromName + " " + chatLocalizable("has_updated") + teamName + + chatLocalizable("team_join_mode") + } + // 被邀请模式 - if let _ = tag[21] { - text = fromName + " " + chatLocalizable("has_updated") + + if updatedTeamInfo.agreeMode.rawValue != -1 { + return fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_be_invited_author") } // 邀请权限,仅高级群有效 - if let value = tag[22] { - text = fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_permission") + " \"" + - chatLocalizable("team_be_invited_permission") + "\" " + chatLocalizable("to") + "\"" + (value == "0" ? chatLocalizable("only_team_owner") : chatLocalizable("user_select_all")) + "\"" + if updatedTeamInfo.inviteMode.rawValue != -1 { + return fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_permission") + " \"" + + chatLocalizable("team_be_invited_permission") + "\" " + chatLocalizable("to") + "\"" + (updatedTeamInfo.inviteMode == .TEAM_INVITE_MODE_MANAGER ? chatLocalizable("only_team_owner") : chatLocalizable("user_select_all")) + "\"" } // 更新群信息权限,仅高级群有效 - if let value = tag[23] { - text = fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_permission") + " \"" + - chatLocalizable("team_update_info_permission") + "\" " + chatLocalizable("to") + "\"" + (value == "0" ? chatLocalizable("only_team_owner") : chatLocalizable("user_select_all")) + "\"" + if updatedTeamInfo.updateInfoMode.rawValue != -1 { + return fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_permission") + " \"" + + chatLocalizable("team_update_info_permission") + "\" " + chatLocalizable("to") + "\"" + (updatedTeamInfo.updateInfoMode == .TEAM_UPDATE_INFO_MODE_MANAGER ? chatLocalizable("only_team_owner") : chatLocalizable("user_select_all")) + "\"" } - // 更新群客户端自定义拓展字段权限 - if let _ = tag[24] { - text = fromName + " " + chatLocalizable("has_updated") + - chatLocalizable("team_update_client_custom") + // 群整体禁言 + if updatedTeamInfo.chatBannedMode.rawValue != -1 { + return updatedTeamInfo.chatBannedMode == .TEAM_CHAT_BANNED_MODE_BANNED_NORMAL ? chatLocalizable("team_all_mute") : chatLocalizable("team_all_no_mute") } - // 群禁言模式 - if let value = tag[100] { - if value == "1" || value == "3" { - text = chatLocalizable("team_all_mute") - } else if value == "0" { - text = chatLocalizable("team_all_no_mute") + // 客户端自定义拓展字段 + if let serverExt = updatedTeamInfo.serverExtension, let extDic = getDictionaryFromJSONString(serverExt) { + var lastOpt = keyAllowAtAll + + // 上一次操作的字段 + if let opt = extDic["lastOpt"] as? String { + lastOpt = opt + } + + // @所有人权限 + if lastOpt == keyAllowAtAll, let allowAt = extDic[keyAllowAtAll] as? String { + if allowAt == allowAtManagerValue { + return chatLocalizable("team_at_permission") + chatLocalizable("only_team_owner") + } + if allowAt == allowAtAllValue { + return chatLocalizable("team_at_permission") + chatLocalizable("everyone") + } } + + // 置顶消息权限 + if lastOpt == keyAllowTopMessage, let allowAt = extDic[keyAllowTopMessage] as? String { + if allowAt == allowAtManagerValue { + return chatLocalizable("team_top_permission") + chatLocalizable("only_team_owner") + } + if allowAt == allowAtAllValue { + return chatLocalizable("team_top_permission") + chatLocalizable("everyone") + } + } + + // 置顶消息 + if lastOpt == keyTopMessage, let topInfo = extDic[keyTopMessage] as? [String: Any] { + if let type = topInfo["operation"] as? Int { + return fromName + " " + (type == 0 ? chatLocalizable("top_message") : chatLocalizable("untop_message")) + } + } + } else { + return fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_custom_info") + } + + // 更新群客户端自定义拓展字段权限 + if updatedTeamInfo.updateExtensionMode.rawValue != -1 { + return fromName + " " + chatLocalizable("has_updated") + + chatLocalizable("team_update_client_custom") } return text diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift index 83e54bf7..88e26b1f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift @@ -13,7 +13,7 @@ open class ReplyMessageUtil: NSObject { } if model.type == .reply { - if let content = NECustomAttachment.contentOfRichText(message: model.message) { + if let content = NECustomAttachment.contentOfRichText(model.message?.attachment) { return text + content } text += "\(model.message?.text ?? chatLocalizable("message_not_found"))" diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/CollectionFileModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/CollectionFileModel.swift new file mode 100644 index 00000000..b574154d --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/CollectionFileModel.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 UIKit + +@objcMembers +open +class CollectionFileModel: NSObject { + public var progress: UInt = 0 + public var size: Float = 0 + + public var state = DownloadState.Success + public weak var cell: NEBaseCollectionMessageFileCell? +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/CollectionMessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/CollectionMessageModel.swift new file mode 100644 index 00000000..d8a427ed --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/CollectionMessageModel.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 NIMSDK +import UIKit + +@objcMembers +open class CollectionMessageModel: NSObject { + /// 消息对象 + var message: V2NIMMessage? { + didSet { + if let m = message { + chatmodel = ChatMessageHelper.modelFromMessage(message: m) + + if chatmodel.type == .file { + fileModel = CollectionFileModel() + if let file = chatmodel as? MessageFileModel { + fileModel?.size = file.size + } + } + } + } + } + + /// 会话名称 + var conversationName: String? + /// 发送者昵称 + var senderName: String? + /// 用户头像 + var avatar: String? + /// 数据对象 + var chatmodel: MessageModel = MessageTextModel(message: nil) + + var fileModel: CollectionFileModel? + + var collection: V2NIMCollection? + + open func cellHeight(contenttMaxW: CGFloat) -> CGFloat { + var height = chatmodel.contentSize.height + let titleFont: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize, weight: .semibold) + let bodyFont: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) + let maxSize = CGSize(width: contenttMaxW, height: CGFloat.greatestFiniteMagnitude) + + if let textModel = chatmodel as? MessageTextModel { + // 文本消息最多显示 3 行 + let textSize = NSAttributedString.getRealSize(textModel.attributeStr, bodyFont, maxSize, 3) + height = textSize.height + } + + if let textModel = chatmodel as? MessageRichTextModel { + // 换行消息中的标题最多显示 1 行 + let titleSize = NSAttributedString.getRealSize(textModel.titleAttributeStr, titleFont, maxSize, 1) + height = titleSize.height + + // 换行消息中的内容最多显示 2 行 + let textSize = NSAttributedString.getRealSize(textModel.attributeStr, bodyFont, maxSize, 2) + height += textSize.height + } + + height += 124 + + if chatmodel.replyedModel?.isReplay == true { + height += 12 + } + + return height + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift index 78b8ddeb..bc70136f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAudioModel.swift @@ -11,19 +11,19 @@ open class MessageAudioModel: MessageContentModel { public var duration: Int = 0 public var isPlaying = false - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .audio var audioW = 96.0 let audioTotalWidth = kScreenWidth <= 325 ? 230 : 265.0 // contentSize - if let obj = message?.messageObject as? NIMAudioObject { - duration = obj.duration / 1000 + if let obj = message?.attachment as? V2NIMMessageAudioAttachment { + duration = Int(obj.duration / 1000) if duration > 2 { audioW = min(Double(duration) * 8 + audioW, audioTotalWidth) } } contentSize = CGSize(width: audioW, height: chat_min_h) - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift index 56acfafe..e35783d4 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift @@ -11,16 +11,20 @@ import UIKit open class MessageCallRecordModel: MessageContentModel { public var attributeStr: NSMutableAttributedString? - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .rtcCallRecord var isAuiodRecord = false - if let object = message?.messageObject as? NIMRtcCallRecordObject, let isSend = message?.isOutgoingMsg { + + if let attach = message?.attachment as? V2NIMMessageCallAttachment { attributeStr = NSMutableAttributedString() + let callType = attach.type + let callStatus = attach.status var image: UIImage? var bound = CGRect.zero let offset: CGFloat = -1 - if object.callType == .audio { + + if callType == 1 { isAuiodRecord = true image = coreLoader.loadImage("audio_record") bound = CGRect(x: 0, y: offset - 5, width: 24, height: 24) @@ -28,20 +32,26 @@ open class MessageCallRecordModel: MessageContentModel { image = coreLoader.loadImage("video_record") bound = CGRect(x: 0, y: offset, width: 24, height: 14) } - switch object.callStatus { - case .complete: - var timeString = "00:00" - if let duration = object.durations[NIMSDK.shared().loginManager.currentAccount()] { - timeString = Date.getFormatPlayTime(duration.doubleValue) + + switch callStatus { + case 1: + var duration: TimeInterval = 0 + for durationModel in attach.durations { + if durationModel.accountId == message?.senderId { + duration = TimeInterval(durationModel.duration) + break + } } + + let timeString = Date.getFormatPlayTime(duration) attributeStr?.append(NSAttributedString(string: chatLocalizable("call_complete") + " \(timeString)")) - case .canceled: + case 2: attributeStr?.append(NSAttributedString(string: chatLocalizable("call_canceled"))) - case .rejected: + case 3: attributeStr?.append(NSAttributedString(string: chatLocalizable("call_rejected"))) - case .timeout: + case 4: attributeStr?.append(NSAttributedString(string: chatLocalizable("call_timeout"))) - case .busy: + case 5: attributeStr?.append(NSAttributedString(string: chatLocalizable("call_busy"))) default: break @@ -49,7 +59,7 @@ open class MessageCallRecordModel: MessageContentModel { let attachment = NSTextAttachment() attachment.image = image attachment.bounds = bound - if isSend { + if message?.isSelf == true { attributeStr?.append(NSAttributedString(string: " ")) attributeStr?.append(NSAttributedString(attachment: attachment)) } else { @@ -57,17 +67,15 @@ open class MessageCallRecordModel: MessageContentModel { attributeStr?.insert(NSAttributedString(attachment: attachment), at: 0) } - attributeStr?.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize), range: NSMakeRange(0, attributeStr?.length ?? 0)) + attributeStr?.addAttribute(NSAttributedString.Key.font, value: messageTextFont, range: NSMakeRange(0, attributeStr?.length ?? 0)) attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: NEKitChatConfig.shared.ui.messageProperties.messageTextColor, range: NSMakeRange(0, attributeStr?.length ?? 0)) } - let textSize = attributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize), CGSize(width: chat_content_maxW, height: CGFloat.greatestFiniteMagnitude)) ?? .zero - + let textSize = NSAttributedString.getRealSize(attributeStr, messageTextFont, messageMaxSize) var h = chat_min_h h = textSize.height + (isAuiodRecord ? 20 : 24) contentSize = CGSize(width: textSize.width + chat_cell_margin * 2, height: h) - - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift index 5616d902..ba3a6336 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift @@ -5,18 +5,22 @@ import CoreAudio import Foundation -import NECoreIMKit +import NECoreIM2Kit import NIMSDK -import simd @objcMembers open class MessageContentModel: NSObject, MessageModel { + public var type: MessageType = .custom // 消息类型(文本、图片、自定义消息...) + public var customType: Int = 0 // 自定义消息的子类型(合并转发、换行消息...) + public var message: V2NIMMessage? + public var offset: CGFloat = 0 + public var contentSize = CGSize(width: 32.0, height: chat_min_h) + public var height: CGFloat = 48 open func cellHeight() -> CGFloat { CGFloat(height) + offset } - public var isReplay: Bool = false public var showSelect: Bool = false // 多选按钮是否展示 public var isSelected: Bool = false // 多选是否选中 public var inMultiForward: Bool = false { // 是否是合并消息中的子消息 @@ -31,20 +35,16 @@ open class MessageContentModel: NSObject, MessageModel { } } - public var pinAccount: String? - public var pinShowName: String? - public var type: MessageType = .custom - public var message: NIMMessage? - public var contentSize = CGSize(width: 32.0, height: chat_min_h) - public var height: CGFloat = 48 + public var avatar: String? public var shortName: String? // 昵称 > uid public var fullName: String? // 备注 >(群昵称)> 昵称 > uid - public var avatar: String? - public var replyText: String? public var fullNameHeight: CGFloat = 0 - public var isRevokedText: Bool = false - public var timeOut = false + public var readCount: Int = 0 + public var unreadCount: Int = 0 + + public var isReplay: Bool = false + public var replyText: String? public var replyedModel: MessageModel? { didSet { if let reply = replyedModel as? MessageContentModel, reply.isReplay == true { @@ -55,11 +55,13 @@ open class MessageContentModel: NSObject, MessageModel { } } + public var isReedit: Bool = false + public var timeOut = false public var isRevoked: Bool = false { didSet { if isRevoked { type = .revoke - if let time = message?.timestamp { + if let time = message?.createTime { let date = Date() let currentTime = date.timeIntervalSince1970 if currentTime - time > 60 * 2 { @@ -67,7 +69,7 @@ open class MessageContentModel: NSObject, MessageModel { } } // 只有文本消息,才计算可编辑按钮的宽度 - if let isSend = message?.isOutgoingMsg, isSend, message?.messageType == .text, timeOut == false { + if let isSend = message?.isSelf, isSend, message?.messageType == .MESSAGE_TYPE_TEXT, timeOut == false { contentSize = CGSize(width: 218, height: chat_min_h) } else { contentSize = CGSize(width: 130, height: chat_min_h) @@ -91,15 +93,9 @@ open class MessageContentModel: NSObject, MessageModel { } } - public var isPined: Bool = false { - didSet { - if isPined { - height += chat_pin_height - } else if oldValue { - height -= chat_pin_height - } - } - } + public var pinAccount: String? + public var pinShowName: String? + public var isPined: Bool = false // 是否显示时间 public var timeContent: String? { @@ -110,12 +106,15 @@ open class MessageContentModel: NSObject, MessageModel { } } - public required init(message: NIMMessage?) { + public let messageTextFont = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + public let messageMaxSize = CGSize(width: chat_content_maxW, height: CGFloat.greatestFiniteMagnitude) + + public required init(message: V2NIMMessage?) { self.message = message - if message?.session?.sessionType == .team, - !IMKitClient.instance.isMySelf(message?.from) { + if message?.conversationType == .CONVERSATION_TYPE_TEAM, + !IMKitClient.instance.isMe(message?.senderId) { fullNameHeight = NEKitChatConfig.shared.ui.messageProperties.showTeamMessageNick ? 20 : 0 } - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift index fcecc360..309df740 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift @@ -2,17 +2,26 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NECoreIM2Kit import NIMSDK import UIKit @objc open class MessageCustomModel: MessageContentModel { - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .custom - if let attachment = NECustomAttachment.attachmentOfCustomMessage(message: message) { - contentSize = CGSize(width: 0, height: Int(attachment.cellHeight)) - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + } + + public init(message: V2NIMMessage?, contentHeight: Int) { + super.init(message: message) + type = .custom + + if let type = NECustomAttachment.typeOfCustomMessage(message?.attachment) { + customType = type } + + contentSize = CGSize(width: 0, height: contentHeight) + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift index 4a64f250..cd10df0b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift @@ -7,27 +7,21 @@ import NIMSDK import UIKit @objcMembers -open class MessageFileModel: MessageContentModel { +open class MessageFileModel: MessageVideoModel { public var displayName: String? public var path: String? - public var url: String? public var fileLength: Int64? - - public var progress: Float = 0 public var size: Float = 0 - public var state = DownloadState.Success - public weak var cell: NEChatBaseCell? - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .file - if let fileObject = message?.messageObject as? NIMFileObject { - displayName = fileObject.displayName + if let fileObject = message?.attachment as? V2NIMMessageFileAttachment { + displayName = fileObject.name path = fileObject.path - url = fileObject.url - fileLength = fileObject.fileLength + fileLength = Int64(fileObject.size) } contentSize = chat_file_size - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift index ccb7bd0f..8557abe3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageImageModel.swift @@ -4,29 +4,33 @@ // found in the LICENSE file. import Foundation +import NEChatKit import NIMSDK @objcMembers open class MessageImageModel: MessageContentModel { - public var imageUrl: String? + public var urlString: String? - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .image - if let imageObject = message?.messageObject as? NIMImageObject { + if let imageObject = message?.attachment as? V2NIMMessageImageAttachment { if let path = imageObject.path, FileManager.default.fileExists(atPath: path) { - imageUrl = path - } else { - imageUrl = imageObject.url + urlString = path + } else if let url = imageObject.url { + if imageObject.ext?.lowercased() != ".gif" { + urlString = ResourceRepo.shared.imageThumbnailURL(url) + } + urlString = url } contentSize = ChatMessageHelper.getSizeWithMaxSize( chat_pic_size, - size: imageObject.size, + size: CGSize(width: Int(imageObject.width), height: Int(imageObject.height)), miniWH: chat_min_h ) } else { contentSize = chat_pic_size } - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift index d193d92e..556386fd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageLocationModel.swift @@ -12,16 +12,16 @@ open class MessageLocationModel: MessageContentModel { public var title: String? public var subTitle: String? - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .location - if let locationObject = message?.messageObject as? NIMLocationObject { + if let locationObject = message?.attachment as? V2NIMMessageLocationAttachment { lat = locationObject.latitude lng = locationObject.longitude - subTitle = locationObject.title + subTitle = locationObject.address title = message?.text contentSize = CGSize(width: 242, height: 140) } - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift index db8fb8a9..77eb54ba 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageModel.swift @@ -32,35 +32,46 @@ public enum MessageType: Int { @objc public protocol MessageModel: NSObjectProtocol { - var message: NIMMessage? { get set } - // 气泡区域的大小 不包含气泡上下到cell上下的边距 - var contentSize: CGSize { get set } + var message: V2NIMMessage? { get set } + var type: MessageType { get set } + var customType: Int { get set } + + // 宽高 + var contentSize: CGSize { get set } // 气泡区域的大小 不包含气泡上下到cell上下的边距 + var offset: CGFloat { get set } var height: CGFloat { get set } -// 名字后2位 - var shortName: String? { get set } -// 名字全长 - var fullName: String? { get set } + func cellHeight() -> CGFloat + + // 名称头像 + var shortName: String? { get set } // 名字后2位 + var fullName: String? { get set } // 名字全长 var avatar: String? { get set } - var type: MessageType { get set } - var isRevoked: Bool { get set } + + // 标记 var isPined: Bool { get set } -// userID var pinAccount: String? { get set } var pinShowName: String? { get set } -// 被回复的消息 - var replyedModel: MessageModel? { get set } - var replyText: String? { get set } - var isRevokedText: Bool { get set } + + // 回复 var isReplay: Bool { get set } + var replyedModel: MessageModel? { get set } // 被回复的消息 + var replyText: String? { get set } + + // 撤回 + var isRevoked: Bool { get set } // 消息是否已撤回 + var isReedit: Bool { get set } // 撤回消息是否可以重新编辑 + + // 已读未读 + var readCount: Int { get set } + var unreadCount: Int { get set } + + // 多选 var showSelect: Bool { get set } // 多选按钮是否展示 var isSelected: Bool { get set } // 多选是否选中 + var inMultiForward: Bool { get set } // 是否是合并消息中的子消息 var timeContent: String? { get set } // 具体时间 - init(message: NIMMessage?) - - var offset: CGFloat { get set } - - func cellHeight() -> CGFloat + init(message: V2NIMMessage?) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift index ce115450..1c614376 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageRichTextModel.swift @@ -12,8 +12,8 @@ open class MessageRichTextModel: MessageTextModel { public var titleAttributeStr: NSMutableAttributedString? public var titleTextHeight: CGFloat = 0 - public required init(message: NIMMessage?) { - guard let data = NECustomAttachment.dataOfCustomMessage(message: message), + public required init(message: V2NIMMessage?) { + guard let data = NECustomAttachment.dataOfCustomMessage(message?.attachment), let title = data["title"] as? String else { super.init(message: message) return @@ -23,6 +23,7 @@ open class MessageRichTextModel: MessageTextModel { message?.text = body super.init(message: message) type = .custom + customType = customRichTextType let font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) titleAttributeStr = NEEmotionTool.getAttWithStr( @@ -30,12 +31,11 @@ open class MessageRichTextModel: MessageTextModel { font: font ) - let textSize = titleAttributeStr?.finalSize(font, CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) ?? .zero - + let textSize = NSAttributedString.getRealSize(titleAttributeStr, messageTextFont, messageMaxSize) titleTextHeight = textSize.height - contentSize = CGSize(width: max(contentSize.width, textSize.width + chat_content_margin * 2), + contentSize = CGSize(width: max(textWidght, textSize.width) + chat_content_margin * 2, height: contentSize.height + titleTextHeight + (body.isEmpty ? 0 : chat_content_margin)) - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift index 0f92533a..0c29ab3c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift @@ -11,24 +11,64 @@ import NIMSDK open class MessageTextModel: MessageContentModel { public var attributeStr: NSMutableAttributedString? public var textHeight: CGFloat = 0 + public var textWidght: CGFloat = 0 - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) type = .text - + let text = message?.text ?? "" attributeStr = NEEmotionTool.getAttWithStr( - str: message?.text ?? "", - font: UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + str: text, + font: messageTextFont ) - if let remoteExt = message?.remoteExt, let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { - dic.forEach { (key: String, value: AnyObject) in + // 兼容老的表情消息,如果前面有表情而位置计算异常则回退回老的解析 + var notFound = false + + // 计算表情(根据转码后的index) + if let remoteExt = getDictionaryFromJSONString(message?.serverExtension ?? ""), let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { + for (_, value) in dic { if let contentDic = value as? [String: AnyObject] { if let array = contentDic[atSegmentsKey] as? [AnyObject] { if let models = NSArray.yx_modelArray(with: MessageAtInfoModel.self, json: array) as? [MessageAtInfoModel] { - models.forEach { model in - if attributeStr?.length ?? 0 > model.end { - attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_blueText, range: NSMakeRange(model.start, model.end - model.start + atRangeOffset)) + for model in models { + // 前面因为表情增加的索引数量 + var count = 0 + if text.count > model.start { + let frontAttributeStr = NEEmotionTool.getAttWithStr( + str: String(text.prefix(model.start)), + font: messageTextFont + ) + count = getReduceIndexCount(frontAttributeStr) + } + let start = model.start - count + if start < 0 { + notFound = true + break + } + var end = model.end - count + + if model.end + atRangeOffset > text.count { + notFound = true + break + } + // 获取起始索引 + let startIndex = text.index(text.startIndex, offsetBy: model.start) + // 获取结束索引 + let endIndex = text.index(text.startIndex, offsetBy: model.end + atRangeOffset) + let frontAttributeStr = NEEmotionTool.getAttWithStr( + str: String(text[startIndex ..< endIndex]), + font: messageTextFont + ) + let innerCount = getReduceIndexCount(frontAttributeStr) + end = end - innerCount + if end <= start { + notFound = true + break + } + + if attributeStr?.length ?? 0 > end { + attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(start, end - start + atRangeOffset)) } } } @@ -37,10 +77,43 @@ open class MessageTextModel: MessageContentModel { } } - let textSize = attributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize), CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) ?? .zero + if notFound == true, let remoteExt = getDictionaryFromJSONString(message?.serverExtension ?? ""), let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { + for (_, value) in dic { + if let contentDic = value as? [String: AnyObject] { + if let array = contentDic[atSegmentsKey] as? [AnyObject] { + if let models = NSArray.yx_modelArray(with: MessageAtInfoModel.self, json: array) as? [MessageAtInfoModel] { + for model in models { + if attributeStr?.length ?? 0 > model.end { + attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_normalTheme, range: NSMakeRange(model.start, model.end - model.start + atRangeOffset)) + } + } + } + } + } + } + } + let textSize = NSAttributedString.getRealSize(attributeStr, messageTextFont, messageMaxSize) textHeight = textSize.height + textWidght = textSize.width contentSize = CGSize(width: textSize.width + chat_content_margin * 2, height: textHeight + chat_content_margin * 2) - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height + } + + /// 计算减少的 + /// - Parameter attribute: at 文本前的文本 + func getReduceIndexCount(_ attribute: NSAttributedString) -> Int { + var count = 0 + attribute.enumerateAttributes( + in: NSMakeRange(0, attribute.length), + options: NSAttributedString.EnumerationOptions(rawValue: 0) + ) { dics, range, stop in + if let neAttachment = dics[NSAttributedString.Key.attachment] as? NEEmotionAttachment { + if let tagCount = neAttachment.emotion?.tag?.count { + count = count + tagCount - 1 + } + } + } + return count } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift index b2e1c071..4e6b578f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift @@ -10,29 +10,23 @@ import NIMSDK open class MessageTipsModel: MessageContentModel { var text: String? - public required init(message: NIMMessage?) { + public required init(message: V2NIMMessage?) { super.init(message: message) - commonInit() - } - - func setText() { + type = .tip if let msg = message { - if msg.messageType == .notification { + if msg.messageType == .MESSAGE_TYPE_NOTIFICATION { text = NotificationMessageUtils.textForNotification(message: msg) type = .notification - } else if msg.messageType == .tip { + } else if msg.messageType == .MESSAGE_TYPE_TIP { text = msg.text type = .tip } } - } - func commonInit() { - setText() let font: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.timeTextSize) - contentSize = text?.finalSize(font, CGSize(width: chat_content_maxW, height: CGFloat.greatestFiniteMagnitude)) ?? .zero - height = ceil(contentSize.height) + contentSize = String.getRealSize(text, font, messageMaxSize) + height = contentSize.height + chat_content_margin * 3 // time if let time = timeContent, !time.isEmpty { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift index 50fd4e6e..8ed5f362 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift @@ -13,24 +13,23 @@ public enum DownloadState: Int { } @objcMembers -open class MessageVideoModel: MessageContentModel { - public var imageUrl: String? +open class MessageVideoModel: MessageImageModel { public var state = DownloadState.Success - public var progress: Float = 0 + public var progress: UInt = 0 public weak var cell: NEChatBaseCell? - public required init(message: NIMMessage?) { + + public required init(message: V2NIMMessage?) { super.init(message: message) type = .video - if let videoObject = message?.messageObject as? NIMVideoObject { - imageUrl = videoObject.url + if let videoObject = message?.attachment as? V2NIMMessageVideoAttachment { contentSize = ChatMessageHelper.getSizeWithMaxSize( chat_pic_size, - size: videoObject.coverSize, + size: CGSize(width: videoObject.width, height: videoObject.height), miniWH: chat_min_h ) } else { contentSize = chat_pic_size } - height = contentSize.height + chat_content_margin * 2 + fullNameHeight + height = contentSize.height + chat_content_margin * 2 + fullNameHeight + chat_pin_height } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEPinMessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEPinMessageModel.swift new file mode 100644 index 00000000..250f6175 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NEPinMessageModel.swift @@ -0,0 +1,70 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIM2Kit +import NIMSDK +import UIKit + +@objcMembers +open class NEPinMessageModel: NSObject { + var chatmodel: MessageModel = MessageTextModel(message: nil) + var message: V2NIMMessage + var item: V2NIMMessagePin + var conversationId: String? + var repo = ChatRepo.shared + var pinFileModel: PinMessageFileModel? + + init(message: V2NIMMessage, item: V2NIMMessagePin) { + self.message = message + conversationId = item.messageRefer?.conversationId + self.item = item + super.init() + chatmodel = modelFromMessage(message: message) + if chatmodel.type == .file { + pinFileModel = PinMessageFileModel() + if let filemodel = chatmodel as? MessageFileModel { + pinFileModel?.size = filemodel.size + } + } + } + + private func modelFromMessage(message: V2NIMMessage) -> MessageModel { + let model = ChatMessageHelper.modelFromMessage(message: message) + model.fullName = message.senderId + model.shortName = NEFriendUserCache.getShortName(message.senderId ?? "") + return model + } + + open func cellHeight(pinContentMaxW: CGFloat) -> CGFloat { + var height = chatmodel.contentSize.height + let titleFont: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize, weight: .semibold) + let bodyFont: UIFont = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) + let maxSize = CGSize(width: pinContentMaxW, height: CGFloat.greatestFiniteMagnitude) + + if let textModel = chatmodel as? MessageTextModel { + // 文本消息最多显示 3 行 + let textSize = NSAttributedString.getRealSize(textModel.attributeStr, bodyFont, maxSize, 3) + height = textSize.height + } + + if let textModel = chatmodel as? MessageRichTextModel { + // 换行消息中的标题最多显示 1 行 + let titleSize = NSAttributedString.getRealSize(textModel.titleAttributeStr, titleFont, maxSize, 1) + height = titleSize.height + + // 换行消息中的内容最多显示 2 行 + let textSize = NSAttributedString.getRealSize(textModel.attributeStr, bodyFont, maxSize, 2) + height += textSize.height + } + + height += 100 + + if chatmodel.replyedModel?.isReplay == true { + height += 12 + } + + return height + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItem.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItem.swift index fad8d57b..cc965ee3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItem.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/OperationItem.swift @@ -16,6 +16,8 @@ public enum OperationType: Int { case collection case delete case recall + case top + case untop } @objcMembers @@ -24,6 +26,7 @@ open class OperationItem: NSObject { public var imageName: String = "" public var type: OperationType? + /// 复制 public static func copyItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_copy") @@ -32,6 +35,7 @@ open class OperationItem: NSObject { return item } + /// 回复 public static func replayItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_replay") @@ -40,6 +44,7 @@ open class OperationItem: NSObject { return item } + /// 转发 public static func forwardItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_forward") @@ -48,6 +53,7 @@ open class OperationItem: NSObject { return item } + /// 标记 public static func pinItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_pin") @@ -56,6 +62,7 @@ open class OperationItem: NSObject { return item } + /// 取消标记 public static func removePinItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_cancel_pin") @@ -64,6 +71,7 @@ open class OperationItem: NSObject { return item } + /// 多选 public static func selectItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_select") @@ -72,14 +80,34 @@ open class OperationItem: NSObject { return item } -// static open func collectionItem() -> OperationItem { -// OperationItem( -// text: chatLocalizable("operation_collection"), -// imageName: "op_collection", -// type: .collection -// ) -// } + /// 收藏 + public static func collectionItem() -> OperationItem { + let item = OperationItem() + item.text = chatLocalizable("operation_collection") + item.imageName = "op_collect" + item.type = .collection + return item + } + + /// 置顶 + public static func topItem() -> OperationItem { + let item = OperationItem() + item.text = chatLocalizable("operation_top") + item.imageName = "op_delete" + item.type = .top + return item + } + + /// 移除置顶 + public static func untopItem() -> OperationItem { + let item = OperationItem() + item.text = chatLocalizable("operation_untop") + item.imageName = "op_delete" + item.type = .untop + return item + } + /// 删除 public static func deleteItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_delete") @@ -88,6 +116,7 @@ open class OperationItem: NSObject { return item } + /// 撤回 public static func recallItem() -> OperationItem { let item = OperationItem() item.text = chatLocalizable("operation_recall") diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift index c8b768ae..681f51cb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageFileModel.swift @@ -7,7 +7,7 @@ import UIKit @objcMembers open class PinMessageFileModel: NSObject { - public var progress: Float = 0 + public var progress: UInt = 0 public var size: Float = 0 public var state = DownloadState.Success diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift deleted file mode 100644 index e1fe22af..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NEChatKit -import NECoreIMKit -import NIMSDK -import UIKit - -@objcMembers -open class PinMessageModel: NSObject { - var chatmodel: MessageModel = MessageTextModel(message: nil) - var message: NIMMessage - var item: NIMMessagePinItem - var session: NIMSession - var repo = ChatRepo.shared - var pinFileModel: PinMessageFileModel? - - init(message: NIMMessage, item: NIMMessagePinItem) { - self.message = message - session = item.session - self.item = item - super.init() - chatmodel = modelFromMessage(message: message) - if chatmodel.type == .file { - pinFileModel = PinMessageFileModel() - if let filemodel = chatmodel as? MessageFileModel { - pinFileModel?.size = filemodel.size - } - } - } - - private func modelFromMessage(message: NIMMessage) -> MessageModel { - let model = ChatMessageHelper.modelFromMessage(message: message) - - if let uid = message.from { - let user = ChatUserCache.getUserInfo(uid) - let fullName = ChatUserCache.getShowName(userId: uid, teamId: session.sessionId) - model.avatar = user?.userInfo?.avatarUrl - model.fullName = fullName - model.shortName = ChatUserCache.getShortName(name: user?.showName(false) ?? "", length: 2) - } - -// model.replyedModel = getReplyMessageWithoutThread(message: message) -// if let pin = repo.searchMessagePinHistory(message) { -// model.isPined = true -// model.pinAccount = pin.accountID -// let pinID = pin.accountID ?? NIMSDK.shared().loginManager.currentAccount() -// model.pinShowName = getShowName(userId: pinID, teamId: session.sessionId) -// } else { -// model.isPined = false -// } - return model - } - - open func cellHeight(pinContentMaxW: CGFloat) -> CGFloat { - var height = chatmodel.contentSize.height - if let textModel = chatmodel as? MessageTextModel { - // 文本消息最多显示 3 行 - let textSize = textModel.attributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize), CGSize(width: pinContentMaxW, height: CGFloat.greatestFiniteMagnitude), 3) ?? .zero - height = textSize.height - } - - if let textModel = chatmodel as? MessageRichTextModel { - // 换行消息中的标题最多显示 1 行 - let titleSize = textModel.titleAttributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize, weight: .semibold), CGSize(width: pinContentMaxW, height: CGFloat.greatestFiniteMagnitude), 1) ?? .zero - height = titleSize.height - - // 换行消息中的内容最多显示 2 行 - let textSize = textModel.attributeStr?.finalSize(.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize), CGSize(width: pinContentMaxW, height: CGFloat.greatestFiniteMagnitude), 2) ?? .zero - height += textSize.height - } - - height += 100 - - if chatmodel.replyedModel?.isReplay == true { - height += 12 - } - - return height - } - - open func getReplyMessageWithoutThread(message: NIMMessage) -> MessageModel? { - var replyId: String? = message.repliedMessageId - if let yxReplyMsg = message.remoteExt?[keyReplyMsgKey] as? [String: Any] { - replyId = yxReplyMsg["idClient"] as? String - } - - guard let id = replyId, !id.isEmpty else { - return nil - } - - if let m = ConversationProvider.shared.messagesInSession(session, messageIds: [id])? - .first { - let model = modelFromMessage(message: m) - model.isReplay = true - return model - } - let message = NIMMessage() - let model = modelFromMessage(message: message) - model.isReplay = true - return model - } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageAudioCell.swift new file mode 100644 index 00000000..940feb3a --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageAudioCell.swift @@ -0,0 +1,110 @@ +//// 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 NEBaseCollectionMessageAudioCell: NEBaseCollectionMessageCell { + public lazy var audioImageView: UIImageView = { + var audioImageView = UIImageView(image: UIImage.ne_imageNamed(name: "left_play_3")) + audioImageView.translatesAutoresizingMaskIntoConstraints = false + audioImageView.contentMode = .center + 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] + } + return audioImageView + }() + + var audioTimeLabel = UILabel() + public var bubbleImage = UIImageView() + + public var isPlaying = false + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// 初始化UI + override open func setupCommonUI() { + super.setupCommonUI() + + let receiveImage = NEKitChatConfig.shared.ui.messageProperties.leftBubbleBg ?? UIImage.ne_imageNamed(name: "chat_message_receive") + bubbleImage.image = receiveImage? + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) + bubbleImage.translatesAutoresizingMaskIntoConstraints = false + backView.addSubview(bubbleImage) + contentWidth = bubbleImage.widthAnchor.constraint(equalToConstant: chat_content_maxW) + contentHeight = bubbleImage.heightAnchor.constraint(equalToConstant: chat_content_maxW) + bubbleImage.isUserInteractionEnabled = true + NSLayoutConstraint.activate([ + contentHeight!, + contentWidth!, + bubbleImage.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + bubbleImage.bottomAnchor.constraint(equalTo: line.bottomAnchor, constant: -12), + ]) + + bubbleImage.addSubview(audioImageView) + NSLayoutConstraint.activate([ + audioImageView.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: 16), + audioImageView.centerYAnchor.constraint(equalTo: bubbleImage.centerYAnchor), + audioImageView.widthAnchor.constraint(equalToConstant: 28), + audioImageView.heightAnchor.constraint(equalToConstant: 28), + ]) + + audioTimeLabel.font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) + audioTimeLabel.textAlignment = .left + audioTimeLabel.textColor = UIColor.ne_darkText + audioTimeLabel.translatesAutoresizingMaskIntoConstraints = false + bubbleImage.addSubview(audioTimeLabel) + NSLayoutConstraint.activate([ + audioTimeLabel.leftAnchor.constraint(equalTo: audioImageView.rightAnchor, constant: 12), + audioTimeLabel.centerYAnchor.constraint(equalTo: bubbleImage.centerYAnchor), + audioTimeLabel.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: -12), + audioTimeLabel.heightAnchor.constraint(equalToConstant: 28), + ]) + + if let gesture = contentGesture { + bubbleImage.addGestureRecognizer(gesture) + } + } + + /// 绑定数据 + override open func configureData(_ model: CollectionMessageModel) { + super.configureData(model) + if let m = model.chatmodel as? MessageAudioModel { + audioTimeLabel.text = "\(m.duration)" + "s" + m.isPlaying == true ? startPlayAnimation() : stopPlayAnimation() + } + } + + /// 开始播放动画 + open func startPlayAnimation() { + if !audioImageView.isAnimating { + audioImageView.startAnimating() + } + if let m = collectionModel?.chatmodel as? MessageAudioModel { + m.isPlaying = true + isPlaying = true + } + } + + /// 停止播放动画 + open func stopPlayAnimation() { + if audioImageView.isAnimating { + audioImageView.stopAnimating() + } + if let m = collectionModel?.chatmodel as? MessageAudioModel { + m.isPlaying = false + isPlaying = false + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageCell.swift new file mode 100644 index 00000000..86582a11 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageCell.swift @@ -0,0 +1,226 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECommonUIKit +import UIKit + +@objc +public protocol CollectionMessageCellDelegate { + func didClickMore(_ model: CollectionMessageModel?) + func didClickContent(_ model: CollectionMessageModel?, _ cell: NEBaseCollectionMessageCell) +} + +@objcMembers +open class NEBaseCollectionMessageCell: UITableViewCell { + public var contentWidth: NSLayoutConstraint? + + public var contentHeight: NSLayoutConstraint? + + public var collectionModel: CollectionMessageModel? + + public var delegate: CollectionMessageCellDelegate? + + public var contentGesture: UITapGestureRecognizer? + + /// 头像 + public lazy var headerView: NEUserHeaderView = { + let header = NEUserHeaderView(frame: .zero) + header.titleLabel.font = NEConstant.defaultTextFont(12) + header.titleLabel.textColor = UIColor.white + header.layer.cornerRadius = 16 + header.clipsToBounds = true + header.translatesAutoresizingMaskIntoConstraints = false + return header + }() + + /// 消息发送者昵称 + public lazy var nameLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 14.0) + label.textColor = .ne_darkText + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.name" + return label + }() + + /// 时间 + public lazy var timeLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 12.0) + label.textColor = .ne_greyText + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.time" + return label + }() + + /// 会话标签 + public lazy var conversationLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 14.0) + label.textColor = .ne_greyText + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.conversation" + label.lineBreakMode = .byTruncatingMiddle + return label + }() + + /// 更多 + public let moreImageView = UIImageView() + + public let backView = UIView() + + public let line = UIView() + + public var backLeftConstraint: NSLayoutConstraint? + public var backRightConstraint: NSLayoutConstraint? + + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + backgroundColor = .clear + contentGesture = UITapGestureRecognizer(target: self, action: #selector(contentClick)) + setupCommonUI() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// UI 初始化 + open func setupCommonUI() { + contentView.backgroundColor = .clear + + backView.translatesAutoresizingMaskIntoConstraints = false + backView.backgroundColor = UIColor.white + backView.clipsToBounds = true + backView.layer.cornerRadius = 8.0 + contentView.addSubview(backView) + + backLeftConstraint = backView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20) + backLeftConstraint?.isActive = true + backRightConstraint = backView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20) + backRightConstraint?.isActive = true + NSLayoutConstraint.activate([ + backView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + backView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + + backView.addSubview(headerView) + NSLayoutConstraint.activate([ + headerView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + headerView.topAnchor.constraint(equalTo: backView.topAnchor, constant: 16), + headerView.widthAnchor.constraint(equalToConstant: 32), + headerView.heightAnchor.constraint(equalToConstant: 32), + ]) + + let image = UIImage.ne_imageNamed(name: "three_point") + moreImageView.image = image + moreImageView.translatesAutoresizingMaskIntoConstraints = false + backView.addSubview(moreImageView) + + let moreButton = UIButton() + moreButton.addTarget(self, action: #selector(moreClick), for: .touchUpInside) + moreButton.accessibilityIdentifier = "id.moreAction" + moreButton.translatesAutoresizingMaskIntoConstraints = false + backView.addSubview(moreButton) + NSLayoutConstraint.activate([ + moreButton.rightAnchor.constraint(equalTo: backView.rightAnchor), + moreButton.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), + moreButton.widthAnchor.constraint(equalToConstant: 50), + moreButton.heightAnchor.constraint(equalToConstant: 40), + ]) + + NSLayoutConstraint.activate([ + moreImageView.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), + moreImageView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -20), + ]) + + backView.addSubview(nameLabel) + NSLayoutConstraint.activate([ + nameLabel.leftAnchor.constraint(equalTo: headerView.rightAnchor, constant: 8), + nameLabel.topAnchor.constraint(equalTo: headerView.topAnchor), + nameLabel.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -50), + ]) + + backView.addSubview(timeLabel) + NSLayoutConstraint.activate([ + timeLabel.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + timeLabel.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -16), + timeLabel.bottomAnchor.constraint(equalTo: backView.bottomAnchor, constant: -12), + ]) + + backView.addSubview(line) + line.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + line.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + line.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -16), + line.bottomAnchor.constraint(equalTo: backView.bottomAnchor, constant: -38), + line.heightAnchor.constraint(equalToConstant: 1), + ]) + line.backgroundColor = .ne_greyLine + + backView.addSubview(conversationLabel) + NSLayoutConstraint.activate([ + conversationLabel.leftAnchor.constraint(equalTo: nameLabel.leftAnchor), + conversationLabel.rightAnchor.constraint(equalTo: nameLabel.rightAnchor), + conversationLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 2), + ]) + } + + /// 数据源绑定 + /// - Parameter model: 数据模型 + open func configureData(_ model: CollectionMessageModel) { + collectionModel = model + headerView.configHeadData(headUrl: model.chatmodel.avatar, + name: model.chatmodel.shortName ?? "", + uid: model.chatmodel.message?.senderId ?? "") + nameLabel.text = model.chatmodel.fullName + if let time = model.collection?.updateTime { + timeLabel.text = String.stringFromDate(date: Date(timeIntervalSince1970: time)) + } else if let time = model.message?.createTime { + timeLabel.text = String.stringFromDate(date: Date(timeIntervalSince1970: time)) + } + + if model.message?.conversationType == .CONVERSATION_TYPE_P2P { + if let conversationName = model.conversationName { + conversationLabel.text = String(format: chatLocalizable("chat_collection_p2p_tip"), conversationName) + } + } else if model.message?.conversationType == .CONVERSATION_TYPE_TEAM || model.message?.conversationType == .CONVERSATION_TYPE_SUPER_TEAM { + if let conversationName = model.conversationName { + conversationLabel.text = String(format: chatLocalizable("chat_collection_team_tip"), conversationName) + } + } + + contentWidth?.constant = model.chatmodel.contentSize.width + contentHeight?.constant = model.chatmodel.contentSize.height + } + + func moreClick() { + delegate?.didClickMore(collectionModel) + } + + open func contentClick() { + delegate?.didClickContent(collectionModel, self) + } + + /// 设置娱乐版边距 + open func setFunStyle() { + backLeftConstraint?.constant = 0 + backRightConstraint?.constant = 0 + backView.layer.cornerRadius = 0 + headerView.layer.cornerRadius = 4.0 + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageDefaultCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageDefaultCell.swift new file mode 100644 index 00000000..fb6b74f9 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageDefaultCell.swift @@ -0,0 +1,36 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class NEBaseCollectionMessageDefaultCell: NEBaseCollectionMessageTextCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// 绑定数据 + override open func configureData(_ model: CollectionMessageModel) { + super.configureData(model) + collectionContentLabel.text = chatLocalizable("unkonw_pin_message") + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageFileCell.swift new file mode 100644 index 00000000..5215bab5 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageFileCell.swift @@ -0,0 +1,210 @@ +//// 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 NEBaseCollectionMessageFileCell: NEBaseCollectionMessageCell { + /// 文件下载状态 + public lazy var fileStateView: FileStateView = { + let stateView = FileStateView() + stateView.translatesAutoresizingMaskIntoConstraints = false + stateView.backgroundColor = .clear + return stateView + }() + + /// 气泡图片 + public var bubbleImage = UIImageView() + + public lazy var imgView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = .clear + return imageView + }() + + /// 标题 + public lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.isUserInteractionEnabled = false + label.numberOfLines = 1 + label.lineBreakMode = .byTruncatingMiddle + label.font = DefaultTextFont(14) + label.textAlignment = .left + return label + }() + + /// 文件大小 + public lazy var sizeLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor(hexString: "#999999") + label.font = NEConstant.defaultTextFont(10.0) + label.textAlignment = .left + return label + }() + + public lazy var labelView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false + view.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: view.leftAnchor), + titleLabel.topAnchor.constraint(equalTo: view.topAnchor), + titleLabel.rightAnchor.constraint(equalTo: view.rightAnchor), + titleLabel.heightAnchor.constraint(equalToConstant: 18), + ]) + view.addSubview(sizeLabel) + NSLayoutConstraint.activate([ + sizeLabel.leftAnchor.constraint(equalTo: view.leftAnchor), + sizeLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 5), + sizeLabel.rightAnchor.constraint(equalTo: view.rightAnchor), + sizeLabel.heightAnchor.constraint(equalToConstant: 10), + ]) + return view + }() + + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + bubbleImage.image = nil + bubbleImage.layer.cornerRadius = 8 + bubbleImage.layer.borderColor = UIColor.ne_borderColor.cgColor + bubbleImage.layer.borderWidth = 1 + bubbleImage.translatesAutoresizingMaskIntoConstraints = false + bubbleImage.isUserInteractionEnabled = true + backView.addSubview(bubbleImage) + contentWidth = bubbleImage.widthAnchor.constraint(equalToConstant: chat_content_maxW) + contentHeight = bubbleImage.heightAnchor.constraint(equalToConstant: chat_content_maxW) + NSLayoutConstraint.activate([ + contentHeight!, + contentWidth!, + bubbleImage.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + bubbleImage.bottomAnchor.constraint(equalTo: line.topAnchor, constant: -12), + ]) + + bubbleImage.addSubview(imgView) + NSLayoutConstraint.activate([ + imgView.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: 10), + imgView.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: 10), + imgView.widthAnchor.constraint(equalToConstant: 32), + imgView.heightAnchor.constraint(equalToConstant: 32), + ]) + + 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(fileStateView) + NSLayoutConstraint.activate([ + fileStateView.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: 10), + fileStateView.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: 10), + fileStateView.widthAnchor.constraint(equalToConstant: 32), + fileStateView.heightAnchor.constraint(equalToConstant: 32), + ]) + + if let gesture = contentGesture { + bubbleImage.addGestureRecognizer(gesture) + } + } + + override open func configureData(_ model: CollectionMessageModel) { + super.configureData(model) + if let fileObject = model.message?.attachment as? V2NIMMessageFileAttachment { + if let fileModel = model.fileModel { + fileModel.cell = self + if fileModel.state == .Success { + fileStateView.state = .FileOpen + } else { + fileStateView.state = .FileDownload + fileStateView.setProgress(Float(fileModel.progress)) + if fileModel.progress >= 100 { + fileModel.state = .Success + } + } + } + var fileName = "file_unknown" + let suffix = (fileObject.name as NSString).pathExtension.lowercased() + switch suffix { + case file_doc_support: + fileName = "file_doc" + case file_xls_support: + fileName = "file_xls" + case file_img_support: + fileName = "file_img" + case file_ppt_support: + fileName = "file_ppt" + case file_txt_support: + fileName = "file_txt" + case file_audio_support: + fileName = "file_audio" + case file_vedio_support: + fileName = "file_vedio" + case file_zip_support: + fileName = "file_zip" + case file_pdf_support: + fileName = "file_pdf" + case file_html_support: + fileName = "file_html" + case "key", "keynote": + fileName = "file_keynote" + default: + fileName = "file_unknown" + } + + imgView.image = UIImage.ne_imageNamed(name: fileName) + titleLabel.text = fileObject.name + + let size_B = Double(fileObject.size) + var size_str = String(format: "%.1f B", size_B) + if size_B > 1e3 { + let size_KB = size_B / 1e3 + size_str = String(format: "%.1f KB", size_KB) + if size_KB > 1e3 { + let size_MB = size_KB / 1e3 + size_str = String(format: "%.1f MB", size_MB) + if size_MB > 1e3 { + let size_GB = size_KB / 1e6 + size_str = String(format: "%.1f GB", size_GB) + } + } + } + sizeLabel.text = size_str + } + } + + /// 更新下载进度 + /// - Parameter progress: 进度比率 + open func uploadProgress(_ progress: UInt) { + fileStateView.setProgress(Float(progress)) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageImageCell.swift new file mode 100644 index 00000000..d57f5d0e --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageImageCell.swift @@ -0,0 +1,84 @@ +//// 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 NEBaseCollectionMessageImageCell: NEBaseCollectionMessageCell { + /// 图片消息内容图片 + public let collectionContentImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + imageView.isUserInteractionEnabled = true + return imageView + }() + + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + + collectionContentImageView.addCustomCorner( + conrners: [.bottomLeft, .bottomRight, .topRight, .topLeft], + radius: 8, + backcolor: .white + ) + backView.addSubview(collectionContentImageView) + NSLayoutConstraint.activate([ + collectionContentImageView.leftAnchor.constraint(equalTo: line.leftAnchor, constant: 0), + collectionContentImageView.bottomAnchor.constraint(equalTo: line.topAnchor, constant: -12), + ]) + contentWidth = collectionContentImageView.widthAnchor.constraint(equalToConstant: 0) + contentWidth?.isActive = true + contentHeight = collectionContentImageView.heightAnchor.constraint(equalToConstant: 0) + contentHeight?.isActive = true + + if let gesture = contentGesture { + collectionContentImageView.addGestureRecognizer(gesture) + } + } + + override open func configureData(_ model: CollectionMessageModel) { + super.configureData(model) + if let m = model.chatmodel as? MessageImageModel, let imageUrl = m.urlString { + if imageUrl.hasPrefix("http") { + collectionContentImageView.sd_setImage( + with: URL(string: imageUrl), + placeholderImage: nil, + options: .retryFailed, + progress: nil, + completed: nil + ) + } else { + let url = URL(fileURLWithPath: imageUrl) + collectionContentImageView.sd_setImage(with: url) + } + + } else { + collectionContentImageView.image = nil + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageLocationCell.swift new file mode 100644 index 00000000..0dbf2cf5 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageLocationCell.swift @@ -0,0 +1,155 @@ +//// 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 UIKit + +@objcMembers +open +class NEBaseCollectionMessageLocationCell: NEBaseCollectionMessageCell { + /// 位置信息 + public lazy var locationTitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor.ne_darkText + label.font = UIFont.systemFont(ofSize: 16.0) + return label + }() + + /// 子标题 + public lazy var locationSubTitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor.ne_lightText + label.font = UIFont.systemFont(ofSize: 12.0) + return label + }() + + /// 空提示 + public lazy var emptyLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 16) + label.text = chatLocalizable("no_map_plugin") + label.textAlignment = .center + label.textColor = UIColor.ne_greyText + label.isHidden = true + return label + }() + + /// 定位图标 + let pointImageView = UIImageView() + + public lazy var mapImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + var mapView: UIView? + + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + let contentBackView = UIView() + contentBackView.backgroundColor = UIColor.white + contentView.addSubview(contentBackView) + contentBackView.translatesAutoresizingMaskIntoConstraints = false + contentBackView.clipsToBounds = true + contentBackView.layer.cornerRadius = 4 + contentBackView.layer.borderWidth = 1 + contentBackView.layer.borderColor = UIColor.ne_outlineColor.cgColor + + backView.addSubview(contentBackView) + contentWidth = contentBackView.widthAnchor.constraint(equalToConstant: chat_content_maxW) + contentHeight = contentBackView.heightAnchor.constraint(equalToConstant: chat_content_maxW) + NSLayoutConstraint.activate([ + contentWidth!, + contentHeight!, + contentBackView.leftAnchor.constraint(equalTo: headerView.leftAnchor), + contentBackView.bottomAnchor.constraint(equalTo: line.topAnchor, constant: -12), + ]) + + contentBackView.addSubview(locationTitleLabel) + NSLayoutConstraint.activate([ + locationTitleLabel.leftAnchor.constraint(equalTo: contentBackView.leftAnchor, constant: 16), + locationTitleLabel.rightAnchor.constraint(equalTo: contentBackView.rightAnchor, constant: -16), + locationTitleLabel.topAnchor.constraint(equalTo: contentBackView.topAnchor, constant: 10), + ]) + + contentBackView.addSubview(locationSubTitleLabel) + NSLayoutConstraint.activate([ + locationSubTitleLabel.leftAnchor.constraint(equalTo: locationTitleLabel.leftAnchor), + locationSubTitleLabel.rightAnchor.constraint(equalTo: locationTitleLabel.rightAnchor), + locationSubTitleLabel.topAnchor.constraint(equalTo: locationTitleLabel.bottomAnchor, constant: 4), + ]) + + contentBackView.addSubview(mapImageView) + NSLayoutConstraint.activate([ + mapImageView.leftAnchor.constraint(equalTo: contentBackView.leftAnchor), + mapImageView.bottomAnchor.constraint(equalTo: contentBackView.bottomAnchor), + mapImageView.rightAnchor.constraint(equalTo: contentBackView.rightAnchor), + mapImageView.topAnchor.constraint(equalTo: locationSubTitleLabel.bottomAnchor, constant: 4), + ]) + + pointImageView.translatesAutoresizingMaskIntoConstraints = false + pointImageView.image = coreLoader.loadImage("location_point") + mapImageView.addSubview(pointImageView) + NSLayoutConstraint.activate([ + pointImageView.centerXAnchor.constraint(equalTo: mapImageView.centerXAnchor), + pointImageView.bottomAnchor.constraint(equalTo: mapImageView.bottomAnchor, constant: -30), + ]) + + contentBackView.addSubview(emptyLabel) + NSLayoutConstraint.activate([ + emptyLabel.leftAnchor.constraint(equalTo: contentBackView.leftAnchor), + emptyLabel.rightAnchor.constraint(equalTo: contentBackView.rightAnchor), + emptyLabel.bottomAnchor.constraint(equalTo: contentBackView.bottomAnchor, constant: -40), + ]) + + if let gesture = contentGesture { + contentBackView.addGestureRecognizer(gesture) + } + } + + override open func configureData(_ model: CollectionMessageModel) { + super.configureData(model) + if let m = model.chatmodel as? MessageLocationModel { + locationTitleLabel.text = m.title + locationSubTitleLabel.text = m.subTitle + if let lat = m.lat, let lng = m.lng { + if let url = NEChatKitClient.instance.delegate?.getMapImageUrl?(lat: lat, lng: lng) { + NEALog.infoLog(className(), desc: #function + "location image url = \(url)") + mapImageView.sd_setImage(with: URL(string: url)) + emptyLabel.isHidden = true + pointImageView.isHidden = false + } else { + emptyLabel.isHidden = false + pointImageView.isHidden = true + } + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageMultiForwardCell.swift new file mode 100644 index 00000000..2b5d842d --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageMultiForwardCell.swift @@ -0,0 +1,180 @@ +//// 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 NEBaseCollectionMessageMultiForwardCell: NEBaseCollectionMessageCell { + public let funMargin: CGFloat = 5.2 + let contentW: CGFloat = 248 + var titleLabelFontSize: CGFloat = 14 + var contentLabelFontSize: CGFloat = 14 + var contentLabelColor: UIColor = .ne_lightText + + public lazy var backImageViewLeft: 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 + }() + + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + backImageViewLeft.layer.cornerRadius = 8 + backImageViewLeft.layer.borderColor = multiForwardborderColor.cgColor + backImageViewLeft.layer.borderWidth = 1 + + backView.addSubview(backImageViewLeft) + NSLayoutConstraint.activate([ + backImageViewLeft.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + backImageViewLeft.bottomAnchor.constraint(equalTo: line.topAnchor, constant: -12), + backImageViewLeft.widthAnchor.constraint(equalToConstant: 276), + backImageViewLeft.heightAnchor.constraint(equalToConstant: 100), + ]) + + if let gesture = contentGesture { + backImageViewLeft.addGestureRecognizer(gesture) + } + } + + override open func configureData(_ model: CollectionMessageModel) { + super.configureData(model) + guard let data = NECustomAttachment.dataOfCustomMessage(model.chatmodel.message?.attachment) 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 + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageRichTextCell.swift new file mode 100644 index 00000000..dcaf5b2d --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageRichTextCell.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 NEBaseCollectionMessageRichTextCell: NEBaseCollectionMessageTextCell { + /// 换行文本 + public lazy var collectionTitleLabel: 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 open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func setupCustomUI() { + collectionContentLabel.numberOfLines = 2 + backView.addSubview(collectionContentLabel) + NSLayoutConstraint.activate([ + collectionContentLabel.leftAnchor.constraint(equalTo: line.leftAnchor), + collectionContentLabel.rightAnchor.constraint(equalTo: line.rightAnchor), + collectionContentLabel.bottomAnchor.constraint(equalTo: line.topAnchor, constant: -12), + ]) + + backView.addSubview(collectionTitleLabel) + NSLayoutConstraint.activate([ + collectionTitleLabel.leftAnchor.constraint(equalTo: line.leftAnchor), + collectionTitleLabel.rightAnchor.constraint(equalTo: line.rightAnchor), + collectionTitleLabel.bottomAnchor.constraint(equalTo: collectionContentLabel.topAnchor, constant: -1), + ]) + + if let gesture = contentGesture { + collectionContentLabel.addGestureRecognizer(gesture) + } + + let titleGesture = UITapGestureRecognizer(target: self, action: #selector(contentClick)) + collectionTitleLabel.addGestureRecognizer(titleGesture) + } + + override open func setupCommonUI() { + super.setupCommonUI() + } + + override open func configureData(_ model: CollectionMessageModel) { + super.configureData(model) + if let m = model.chatmodel as? MessageRichTextModel { + collectionTitleLabel.attributedText = m.titleAttributeStr + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageTextCell.swift new file mode 100644 index 00000000..8de34c7f --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageTextCell.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 UIKit + +@objcMembers +open +class NEBaseCollectionMessageTextCell: NEBaseCollectionMessageCell { + /// 内容文本 + public lazy var collectionContentLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) + label.textColor = .ne_darkText + label.translatesAutoresizingMaskIntoConstraints = false + label.isUserInteractionEnabled = true + label.numberOfLines = 3 + label.accessibilityIdentifier = "id.message" + return label + }() + + /// 回复文本 + public let replyLabel = UILabel() + + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func setupCustomUI() { + backView.addSubview(collectionContentLabel) + NSLayoutConstraint.activate([ + collectionContentLabel.leftAnchor.constraint(equalTo: line.leftAnchor), + collectionContentLabel.rightAnchor.constraint(equalTo: line.rightAnchor), + collectionContentLabel.bottomAnchor.constraint(equalTo: line.topAnchor, constant: -12), + ]) + if let gesture = contentGesture { + collectionContentLabel.addGestureRecognizer(gesture) + } + } + + override open func setupCommonUI() { + super.setupCommonUI() + setupCustomUI() + } + + override open func configureData(_ model: CollectionMessageModel) { + super.configureData(model) + if let model = model.chatmodel as? MessageTextModel { + collectionContentLabel.attributedText = model.attributeStr + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageVideoCell.swift new file mode 100644 index 00000000..2bb419fa --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/CollectionCell/NEBaseCollectionMessageVideoCell.swift @@ -0,0 +1,109 @@ +//// 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 NEBaseCollectionMessageVideoCell: NEBaseCollectionMessageImageCell { + /// 状态视图 + public lazy var collectionStateView: VideoStateView = { + let state = VideoStateView() + state.translatesAutoresizingMaskIntoConstraints = false + state.backgroundColor = .clear + return state + }() + + /// 视频时间 + public lazy var collectionVideoTimeLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .white + label.font = NEConstant.defaultTextFont(10.0) + label.textAlignment = .center + return label + }() + + public lazy var collectionTimeView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(collectionVideoTimeLabel) + NSLayoutConstraint.activate([ + collectionVideoTimeLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 4), + collectionVideoTimeLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 2), + collectionVideoTimeLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -4), + collectionVideoTimeLabel.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -2), + ]) + view.clipsToBounds = true + view.layer.cornerRadius = 4.0 + view.backgroundColor = NEConstant.hexRGB(0x000000).withAlphaComponent(0.6) + return view + }() + + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + collectionContentImageView.addSubview(collectionStateView) + collectionContentImageView.addCustomCorner(conrners: [.topLeft], radius: 8, backcolor: .white) + NSLayoutConstraint.activate([ + collectionStateView.centerXAnchor.constraint(equalTo: collectionContentImageView.centerXAnchor), + collectionStateView.centerYAnchor.constraint(equalTo: collectionContentImageView.centerYAnchor), + collectionStateView.heightAnchor.constraint(equalToConstant: 60), + collectionStateView.widthAnchor.constraint(equalToConstant: 60), + ]) + + collectionContentImageView.addSubview(collectionTimeView) + NSLayoutConstraint.activate([ + collectionTimeView.rightAnchor.constraint(equalTo: collectionContentImageView.rightAnchor, constant: -7), + collectionTimeView.bottomAnchor.constraint(equalTo: collectionContentImageView.bottomAnchor, constant: -7), + ]) + + collectionStateView.isUserInteractionEnabled = false + } + + override open func configureData(_ model: CollectionMessageModel) { + super.configureData(model) + if let videoObject = model.chatmodel.message?.attachment as? V2NIMMessageVideoAttachment { + // 获取首帧 + let videoUrl = videoObject.url ?? "" + let thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + collectionContentImageView.sd_setImage( + with: URL(string: thumbUrl), + placeholderImage: nil, + options: .retryFailed, + progress: nil, + completed: nil + ) + + if videoObject.duration > 0 { + collectionTimeView.isHidden = false + collectionVideoTimeLabel.text = Date.getFormatPlayTime(TimeInterval(videoObject.duration / 1000)) + } else { + collectionTimeView.isHidden = true + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift index e0c1ece7..7dd0654b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageCell.swift @@ -3,7 +3,8 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NEChatKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -17,7 +18,7 @@ public protocol ChatBaseCellDelegate: NSObjectProtocol { func didLongPressAvatar(_ cell: UITableViewCell, _ model: MessageContentModel?) // 单击消息体 - func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) + func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?, _ replyModel: MessageModel?) // 长按消息体 func didLongPressMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) @@ -45,7 +46,6 @@ protocol ChatAudioCellProtocol { @objcMembers open class NEBaseChatMessageCell: NEChatBaseCell { - private let bubbleWidth: CGFloat = 218 // 气泡默认宽度 private let pinLabelMaxWidth: CGFloat = 280 // pin 文案最大宽度 public weak var delegate: ChatBaseCellDelegate? public var contentModel: MessageContentModel? // 消息模型 @@ -60,7 +60,6 @@ open class NEBaseChatMessageCell: NEChatBaseCell { public var bubbleHLeft: NSLayoutConstraint? // 左侧气泡高度布局约束 public var pinImageLeft = UIImageView() // 左侧标记图片 public var pinLabelLeft = UILabel() // 左侧标记文案 - public var pinLabelLeftTopAnchor: NSLayoutConstraint? // 左侧标记文案顶部布局约束 private var pinLabelHLeft: NSLayoutConstraint? // 左侧标记文案宽度布局约束 private var pinLabelWLeft: NSLayoutConstraint? // 左侧标记文案高度布局约束 public var fullNameLabel = UILabel() // 群昵称(只在群聊中有效) @@ -74,20 +73,23 @@ 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 activityViewCenterYAnchor: NSLayoutConstraint? // 消息状态视图 Y 布局约束 + public var selectedButton = UIButton(type: .custom) // 多选按钮 + public var selectedButtonCenterYAnchor: NSLayoutConstraint? // 多选按钮中心 Y 布局约束 public var timeLabel = UILabel() // 消息时间 public var timeLabelHeightAnchor: NSLayoutConstraint? // 消息时间高度约束 // 已读未读点击手势 private var tapGesture: UITapGestureRecognizer? + public let messageTextFont = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) initProperty() @@ -97,7 +99,7 @@ open class NEBaseChatMessageCell: NEChatBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } deinit { @@ -184,12 +186,14 @@ open class NEBaseChatMessageCell: NEChatBaseCell { readView.translatesAutoresizingMaskIntoConstraints = false readView.accessibilityIdentifier = "id.readView" + activityView.translatesAutoresizingMaskIntoConstraints = false + activityView.failButton.addTarget(self, action: #selector(resend), for: .touchUpInside) activityView.accessibilityIdentifier = "id.status" - seletedBtn.translatesAutoresizingMaskIntoConstraints = false - seletedBtn.setImage(.ne_imageNamed(name: "unselect"), for: .normal) - seletedBtn.setImage(.ne_imageNamed(name: "select"), for: .selected) - seletedBtn.addTarget(self, action: #selector(selectButtonClicked), for: .touchUpInside) + selectedButton.translatesAutoresizingMaskIntoConstraints = false + selectedButton.setImage(.ne_imageNamed(name: "unselect"), for: .normal) + selectedButton.setImage(.ne_imageNamed(name: "select"), for: .selected) + selectedButton.addTarget(self, action: #selector(selectButtonClicked), for: .touchUpInside) } open func baseCommonUI() { @@ -198,12 +202,12 @@ open class NEBaseChatMessageCell: NEChatBaseCell { // time contentView.addSubview(timeLabel) - timeLabelHeightAnchor = timeLabel.heightAnchor.constraint(equalToConstant: chat_timeCellH) + timeLabelHeightAnchor = timeLabel.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + timeLabelHeightAnchor?.isActive = true NSLayoutConstraint.activate([ timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), timeLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0), timeLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -0), - timeLabelHeightAnchor!, ]) baseCommonUILeft() @@ -213,9 +217,9 @@ open class NEBaseChatMessageCell: NEChatBaseCell { open func baseCommonUILeft() { contentView.addSubview(avatarImageLeft) avatarImageLeftAnchor = avatarImageLeft.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16) + avatarImageLeftAnchor?.isActive = true NSLayoutConstraint.activate([ avatarImageLeft.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: chat_content_margin), - avatarImageLeftAnchor!, avatarImageLeft.widthAnchor.constraint(equalToConstant: 32), avatarImageLeft.heightAnchor.constraint(equalToConstant: 32), ]) @@ -229,35 +233,32 @@ open class NEBaseChatMessageCell: NEChatBaseCell { ]) contentView.addSubview(fullNameLabel) - fullNameH = fullNameLabel.heightAnchor.constraint(equalToConstant: 0) + fullNameH = fullNameLabel.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + fullNameH?.isActive = true NSLayoutConstraint.activate([ fullNameLabel.leftAnchor.constraint(equalTo: avatarImageLeft.rightAnchor, constant: chat_content_margin), fullNameLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), fullNameLabel.topAnchor.constraint(equalTo: avatarImageLeft.topAnchor), - fullNameH!, ]) // bubbleImageLeft contentView.addSubview(bubbleImageLeft) bubbleTopAnchorLeft = bubbleImageLeft.topAnchor.constraint(equalTo: fullNameLabel.bottomAnchor, constant: 0) - bubbleWLeft = bubbleImageLeft.widthAnchor.constraint(equalToConstant: bubbleWidth) - bubbleHLeft = bubbleImageLeft.heightAnchor.constraint(equalToConstant: bubbleWidth) - NSLayoutConstraint.activate([ - bubbleTopAnchorLeft!, - bubbleImageLeft.leftAnchor.constraint(equalTo: avatarImageLeft.rightAnchor, constant: chat_content_margin), - bubbleWLeft!, - bubbleHLeft!, - ]) + bubbleTopAnchorLeft?.isActive = true + bubbleWLeft = bubbleImageLeft.widthAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + bubbleWLeft?.isActive = true + bubbleHLeft = bubbleImageLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + bubbleHLeft?.isActive = true + bubbleImageLeft.leftAnchor.constraint(equalTo: avatarImageLeft.rightAnchor, constant: chat_content_margin).isActive = true contentView.addSubview(pinLabelLeft) - pinLabelLeftTopAnchor = pinLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.bottomAnchor, constant: 4) - pinLabelHLeft = pinLabelLeft.heightAnchor.constraint(equalToConstant: 0) + pinLabelHLeft = pinLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + pinLabelHLeft?.isActive = true pinLabelWLeft = pinLabelLeft.widthAnchor.constraint(equalToConstant: pinLabelMaxWidth) + pinLabelWLeft?.isActive = true NSLayoutConstraint.activate([ - pinLabelLeftTopAnchor!, + pinLabelLeft.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4), pinLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: 14), - pinLabelWLeft!, - pinLabelHLeft!, ]) contentView.addSubview(pinImageLeft) @@ -286,24 +287,23 @@ open class NEBaseChatMessageCell: NEChatBaseCell { ]) contentView.addSubview(bubbleImageRight) - bubbleWRight = bubbleImageRight.widthAnchor.constraint(equalToConstant: bubbleWidth) - bubbleHRight = bubbleImageRight.heightAnchor.constraint(equalToConstant: bubbleWidth) + bubbleWRight = bubbleImageRight.widthAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + bubbleWRight?.isActive = true + bubbleHRight = bubbleImageRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + bubbleHRight?.isActive = true NSLayoutConstraint.activate([ bubbleImageRight.topAnchor.constraint(equalTo: avatarImageRight.topAnchor, constant: 0), bubbleImageRight.rightAnchor.constraint(equalTo: avatarImageRight.leftAnchor, constant: -chat_content_margin), - bubbleWRight!, - bubbleHRight!, ]) // activityView contentView.addSubview(activityView) - activityView.translatesAutoresizingMaskIntoConstraints = false - activityView.failBtn.addTarget(self, action: #selector(resend), for: .touchUpInside) + activityViewCenterYAnchor = activityView.centerYAnchor.constraint(equalTo: bubbleImageRight.centerYAnchor, constant: 0) + activityViewCenterYAnchor?.isActive = true NSLayoutConstraint.activate([ activityView.rightAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: -chat_content_margin), - activityView.centerYAnchor.constraint(equalTo: bubbleImageRight.centerYAnchor, constant: 0), - activityView.widthAnchor.constraint(equalToConstant: 25), - activityView.heightAnchor.constraint(equalToConstant: 25), + activityView.widthAnchor.constraint(equalToConstant: 22), + activityView.heightAnchor.constraint(equalToConstant: 22), ]) // readView @@ -315,24 +315,22 @@ open class NEBaseChatMessageCell: NEChatBaseCell { readView.heightAnchor.constraint(equalToConstant: 16), ]) -// seletedBtn - contentView.addSubview(seletedBtn) +// selectedButton + contentView.addSubview(selectedButton) NSLayoutConstraint.activate([ - seletedBtn.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), - seletedBtn.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), - seletedBtn.widthAnchor.constraint(equalToConstant: 18), - seletedBtn.heightAnchor.constraint(equalToConstant: 18), + selectedButton.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + selectedButton.widthAnchor.constraint(equalToConstant: 18), + selectedButton.heightAnchor.constraint(equalToConstant: 18), ]) contentView.addSubview(pinLabelRight) - pinLabelRightTopAnchor = pinLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: 4) - pinLabelHRight = pinLabelRight.heightAnchor.constraint(equalToConstant: 0) + pinLabelHRight = pinLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + pinLabelHRight?.isActive = true pinLabelWRight = pinLabelRight.widthAnchor.constraint(equalToConstant: 210) + pinLabelWRight?.isActive = true NSLayoutConstraint.activate([ - pinLabelRightTopAnchor!, + pinLabelRight.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -4), pinLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: 0), - pinLabelWRight!, - pinLabelHRight!, ]) contentView.addSubview(pinImageRight) @@ -391,13 +389,11 @@ open class NEBaseChatMessageCell: NEChatBaseCell { // MARK: event open func tapAvatar(tap: UITapGestureRecognizer) { - print(#function) delegate?.didTapAvatarView(self, contentModel) } open func tapMessage(tap: UITapGestureRecognizer) { - print(#function) - delegate?.didTapMessageView(self, contentModel) + delegate?.didTapMessageView(self, contentModel, contentModel?.replyedModel) } open func longPressAvatar(longPress: UITapGestureRecognizer) { @@ -407,7 +403,6 @@ open class NEBaseChatMessageCell: NEChatBaseCell { } open func longPress(longPress: UILongPressGestureRecognizer) { - print(#function) switch longPress.state { case .began: print("state:begin") @@ -431,12 +426,11 @@ open class NEBaseChatMessageCell: NEChatBaseCell { } open func tapReadView(tap: UITapGestureRecognizer) { - print(#function) delegate?.didTapReadView(self, contentModel) } open func selectButtonClicked() { - seletedBtn.isSelected = !seletedBtn.isSelected + selectedButton.isSelected = !selectedButton.isSelected if let model = contentModel { model.isSelected = !model.isSelected } @@ -447,13 +441,18 @@ open class NEBaseChatMessageCell: NEChatBaseCell { open func setSelect(_ model: MessageContentModel, _ enableSelect: Bool = false) { // 多选框 - seletedBtn.isHidden = model.isRevoked || !enableSelect - seletedBtn.isSelected = model.isSelected + selectedButton.isHidden = model.isRevoked || !enableSelect + selectedButton.isSelected = model.isSelected + + // 多选状态下,头像右移 avatarImageLeftAnchor?.constant = enableSelect ? 42 : 16 + + // 多选状态下,消息状态视图(发送失败)位置下移,避免与多选重叠 + activityViewCenterYAnchor?.constant = enableSelect ? model.contentSize.height / 2 - 12 : 0 } override open func setModel(_ model: MessageContentModel) { - setModel(model, model.message?.isOutgoingMsg ?? false) + setModel(model, model.message?.isSelf ?? false) } override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { @@ -480,9 +479,14 @@ open class NEBaseChatMessageCell: NEChatBaseCell { bubbleW?.constant = model.contentSize.width bubbleH?.constant = model.contentSize.height + selectedButtonCenterYAnchor = selectedButton.centerYAnchor.constraint(equalTo: isSend ? bubbleImageRight.centerYAnchor : bubbleImageLeft.centerYAnchor) + selectedButtonCenterYAnchor?.priority = .defaultHigh + selectedButtonCenterYAnchor?.isActive = true // avatar nameLabel.text = model.shortName + nameLabel.isHidden = true + avatarImage.backgroundColor = .clear if let avatarURL = model.avatar, !avatarURL.isEmpty { avatarImage .sd_setImage(with: URL(string: avatarURL)) { image, error, type, url in @@ -494,14 +498,14 @@ open class NEBaseChatMessageCell: NEChatBaseCell { avatarImage.image = nil nameLabel.isHidden = false avatarImage.backgroundColor = UIColor - .colorWithString(string: model.message?.from) + .colorWithString(string: model.message?.senderId) } } } else { avatarImage.image = nil nameLabel.isHidden = false avatarImage.backgroundColor = UIColor - .colorWithString(string: model.message?.from) + .colorWithString(string: model.message?.senderId) } if model.fullNameHeight > 0 { @@ -516,60 +520,54 @@ open class NEBaseChatMessageCell: NEChatBaseCell { fullNameH?.constant = CGFloat(model.fullNameHeight) if isSend { - switch model.message?.deliveryState { - case .delivering: + switch model.message?.sendingState { + case .MESSAGE_SENDING_STATE_SENDING: activityView.messageStatus = .sending - case .deliveried: - // 同一个账号,在多端登录,被对方拉黑,需要根据isBlackListed判断,进而更新信息状态 - if let isBlackMsg = model.message?.isBlackListed, isBlackMsg { - activityView.messageStatus = .failed - } else { - activityView.messageStatus = .successed - } - case .failed: + case .MESSAGE_SENDING_STATE_SUCCEEDED: + activityView.messageStatus = .successed + case .MESSAGE_SENDING_STATE_FAILED: activityView.messageStatus = .failed - default: break + default: + activityView.messageStatus = .sending } + } else { + activityView.messageStatus = .successed } - if isSend, model.message?.deliveryState == .deliveried { - if model.message?.session?.sessionType == .P2P { - let receiptEnable = model.message?.setting?.teamReceiptEnabled ?? false + if isSend, model.message?.sendingState == .MESSAGE_SENDING_STATE_SUCCEEDED { + if model.message?.conversationType == .CONVERSATION_TYPE_P2P { + let receiptEnable = model.message?.messageConfig?.readReceiptEnabled ?? false if receiptEnable, - IMKitClient.instance.getSettingRepo().getShowReadStatus(), + !model.isRevoked, + SettingRepo.shared.getShowReadStatus(), NEKitChatConfig.shared.ui.messageProperties.showP2pMessageStatus == true { readView.isHidden = false - if let read = model.message?.isRemoteRead, read { + if model.readCount == 1, model.unreadCount == 0 { readView.progress = 1 } else { readView.progress = 0 } - // 未读消息需要判断是否被拉黑,拉黑情况,已读未读状态不展示。 - if let isBlackMsg = model.message?.isBlackListed, isBlackMsg { - readView.isHidden = true - } else { - readView.isHidden = false - } - } else { readView.isHidden = true } - } else if model.message?.session?.sessionType == .team { - let receiptEnable = model.message?.setting?.teamReceiptEnabled ?? false + } else if model.message?.conversationType == .CONVERSATION_TYPE_TEAM { + let receiptEnable = model.message?.messageConfig?.readReceiptEnabled ?? false if receiptEnable, - IMKitClient.instance.getSettingRepo().getShowReadStatus(), + !model.isRevoked, + SettingRepo.shared.getShowReadStatus(), NEKitChatConfig.shared.ui.messageProperties.showTeamMessageStatus == true { readView.isHidden = false - let readCount = model.message?.teamReceiptInfo?.readCount ?? 0 - let unreadCount = model.message?.teamReceiptInfo?.unreadCount ?? 0 - let total = Float(readCount + unreadCount) - if (readCount + unreadCount) >= NEKitChatConfig.shared.maxReadingNum { + var total = ChatTeamCache.shared.getTeamInfo()?.memberCount ?? 0 + if model.readCount + model.unreadCount != 0 { + total = model.readCount + model.unreadCount + 1 + } + if total >= NEKitChatConfig.shared.maxReadingNum { readView.isHidden = true return } - if total > 0 { - let progress = Float(readCount) / total + if total - 1 > 0 { + let progress = Float(model.readCount) / Float(total - 1) readView.progress = progress if progress >= 1.0 { tapGesture?.isEnabled = false @@ -607,7 +605,7 @@ open class NEBaseChatMessageCell: NEChatBaseCell { /// 更新标记状态 open func updatePinStatus(_ model: MessageContentModel) { - guard let isSend = model.message?.isOutgoingMsg else { + guard let isSend = model.message?.isSelf else { return } let pinLabel = isSend ? pinLabelRight : pinLabelLeft @@ -620,21 +618,18 @@ open class NEBaseChatMessageCell: NEChatBaseCell { contentView.backgroundColor = model.isPined ? NEKitChatConfig.shared.ui .messageProperties.signalBgColor : .clear if model.isPined { - let pinText = model.message?.session?.sessionType == .P2P ? chatLocalizable("pin_text_P2P") : chatLocalizable("pin_text_team") + let pinText = model.message?.conversationType == .CONVERSATION_TYPE_P2P ? chatLocalizable("pin_text_P2P") : chatLocalizable("pin_text_team") if model.pinAccount == nil { pinLabel.text = chatLocalizable("You") + " " + pinText - } else if let account = model.pinAccount, account == NIMSDK.shared().loginManager.currentAccount() { + } else if let account = model.pinAccount, account == IMKitClient.instance.account() { pinLabel.text = chatLocalizable("You") + " " + pinText } else if let text = model.pinShowName { pinLabel.text = text + pinText } pinImage.image = UIImage.ne_imageNamed(name: "msg_pin") - let size = String.getTextRectSize( - pinLabel.text ?? pinText, - font: UIFont.systemFont(ofSize: 12.0), - size: CGSize(width: pinLabelMaxWidth, height: CGFloat.greatestFiniteMagnitude) - ) + let showText = pinLabel.text ?? pinText + let size = String.getRealSize(showText, .systemFont(ofSize: 12), CGSize(width: pinLabelMaxWidth, height: CGFloat.greatestFiniteMagnitude)) pinLabelH?.constant = CGFloat(chat_pin_height) pinLabelW?.constant = min(size.width + 1, pinLabelMaxWidth) } else { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift index fa9858c8..2cda53ba 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatMessageTipCell.swift @@ -8,6 +8,30 @@ import UIKit @objcMembers open class NEBaseChatMessageTipCell: UITableViewCell { var timeLabelHeightAnchor: NSLayoutConstraint? // 消息时间高度约束 + var contentLabelCenterYAnchor: NSLayoutConstraint? // 消息内容中心Y约束 + + /// 时间 + public lazy var timeLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.timeTextSize) + label.textColor = NEKitChatConfig.shared.ui.messageProperties.timeTextColor + label.textAlignment = .center + label.translatesAutoresizingMaskIntoConstraints = false + 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 + }() override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -17,22 +41,23 @@ open class NEBaseChatMessageTipCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { contentView.addSubview(timeLabel) timeLabelHeightAnchor = timeLabel.heightAnchor.constraint(equalToConstant: 22) + timeLabelHeightAnchor?.isActive = true NSLayoutConstraint.activate([ timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 0), timeLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), timeLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), - timeLabelHeightAnchor!, ]) contentView.addSubview(contentLabel) + contentLabelCenterYAnchor = contentLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor) + contentLabelCenterYAnchor?.isActive = true NSLayoutConstraint.activate([ - contentLabel.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 4), contentLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), contentLabel.widthAnchor.constraint(equalToConstant: chat_content_maxW), ]) @@ -42,35 +67,16 @@ open class NEBaseChatMessageTipCell: UITableViewCell { // time if let time = model.timeContent, !time.isEmpty { timeLabelHeightAnchor?.constant = chat_timeCellH + contentLabelCenterYAnchor?.constant = chat_timeCellH / 2 timeLabel.text = time timeLabel.isHidden = false } else { timeLabelHeightAnchor?.constant = 0 + contentLabelCenterYAnchor?.constant = 0 timeLabel.text = "" timeLabel.isHidden = true } contentLabel.text = model.text } - - public lazy var timeLabel: UILabel = { - let label = UILabel() - label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.timeTextSize) - label.textColor = NEKitChatConfig.shared.ui.messageProperties.timeTextColor - label.textAlignment = .center - label.translatesAutoresizingMaskIntoConstraints = false - 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 334d7de9..a47bb2b0 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseChatTeamMemberCell.swift @@ -60,14 +60,14 @@ open class NEBaseChatTeamMemberCell: UITableViewCell { ]) } - open func configure(_ model: ChatTeamMemberInfoModel) { - if let url = model.nimUser?.userInfo?.avatarUrl, !url.isEmpty { + open func configure(_ model: NETeamMemberInfoModel) { + if let url = model.nimUser?.user?.avatar, !url.isEmpty { headerView.sd_setImage(with: URL(string: url), completed: nil) headerView.setTitle("") } else { headerView.image = nil - headerView.setTitle(model.showNickInTeam()) - headerView.backgroundColor = UIColor.colorWithString(string: model.nimUser?.userId) + headerView.setTitle(model.showNickInTeam() ?? "") + headerView.backgroundColor = UIColor.colorWithString(string: model.nimUser?.user?.accountId) } nameLabel.text = model.atNameInTeam() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift index 8d059d2e..9b9554b6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingCell.swift @@ -15,7 +15,7 @@ open class NEBaseUserSettingCell: CornerCell { return label }() - public lazy var arrow: UIImageView = { + public lazy var arrowImageView: UIImageView = { let imageView = UIImageView(image: coreLoader.loadImage("arrowRight")) imageView.translatesAutoresizingMaskIntoConstraints = false return imageView diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSelectCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSelectCell.swift index 86a8fadf..9d99df1b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSelectCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/NEBaseUserSettingSelectCell.swift @@ -26,7 +26,7 @@ open class NEBaseUserSettingSelectCell: NEBaseUserSettingCell { open func setupUI() { contentView.addSubview(titleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowImageView) contentView.addSubview(subTitleLabel) NSLayoutConstraint.activate([ diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift index 86a43d98..3f7ed17f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/OperationCell.swift @@ -53,7 +53,7 @@ open class OperationCell: UICollectionViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } // @objc func tapEvent(tap: UITapGestureRecognizer) { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift index a5ce8d4d..3b3b4694 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageAudioCell.swift @@ -71,7 +71,7 @@ open class NEBasePinMessageAudioCell: NEBasePinMessageCell { } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) if let m = item.chatmodel as? MessageAudioModel { audioTimeLabel.text = "\(m.duration)" + "s" diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift index 1bdb169c..d28a0979 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageCell.swift @@ -9,8 +9,8 @@ import UIKit @objc public protocol PinMessageCellDelegate { - func didClickMore(_ model: PinMessageModel?) - func didClickContent(_ model: PinMessageModel?, _ cell: NEBasePinMessageCell) + func didClickMore(_ model: NEPinMessageModel?) + func didClickContent(_ model: NEPinMessageModel?, _ cell: NEBasePinMessageCell) } @objcMembers @@ -19,13 +19,13 @@ open class NEBasePinMessageCell: UITableViewCell { public var contentHeight: NSLayoutConstraint? - public var pinModel: PinMessageModel? + public var pinModel: NEPinMessageModel? public var delegate: PinMessageCellDelegate? public var contentGesture: UITapGestureRecognizer? - lazy var headerView: NEUserHeaderView = { + public lazy var headerView: NEUserHeaderView = { let header = NEUserHeaderView(frame: .zero) header.titleLabel.font = NEConstant.defaultTextFont(12) header.titleLabel.textColor = UIColor.white @@ -35,7 +35,7 @@ open class NEBasePinMessageCell: UITableViewCell { return header }() - lazy var nameLabel: UILabel = { + public lazy var nameLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 12.0) label.textColor = .ne_darkText @@ -44,11 +44,12 @@ open class NEBasePinMessageCell: UITableViewCell { return label }() - lazy var timeLabel: UILabel = { + public lazy var timeLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: 12.0) label.textColor = .ne_greyText label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.time" return label }() @@ -84,21 +85,21 @@ open class NEBasePinMessageCell: UITableViewCell { open func setupUI() { contentView.backgroundColor = .clear + backView.translatesAutoresizingMaskIntoConstraints = false backView.backgroundColor = UIColor.white + backView.clipsToBounds = true + backView.layer.cornerRadius = 8.0 contentView.addSubview(backView) backLeftConstraint = backView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20) + backLeftConstraint?.isActive = true backRightConstraint = backView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20) - + backRightConstraint?.isActive = true NSLayoutConstraint.activate([ - backLeftConstraint!, - backRightConstraint!, backView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), backView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) - backView.clipsToBounds = true - backView.layer.cornerRadius = 8.0 backView.addSubview(headerView) NSLayoutConstraint.activate([ @@ -114,16 +115,16 @@ open class NEBasePinMessageCell: UITableViewCell { imageView.translatesAutoresizingMaskIntoConstraints = false backView.addSubview(imageView) - let moreBtn = UIButton() - moreBtn.addTarget(self, action: #selector(moreClick), for: .touchUpInside) - moreBtn.accessibilityIdentifier = "id.moreAction" - moreBtn.translatesAutoresizingMaskIntoConstraints = false - backView.addSubview(moreBtn) + let moreButton = UIButton() + moreButton.addTarget(self, action: #selector(moreClick), for: .touchUpInside) + moreButton.accessibilityIdentifier = "id.moreAction" + moreButton.translatesAutoresizingMaskIntoConstraints = false + backView.addSubview(moreButton) NSLayoutConstraint.activate([ - moreBtn.rightAnchor.constraint(equalTo: backView.rightAnchor), - moreBtn.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), - moreBtn.widthAnchor.constraint(equalToConstant: 50), - moreBtn.heightAnchor.constraint(equalToConstant: 40), + moreButton.rightAnchor.constraint(equalTo: backView.rightAnchor), + moreButton.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), + moreButton.widthAnchor.constraint(equalToConstant: 50), + moreButton.heightAnchor.constraint(equalToConstant: 40), ]) NSLayoutConstraint.activate([ @@ -156,14 +157,13 @@ open class NEBasePinMessageCell: UITableViewCell { line.backgroundColor = .ne_greyLine } - open func configure(_ item: PinMessageModel) { + open func configure(_ item: NEPinMessageModel) { pinModel = item headerView.configHeadData(headUrl: item.chatmodel.avatar, - name: item.chatmodel.fullName ?? "", - uid: item.chatmodel.message?.from ?? "") + name: item.chatmodel.shortName ?? "", + uid: item.chatmodel.message?.senderId ?? "") nameLabel.text = item.chatmodel.fullName - print("config time : ", item.message.timestamp) - timeLabel.text = String.stringFromDate(date: Date(timeIntervalSince1970: item.message.timestamp)) + timeLabel.text = String.stringFromDate(date: Date(timeIntervalSince1970: item.message.createTime)) contentWidth?.constant = item.chatmodel.contentSize.width contentHeight?.constant = item.chatmodel.contentSize.height diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift index 524b562a..f11ff48d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageDefaultCell.swift @@ -29,7 +29,7 @@ open class NEBasePinMessageDefaultCell: NEBasePinMessageTextCell { super.setupUI() } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) contentLabel.text = chatLocalizable("unkonw_pin_message") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift index edab5229..3cd4e96f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageFileCell.swift @@ -8,22 +8,22 @@ import UIKit @objcMembers open class NEBasePinMessageFileCell: NEBasePinMessageCell { public lazy var stateView: FileStateView = { - let state = FileStateView() - state.translatesAutoresizingMaskIntoConstraints = false - state.backgroundColor = .clear - return state + let stateView = FileStateView() + stateView.translatesAutoresizingMaskIntoConstraints = false + stateView.backgroundColor = .clear + return stateView }() public var bubbleImage = UIImageView() - lazy var imgView: UIImageView = { - let view_img = UIImageView() - view_img.translatesAutoresizingMaskIntoConstraints = false - view_img.backgroundColor = .clear - return view_img + public lazy var imgView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = .clear + return imageView }() - lazy var titleLabel: UILabel = { + public lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.isUserInteractionEnabled = false @@ -34,7 +34,7 @@ open class NEBasePinMessageFileCell: NEBasePinMessageCell { return label }() - lazy var sizeLabel: UILabel = { + public lazy var sizeLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor(hexString: "#999999") @@ -43,7 +43,7 @@ open class NEBasePinMessageFileCell: NEBasePinMessageCell { return label }() - lazy var labelView: UIView = { + public lazy var labelView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.isUserInteractionEnabled = false @@ -131,56 +131,55 @@ open class NEBasePinMessageFileCell: NEBasePinMessageCell { } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) - if let fileObject = item.message.messageObject as? NIMFileObject { + if let fileObject = item.message.attachment as? V2NIMMessageFileAttachment { if let fileModel = item.pinFileModel { fileModel.cell = self if fileModel.state == .Success { stateView.state = .FileOpen } else { stateView.state = .FileDownload - stateView.setProgress(fileModel.progress) - if fileModel.progress >= 1 { + stateView.setProgress(Float(fileModel.progress)) + if fileModel.progress >= 100 { fileModel.state = .Success } } } var imageName = "file_unknown" - var displayName = "未知文件" - if let filePath = fileObject.path as? NSString { - displayName = filePath.lastPathComponent - switch filePath.pathExtension.lowercased() { - case file_doc_support: - imageName = "file_doc" - case file_xls_support: - imageName = "file_xls" - case file_img_support: - imageName = "file_img" - case file_ppt_support: - imageName = "file_ppt" - case file_txt_support: - imageName = "file_txt" - case file_audio_support: - imageName = "file_audio" - case file_vedio_support: - imageName = "file_vedio" - case file_zip_support: - imageName = "file_zip" - case file_pdf_support: - imageName = "file_pdf" - case file_html_support: - imageName = "file_html" - case "key", "keynote": - imageName = "file_keynote" - default: - imageName = "file_unknown" - } + let suffix = (fileObject.name as NSString).pathExtension.lowercased() + switch suffix { + case file_doc_support: + imageName = "file_doc" + case file_xls_support: + imageName = "file_xls" + case file_img_support: + imageName = "file_img" + case file_ppt_support: + imageName = "file_ppt" + case file_txt_support: + imageName = "file_txt" + case file_audio_support: + imageName = "file_audio" + case file_vedio_support: + imageName = "file_vedio" + case file_zip_support: + imageName = "file_zip" + case file_pdf_support: + imageName = "file_pdf" + case file_html_support: + imageName = "file_html" + case "key", "keynote": + imageName = "file_keynote" + default: + imageName = "file_unknown" } + imgView.image = UIImage.ne_imageNamed(name: imageName) - titleLabel.text = fileObject.displayName ?? displayName - let size_B = Double(fileObject.fileLength) + titleLabel.text = fileObject.name + + let size_B = Double(fileObject.size) var size_str = String(format: "%.1f B", size_B) if size_B > 1e3 { let size_KB = size_B / 1e3 @@ -198,7 +197,7 @@ open class NEBasePinMessageFileCell: NEBasePinMessageCell { } } - open func uploadProgress(progress: Float) { - stateView.setProgress(progress) + open func uploadProgress(progress: UInt) { + stateView.setProgress(Float(progress)) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift index 23198d9d..da2bea39 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageImageCell.swift @@ -51,10 +51,10 @@ open class NEBasePinMessageImageCell: NEBasePinMessageCell { } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) - if let m = item.chatmodel as? MessageImageModel, let imageUrl = m.imageUrl { + if let m = item.chatmodel as? MessageImageModel, let imageUrl = m.urlString { if imageUrl.hasPrefix("http") { contentImageView.sd_setImage( with: URL(string: imageUrl), diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift index 517a4488..65e8cebb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageLocationCell.swift @@ -31,9 +31,18 @@ open class NEBasePinMessageLocationCell: NEBasePinMessageCell { label.text = chatLocalizable("no_map_plugin") label.textAlignment = .center label.textColor = UIColor.ne_greyText + label.isHidden = true return label }() + let pointImageView = UIImageView() + + public lazy var mapImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + var mapView: UIView? override open func awakeFromNib() { @@ -58,79 +67,82 @@ open class NEBasePinMessageLocationCell: NEBasePinMessageCell { override open func setupUI() { super.setupUI() - let back = UIView() - back.backgroundColor = UIColor.white - contentView.addSubview(back) - back.translatesAutoresizingMaskIntoConstraints = false - back.clipsToBounds = true - back.layer.cornerRadius = 4 - back.layer.borderWidth = 1 - back.layer.borderColor = UIColor.ne_outlineColor.cgColor - - backView.addSubview(back) - contentWidth = back.widthAnchor.constraint(equalToConstant: chat_content_maxW) - contentHeight = back.heightAnchor.constraint(equalToConstant: chat_content_maxW) + let contentBackView = UIView() + contentBackView.backgroundColor = UIColor.white + contentView.addSubview(contentBackView) + contentBackView.translatesAutoresizingMaskIntoConstraints = false + contentBackView.clipsToBounds = true + contentBackView.layer.cornerRadius = 4 + contentBackView.layer.borderWidth = 1 + contentBackView.layer.borderColor = UIColor.ne_outlineColor.cgColor + + backView.addSubview(contentBackView) + contentWidth = contentBackView.widthAnchor.constraint(equalToConstant: chat_content_maxW) + contentHeight = contentBackView.heightAnchor.constraint(equalToConstant: chat_content_maxW) NSLayoutConstraint.activate([ contentWidth!, contentHeight!, - back.leftAnchor.constraint(equalTo: headerView.leftAnchor), - back.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), + contentBackView.leftAnchor.constraint(equalTo: headerView.leftAnchor), + contentBackView.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), ]) - back.addSubview(locationTitleLabel) + contentBackView.addSubview(locationTitleLabel) NSLayoutConstraint.activate([ - locationTitleLabel.leftAnchor.constraint(equalTo: back.leftAnchor, constant: 16), - locationTitleLabel.rightAnchor.constraint(equalTo: back.rightAnchor, constant: -16), - locationTitleLabel.topAnchor.constraint(equalTo: back.topAnchor, constant: 10), + locationTitleLabel.leftAnchor.constraint(equalTo: contentBackView.leftAnchor, constant: 16), + locationTitleLabel.rightAnchor.constraint(equalTo: contentBackView.rightAnchor, constant: -16), + locationTitleLabel.topAnchor.constraint(equalTo: contentBackView.topAnchor, constant: 10), ]) - back.addSubview(subTitleLabel) + contentBackView.addSubview(subTitleLabel) NSLayoutConstraint.activate([ subTitleLabel.leftAnchor.constraint(equalTo: locationTitleLabel.leftAnchor), subTitleLabel.rightAnchor.constraint(equalTo: locationTitleLabel.rightAnchor), subTitleLabel.topAnchor.constraint(equalTo: locationTitleLabel.bottomAnchor, constant: 4), ]) - if let map = NEChatKitClient.instance.delegate?.getCellMapView?() as? UIView { - mapView = map - back.addSubview(map) - map.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - map.leftAnchor.constraint(equalTo: back.leftAnchor), - map.bottomAnchor.constraint(equalTo: back.bottomAnchor), - map.rightAnchor.constraint(equalTo: back.rightAnchor), - map.topAnchor.constraint(equalTo: subTitleLabel.bottomAnchor, constant: 4), - ]) - - let pointImage = UIImageView() - pointImage.translatesAutoresizingMaskIntoConstraints = false - pointImage.image = coreLoader.loadImage("location_point") - map.addSubview(pointImage) - NSLayoutConstraint.activate([ - pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), - pointImage.bottomAnchor.constraint(equalTo: map.bottomAnchor, constant: -30), - ]) - } else { - back.addSubview(emptyLabel) - NSLayoutConstraint.activate([ - emptyLabel.leftAnchor.constraint(equalTo: back.leftAnchor), - emptyLabel.rightAnchor.constraint(equalTo: back.rightAnchor), - emptyLabel.bottomAnchor.constraint(equalTo: back.bottomAnchor, constant: -40), - ]) - } - mapView?.isUserInteractionEnabled = false + contentBackView.addSubview(mapImageView) + NSLayoutConstraint.activate([ + mapImageView.leftAnchor.constraint(equalTo: contentBackView.leftAnchor), + mapImageView.bottomAnchor.constraint(equalTo: contentBackView.bottomAnchor), + mapImageView.rightAnchor.constraint(equalTo: contentBackView.rightAnchor), + mapImageView.topAnchor.constraint(equalTo: subTitleLabel.bottomAnchor, constant: 4), + ]) + + pointImageView.translatesAutoresizingMaskIntoConstraints = false + pointImageView.image = coreLoader.loadImage("location_point") + mapImageView.addSubview(pointImageView) + NSLayoutConstraint.activate([ + pointImageView.centerXAnchor.constraint(equalTo: mapImageView.centerXAnchor), + pointImageView.bottomAnchor.constraint(equalTo: mapImageView.bottomAnchor, constant: -30), + ]) + + contentBackView.addSubview(emptyLabel) + NSLayoutConstraint.activate([ + emptyLabel.leftAnchor.constraint(equalTo: contentBackView.leftAnchor), + emptyLabel.rightAnchor.constraint(equalTo: contentBackView.rightAnchor), + emptyLabel.bottomAnchor.constraint(equalTo: contentBackView.bottomAnchor, constant: -40), + ]) + if let gesture = contentGesture { - back.addGestureRecognizer(gesture) + contentBackView.addGestureRecognizer(gesture) } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) if let m = item.chatmodel as? MessageLocationModel { locationTitleLabel.text = m.title subTitleLabel.text = m.subTitle - if let lat = m.lat, let lng = m.lng, let map = mapView { - NEChatKitClient.instance.delegate?.setMapviewLocation?(lat: lat, lng: lng, mapview: map) + if let lat = m.lat, let lng = m.lng { + if let url = NEChatKitClient.instance.delegate?.getMapImageUrl?(lat: lat, lng: lng) { + NEALog.infoLog(className(), desc: #function + "location image url = \(url)") + mapImageView.sd_setImage(with: URL(string: url)) + emptyLabel.isHidden = true + pointImageView.isHidden = false + } else { + emptyLabel.isHidden = false + pointImageView.isHidden = true + } } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift index 4498509b..7b5cc0fa 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageMultiForwardCell.swift @@ -19,7 +19,7 @@ open class NEBasePinMessageMultiForwardCell: NEBasePinMessageCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { @@ -45,9 +45,9 @@ open class NEBasePinMessageMultiForwardCell: NEBasePinMessageCell { } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) - guard let data = NECustomAttachment.dataOfCustomMessage(message: item.chatmodel.message) else { + guard let data = NECustomAttachment.dataOfCustomMessage(item.chatmodel.message?.attachment) else { return } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift index 539049e7..37cdd91f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageRichTextCell.swift @@ -6,7 +6,7 @@ import UIKit @objcMembers open class NEBasePinMessageRichTextCell: NEBasePinMessageTextCell { - lazy var titleLabel: UILabel = { + public lazy var titleLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) label.textColor = .ne_darkText @@ -48,7 +48,7 @@ open class NEBasePinMessageRichTextCell: NEBasePinMessageTextCell { titleLabel.addGestureRecognizer(titleGesture) } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) if let model = item.chatmodel as? MessageRichTextModel { titleLabel.attributedText = model.titleAttributeStr diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift index 74fcbc82..6bf09770 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageTextCell.swift @@ -6,13 +6,14 @@ import UIKit @objcMembers open class NEBasePinMessageTextCell: NEBasePinMessageCell { - lazy var contentLabel: UILabel = { + public lazy var contentLabel: UILabel = { let label = UILabel() label.font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.pinMessageTextSize) label.textColor = .ne_darkText label.translatesAutoresizingMaskIntoConstraints = false label.isUserInteractionEnabled = true label.numberOfLines = 3 + label.accessibilityIdentifier = "id.message" return label }() @@ -64,7 +65,7 @@ open class NEBasePinMessageTextCell: NEBasePinMessageCell { } } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) if let model = item.chatmodel as? MessageTextModel { contentLabel.attributedText = model.attributeStr diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift index 26b70bc1..e40ec909 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/NEBasePinMessageVideoCell.swift @@ -2,19 +2,20 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NIMSDK import UIKit @objcMembers open class NEBasePinMessageVideoCell: NEBasePinMessageImageCell { - lazy var stateView: VideoStateView = { + public lazy var stateView: VideoStateView = { let state = VideoStateView() state.translatesAutoresizingMaskIntoConstraints = false state.backgroundColor = .clear return state }() - lazy var videoTimeLabel: UILabel = { + public lazy var videoTimeLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = .white @@ -23,7 +24,7 @@ open class NEBasePinMessageVideoCell: NEBasePinMessageImageCell { return label }() - lazy var timeView: UIView = { + public lazy var timeView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.addSubview(videoTimeLabel) @@ -67,27 +68,20 @@ open class NEBasePinMessageVideoCell: NEBasePinMessageImageCell { stateView.isUserInteractionEnabled = false } - override open func configure(_ item: PinMessageModel) { + override open func configure(_ item: NEPinMessageModel) { super.configure(item) - if let videoObject = item.chatmodel.message?.messageObject as? NIMVideoObject { - if let path = videoObject.coverUrl { - contentImageView.sd_setImage( - with: URL(string: path), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } else { - contentImageView.sd_setImage( - with: URL(string: videoObject.coverUrl ?? ""), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } + if let videoObject = item.chatmodel.message?.attachment as? V2NIMMessageVideoAttachment { + // 获取首帧 + let videoUrl = videoObject.url ?? "" + let thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + contentImageView.sd_setImage( + with: URL(string: thumbUrl), + placeholderImage: nil, + options: .retryFailed, + progress: nil, + completed: nil + ) if videoObject.duration > 0 { timeView.isHidden = false diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift index 93483e69..836e3b08 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserBaseTableViewCell.swift @@ -3,20 +3,22 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NEChatKit +import NECoreIM2Kit import UIKit @objcMembers open class UserBaseTableViewCell: UITableViewCell { - public lazy var avatarImage: UIImageView = { - let avatarImage = UIImageView() - avatarImage.backgroundColor = UIColor(hexString: "#537FF4") - avatarImage.translatesAutoresizingMaskIntoConstraints = false - avatarImage.clipsToBounds = true - avatarImage.isUserInteractionEnabled = true - avatarImage.contentMode = .scaleAspectFill - avatarImage.accessibilityIdentifier = "id.avatar" - return avatarImage + /// 用户头像 + public lazy var avatarImageView: UIImageView = { + let avatarImageView = UIImageView() + avatarImageView.backgroundColor = UIColor(hexString: "#537FF4") + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + avatarImageView.clipsToBounds = true + avatarImageView.isUserInteractionEnabled = true + avatarImageView.contentMode = .scaleAspectFill + avatarImageView.accessibilityIdentifier = "id.avatar" + return avatarImageView }() public lazy var nameLabel: UILabel = { @@ -38,7 +40,7 @@ open class UserBaseTableViewCell: UITableViewCell { return titleLabel }() - public var userModel: NEKitUser? + public var userModel: NETeamMemberInfoModel? override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -48,50 +50,50 @@ open class UserBaseTableViewCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func baseCommonUI() { selectionStyle = .none backgroundColor = .white - contentView.addSubview(avatarImage) + contentView.addSubview(avatarImageView) contentView.addSubview(nameLabel) contentView.addSubview(titleLabel) // name NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), - nameLabel.rightAnchor.constraint(equalTo: avatarImage.rightAnchor), - nameLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor), - nameLabel.bottomAnchor.constraint(equalTo: avatarImage.bottomAnchor), + nameLabel.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), + nameLabel.rightAnchor.constraint(equalTo: avatarImageView.rightAnchor), + nameLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor), + nameLabel.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), ]) titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.text = "placeholder" } - open func setModel(_ model: NEKitUser) { + open func setModel(_ model: NETeamMemberInfoModel) { userModel = model - nameLabel.text = model.shortName(showAlias: false, count: 2) - titleLabel.text = model.showName() + nameLabel.text = NEFriendUserCache.getShortName(model.showNickInTeam() ?? "") + titleLabel.text = model.atNameInTeam() - if let avatarURL = model.userInfo?.avatarUrl, !avatarURL.isEmpty { - avatarImage + if let avatarURL = model.nimUser?.user?.avatar, !avatarURL.isEmpty { + avatarImageView .sd_setImage(with: URL(string: avatarURL)) { [weak self] image, error, type, url in if image != nil { - self?.avatarImage.image = image + self?.avatarImageView.image = image self?.nameLabel.isHidden = true - self?.avatarImage.backgroundColor = .clear + self?.avatarImageView.backgroundColor = .clear } else { - self?.avatarImage.image = nil + self?.avatarImageView.image = nil self?.nameLabel.isHidden = false - self?.avatarImage.backgroundColor = UIColor.colorWithString(string: model.userId) + self?.avatarImageView.backgroundColor = UIColor.colorWithString(string: model.teamMember?.accountId) } } } else { - avatarImage.image = nil + avatarImageView.image = nil nameLabel.isHidden = false - avatarImage.backgroundColor = UIColor.colorWithString(string: model.userId) + avatarImageView.backgroundColor = UIColor.colorWithString(string: model.teamMember?.accountId) } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift index d61d5336..3b60f9b9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatActivityIndicatorView.swift @@ -16,22 +16,22 @@ public enum ChatSendMessageStatus: Int { open class ChatActivityIndicatorView: UIView { public var messageStatus: ChatSendMessageStatus? { didSet { - failBtn.isHidden = true - activity.isHidden = true - activity.stopAnimating() + failButton.isHidden = true + activityView.isHidden = true + activityView.stopAnimating() switch messageStatus { case .sending: - self.isHidden = false - activity.isHidden = false - failBtn.isHidden = true - activity.startAnimating() + isHidden = false + activityView.isHidden = false + failButton.isHidden = true + activityView.startAnimating() case .failed: - self.isHidden = false - activity.isHidden = true - failBtn.isHidden = false + isHidden = false + activityView.isHidden = true + failButton.isHidden = false case .successed: - self.isHidden = true + isHidden = true default: print("default") @@ -39,49 +39,47 @@ open class ChatActivityIndicatorView: UIView { } } + public lazy var failButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.contentMode = .scaleAspectFit + button.setImage(UIImage.ne_imageNamed(name: "sendMessage_failed"), for: .normal) + button.accessibilityIdentifier = "id.sendMessageFailed" + return button + }() + + private lazy var activityView: UIActivityIndicatorView = { + let activityView = UIActivityIndicatorView() + activityView.translatesAutoresizingMaskIntoConstraints = false + activityView.color = .gray + return activityView + }() + override init(frame: CGRect) { super.init(frame: frame) commonUI() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { backgroundColor = .clear - addSubview(failBtn) - addSubview(activity) + addSubview(failButton) + addSubview(activityView) NSLayoutConstraint.activate([ - failBtn.topAnchor.constraint(equalTo: topAnchor), - failBtn.leftAnchor.constraint(equalTo: leftAnchor), - failBtn.bottomAnchor.constraint(equalTo: bottomAnchor), - failBtn.rightAnchor.constraint(equalTo: rightAnchor), + failButton.topAnchor.constraint(equalTo: topAnchor), + failButton.leftAnchor.constraint(equalTo: leftAnchor), + failButton.bottomAnchor.constraint(equalTo: bottomAnchor), + failButton.rightAnchor.constraint(equalTo: rightAnchor), ]) NSLayoutConstraint.activate([ - activity.topAnchor.constraint(equalTo: topAnchor), - activity.leftAnchor.constraint(equalTo: leftAnchor), - activity.bottomAnchor.constraint(equalTo: bottomAnchor), - activity.rightAnchor.constraint(equalTo: rightAnchor), + activityView.topAnchor.constraint(equalTo: topAnchor), + activityView.leftAnchor.constraint(equalTo: leftAnchor), + activityView.bottomAnchor.constraint(equalTo: bottomAnchor), + activityView.rightAnchor.constraint(equalTo: rightAnchor), ]) } - - // MARK: lazy Method - - public lazy var failBtn: UIButton = { - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.imageView?.contentMode = .center - button.setBackgroundImage(UIImage.ne_imageNamed(name: "sendMessage_failed"), for: .normal) - button.accessibilityIdentifier = "id.sendMessageFailed" - return button - }() - - private lazy var activity: UIActivityIndicatorView = { - let activity = UIActivityIndicatorView() - activity.translatesAutoresizingMaskIntoConstraints = false - activity.color = .gray - return activity - }() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift index a60b7cf4..d4485a48 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatBrokenNetworkView.swift @@ -7,32 +7,33 @@ import UIKit @objcMembers open class ChatBrokenNetworkView: UIView { + /// 内容文本 + private lazy var contentLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = DefaultTextFont(14) + label.textColor = HexRGB(0xFC596A) + label.textAlignment = .center + label.text = commonLocalizable("network_error") + return label + }() + override init(frame: CGRect) { super.init(frame: frame) commonUI() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { backgroundColor = HexRGB(0xFEE3E6) - addSubview(content) + addSubview(contentLabel) NSLayoutConstraint.activate([ - content.leftAnchor.constraint(equalTo: leftAnchor, constant: 15), - content.centerYAnchor.constraint(equalTo: centerYAnchor), - content.rightAnchor.constraint(equalTo: rightAnchor, constant: -15), + contentLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 15), + contentLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + contentLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -15), ]) } - - private lazy var content: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = DefaultTextFont(14) - label.textColor = HexRGB(0xFC596A) - label.textAlignment = .center - 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 87118f70..c91ce70b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatRecordView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatRecordView.swift @@ -26,7 +26,7 @@ open class ChatRecordView: UIView, UIGestureRecognizerDelegate { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { @@ -35,6 +35,7 @@ open class ChatRecordView: UIView, UIGestureRecognizerDelegate { topTipLabel.font = UIFont.systemFont(ofSize: 12) topTipLabel.textColor = .ne_lightText topTipLabel.textAlignment = .center + topTipLabel.isHidden = true // 不展示 addSubview(topTipLabel) NSLayoutConstraint.activate([ topTipLabel.topAnchor.constraint(equalTo: topAnchor, constant: 0), diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift index df1b9fb5..1cf574ac 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CirleProgressView.swift @@ -52,19 +52,19 @@ open class CirleProgressView: UIView { clockwise: false ) borderLayer.path = borderPath.cgPath - borderLayer.strokeColor = UIColor.ne_blueText.cgColor + borderLayer.strokeColor = UIColor.ne_normalTheme.cgColor borderLayer.fillColor = UIColor.clear.cgColor borderLayer.lineWidth = 2 borderLayer.frame = bounds layer.addSublayer(borderLayer) sectorLayer.frame = bounds - sectorLayer.fillColor = UIColor.ne_blueText.cgColor + sectorLayer.fillColor = UIColor.ne_normalTheme.cgColor layer.addSublayer(sectorLayer) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } private func drawCircle(progress: Float) { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CopyableLabel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CopyableLabel.swift deleted file mode 100644 index 1ea008b1..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CopyableLabel.swift +++ /dev/null @@ -1,70 +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 MobileCoreServices -import NECommonUIKit -import UIKit - -@objcMembers -open class CopyableLabel: UILabel { - var copyString: String? - - override public var canBecomeFirstResponder: Bool { - true - } - - override init(frame: CGRect) { - super.init(frame: frame) - sharedInit() - } - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - sharedInit() - } - - func sharedInit() { - isUserInteractionEnabled = true - addGestureRecognizer(UILongPressGestureRecognizer( - target: self, - action: #selector(showMenu(sender:)) - )) - } - - func copyText(_ sender: Any?) { - if let copy = copyString { - UIPasteboard.general.string = copy - } - UIMenuController.shared.setMenuVisible(false, animated: true) - makeToast(chatLocalizable("copy_success"), duration: 2, position: .bottom) - } - - override open func copy(_ sender: Any?) { - if let attribute = attributedText { - if let data = try? attribute.data(from: NSMakeRange(0, attribute.length)) { - UIPasteboard.general.setData(data, forPasteboardType: (kUTTypeRTF as NSString) as String) - } - } - UIMenuController.shared.setMenuVisible(false, animated: true) - } - - func showMenu(sender: Any?) { - becomeFirstResponder() - let menu = UIMenuController.shared - if !menu.isMenuVisible { - let copyMenu = UIMenuItem(title: chatLocalizable("operation_copy"), action: #selector(copyText)) - menu.menuItems = [copyMenu] - menu.setTargetRect(bounds, in: self) - menu.setMenuVisible(true, animated: true) - } - } - - override open func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { - if action == #selector(copyText) { - return true - } - return false - } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift index 19b1b58c..0efe4ccf 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/MessageOperationView.swift @@ -57,7 +57,8 @@ open class MessageOperationView: UIView, UICollectionViewDataSource, UICollectio } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + collcetionView = UICollectionView(frame: .zero) + super.init(coder: coder) } // MARK: UICollectionViewDataSource diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift index 4d054363..9ba6cabd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEBaseChatInputView.swift @@ -88,6 +88,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, let button = ExpandButton() button.translatesAutoresizingMaskIntoConstraints = false button.backgroundColor = .clear + button.accessibilityIdentifier = "id.chatExpandButton" return button }() @@ -101,13 +102,80 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, public var textviewLeftConstraint: NSLayoutConstraint? public var textviewRightConstraint: NSLayoutConstraint? + public var multipleLineView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.white + view.clipsToBounds = true + view.layer.cornerRadius = 6.0 + view.isHidden = true + + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOpacity = 0.5 + view.layer.shadowOffset = CGSize(width: 3, height: 3) + view.layer.shadowRadius = 5 + view.layer.masksToBounds = false + return view + }() + + public var titleField: UITextField = { + let textField = UITextField() + textField.translatesAutoresizingMaskIntoConstraints = false + textField.font = UIFont.systemFont(ofSize: 18.0) + textField.textColor = .ne_darkText + textField.returnKeyType = .send + textField.attributedPlaceholder = NSAttributedString(string: coreLoader.localizable("multiple_line_placleholder"), attributes: [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText]) + textField.addTarget(self, action: #selector(textFieldChange), for: .editingChanged) + return textField + }() + + public var multipleLineExpandButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = .clear + return button + }() + + public var multipleSendButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = .clear + button.setImage(coreLoader.loadImage("multiple_send_image"), for: .normal) + return button + }() + + public lazy var emojiView: UIView = { + let backView = UIView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) + let view = + InputEmoticonContainerView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) + view.delegate = self + backView.isHidden = true + + backView.backgroundColor = UIColor.clear + backView.addSubview(view) + let tap = UITapGestureRecognizer() + backView.addGestureRecognizer(tap) + tap.addTarget(self, action: #selector(missClickEmoj)) + return backView + }() + + public lazy var chatAddMoreView: NEChatMoreActionView = { + let view = NEChatMoreActionView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + view.delegate = self + return view + }() + + public var multipleLineViewHeight: NSLayoutConstraint? + override init(frame: CGRect) { super.init(frame: frame) commonUI() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } deinit { @@ -154,31 +222,6 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, atCache?.clean() } - // MARK: ===================== lazy method ===================== - - public lazy var emojiView: UIView = { - let backView = UIView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) - let view = - InputEmoticonContainerView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) - view.delegate = self - backView.isHidden = true - - backView.backgroundColor = UIColor.clear - backView.addSubview(view) - let tap = UITapGestureRecognizer() - backView.addGestureRecognizer(tap) - tap.addTarget(self, action: #selector(missClickEmoj)) - return backView - }() - - public lazy var chatAddMoreView: NEChatMoreActionView = { - let view = NEChatMoreActionView(frame: CGRect(x: 0, y: 0, width: kScreenWidth, height: 200)) - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - view.delegate = self - return view - }() - open func textViewDidChange(_ textView: UITextView) { delegate?.textFieldDidChange(textView.text) } @@ -206,7 +249,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, guard let findColor = value as? UIColor else { return } - if isEqualToColor(findColor, UIColor.ne_blueText) == false { + if isEqualToColor(findColor, UIColor.ne_normalTheme) == false { return } if findRange.location <= start, start < findRange.location + findRange.length + atRangeOffset { @@ -228,7 +271,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, guard let findColor = value as? UIColor else { return } - if isEqualToColor(findColor, UIColor.ne_blueText) == false { + if isEqualToColor(findColor, UIColor.ne_normalTheme) == false { return } let findStart = findRange.location @@ -267,6 +310,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, let addString = NEEmotionTool.getAttWithStr(str: text, font: .systemFont(ofSize: 16)) mutaString.replaceCharacters(in: range, with: addString) textView.attributedText = mutaString + textView.accessibilityValue = text DispatchQueue.main.async { textView.selectedRange = NSMakeRange(range.location + addString.length, 0) } @@ -305,6 +349,9 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, if let findRange = findShowPosition(range: range, attribute: textView.attributedText) { textView.selectedRange = NSMakeRange(findRange.location + findRange.length, 0) } + + textView.scrollRangeToVisible(NSMakeRange(textView.selectedRange.location, 1)) + textView.accessibilityValue = getRealSendText(textView.attributedText) } @available(iOS 10.0, *) @@ -314,12 +361,6 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, return true } -// @available(iOS 10.0, *) -// open func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { -// -// return true -// } - open func buttonEvent(button: UIButton) { button.isSelected = !button.isSelected if button.tag - 5 != 2, button != currentButton { @@ -349,37 +390,32 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, textView.deleteBackward() print("delete ward") } else { - if let font = textView.font { - let attribute = NEEmotionTool.getAttWithStr( - str: description, - font: font, - CGPoint(x: 0, y: -4) - ) - print("attribute : ", attribute) - let mutaAttribute = NSMutableAttributedString() - if let origin = textView.attributedText { - mutaAttribute.append(origin) - } - attribute.enumerateAttribute( - NSAttributedString.Key.attachment, - in: NSMakeRange(0, attribute.length) - ) { value, range, stop in - if let neAttachment = value as? NEEmotionAttachment { - print("ne attachment bounds ", neAttachment.bounds) - } - } - mutaAttribute.append(attribute) - mutaAttribute.addAttribute( - NSAttributedString.Key.font, - value: font, - range: NSMakeRange(0, mutaAttribute.length) - ) - textView.attributedText = mutaAttribute - textView.scrollRangeToVisible(NSMakeRange(textView.attributedText.length, 1)) - } + let range = textView.selectedRange + let attribute = NEEmotionTool.getAttWithStr(str: description, font: .systemFont(ofSize: 16)) + let mutaAttribute = NSMutableAttributedString(attributedString: textView.attributedText) + mutaAttribute.insert(attribute, at: range.location) + textView.attributedText = mutaAttribute + textView.selectedRange = NSMakeRange(range.location + attribute.length, 0) } } + /// 点击富文本图片 + public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + textView.becomeFirstResponder() + + var offset = characterRange.location + // 修复iOS 14.1,点击空白识别为点击最后一个富文本图片的问题,待优化 + if characterRange.location + characterRange.length == textView.text.count { + offset += 1 + } + + if let newPosition = textView.position(from: textView.beginningOfDocument, offset: offset) { + textView.selectedTextRange = textView.textRange(from: newPosition, to: newPosition) + } + + return true + } + open func didPressSend(sender: UIButton) { sendText(textView: textView) } @@ -415,10 +451,6 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, delegate?.endRecord(insideView: insideView) } -// func textFieldChangeNoti() { -// delegate?.textFieldDidChange(textField) -// } - func getRealSendText(_ attribute: NSAttributedString) -> String? { let muta = NSMutableString() @@ -451,7 +483,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, guard let findColor = value as? UIColor else { return } - if isEqualToColor(findColor, UIColor.ne_blueText) == false { + if isEqualToColor(findColor, UIColor.ne_normalTheme) == false { return } if let range = Range(findRange, in: string) { @@ -491,33 +523,84 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, return nil } - open func getAtRemoteExtension() -> [String: Any]? { - var atDic = [String: Any]() - NELog.infoLog(className(), desc: "at range cache : \(atRangeCache)") - atRangeCache.forEach { (key: String, value: MessageAtCacheModel) in - if let userValue = atDic[value.accid] as? [String: AnyObject], var array = userValue[atSegmentsKey] as? [Any], let object = value.atModel.yx_modelToJSONObject() { - array.append(object) - if var dic = atDic[value.accid] as? [String: Any] { - dic[atSegmentsKey] = array - atDic[value.accid] = dic + open func getAtRemoteExtension(_ attri: NSAttributedString?) -> [String: Any]? { + guard let attribute = attri else { + return nil + } + var atDic = [String: [String: Any]]() + let string = attribute.string + 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_normalTheme) == false { + return + } + if let range = Range(findRange, in: string) { + let text = string[range] + let model = MessageAtInfoModel() + print("range text : ", String(text)) + // 计算at前有表情导致索引新增的数量 + let expandIndex = getConvertedExtraIndex(attribute.attributedSubstring(from: NSRange(location: 0, length: findRange.location))) + print("expand index value ", expandIndex) + model.start = findRange.location + expandIndex + let nameExpandCount = getConvertedExtraIndex(attribute.attributedSubstring(from: findRange)) + print("name expand index value ", nameExpandCount) + model.end = model.start + findRange.length + nameExpandCount + print("model start : ", model.start, " model end : ", model.end) + var dic: [String: Any]? + var array: [Any]? + if let accid = nickAccidDic[String(text)] { + if let atCacheDic = atDic[accid] { + dic = atCacheDic + } else { + dic = [String: Any]() + } + + if let atCacheArray = dic?[atSegmentsKey] as? [Any] { + array = atCacheArray + } else { + array = [Any]() + } + + if let object = model.yx_modelToJSONObject() { + array?.append(object) + } + dic?[atSegmentsKey] = array + dic?[atTextKey] = String(text) + " " + dic?[#keyPath(MessageAtCacheModel.accid)] = accid + atDic[accid] = dic } - } else if let object = value.atModel.yx_modelToJSONObject() { - var array = [Any]() - array.append(object) - var dic = [String: Any]() - dic[atTextKey] = value.text - dic[atSegmentsKey] = array - atDic[value.accid] = dic } } - NELog.infoLog(className(), desc: "at dic value : \(atDic)") if atDic.count > 0 { return [yxAtMsg: atDic] } return nil } - open func cleartAtCache() { + /// 把表情转换成字符编码计算index的增量 + /// - Parameter attribute: at 文本前的文本 + func getConvertedExtraIndex(_ attribute: NSAttributedString) -> Int { + var count = 0 + attribute.enumerateAttributes( + in: NSMakeRange(0, attribute.length), + options: NSAttributedString.EnumerationOptions(rawValue: 0) + ) { dics, range, stop in + if let neAttachment = dics[NSAttributedString.Key.attachment] as? NEEmotionAttachment { + if let tagCount = neAttachment.emotion?.tag?.count { + print(" \(count) getConvertedExtraIndex tag : ", neAttachment.emotion?.tag as Any) + count = count + tagCount - 1 + } + } + } + return count + } + + open func clearAtCache() { nickAccidDic.removeAll() } @@ -588,50 +671,6 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, } } - public var multipleLineView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.white - view.clipsToBounds = true - view.layer.cornerRadius = 6.0 - view.isHidden = true - - view.layer.shadowColor = UIColor.black.cgColor - view.layer.shadowOpacity = 0.5 - view.layer.shadowOffset = CGSize(width: 3, height: 3) - view.layer.shadowRadius = 5 - view.layer.masksToBounds = false - return view - }() - - public var titleField: UITextField = { - let text = UITextField() - text.translatesAutoresizingMaskIntoConstraints = false - text.font = UIFont.systemFont(ofSize: 18.0) - text.textColor = .ne_darkText - text.returnKeyType = .send - text.attributedPlaceholder = NSAttributedString(string: coreLoader.localizable("multiple_line_placleholder"), attributes: [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText]) - text.addTarget(self, action: #selector(textFieldChange), for: .editingChanged) - return text - }() - - public var multipleLineExpandButton: ExpandButton = { - let button = ExpandButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.backgroundColor = .clear - return button - }() - - public var multipleSendButton: ExpandButton = { - let button = ExpandButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.backgroundColor = .clear - button.setImage(coreLoader.loadImage("multiple_send_image"), for: .normal) - return button - }() - - public var multipleLineViewHeight: NSLayoutConstraint? - func setupMultipleLineView() { addSubview(multipleLineView) multipleLineViewHeight = multipleLineView.heightAnchor.constraint(equalToConstant: 400) @@ -710,7 +749,7 @@ open class NEBaseChatInputView: UIView, ChatRecordViewDelegate, } open func setMuteInputStyle() { - cleartAtCache() + clearAtCache() expandButton.isEnabled = false textView.attributedText = nil textView.text = nil diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift index 2498f77e..ebb0acc1 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEChatMoreActionView.swift @@ -25,6 +25,31 @@ open class NEChatMoreActionView: UIView { public weak var delegate: NEMoreViewDelegate? + /// 更多面板列表 + lazy var collcetionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 0 + layout.minimumInteritemSpacing = 0 + layout.sectionInset = UIEdgeInsets(top: 0, left: NEMoreView_Section_Padding, bottom: 0, right: NEMoreView_Section_Padding) + self.moreFlowLayout = layout + let collcetionView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collcetionView.backgroundColor = UIColor.ne_backgroundColor + collcetionView.translatesAutoresizingMaskIntoConstraints = false + collcetionView.dataSource = self + collcetionView.delegate = self + collcetionView.isUserInteractionEnabled = true + collcetionView.isPagingEnabled = true + collcetionView.showsHorizontalScrollIndicator = false + collcetionView.showsVerticalScrollIndicator = false + collcetionView.alwaysBounceHorizontal = true + collcetionView.register( + NEInputMoreCell.self, + forCellWithReuseIdentifier: NEMoreCell_ReuseId + ) + return collcetionView + }() + override init(frame: CGRect) { super.init(frame: frame) addSubview(collcetionView) @@ -32,7 +57,7 @@ open class NEChatMoreActionView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func configData(data: [NEMoreItemModel]) { @@ -73,33 +98,6 @@ open class NEChatMoreActionView: UIView { // let height: CGFloat = collcetionView.frame.origin.y + collcetionView.height + NEMoreView_Margin } - - // MARK: 懒加载方法 - - lazy var collcetionView: UICollectionView = { - let layout = UICollectionViewFlowLayout() - layout.scrollDirection = .horizontal -// layout.itemSize = CGSize(width: 56, height: 80) - layout.minimumLineSpacing = 0 - layout.minimumInteritemSpacing = 0 - layout.sectionInset = UIEdgeInsets(top: 0, left: NEMoreView_Section_Padding, bottom: 0, right: NEMoreView_Section_Padding) - self.moreFlowLayout = layout - let collcetionView = UICollectionView(frame: .zero, collectionViewLayout: layout) - collcetionView.backgroundColor = UIColor.ne_backgroundColor - collcetionView.translatesAutoresizingMaskIntoConstraints = false - collcetionView.dataSource = self - collcetionView.delegate = self - collcetionView.isUserInteractionEnabled = true - collcetionView.isPagingEnabled = true - collcetionView.showsHorizontalScrollIndicator = false - collcetionView.showsVerticalScrollIndicator = false - collcetionView.alwaysBounceHorizontal = true - collcetionView.register( - NEInputMoreCell.self, - forCellWithReuseIdentifier: NEMoreCell_ReuseId - ) - return collcetionView - }() } extension NEChatMoreActionView: UICollectionViewDataSource, UICollectionViewDelegate, UICollectionViewDelegateFlowLayout { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift index 7f151a83..7fe15cee 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEInputMoreCell.swift @@ -8,6 +8,25 @@ import UIKit @objcMembers open class NEInputMoreCell: UICollectionViewCell { public var cellData: NEMoreItemModel? + /// 功能标识图片 + public lazy var avatarImageView: UIImageView = { + let imageView = UIImageView() + imageView.isUserInteractionEnabled = true + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.accessibilityIdentifier = "id.actionIcon" + return imageView + }() + + /// 功能说明文本 + public lazy var titleLabel: UILabel = { + let titleLabel = UILabel() + titleLabel.textColor = UIColor.ne_greyText + titleLabel.font = UIFont.systemFont(ofSize: 10) + titleLabel.textAlignment = .center + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.accessibilityIdentifier = "id.menuIcon" + return titleLabel + }() override init(frame: CGRect) { super.init(frame: frame) @@ -19,45 +38,27 @@ open class NEInputMoreCell: UICollectionViewCell { } func setupViews() { - contentView.addSubview(avatarImage) + contentView.addSubview(avatarImageView) contentView.addSubview(titleLabel) NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: contentView.leftAnchor), - avatarImage.topAnchor.constraint(equalTo: contentView.topAnchor), - avatarImage.widthAnchor.constraint(equalToConstant: NEMoreCell_Image_Size.width), - avatarImage.heightAnchor.constraint(equalToConstant: NEMoreCell_Image_Size.height), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: NEMoreCell_Image_Size.width), + avatarImageView.heightAnchor.constraint(equalToConstant: NEMoreCell_Image_Size.height), ]) NSLayoutConstraint.activate([ - titleLabel.topAnchor.constraint(equalTo: avatarImage.bottomAnchor), + titleLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor), titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor), titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor), titleLabel.heightAnchor.constraint(equalToConstant: NEMoreCell_Title_Height), ]) } - lazy var avatarImage: UIImageView = { - let imageView = UIImageView() - imageView.isUserInteractionEnabled = true - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.accessibilityIdentifier = "id.actionIcon" - return imageView - }() - - lazy var titleLabel: UILabel = { - let title = UILabel() - title.textColor = UIColor.ne_greyText - title.font = UIFont.systemFont(ofSize: 10) - title.textAlignment = .center - title.translatesAutoresizingMaskIntoConstraints = false - title.accessibilityIdentifier = "id.menuIcon" - return title - }() - func config(_ itemModel: NEMoreItemModel) { cellData = itemModel - avatarImage.image = itemModel.customImage == nil ? itemModel.image : itemModel.customImage + avatarImageView.image = itemModel.customImage == nil ? itemModel.image : itemModel.customImage titleLabel.text = itemModel.title } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift index ce90656f..dc59e387 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/NEMutilSelectBottomView.swift @@ -21,15 +21,15 @@ open class NEMutilSelectBottomView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupSubview() { // 逐条转发 addSubview(singleForwardButton) buttonTopAnchor = singleForwardButton.topAnchor.constraint(equalTo: topAnchor, constant: 12) + buttonTopAnchor?.isActive = true NSLayoutConstraint.activate([ - buttonTopAnchor!, singleForwardButton.centerXAnchor.constraint(equalTo: centerXAnchor), singleForwardButton.widthAnchor.constraint(equalToConstant: 48), singleForwardButton.heightAnchor.constraint(equalToConstant: 48), diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift index 380faa5a..749bca93 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ReplyView.swift @@ -50,7 +50,7 @@ open class ReplyView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } // @objc func closeButtonEvent(button: UIButton) { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/TopMessageView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/TopMessageView.swift new file mode 100644 index 00000000..0569bf87 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/TopMessageView.swift @@ -0,0 +1,165 @@ + +// 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 + +public protocol TopMessageViewDelegate: AnyObject { + func didClickCloseButton() + func didTapTopMessageView() +} + +@objcMembers +open class TopMessageView: UIView { + public weak var delegate: TopMessageViewDelegate? + var topContentLabelLeftAnchor: NSLayoutConstraint? // 顶部文案左侧约束 + var content: String? + + /// 置顶 icon + public lazy var topImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + /// 缩略图 + public lazy var thumbImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + + imageView.addSubview(playIcon) + NSLayoutConstraint.activate([ + playIcon.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + playIcon.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), + playIcon.widthAnchor.constraint(equalToConstant: 16), + playIcon.heightAnchor.constraint(equalToConstant: 16), + ]) + + return imageView + }() + + /// 缩略图播放图片 + public lazy var playIcon: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = NECommonUIKit.coreLoader.loadImage("video_play") + return imageView + }() + + /// 置顶文案 + public lazy var topContentLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 12) + label.textColor = .ne_darkText + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.topContent" + return label + }() + + /// 关闭按钮 + public lazy var closeButton: UIButton = { + let button = UIButton(type: .custom) + button.setImage(UIImage.ne_imageNamed(name: "top_close"), for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityIdentifier = "id.topClose" + button.addTarget(self, action: #selector(didClickCloseButton), for: .touchUpInside) + + return button + }() + + override init(frame: CGRect) { + super.init(frame: frame) + backgroundColor = .white + layer.cornerRadius = 8 + + addSubview(topImageView) + NSLayoutConstraint.activate([ + topImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: chat_content_margin), + topImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + topImageView.widthAnchor.constraint(equalToConstant: 18), + topImageView.heightAnchor.constraint(equalToConstant: 18), + ]) + + addSubview(thumbImageView) + NSLayoutConstraint.activate([ + thumbImageView.leftAnchor.constraint(equalTo: topImageView.rightAnchor, constant: chat_content_margin), + thumbImageView.centerYAnchor.constraint(equalTo: centerYAnchor), + thumbImageView.widthAnchor.constraint(equalToConstant: 28), + thumbImageView.heightAnchor.constraint(equalToConstant: 28), + ]) + + addSubview(topContentLabel) + topContentLabelLeftAnchor = topContentLabel.leftAnchor.constraint(equalTo: topImageView.rightAnchor, constant: chat_content_margin) + topContentLabelLeftAnchor?.isActive = true + NSLayoutConstraint.activate([ + topContentLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -44), + topContentLabel.centerYAnchor.constraint(equalTo: centerYAnchor), + ]) + + addSubview(closeButton) + NSLayoutConstraint.activate([ + closeButton.rightAnchor.constraint(equalTo: rightAnchor, constant: -chat_content_margin), + closeButton.centerYAnchor.constraint(equalTo: centerYAnchor), + closeButton.widthAnchor.constraint(equalToConstant: 30), + closeButton.heightAnchor.constraint(equalToConstant: 30), + ]) + + let tap = UITapGestureRecognizer(target: self, action: #selector(tapTopMessageView)) + addGestureRecognizer(tap) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// 设置置顶文案 + /// - Parameter name: 置顶消息发送者昵称 + /// - Parameter content: 置顶消息内容文案 + /// - Parameter url: 置顶消息 图片缩略图/视频首帧 地址 + /// - Parameter isVideo: 置顶消息是否是视频消息 + /// - Parameter hideClose: 是否隐藏移除置顶按钮 + public func setTopContent(name: String?, content: String?, url: String?, isVideo: Bool, hideClose: Bool) { + self.content = content + updateTopName(name: name) + + if let url = url { + thumbImageView.isHidden = false + thumbImageView.sd_setImage(with: URL(string: url), placeholderImage: nil) + playIcon.isHidden = !isVideo + topContentLabelLeftAnchor?.constant = chat_content_margin + 32 + } else { + thumbImageView.isHidden = true + topContentLabelLeftAnchor?.constant = chat_content_margin + } + + closeButton.isHidden = hideClose + } + + /// 设置(更新)置顶消息发送者昵称 + /// - Parameter content: 发送者昵称 + public func updateTopName(name: String?) { + var text = "" + + if let fullName = name, !fullName.isEmpty { + let cutName = NEFriendUserCache.getCutName(fullName) + text = cutName + ":" + } + + text += content ?? "" + topContentLabel.text = text + } + + /// 点击关闭按钮 + func didClickCloseButton() { + removeFromSuperview() + delegate?.didClickCloseButton() + } + + /// 点击置顶视图 + func tapTopMessageView() { + delegate?.didTapTopMessageView() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapAddressCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapAddressCell.swift deleted file mode 100644 index b60e2c77..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapAddressCell.swift +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NEChatKit -import UIKit - -@objcMembers -open class NEMapAddressCell: UITableViewCell { - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - selectionStyle = .none - setupSubviews() - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupSubviews() { - selectionStyle = .none - contentView.addSubview(locationImg) - contentView.addSubview(selectImg) - contentView.addSubview(title) - contentView.addSubview(subTitle) - contentView.addSubview(bottomLine) - - NSLayoutConstraint.activate([ - locationImg.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 15), - locationImg.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 17), - locationImg.heightAnchor.constraint(equalToConstant: 18), - locationImg.widthAnchor.constraint(equalToConstant: 18), - ]) - - NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: locationImg.rightAnchor, constant: 7), - title.centerYAnchor.constraint(equalTo: locationImg.centerYAnchor), - title.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -70), - ]) - - NSLayoutConstraint.activate([ - subTitle.leftAnchor.constraint(equalTo: title.leftAnchor), - subTitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), - subTitle.rightAnchor.constraint(equalTo: title.rightAnchor), - ]) - - NSLayoutConstraint.activate([ - selectImg.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -13), - selectImg.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - ]) - - NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 12), - bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -12), - bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -1), - bottomLine.heightAnchor.constraint(equalToConstant: 1), - ]) - } - - public lazy var locationImg: UIImageView = { - let location = UIImageView(image: UIImage.ne_imageNamed(name: "chat_loacaiton_img")) - location.translatesAutoresizingMaskIntoConstraints = false - return location - }() - - public lazy var selectImg: UIImageView = { - let img = UIImageView(image: UIImage.ne_imageNamed(name: "chat_map_select")) - img.translatesAutoresizingMaskIntoConstraints = false - return img - }() - - public lazy var title: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = UIColor.ne_darkText - label.font = UIFont.systemFont(ofSize: 16) - label.text = "" - return label - }() - - public lazy var subTitle: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = UIColor.ne_emptyTitleColor - label.font = UIFont.systemFont(ofSize: 14) - label.text = "" - return label - }() - - private lazy var bottomLine: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.ne_navLineColor - return view - }() - - func configure(_ model: ChatLocaitonModel, _ select: Bool) { - title.attributedText = model.attribute - var distanceStr = "" - if model.distance > 0 { - if model.distance <= 1000 { - distanceStr = "\(model.distance)m" - } else { - let kilometer = model.distance / 1000 - distanceStr = "\(kilometer)km" - } - subTitle.text = "\(distanceStr)\(chatLocalizable("distance_inner"))|\(model.address)" - } else { - subTitle.text = model.address - } - - if select == true { - selectImg.isHidden = false - } else { - selectImg.isHidden = true - } - } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapGuideBottomView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapGuideBottomView.swift deleted file mode 100644 index 97f79ba7..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/MapView/NEMapGuideBottomView.swift +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import UIKit - -@objc -public protocol NEMapGuideBottomViewDelegate: NSObjectProtocol { - func didClickGuide() -} - -@objcMembers -open class NEMapGuideBottomView: UIView { - public weak var delegate: NEMapGuideBottomViewDelegate? - - override public init(frame: CGRect) { - super.init(frame: frame) - setupSubviews() - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupSubviews() { - backgroundColor = .white - addSubview(guideBtn) - addSubview(title) - addSubview(subtitle) - - NSLayoutConstraint.activate([ - guideBtn.topAnchor.constraint(equalTo: topAnchor, constant: 16), - guideBtn.rightAnchor.constraint(equalTo: rightAnchor, constant: -12), - guideBtn.widthAnchor.constraint(equalToConstant: 40), - guideBtn.heightAnchor.constraint(equalToConstant: 40), - ]) - - NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: leftAnchor, constant: 12), - title.rightAnchor.constraint(equalTo: rightAnchor, constant: -52), - title.topAnchor.constraint(equalTo: topAnchor, constant: 16), - ]) - - NSLayoutConstraint.activate([ - subtitle.leftAnchor.constraint(equalTo: title.leftAnchor), - subtitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), - subtitle.rightAnchor.constraint(equalTo: rightAnchor, constant: -52), - ]) - } - - lazy var guideBtn: UIButton = { - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setImage(UIImage.ne_imageNamed(name: "chat_map_path"), for: .normal) - button.setImage(UIImage.ne_imageNamed(name: "chat_map_path"), for: .highlighted) - button.addTarget(self, action: #selector(guideBtnClick), for: .touchUpInside) - return button - }() - - lazy var title: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = UIFont.systemFont(ofSize: 16) - label.textColor = UIColor.ne_darkText - label.text = "" - return label - }() - - lazy var subtitle: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = UIFont.systemFont(ofSize: 14) - label.textColor = UIColor.ne_emptyTitleColor - label.text = "" - - return label - }() - - func guideBtnClick() { - if let delegate = delegate { - delegate.didClickGuide() - } - } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift index 8637638b..4ffb902b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift @@ -5,455 +5,192 @@ import Foundation import NEChatKit import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK -@objc -public enum LoadMessageDirection: Int { - case old = 1 - case new -} - @objc public protocol ChatViewModelDelegate: NSObjectProtocol { - func onRecvMessages(_ messages: [NIMMessage]) - func willSend(_ message: NIMMessage) - func send(_ message: NIMMessage, didCompleteWithError error: Error?) - func send(_ message: NIMMessage, progress: Float) - func didReadedMessageIndexs() - func onDeleteMessage(_ message: NIMMessage, atIndexs: [IndexPath], reloadIndex: [IndexPath]) - func onRevokeMessage(_ message: NIMMessage, atIndexs: [IndexPath]) - func onAddMessagePin(_ message: NIMMessage, atIndexs: [IndexPath]) - func onRemoveMessagePin(_ message: NIMMessage, atIndexs: [IndexPath]) - func updateDownloadProgress(_ message: NIMMessage, atIndex: IndexPath, progress: Float) + /// 本端即将发送消息状态回调,此时消息还未发送,可对消息进行修改或者拦截发送 + /// 来源: 发送消息, 插入消息 + /// - Parameter message: 消息 + /// - Parameter completion: 是否继续发送消息 + @objc optional func readySendMessage(_ message: V2NIMMessage, _ completion: @escaping (Bool) -> Void) + + /// 消息发送中,此时消息已经发送 + /// - Parameters: + /// - message: 消息 + /// - index: 消息下标 + func sending(_ message: V2NIMMessage, _ index: IndexPath) + + /// 消息发送完成,发送结果为成功或失败 + /// - Parameters: + /// - message: 消息 + /// - index: 消息下标 + func sendSuccess(_ message: V2NIMMessage, _ index: IndexPath) + + /// 消息重发成功 + /// - Parameters: + /// - fromIndex: 消息原下标 + /// - toIndexPath: 消息新下标 + func onResendSuccess(_ fromIndex: IndexPath, _ toIndexPath: IndexPath) + + /// 收到消息 + /// - Parameters: + /// - messages: 消息列表 + /// - indexs: 消息下标列表 + func onRecvMessages(_ messages: [V2NIMMessage], _ indexs: [IndexPath]) + + /// 消息加载更多完成 + /// - Parameter indexs: 消息下标列表 + func onLoadMoreWithMessage(_ indexs: [IndexPath]) + + /// 删除消息或收到删除消息回调 + /// - Parameters: + /// - messages: 消息列表 + /// - deleteIndexs: 删除消息的下标列表 + /// - reloadIndex: 需要刷新的消息的下标列表 + func onDeleteMessage(_ messages: [V2NIMMessage], deleteIndexs: [IndexPath], reloadIndex: [IndexPath]) + + /// 撤回消息或收到撤回消息回调 + /// - Parameters: + /// - message: 消息 + /// - atIndexs: 消息下标列表 + func onRevokeMessage(_ message: V2NIMMessage, atIndexs: [IndexPath]) + + /// 收到消息标记状态变更 + /// - Parameter message: 消息 + /// - Parameter atIndexs: 消息下标 + func onMessagePinStatusChange(_ message: V2NIMMessage?, atIndexs: [IndexPath]) + + /// 单聊对方正在输入中 func remoteUserEditing() + + /// 单聊对方停止输入 func remoteUserEndEditing() - func didLeaveTeam() - func didDismissTeam() - func didRefreshTable() - func onTeamMemberChange(team: NIMTeam) + /// 消息列表重新加载 + func tableViewReload() + + /// 设置消息置顶内容 + /// - Parameters: + /// - name: 消息发送者昵称 + /// - content: 消息内容 + /// - url: 图片或视频链接 + /// - isVideo: 是否是视频 + /// - hideClose: 是否隐藏关闭按钮 + func setTopValue(name: String?, content: String?, url: String?, isVideo: Bool, hideClose: Bool) + + /// 更新消息置顶浮窗中的昵称 + /// - Parameter name: 昵称 + func updateTopName(name: String?) + + /// 显示错误浮窗 + /// - Parameter error: 错误信息 @objc optional func showErrorToast(error: Error?) + + /// 消息重新加载 + @objc optional func dataReload() + + /// 获取消息模型 + /// - Parameter model: 模型 @objc optional func getMessageModel(model: MessageModel) + + /// 多选列表变更 + /// - Parameter count: 选中的消息数量 @objc optional func selectedMessagesChanged(_ count: Int) } -let revokeLocalMessage = "revoke_message_local" -let revokeLocalMessageContent = "revoke_message_local_content" -let removePinMessageNoti = "remove_pin_message_noti" - @objcMembers -open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDelegate, - NIMConversationManagerDelegate, NIMSystemNotificationManagerDelegate, ChatExtendProviderDelegate, FriendProviderDelegate, NIMTeamManagerDelegate { - public var team: NIMTeam? - /// 当前成员的群成员对象类 - public var teamMember: NIMTeamMember? - public var session: NIMSession +open class ChatViewModel: NSObject { + public var conversationId: String + public var sessionId: String public var messages = [MessageModel]() public weak var delegate: ChatViewModelDelegate? // 多选选中的消息 - public var selectedMessages = [NIMMessage]() { + public var selectedMessages = [V2NIMMessage]() { didSet { delegate?.selectedMessagesChanged?(selectedMessages.count) } } // 上拉时间戳 - private var newMsg: NIMMessage? + private var newMsg: V2NIMMessage? // 下拉时间戳 - private var oldMsg: NIMMessage? + private var oldMsg: V2NIMMessage? - public var repo = ChatRepo.shared + public let chatRepo = ChatRepo.shared + public let contactRepo = ContactRepo.shared public var operationModel: MessageContentModel? + public var topMessage: V2NIMMessage? // 置顶消息 public var isReplying = false - public let messagPageNum: UInt = 100 - - // 可信时间戳 - public var credibleTimestamp: TimeInterval = 0 - public var anchor: NIMMessage? + public let messagPageNum: Int = 100 + public var anchor: V2NIMMessage? public var isHistoryChat = false - public var filterInviteSet = Set() - public var deletingMsgDic = Set() - init(session: NIMSession) { - NELog.infoLog(ModuleName + " ChatViewModel", desc: #function + ", sessionId:" + session.sessionId) - self.session = session + override init() { + conversationId = "" + sessionId = "" + super.init() + } + + init(conversationId: String) { + NEALog.infoLog(ModuleName + " " + ChatViewModel.className(), desc: #function + ", conversationId:\(conversationId)") + self.conversationId = conversationId + sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId) ?? "" anchor = nil super.init() - repo.addChatDelegate(delegate: self) - repo.addContactDelegate(delegate: self) - repo.addSessionDelegate(delegate: self) - repo.addSystemNotificationDelegate(delegate: self) - repo.addChatExtendDelegate(delegate: self) - repo.addTeamDelegate(delegate: self) - addObserver() - } - - init(session: NIMSession, anchor: NIMMessage?) { - NELog.infoLog(ModuleName + " ChatViewModel", desc: #function + ", sessionId:" + session.sessionId) - self.session = session + chatRepo.addChatListener(self) + contactRepo.addContactListener(self) + } + + init(conversationId: String, anchor: V2NIMMessage?) { + NEALog.infoLog(ModuleName + " " + ChatViewModel.className(), desc: #function + ", conversationId:\(conversationId)") + self.conversationId = conversationId self.anchor = anchor + sessionId = V2NIMConversationIdUtil.conversationTargetId(conversationId) ?? "" super.init() if anchor != nil { isHistoryChat = true } - repo.addChatDelegate(delegate: self) - repo.addContactDelegate(delegate: self) - repo.addSessionDelegate(delegate: self) - repo.addSystemNotificationDelegate(delegate: self) - repo.addChatExtendDelegate(delegate: self) - repo.addTeamDelegate(delegate: self) - addObserver() - } - - func addObserver() { - NotificationCenter.default.addObserver(self, selector: #selector(removePinNoti), name: Notification.Name(removePinMessageNoti), object: nil) - } - - func removePinNoti(_ noti: Notification) { - if let message = noti.object as? NIMMessage { - removeLocalPinMessage(message) - delegate?.didRefreshTable() - } - } - - /// 发送文本消息(当前会话) - open func sendTextMessage(text: String, remoteExt: [String: Any]?, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") - if text.count <= 0 { - return - } - repo.sendMessage( - message: MessageUtils.textMessage(text: text, remoteExt: remoteExt), - session: session, - completion - ) - } - - /// 发送文本消息(当前会话) - open func sendTextMessage(text: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") - if text.count <= 0 { - return - } - repo.sendMessage( - message: MessageUtils.textMessage(text: text), - session: session, - completion - ) - } - - /// 发送文本消息(非当前会话) - open func sendTextMessage(text: String, session: NIMSession, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") - if text.count <= 0 { - return - } - repo.sendMessage( - message: MessageUtils.textMessage(text: text), - session: session, - completion - ) - } - - open func sendAudioMessage(filePath: String, _ completion: @escaping (Error?) -> Void) { - if ChatDeduplicationHelper.instance.isRecordAudioSended(path: filePath) == true { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ",duplicate send audio at filePath:" + filePath) - return - } - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", filePath:" + filePath) - repo.sendMessage( - message: MessageUtils.audioMessage(filePath: filePath), - session: session, - completion - ) - } - - open func sendImageMessage(image: UIImage, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", image.size: \(image.size)") - repo.sendMessage( - message: MessageUtils.imageMessage(image: image), - session: session, - completion - ) - } - - open func sendImageMessage(data: Data, ext: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", image data count: \(data.count)") - repo.sendMessage( - message: MessageUtils.imageMessage(data: data, ext: ext), - session: session, - completion - ) - } - - open func sendImageMessage(path: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", image path: \(path)") - repo.sendMessage( - message: MessageUtils.imageMessage(path: path), - session: session, - completion - ) - } - - open func sendVideoMessage(url: URL, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ",video url.path:" + url.path) - weak var weakSelf = self - - convertVideoToMP4(videoURL: url) { url, error in - if let p = url?.path, let s = weakSelf?.session { - weakSelf?.repo.sendMessage( - message: MessageUtils.videoMessage(filePath: p), - session: s, - completion - ) - } else { - NELog.errorLog("chat veiw model", desc: "convert mov to mp4 failed") - } - } - } - - func convertVideoToMP4(videoURL: URL, completion: @escaping (URL?, Error?) -> Void) { - let outputFileName = NIMKitFileLocationHelper.genFilename(withExt: "mp4") - guard let outputPath = NIMKitFileLocationHelper.filepath(forVideo: outputFileName) else { - return - } - let asset = AVURLAsset(url: videoURL, options: nil) - let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) - let outputUrl = URL(fileURLWithPath: outputPath) - session?.outputURL = outputUrl - session?.outputFileType = AVFileType.mp4 - session?.shouldOptimizeForNetworkUse = true - session?.exportAsynchronously { - DispatchQueue.main.async { - if session?.status == AVAssetExportSession.Status.completed { - completion(outputUrl, nil) - } else { - completion(nil, nil) - } - } - } - } - - open func sendLocationMessage(_ model: ChatLocaitonModel, _ completion: @escaping (Error?) -> Void) { - let message = MessageUtils.locationMessage(model.lat, model.lng, model.title, model.address) - repo.sendMessage(message: message, session: session, completion) - } - - open func sendFileMessage(filePath: String, displayName: String?, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", filePath:\(filePath)") - repo.sendMessage( - message: MessageUtils.fileMessage(filePath: filePath, displayName: displayName), - session: session, - completion - ) - } - - open func sendFileMessage(data: Data, displayName: String?, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", data.count:\(data.count)") - repo.sendMessage( - message: MessageUtils.fileMessage(data: data, displayName: displayName), - session: session, - completion - ) + chatRepo.addChatListener(self) + contactRepo.addContactListener(self) } - /// 发送自定义消息(当前会话) - open func sendCustomMessage(attachment: NIMCustomAttachment, - remoteExt: [String: Any]?, - apnsConstent: String?, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", apnsConstent:\(String(describing: apnsConstent))") - repo.sendMessage( - message: MessageUtils.customMessage(attachment: attachment, - remoteExt: remoteExt, - apnsContent: apnsConstent), - session: session, - completion - ) - } - - /// 发送自定义消息 - open func sendCustomMessage(attachment: NIMCustomAttachment, - remoteExt: [String: Any]?, - apnsConstent: String?, - session: NIMSession, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", apnsConstent:\(String(describing: apnsConstent))") - repo.sendMessage( - message: MessageUtils.customMessage(attachment: attachment, - remoteExt: remoteExt, - apnsContent: apnsConstent), - session: session, - completion - ) - } - - open func sendBlackListTip(_ errorSession: NIMSession?, _ message: NIMMessage) { -// if DeduplicationHelper.instance.isBlackTipSended(messageId: message.messageId) == true { -// NELog.infoLog(ModuleName + " " + className(), desc: #function + "sendBlackListTip") -// return -// } - guard let eSession = errorSession else { - return - } - NELog.infoLog(ModuleName + " " + className(), desc: #function + "sendBlackListTip") - let content = chatLocalizable("black_list_tip") - let tip = NIMMessage() - let object = NIMTipObject(attach: nil, callbackExt: nil) - tip.messageObject = object - tip.text = content - let setting = NIMMessageSetting() - setting.shouldBeCounted = false - tip.setting = setting - repo.saveMessageToDB(tip, eSession) { [weak self] error in - NELog.infoLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + "save black tip list tip result \(error?.localizedDescription ?? "")") - if let model = self?.modelFromMessage(message: tip) { - self?.messages.append(model) - if let currentSid = self?.session.sessionId, let errorSid = errorSession?.sessionId, currentSid == errorSid { - self?.delegate?.willSend(tip) - } - } + /// 根据会话id列表清空相应会话的未读数 + public func clearUnreadCount() { + ConversationProvider.shared.clearUnreadCountByIds([conversationId]) { result, error in + NEALog.infoLog(ModuleName, desc: #function + " error" + (error?.localizedDescription ?? "")) } } - // 动态查询历史消息解决方案 - open func getMessagesModelDynamically(_ order: NIMMessageSearchOrder, message: NIMMessage?, - _ completion: @escaping (Error?, NSInteger, [MessageModel]?) - -> Void) { - let param = NIMGetMessagesDynamicallyParam() - param.limit = messagPageNum - param.session = session - param.order = order - if let msg = message { - if order == .desc { - param.endTime = msg.timestamp - } else { - param.startTime = msg.timestamp - } - param.anchorClientId = msg.messageId - param.anchorServerId = msg.serverID - } + /// 加载数据 + /// - Parameter completion: 完成回调 + open func loadData(_ completion: @escaping (Error?, NSInteger, NSInteger, Int) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) weak var weakSelf = self - repo.getMessagesDynamically(param) { error, isReliable, messages in - if let messageArray = messages, messageArray.count > 0 { - var count = 0 - var readMsg: NIMMessage? - if order == .desc { - weakSelf?.oldMsg = messageArray.last - readMsg = messageArray.first - } else { - readMsg = messageArray.last - weakSelf?.newMsg = messageArray.last - } - for msg in messageArray { - // 是否需要进行重复消息过滤 - var needFilter = msg.serverID.isEmpty - if let object = msg.messageObject as? NIMNotificationObject, - let content = object.content as? NIMTeamNotificationContent, - content.operationType == .invite { - needFilter = true - } - - if needFilter { - if weakSelf?.filterInviteSet.contains(msg.messageId) == true { - continue - } else { - weakSelf?.filterInviteSet.insert(msg.messageId) - } - } - - print("message text : ", msg.text as Any) - if let model = weakSelf?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - weakSelf?.filterRevokeMessage([model]) - if order == .desc { - weakSelf?.addTimeForHistoryMessage(model) - weakSelf?.messages.insert(model, at: 0) - count += 1 - } else { - if let last = weakSelf?.messages.last { - ChatMessageHelper.addTimeMessage(model, last) - } - weakSelf?.messages.append(model) - count += 1 - } - } - } - - // 第一条消息默认显示时间 - if let firstModel = weakSelf?.messages.first, - let msg = firstModel.message { - let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: msg.timestamp)) - firstModel.timeContent = timeText - } - weakSelf?.checkAudioFile(messages: weakSelf?.messages) - completion(error, count, weakSelf?.messages) - - if weakSelf?.session.sessionType == .P2P { - if let nearMsg = readMsg { - weakSelf?.markRead(messages: [nearMsg]) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), - desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") - ) - } - } - } else if weakSelf?.session.sessionType == .team { - weakSelf?.markRead(messages: messageArray) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), - desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") - ) - } - weakSelf?.refreshReceipts(messages: messageArray) - } - let group = DispatchGroup() - for msg in messageArray { - if let object = msg.messageObject as? NIMNotificationObject, - let content = object.content as? NIMTeamNotificationContent { - let targetIDs = content.targetIDs ?? [] - targetIDs.forEach { uid in - if ChatUserCache.getUserInfo(uid) == nil { - group.enter() - ChatUserCache.getUserInfo(uid) { _, _ in - group.leave() - } - } - } - } - } - - group.notify(queue: .main) { - weakSelf?.delegate?.didRefreshTable() - } - - } else { - weakSelf?.checkAudioFile(messages: weakSelf?.messages) - completion(error, 0, weakSelf?.messages) - } - } - } + messages.removeAll() - open func queryRoamMsgHasMoreTime_v2(_ completion: @escaping (Error?, NSInteger, NSInteger, Int) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - weak var weakSelf = self // 记录可信时间戳 if anchor == nil { - weakSelf?.getMessagesModelDynamically(.desc, message: nil) { error, count, models in - NELog.infoLog( - ModuleName + " " + self.className(), - desc: "CALLBACK getMessageHistory " + (error?.localizedDescription ?? "no error") + isHistoryChat = false + + weakSelf?.getHistoryMessage(order: .QUERY_DIRECTION_DESC, message: nil) { error, count, models in + NEALog.infoLog( + ModuleName + " " + ChatViewModel.className(), + desc: "CALLBACK getMessageList " + (error?.localizedDescription ?? "no error") ) completion(error, count, 0, 0) + weakSelf?.loadMoreWithMessage(models) } - } else { + isHistoryChat = true + // 有锚点消息,从两个方向拉去消息 weakSelf?.newMsg = weakSelf?.anchor weakSelf?.oldMsg = weakSelf?.anchor @@ -462,204 +199,292 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg var moreEnd = 0 var newEnd = 0 - var historyDatas = [MessageModel]() - var newDatas = [MessageModel]() + var historyDatas = [V2NIMMessage]() + var newDatas = [V2NIMMessage]() + var loadMessages = [V2NIMMessage]() var err: Error? group.enter() - weakSelf?.getMessagesModelDynamically(.desc, message: weakSelf?.anchor) { error, value, models in + weakSelf?.getHistoryMessage(order: .QUERY_DIRECTION_DESC, message: weakSelf?.anchor) { error, value, models in moreEnd = value if error != nil { err = error } - if let ms = models { - historyDatas.append(contentsOf: ms) - } - print("drop down remote refresh : ", historyDatas.count) + + historyDatas.append(contentsOf: models) + loadMessages.append(contentsOf: historyDatas) + + group.enter() if let anchorMessage = weakSelf?.anchor { - let model = self.modelFromMessage(message: anchorMessage) - weakSelf?.filterRevokeMessage([model]) - if NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: anchorMessage) == false { + loadMessages.append(anchorMessage) + weakSelf?.modelFromMessage(message: anchorMessage) { model in weakSelf?.messages.append(model) + group.leave() } } - weakSelf?.getMessagesModelDynamically(.asc, message: weakSelf?.anchor) { error, value, models in - NELog.infoLog( - ModuleName + " " + self.className(), + + group.enter() + weakSelf?.getHistoryMessage(order: .QUERY_DIRECTION_ASC, message: weakSelf?.anchor) { error, value, models in + NEALog.infoLog( + ModuleName + " " + ChatViewModel.className(), desc: "CALLBACK pullRemoteRefresh " + (error?.localizedDescription ?? "no error") ) newEnd = value if err != nil { err = error } - if let ms = models { - newDatas.append(contentsOf: ms) - } - print("pull remote refresh : ", newDatas.count) + + newDatas.append(contentsOf: models) + loadMessages.append(contentsOf: newDatas) group.leave() } + group.leave() } - group.notify(queue: DispatchQueue.main, execute: { + group.notify(queue: .main) { completion(err, moreEnd, newEnd, historyDatas.count) - }) + weakSelf?.loadMoreWithMessage(loadMessages) + } } } - // 查询本地历史消息 - open func getMessageHistory(_ message: NIMMessage?, - _ completion: @escaping (Error?, NSInteger, [MessageModel]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + (message?.messageId ?? "nil")) - ChatProvider.shared.getMessageHistory( - session: session, - message: message, - limit: messagPageNum - ) { [weak self] error, messages in - if let messageArray = messages, messageArray.count > 0 { - self?.oldMsg = messageArray.first - for msg in messageArray { - if let model = self?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - if let last = self?.messages.last { - ChatMessageHelper.addTimeMessage(model, last) - } - self?.filterRevokeMessage([model]) - self?.messages.append(model) + /// 查询回复 + /// - Parameters: + /// - model: 消息体 + /// - completion: 完成回调 + func loadReply(_ model: MessageModel, _ completion: @escaping () -> Void) { + if model.replyedModel != nil, + model.replyedModel?.message?.messageServerId == nil || + model.replyedModel?.message?.messageServerId?.isEmpty == true { + if let message = model.message { + getReplyMessageWithoutThread(message: message) { replyedModel in + if let reply = replyedModel as? MessageContentModel, + model.replyText != ReplyMessageUtil.textForReplyModel(model: reply) { + model.replyedModel = replyedModel + } else { + model.replyText = chatLocalizable("message_not_found") } + completion() } - completion(error, messageArray.count, self?.messages) - // mark read - self?.markRead(messages: messageArray) { error in - NELog.infoLog( - ModuleName + " " + (self?.className() ?? "ChatViewModel"), - desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") - ) - } - - } else { - completion(error, 0, self?.messages) + return } } + completion() } - // 查询更多本地历史消息 - open func getMoreMessageHistory(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function) + /// 加载消息的更多信息(回复、标记、发送者信息) + /// - Parameter messageArray: 消息列表 + func loadMoreWithMessage(_ messageArray: [V2NIMMessage]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) weak var weakSelf = self - - let messageParam = oldMsg ?? newMsg - - ChatProvider.shared.getMessageHistory( - session: session, - message: messageParam, - limit: messagPageNum - ) { [weak self] error, messages in - if let messageArray = messages, messageArray.count > 0 { - weakSelf?.oldMsg = messageArray.first - - // 如果可信就使用本次请求数据,如果不可信就去远端拉去数据,并更新可信时间戳 - let isCredible = weakSelf? - .isMessageCredible(message: messageArray.first ?? NIMMessage()) - if let isTrust = isCredible, isTrust { - for msg in messageArray.reversed() { - if let model = self?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - self?.addTimeForHistoryMessage(model) - self?.messages.insert(model, at: 0) + let conversationId = weakSelf?.conversationId ?? "" + let group = DispatchGroup() + let sema = DispatchSemaphore(value: 0) + + DispatchQueue.global().async { [self] in + + // 群聊需要获取群昵称 + if V2NIMConversationIdUtil.conversationType(conversationId) != .CONVERSATION_TYPE_P2P { + let userIds = messages.compactMap { $0.message?.senderId } + loadShowName(userIds, weakSelf?.sessionId) { + // 获取头像昵称 + for model in weakSelf?.messages ?? [] { + if let uid = model.message?.senderId, + let fullName = weakSelf?.getShowName(uid) { + let userFriend = NEFriendUserCache.shared.getFriendInfo(uid) ?? ChatUserCache.shared.getUserInfo(uid) + model.avatar = userFriend?.user?.avatar + model.fullName = fullName + model.shortName = NEFriendUserCache.getShortName(userFriend?.showName() ?? "") } } - completion(error, messageArray.count, self?.messages) - } else { - let option = NIMHistoryMessageSearchOption() - option.startTime = 0 - option.endTime = self?.oldMsg?.timestamp ?? 0 - option.limit = self?.messagPageNum ?? 100 - option.sync = true - weakSelf?.getRemoteHistoryMessage( - direction: .old, - updateCredible: true, - option: option, - completion - ) + sema.signal() } + sema.wait() + } - weakSelf?.markRead(messages: messageArray) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), - desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") - ) + // 查询回复 + for model in messages { + group.enter() + loadReply(model) { + group.leave() } - } else { - if let messageArray = messages, messageArray.isEmpty, - weakSelf?.credibleTimestamp ?? 0 > 0 { - // 如果远端拉倒了信息 就去更新可信时间戳,拉不到就不更新。 - let option = NIMHistoryMessageSearchOption() - option.startTime = 0 - option.endTime = self?.oldMsg?.timestamp ?? 0 - option.limit = self?.messagPageNum ?? 100 - weakSelf?.getRemoteHistoryMessage( - direction: .old, - updateCredible: true, - option: option, - completion - ) - } else { - completion(error, 0, self?.messages) + } + + // 查找标记记录 + group.enter() + chatRepo.getPinnedMessageList(conversationId: weakSelf?.conversationId ?? "") { [weak self] pinList, error in + + if let pinList = pinList { + let userIds = pinList.map(\.operatorId) + group.enter() + self?.loadShowName(userIds, weakSelf?.sessionId) { + for pin in pinList { + for model in weakSelf?.messages ?? [] { + if model.message?.messageClientId == pin.messageRefer?.messageClientId { + model.isPined = true + model.pinAccount = pin.operatorId + model.pinShowName = self?.getShowName(pin.operatorId) + break + } + } + } + group.leave() + } } + group.leave() + } + + // 获取消息已读未读 + group.enter() + getMessageReceipts(messages: messageArray) { reloadIndexs, error in + NEALog.infoLog( + ModuleName + " " + ChatViewModel.className(), + desc: "CALLBACK getP2PMessageReceipt " + (error?.localizedDescription ?? "no error") + ) + group.leave() + } + + group.notify(queue: .main) { + weakSelf?.delegate?.tableViewReload() + } + } + + // 下载语音附件 + downloadAudioFile(messages) + + // 加载置顶消息 + loadTopMessage() + } + + /// 更新消息发送者的信息 + /// - Parameter accid: 发送者 accid + func updateMessageInfo(_ accid: String?) { + guard let accid = accid else { return } + + let showName = getShowName(accid) + var indexPaths = [IndexPath]() + for (i, model) in messages.enumerated() { + // 更新消息发送者昵称和头像 + if model.message?.senderId == accid { + let user = NEFriendUserCache.shared.getFriendInfo(accid) ?? ChatUserCache.shared.getUserInfo(accid) + model.fullName = showName + model.shortName = NEFriendUserCache.getShortName(showName) + model.avatar = user?.user?.avatar + indexPaths.append(IndexPath(row: i, section: 0)) + } + + // 更新标记者昵称 + if model.isPined, model.pinAccount == accid { + model.pinShowName = showName + indexPaths.append(IndexPath(row: i, section: 0)) + } + } + delegate?.onLoadMoreWithMessage(indexPaths) + delegate?.updateTopName(name: showName) + } + + /// 插入消息 + /// - Parameter newModel: 新消息模型 + /// - Returns: 插入位置 + @discardableResult + func insertToMessages(_ newModel: MessageModel) -> Int { + var index = -1 + + // 无消息时直接尾插 + if messages.isEmpty { + messages.append(newModel) + return 0 + } + + // 最新的消息直接尾插 + if newModel.message?.createTime ?? 0 >= (messages.last?.message?.createTime ?? 0) { + messages.append(newModel) + return messages.count - 1 + } + + for (i, model) in messages.enumerated() { + if (newModel.message?.createTime ?? 0) < (model.message?.createTime ?? 0) { + messages.insert(newModel, at: i) + index = i + break } } + + return index } - // 查询远端历史消息 - open func getRemoteHistoryMessage(direction: LoadMessageDirection, updateCredible: Bool, - option: NIMHistoryMessageSearchOption, - _ completion: @escaping (Error?, NSInteger, - [MessageModel]?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", direction: \(direction.rawValue)") + /// 查询历史消息 + /// - Parameters: + /// - order: 查询方向 + /// - message: 锚点消息 + /// - completion: 完成回调 + open func getHistoryMessage(order: V2NIMQueryDirection, + message: V2NIMMessage?, + _ completion: @escaping (Error?, NSInteger, [V2NIMMessage]) + -> Void) { + let opt = V2NIMMessageListOption() + opt.limit = messagPageNum + opt.anchorMessage = message + opt.conversationId = conversationId + opt.direction = order + + if let msg = message { + if order == .QUERY_DIRECTION_DESC { + opt.endTime = msg.createTime + } else { + opt.beginTime = msg.createTime + } + } + weak var weakSelf = self - repo.getHistoryMessage(session: session, option: option) { error, messages in - if error == nil { - if let messageArray = messages, messageArray.count > 0 { - if direction == .old { - weakSelf?.oldMsg = messageArray.last - } else { - weakSelf?.newMsg = messageArray.first - } - for msg in messageArray { - if let model = weakSelf?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - weakSelf?.addTimeForHistoryMessage(model) - weakSelf?.messages.insert(model, at: 0) + chatRepo.getMessageList(option: opt) { error, messages in + if let messageArray = messages, messageArray.count > 0 { + let group = DispatchGroup() + + if order == .QUERY_DIRECTION_DESC { + weakSelf?.oldMsg = messageArray.last + } else { + weakSelf?.newMsg = messageArray.last + } + for msg in messageArray { + group.enter() + weakSelf?.modelFromMessage(message: msg) { model in + if weakSelf?.messages.contains(where: { $0.message?.messageClientId == model.message?.messageClientId }) == false { + weakSelf?.insertToMessages(model) } + group.leave() } + } - if let updateMessage = messageArray.first, updateCredible { - // 更新可信时间戳 - weakSelf?.credibleTimestamp = updateMessage.timestamp - weakSelf?.repo - .updateIncompleteSessions(messages: [updateMessage]) { error, recentSessions in - if error != nil { - NELog.errorLog( - ModuleName + " " + (weakSelf?.className() ?? "ChatViewModel"), - desc: "❌updateIncompleteSessions failed,error = \(error!)" - ) - } - } - } - completion(error, messageArray.count, weakSelf?.messages) - } else { - completion(error, 0, weakSelf?.messages) + group.notify(queue: .main) { + // 显示时间 + weakSelf?.addTimeForHistoryMessage() + + // 回调消息列表 + completion(error, messageArray.count, messageArray) + } + + // 标记已读 + weakSelf?.markRead(messages: messageArray) { error in + NEALog.infoLog( + ModuleName + " " + ChatViewModel.className(), + desc: "CALLBACK markRead " + (error?.localizedDescription ?? "no error") + ) } } else { - completion(error, 0, nil) + completion(error, 0, []) } } } - // 下拉获取历史消息 - open func dropDownRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) + /// 下拉获取历史消息 + /// - Parameter completion: 完成回调 + open func dropDownRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [V2NIMMessage]) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + // 首次会话下拉,没有锚点消息 || 锚点消息被删除,需要手动设置锚点消息 - if oldMsg == nil || !messages.contains(where: { $0.message?.messageId == oldMsg?.messageId }) { + if oldMsg == nil || !messages.contains(where: { $0.message?.messageClientId == oldMsg?.messageClientId }) { for msg in messages { if let mmsg = msg.message { oldMsg = mmsg @@ -672,504 +497,456 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg oldMsg = nil } - getMessagesModelDynamically(.desc, message: oldMsg, completion) - NELog.infoLog(ModuleName + " " + className(), desc: #function) + getHistoryMessage(order: .QUERY_DIRECTION_DESC, message: oldMsg) { [weak self] error, count, messages in + completion(error, count, messages) + if count > 0 { + self?.loadMoreWithMessage(messages) + } + } } - // 上拉获取最新消息 - open func pullRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [MessageModel]?) + /// 上拉获取最新消息 + /// - Parameter completion: 完成回调 + open func pullRemoteRefresh(_ completion: @escaping (Error?, NSInteger, [V2NIMMessage]) -> Void) { - getMessagesModelDynamically(.asc, message: newMsg, completion) - NELog.infoLog(ModuleName + " " + className(), desc: #function) - } - - // 搜索历史记录查询的本地消息 - open func searchMessageHistory(direction: LoadMessageDirection, startTime: TimeInterval, - endTime: TimeInterval, - _ completion: @escaping (Error?, NSInteger, [MessageModel]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", direction: \(direction.rawValue)") - let option = NIMMessageSearchOption() - option.startTime = startTime - option.endTime = endTime - option.order = .asc - option.limit = messagPageNum - weak var weakSelf = self - - repo.searchMessages(session, option: option) { error, messages in - if error == nil { - if let messageArray = messages, messageArray.count > 0 { - if direction == .old { - weakSelf?.oldMsg = messageArray.first - } else { - weakSelf?.newMsg = messageArray.last - } - for msg in messageArray { - if let model = weakSelf?.modelFromMessage(message: msg), NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) == false { - ChatMessageHelper.addTimeMessage(model, weakSelf?.messages.last) - weakSelf?.messages.append(model) + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + getHistoryMessage(order: .QUERY_DIRECTION_ASC, message: newMsg) { [weak self] error, count, messages in + completion(error, count, messages) + self?.loadMoreWithMessage(messages) + } + } + + /// 下载语音消息附件 + /// - Parameter models: 消息列表 + open func downloadAudioFile(_ models: [MessageModel]) { + DispatchQueue.global().async { [weak self] in + for model in models { + if model.type == .audio, let audioAttach = model.message?.attachment as? V2NIMMessageAudioAttachment { + let path = audioAttach.path ?? ChatMessageHelper.createFilePath(model.message) + if !FileManager.default.fileExists(atPath: path) { + if let urlString = audioAttach.url { + self?.downLoad(urlString, path, nil) { _, error in + if error == nil { + NEALog.infoLog(ModuleName + " " + ChatViewController.className(), desc: #function + "CALLBACK downLoad") + } + } } } - completion(error, messageArray.count, weakSelf?.messages) - } else { - completion(error, 0, weakSelf?.messages) } - } else { - completion(error, 0, nil) - } - } - } - - // 判断消息是否可信 - open func isMessageCredible(message: NIMMessage) -> Bool { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - return credibleTimestamp <= 0 || message.timestamp >= credibleTimestamp - } - - open func markRead(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") - if session.sessionType == .P2P { - markReadInP2P(messages: messages, completion) - } else if session.sessionType == .team { - markReadInTeam(messages: messages, completion) - } - // mark session read - weak var weakself = self - repo.markMessageRead(session) { error in - if error != nil { - NELog.errorLog( - ModuleName + " " + (weakself?.className() ?? "ChatViewModel"), - desc: "❌markReadInSession failed,error = \(error!)" - ) - } - } - } - - // 单人会话消息已读标记 - private func markReadInP2P(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") - for message in messages.reversed() { - if message.isReceivedMsg { - let param = NIMMessageReceipt(message: message) - repo.markP2pMessageRead(param: param, completion) - break } } - completion(nil) } - // 群消息已读标记 - private func markReadInTeam(messages: [NIMMessage], _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") - var receipts = [NIMMessageReceipt]() - for message in messages { - let receiptEnable = message.setting?.teamReceiptEnabled ?? false - if receiptEnable, !message.isTeamReceiptSended { - let receipt = NIMMessageReceipt(message: message) - receipts.append(receipt) - } - } - let receiptsChunk = receipts.chunk(50) - for receipt in receiptsChunk { - repo.markTeamMessageRead(param: receipt) { error, failedReceipts in - print("!! chatViewModel markReadInTeam error:\(String(describing: error))") - completion(error) - } + /// 加载置顶消息 + open func loadTopMessage() {} + + /// 发送消息 + /// - Parameters: + /// - message: 需要发送的消息体 + /// - conversationId: 会话id + /// - completion: 回调 + open func sendMessage(message: V2NIMMessage, + conversationId: String? = nil, + _ completion: @escaping (V2NIMMessage?, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text: \(String(describing: message.text))") + + chatRepo.sendMessage(message: message, + conversationId: conversationId ?? self.conversationId) { result, error, pro in + completion(result?.message, error) } } - // 删除消息 - open func deleteMessage(_ completion: @escaping (Error?) -> Void) { - guard let message = operationModel?.message else { - NELog.errorLog(ModuleName + " " + className(), desc: #function + ", message is nil") + /// 发送文本消息 + /// - Parameters: + /// - text: 文本内容 + /// - remoteExt: 扩展字段 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendTextMessage(text: String, + conversationId: String? = nil, + remoteExt: [String: Any]?, + _ completion: @escaping (V2NIMMessage?, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") + if text.count <= 0 { + completion(nil, nil) return } - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - // 已撤回的消息不能删除 - if operationModel?.isRevoked == true { - return + let message = MessageUtils.textMessage(text: text, remoteExt: remoteExt) + sendMessage(message: message, conversationId: conversationId) { message, error in + completion(message, error) } + } - if deletingMsgDic.contains(message.messageId) { - return - } - deletingMsgDic.insert(message.messageId) - if message.serverID.count <= 0 { - repo.deleteMessage(message: message) - deleteMessageUpdateUI(message) - deletingMsgDic.remove(message.messageId) - completion(nil) + /// 发送语音消息 + /// - Parameters: + /// - filePath: 语音文件路径 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendAudioMessage(filePath: String, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + if ChatDeduplicationHelper.instance.isRecordAudioSended(path: filePath) == true { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ",duplicate send audio at filePath:" + filePath) return } - weak var weakSelf = self - if message.serverID == "0" { - repo.deleteMessage(message: message) - deleteMessageUpdateUI(message) - weakSelf?.deletingMsgDic.remove(message.messageId) - completion(nil) - return + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", filePath:" + filePath) + let message = MessageUtils.audioMessage(filePath: filePath, name: nil, sceneName: nil, duration: 0) + sendMessage(message: message, conversationId: conversationId) { _, error in + completion(error) } + } - repo.deleteServerMessage(message: message, ext: nil) { error in - if error == nil { - weakSelf?.deleteMessageUpdateUI(message) - } else { - completion(error) - } - weakSelf?.deletingMsgDic.remove(message.messageId) + /// 发送图片消息 + /// - Parameters: + /// - path: 图片文件路径 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendImageMessage(path: String, + name: String? = "image", + width: Int32 = 0, + height: Int32 = 0, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", image path: \(path)") + let message = MessageUtils.imageMessage(path: path, name: name, sceneName: nil, width: width, height: height) + sendMessage(message: message, conversationId: conversationId) { _, error in + completion(error) } } - 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) + /// 发送视频消息 + /// - Parameters: + /// - url: 视频文件路径 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendVideoMessage(url: URL, + name: String? = "video", + width: Int32 = 0, + height: Int32 = 0, + duration: Int32 = 0, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ",video url.path:" + url.path) + weak var weakSelf = self + + convertVideoToMP4(videoURL: url) { url, error in + if let path = url?.path, let conversationId = weakSelf?.conversationId { + let message = MessageUtils.videoMessage(filePath: path, name: name, sceneName: nil, width: width, height: height, duration: duration) + weakSelf?.sendMessage(message: message, conversationId: conversationId) { _, error in + completion(error) + } } else { - remoteMsgs.append(msg) + NEALog.errorLog("chat veiw model", desc: "convert mov to mp4 failed") + completion(NSError(domain: "convert mov to mp4 failed", code: 414)) } } + } - localMsgs.forEach { msg in - repo.deleteMessage(message: msg) - deleteMessageUpdateUI(msg) - deletingMsgDic.remove(msg.messageId) + /// 将视频格式转为 MP4 + /// - Parameters: + /// - videoURL: 视频文件路径 + /// - completion: 完成回调 + func convertVideoToMP4(videoURL: URL, completion: @escaping (URL?, Error?) -> Void) { + let outputFileName = NIMKitFileLocationHelper.genFilename(withExt: "mp4") + guard let outputPath = NIMKitFileLocationHelper.filepath(forVideo: outputFileName) else { + return } - - weak var weakSelf = self - repo.deleteRemoteMessages(messages: remoteMsgs, exts: nil) { error in - if error == nil { - remoteMsgs.forEach { msg in - weakSelf?.deleteMessageUpdateUI(msg) - weakSelf?.deletingMsgDic.remove(msg.messageId) + let asset = AVURLAsset(url: videoURL, options: nil) + let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) + let outputUrl = URL(fileURLWithPath: outputPath) + session?.outputURL = outputUrl + session?.outputFileType = AVFileType.mp4 + session?.shouldOptimizeForNetworkUse = true + session?.exportAsynchronously { + DispatchQueue.main.async { + if session?.status == AVAssetExportSession.Status.completed { + completion(outputUrl, nil) + } else { + completion(nil, nil) } - } else { - completion(error) } } } - // 回复消息 - open func replyMessage(_ message: NIMMessage, _ target: NIMMessage, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - repo.replyMessage(message, target) { error in + /// 发送地理位置消息 + /// - Parameters: + /// - model: 位置信息 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendLocationMessage(model: ChatLocaitonModel, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + let message = MessageUtils.locationMessage(lat: model.lat, + lng: model.lng, + address: model.title + model.address) + message.text = model.title + sendMessage(message: message, conversationId: conversationId) { _, error in completion(error) } } - open func replyMessageWithoutThread(message: NIMMessage, - target: NIMMessage, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - repo.replyMessageWithoutThread(message: message, session: session, target: target) { error in + /// 发送文件消息 + /// - Parameters: + /// - filePath: 源文件路径 + /// - displayName: 文件展示名称 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendFileMessage(filePath: String, + displayName: String?, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", filePath:\(filePath)") + let message = MessageUtils.fileMessage(filePath: filePath, displayName: displayName, sceneName: nil) + sendMessage(message: message, conversationId: conversationId) { _, error in completion(error) } } - // 撤回消息 - open func revokeMessage(message: NIMMessage, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - repo.revokeMessage(message: message) { error in - if error == nil { - self.revokeMessageUpdateUI(message) - } + /// 发送自定义消息 + /// - Parameters: + /// - text: 文本内容 + /// - rawAttachment: 附件内容 + /// - conversationId: 会话 id + /// - completion: 完成回调 + open func sendCustomMessage(text: String, + rawAttachment: String, + conversationId: String? = nil, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text:\(text)") + let message = MessageUtils.customMessage(text: text, rawAttachment: rawAttachment) + sendMessage(message: message, conversationId: conversationId) { _, error in completion(error) } } - // 消息重发 - @discardableResult - open func resendMessage(message: NIMMessage) -> NSError? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - ChatDeduplicationHelper.instance.clearCache() - return repo.resendMessage(message: message) - } - - // 从本地获取用户信息 - open func getUserInfo(userId: String) -> NEKitUser? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", userId:" + userId) - return repo.getUserInfo(userId: userId) - } - - // 获取指定的群成员 - open func getTeamMember(userId: String, teamId: String) -> NIMTeamMember? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", userId:" + userId) - return repo.getTeamMemberList(userId: userId, teamId: teamId) - } + /// 本地插入提示消息 + /// - Parameter conversationId: 会话 id + open func insertTipMessage(_ text: String, _ conversationId: String) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text:\(text)") - // 系统通知回调 - // 自定义系统通知回调 - open func onReceive(_ notification: NIMCustomSystemNotification) { - NELog.infoLog( - ModuleName + " " + className(), - desc: #function + ", notification.description:" + notification.description - ) - print("on receive custom noti : ", notification) - if session.sessionType != .P2P { - return - } - if session.sessionId != notification.sender { - return - } - if let content = notification.content, - let dic = getDictionaryFromJSONString(content) as? [String: Any], - let typing = dic["typing"] as? Int { - if typing == 1 { - delegate?.remoteUserEditing() - } else { - delegate?.remoteUserEndEditing() + let tip = MessageUtils.tipMessage(text: text) + chatRepo.insertMessageToLocal(message: tip, conversationId: conversationId) { [weak self] _, error in + if let currentSid = self?.conversationId, currentSid == conversationId { + self?.modelFromMessage(message: tip) { model in + if let index = self?.insertToMessages(model) { + self?.delegate?.sending(tip, IndexPath(row: index, section: 0)) + } + } } } } - // MARK: FriendProviderDelegate + /// 发送消息已读回执 + /// - Parameters: + /// - messages: 需要发送已读回执的消息 + /// - completion: 完成回调 + open func markRead(messages: [V2NIMMessage], _ completion: @escaping (Error?) -> Void) {} - open func onFriendChanged(user: NEKitUser) { - ChatUserCache.updateUserInfo(user) - } + /// 获取消息已读未读回执 + /// - Parameters: + /// - messages: 消息列表 + /// - completion: 完成回调 + open func getMessageReceipts(messages: [V2NIMMessage], + _ completion: @escaping ([IndexPath], Error?) -> Void) {} - open func onUserInfoChanged(user: NEKitUser) { - ChatUserCache.updateUserInfo(user) - } + /// 删除消息 + /// - Parameter completion: 完成回调 + open func deleteMessage(_ completion: @escaping (Error?) -> Void) { + guard let message = operationModel?.message, + let messageId = message.messageClientId else { + NEALog.errorLog(ModuleName + " " + className(), desc: #function + ", message is nil") + return + } + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId:\(messageId)") - open func onBlackListChanged() {} + // 已撤回的消息不能删除 + if operationModel?.isRevoked == true { + return + } - // MARK: NIMChatManagerDelegate + if deletingMsgDic.contains(messageId) { + return + } + deletingMsgDic.insert(messageId) - // 收到消息 - open func onRecvMessages(_ messages: [NIMMessage]) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count), first.messageID: \(messages.first?.messageId ?? "")") - var count = 0 - for msg in messages { - if msg.session?.sessionId == session.sessionId { - if msg.serverID.count <= 0, msg.messageType != .custom { - continue - } - if msg.isDeleted == true { - continue - } - if NotificationMessageUtils.isDiscussSeniorTeamUpdateCustomNoti(message: msg) { - continue - } - if let object = msg.messageObject as? NIMNotificationObject { - if let content = object.content as? NIMTeamNotificationContent, content.operationType == .invite { - if filterInviteSet.contains(msg.messageId) { - continue - } else { - filterInviteSet.insert(msg.messageId) - } - } + weak var weakSelf = self + // 本地消息 + if !(message.messageServerId?.isEmpty == false) { + chatRepo.deleteMessage(message: message, onlyDeleteLocal: true) { error in + if error == nil { + weakSelf?.deleteMessageUpdateUI([message]) + weakSelf?.deletingMsgDic.remove(messageId) } - /* 后续解散群离开群弹框优化 - if msg.messageType == .notification, session.sessionType == .team { - if team?.clientCustomInfo?.contains(discussTeamKey) == true { - return - } - let value = NotificationMessageUtils.isTeamLeaveOrDismiss(message: msg) - if value.isLeave == true { - delegate?.didLeaveTeam() - } else if value.isDismiss == true { - delegate?.didDismissTeam() - } - - }*/ - count += 1 - // 自定义消息处理 - newMsg = msg - let model = modelFromMessage(message: msg) - ChatMessageHelper.addTimeMessage(model, self.messages.last) - self.messages.append(model) - } - } - if count > 0 { delegate?.onRecvMessages(messages) } - } - - open func willSend(_ message: NIMMessage) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:" + message.messageId) - print("\(#function)") - - if message.session?.sessionId != session.sessionId { + completion(error) + } return } - // 自定义消息发送之前的处理 - if newMsg == nil { - newMsg = message - } - var isResend = false - for (i, msg) in messages.enumerated() { - if message.messageId == msg.message?.messageId { - messages[i].message = message - isResend = true - break + if message.messageServerId == "0" { + chatRepo.deleteMessage(message: message, onlyDeleteLocal: true) { error in + if error == nil { + weakSelf?.deleteMessageUpdateUI([message]) + weakSelf?.deletingMsgDic.remove(messageId) + } + completion(error) } + return } - if !isResend { - let model = modelFromMessage(message: message) - ChatMessageHelper.addTimeMessage(model, messages.last) - filterRevokeMessage([model]) - messages.append(model) + chatRepo.deleteMessage(message: message, onlyDeleteLocal: false) { error in + if error == nil { + weakSelf?.deleteMessageUpdateUI([message]) + weakSelf?.deletingMsgDic.remove(messageId) + } + completion(error) } - - delegate?.willSend(message) } - // 发送消息进度回调 - open func send(_ message: NIMMessage, progress: Float) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - print("\(#function) progress\(progress)") - delegate?.send(message, progress: progress) - } + /// 批量删除消息 + /// - Parameters: + /// - messages: 需要删除的消息 + /// - completion: 完成回调 + open func deleteMessages(messages: [V2NIMMessage], _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", message count:\(messages.count)") + var localMsgs = [V2NIMMessage]() + var remoteMsgs = [V2NIMMessage]() + for msg in messages { + guard let messageId = msg.messageClientId else { + continue + } - // 发送消息完成回调 - open func send(_ message: NIMMessage, didCompleteWithError error: Error?) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - print("\(#function) message deliveryState:\(message.deliveryState) error:\(error)") - for (i, msg) in messages.enumerated() { - if message.messageId == msg.message?.messageId { - messages[i].message = message - break + if deletingMsgDic.contains(messageId) { + continue } - } - // 判断发送失败原因是否是因为在黑名单中 - if error != nil { - if let err = error as NSError? { - if err.code == inBlackListCode { - weak var weakSelf = self - DispatchQueue.main.async { - weakSelf?.sendBlackListTip(message.session, message) - } - } + + deletingMsgDic.insert(messageId) + if !(msg.messageServerId?.isEmpty == false) { + localMsgs.append(msg) + } else { + remoteMsgs.append(msg) } } - delegate?.send(message, didCompleteWithError: error) - } - -// MARK: ChatExtendProviderDelegate - - // 添加标记消息回调 - open func onNotifyAddMessagePin(pinItem: NIMMessagePinItem) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + pinItem.messageId) - var index = -1 - for (i, model) in messages.enumerated() { - if pinItem.messageServerID == model.message?.serverID { - messages[i].isPined = true - let pinID = pinItem.accountID ?? NIMSDK.shared().loginManager.currentAccount() - messages[i].pinAccount = pinID - messages[i].pinShowName = ChatUserCache.getShowName(userId: pinID, teamId: session.sessionId) - index = i - break + weak var weakSelf = self + chatRepo.deleteMessages(messages: localMsgs, onlyDeleteLocal: true) { error in + if error == nil { + weakSelf?.deleteMessageUpdateUI(localMsgs) + for msg in localMsgs { + if let msgId = msg.messageClientId { + weakSelf?.deletingMsgDic.remove(msgId) + } + } } + completion(error) } - if index >= 0, let msg = messages[index].message { - delegate?.onAddMessagePin(msg, atIndexs: [IndexPath(row: index, section: 0)]) - } - } - // 移除标记消息回调 - open func onNotifyRemoveMessagePin(pinItem: NIMMessagePinItem) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + pinItem.messageId) - var index = -1 - for (i, model) in messages.enumerated() { - if pinItem.messageServerID == model.message?.serverID { - if !messages[i].isPined { - return + chatRepo.deleteMessages(messages: remoteMsgs, onlyDeleteLocal: false) { error in + if error == nil { + weakSelf?.deleteMessageUpdateUI(remoteMsgs) + for msg in remoteMsgs { + if let msgId = msg.messageClientId { + weakSelf?.deletingMsgDic.remove(msgId) + } } - messages[i].isPined = false - messages[i].pinAccount = nil - messages[i].pinShowName = nil - index = i - break } - } - if index >= 0, let msg = messages[index].message { - delegate?.onRemoveMessagePin(msg, atIndexs: [IndexPath(row: index, section: 0)]) + completion(error) } } - open func onNotifySyncStickTopSessions(_ response: NIMSyncStickTopSessionResponse) {} - - open func onNotifyAddStickTopSession(_ newInfo: NIMStickTopSessionInfo) {} - - open func onNotifyRemoveStickTopSession(_ removedInfo: NIMStickTopSessionInfo) {} - -// MARK: collection - - func addColletion(_ message: NIMMessage, - completion: @escaping (NSError?, NIMCollectInfo?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - let param = NIMAddCollectParams() - var string: String? - if message.messageType == .text { - string = message.text - param.type = 1024 + /// 回复消息(不使用 thread ) + /// - Parameters: + /// - message: 新生成的消息 + /// - replyMessage: 被回复的消息 + open func replyMessageWithoutThread(message: V2NIMMessage, + replyMessage: V2NIMMessage, + _ completion: @escaping (V2NIMMessage?, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId:\(String(describing: message.messageClientId))") + + let yxReplyMsg: [String: Any] = [ + "idClient": replyMessage.messageClientId as Any, + "scene": replyMessage.conversationType.rawValue, + "from": replyMessage.senderId as Any, + "to": replyMessage.conversationId as Any, + "idServer": replyMessage.messageServerId as Any, + "time": Int(replyMessage.createTime * 1000), + ] + + var remoteExt = NECommonUtil.getDictionaryFromJSONString(message.serverExtension ?? "") as? [String: Any] + if remoteExt == nil { + remoteExt = [keyReplyMsgKey: yxReplyMsg] } else { - switch message.messageType { - case .audio: - if let obj = message.messageObject as? NIMAudioObject { - string = obj.url - } - param.type = message.messageType.rawValue - case .image: - if let obj = message.messageObject as? NIMImageObject { - string = obj.url - } - param.type = message.messageType.rawValue - case .video: - if let obj = message.messageObject as? NIMVideoObject { - string = obj.url - } - param.type = message.messageType.rawValue - default: - param.type = 0 - } - param.data = string ?? "" + remoteExt![keyReplyMsgKey] = yxReplyMsg } - param.uniqueId = message.serverID - repo.collectMessage(param, completion) + message.serverExtension = NECommonUtil.getJSONStringFromDictionary(remoteExt ?? [:]) + + sendMessage(message: message, conversationId: conversationId, completion) } -// MARK: revoke + /// 撤回消息 + /// - Parameters: + /// - message: 消息 + /// - completion: 完成回调 + open func revokeMessage(message: V2NIMMessage, _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId:\(String(describing: message.messageClientId))") - // 撤回消息回调 - open func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - guard let msg = notification.message else { - return + var muta = [String: Any]() + muta[revokeLocalMessage] = true + if message.messageType == .MESSAGE_TYPE_TEXT { + muta[revokeLocalMessageContent] = message.text } - NELog.infoLog(ModuleName + className(), desc: #function + "messageId:\(msg.messageId), serverID:\(msg.serverID)") + if message.messageType == .MESSAGE_TYPE_CUSTOM { + if let title = NECustomAttachment.titleOfRichText(message.attachment), !title.isEmpty { + muta[revokeLocalMessageContent] = title + } + if let body = NECustomAttachment.bodyOfRichText(message.attachment), !body.isEmpty { + muta[revokeLocalMessageContent] = body + } + } - revokeMessageUpdateUI(msg) + let revokeParams = V2NIMMessageRevokeParams() + revokeParams.serverExtension = getJSONStringFromDictionary(muta) + chatRepo.revokeMessage(message: message, params: revokeParams) { [weak self] error in + if error == nil { + self?.revokeMessageUpdateUI(message) + } + completion(error) + } } - open func onRecvMessageReceipts(_ receipts: [NIMMessageReceipt]) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", receipts.count: \(receipts.count)") - print( - "chatViewModel: :\(receipts.count) messageId:\(receipts.first?.messageId) messageId:\(receipts.first?.timestamp)" - ) - delegate?.didReadedMessageIndexs() + /// 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - Returns: 名称和好友信息 + open func getShowName(_ accountId: String, + _ showAlias: Bool = true) -> String { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:" + accountId) + return NEFriendUserCache.shared.getShowName(accountId, showAlias) } + /// 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - completion: 完成回调 + open func loadShowName(_ accountIds: [String], + _ teamId: String? = nil, + _ completion: @escaping () -> Void) {} + + /// 获取消息所支持的操作列表 + /// - Parameter model: 消息模型 + /// - Returns: 操作列表 open func avalibleOperationsForMessage(_ model: MessageContentModel?) -> [OperationItem]? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", pinAccount: " + (model?.pinAccount ?? "nil")) + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", pinAccount: " + (model?.pinAccount ?? "nil")) var items = [OperationItem]() /// 消息发送中的消息只能删除(文本可复制) - if model?.message?.deliveryState == .delivering { + if model?.message?.sendingState == .MESSAGE_SENDING_STATE_SENDING { switch model?.message?.messageType { - case .text: + case .MESSAGE_TYPE_TEXT: items.append(contentsOf: [ OperationItem.copyItem(), OperationItem.deleteItem(), @@ -1183,11 +960,10 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg } /// 发送失败 || 黑名单中的消息 || 话单消息 只能多选和删除(文本可复制) - if model?.message?.deliveryState == .failed || - model?.message?.messageType == .rtcCallRecord || - model?.message?.isBlackListed == true { + if model?.message?.sendingState == .MESSAGE_SENDING_STATE_FAILED || + model?.message?.messageType == .MESSAGE_TYPE_CALL { switch model?.message?.messageType { - case .text: + case .MESSAGE_TYPE_TEXT: items.append(contentsOf: [ OperationItem.copyItem(), OperationItem.deleteItem(), @@ -1204,42 +980,51 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg /// 消息发送成功 let pinItem = model?.isPined == false ? OperationItem.pinItem() : OperationItem.removePinItem() + let topItem = model?.message?.messageClientId == topMessage?.messageClientId ? OperationItem.untopItem() : OperationItem.topItem() switch model?.message?.messageType { - case .location: + case .MESSAGE_TYPE_LOCATION: items.append(contentsOf: [ OperationItem.replayItem(), OperationItem.forwardItem(), pinItem, + topItem, + OperationItem.collectionItem(), OperationItem.deleteItem(), OperationItem.selectItem(), ]) - case .text: + case .MESSAGE_TYPE_TEXT: items = [ OperationItem.copyItem(), OperationItem.replayItem(), OperationItem.forwardItem(), pinItem, + topItem, + OperationItem.collectionItem(), OperationItem.deleteItem(), OperationItem.selectItem(), ] - case .image, .video, .file: + case .MESSAGE_TYPE_IMAGE, .MESSAGE_TYPE_VIDEO, .MESSAGE_TYPE_FILE: items = [ OperationItem.replayItem(), OperationItem.forwardItem(), pinItem, + topItem, + OperationItem.collectionItem(), OperationItem.deleteItem(), OperationItem.selectItem(), ] - case .audio: + case .MESSAGE_TYPE_AUDIO: items = [ OperationItem.replayItem(), pinItem, + topItem, + OperationItem.collectionItem(), OperationItem.deleteItem(), OperationItem.selectItem(), ] - case .custom: - if let attach = NECustomAttachment.attachmentOfCustomMessage(message: model?.message) { - if attach.customType == customRichTextType { + case .MESSAGE_TYPE_CUSTOM: + if (model?.customType ?? 0) > 0 { + if model?.customType == customRichTextType { items = [ OperationItem.copyItem(), ] @@ -1248,13 +1033,17 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg OperationItem.replayItem(), OperationItem.forwardItem(), pinItem, + topItem, + OperationItem.collectionItem(), OperationItem.deleteItem(), OperationItem.selectItem(), ]) } else { // 未知消息体 items = [ + OperationItem.collectionItem(), OperationItem.deleteItem(), + OperationItem.selectItem(), ] } default: @@ -1266,299 +1055,429 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg ] } - // 自己发送且非未知消息可以撤回 - if model?.message?.from == NIMSDK.shared().loginManager.currentAccount() { - if model?.message?.messageType == .custom, - NECustomAttachment.dataOfCustomMessage(message: model?.message) == nil { + // 自己发送且非未知消息可以 【撤回】 + if model?.message?.isSelf == true { + if model?.message?.messageType == .MESSAGE_TYPE_CUSTOM, + NECustomAttachment.dataOfCustomMessage(model?.message?.attachment) == nil { return items } items.append(OperationItem.recallItem()) } - return items - } - private func indexPathsForTeamMarkRead(_ receipts: [NIMMessageReceipt]) -> [IndexPath] { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", receipts.count: \(receipts.count)") - var indexs = [IndexPath]() -// find messages that need to update UI - for receipt in receipts { - for (i, model) in messages.enumerated() { - if model.message?.messageId == receipt.messageId { - indexs.append(IndexPath(row: i, section: 0)) - } + // 根据配置项移除 【收藏】 + if IMKitConfigCenter.shared.collectionEnable == false { + items.removeAll { item in + item.type == .collection } } - print("mark read indexs:\(indexs)") - return indexs - } - private func indexPathsForP2PMarkRead(_ receipts: [NIMMessageReceipt]) -> [IndexPath] { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", receipts.count: \(receipts.count)") - var updateIndexs = [IndexPath]() -// find messages that need to update UI - var i = messages.count - 1 - for model in messages.reversed() { - if let msg = model.message, msg.isRemoteRead { - updateIndexs.append(IndexPath(row: i, section: 0)) - break - } else { - updateIndexs.append(IndexPath(row: i, section: 0)) - i -= 1 + // 根据配置项移除 【标记】 + if IMKitConfigCenter.shared.pinEnable == false { + items.removeAll { item in + item.type == .pin || item.type == .removePin } } - return updateIndexs - } - private func addTimeForHistoryMessage(_ model: MessageModel) { - guard let first = messages.first, - let firstMsg = first.message else { - NELog.errorLog(ModuleName + " " + className(), desc: #function + ", model.message is nil") - return + // 根据配置项移除 【置顶】 + if IMKitConfigCenter.shared.topEnable == false { + items.removeAll { item in + item.type == .top || item.type == .untop + } } - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + firstMsg.messageId) - if NotificationMessageUtils.isDiscussSeniorTeamNoti(message: firstMsg) { - return + return items + } + + /// 消息列表添加时间 + private func addTimeForHistoryMessage() { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + for (i, model) in messages.enumerated() { + if i == 0, let createTime = model.message?.createTime { + // 第一条消息默认显示时间 + let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: createTime)) + model.timeContent = timeText + continue + } + + if let message = model.message, NotificationMessageUtils.isDiscussSeniorTeamNoti(message: message) { + continue + } + + // 当前消息时间 - 上一条消息时间 > 5s, 则显示当前消息的创建时间 + let lastModel = messages[i - 1] + let lastTime = lastModel.message?.createTime ?? 0.0 + let curTime = model.message?.createTime ?? 0 + let dur = curTime - lastTime + if (dur / 60) > 5 { + let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: curTime)) + model.timeContent = timeText + } } + } + + /// 构建聊天页面UI显示model + /// - Parameters: + /// - message: 消息 + /// - completion: 完成回调 + open func modelFromMessage(message: V2NIMMessage, _ completion: @escaping (MessageModel) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + ChatMessageHelper.modelFromMessage(message: message) { [weak self] model in + if ChatMessageHelper.isRevokeMessage(message: model.message) { + if let content = ChatMessageHelper.getRevokeMessageContent(message: model.message) { + model.isReedit = true + model.message?.text = content + } + model.isRevoked = true + } + + if let uid = message.senderId, + let fullName = self?.getShowName(uid) { + let user = NEFriendUserCache.shared.getFriendInfo(uid) ?? ChatUserCache.shared.getUserInfo(uid) + model.avatar = user?.user?.avatar + model.fullName = fullName + model.shortName = NEFriendUserCache.getShortName(fullName) + } - let firstTs = firstMsg.timestamp - let curTs = model.message?.timestamp ?? 0.0 - let dur = firstTs - curTs - if (dur / 60) > 5 { - let timeText = String.stringFromDate(date: Date(timeIntervalSince1970: firstTs)) - first.timeContent = timeText + if let replyModel = self?.getReplyMessageWithoutThread(message: message) { + model.replyedModel = replyModel + } + self?.delegate?.getMessageModel?(model: model) + completion(model) } } - // 构建聊天页面UI显示model - open func modelFromMessage(message: NIMMessage) -> MessageModel { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) + /// 构建聊天页面UI显示model + /// - Parameter message: 消息 + /// - Returns: 消息体 + open func modelFromMessage(message: V2NIMMessage) -> MessageModel { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") let model = ChatMessageHelper.modelFromMessage(message: message) - if let uid = message.from { - let user = ChatUserCache.getUserInfo(uid) - let fullName = ChatUserCache.getShowName(userId: uid, teamId: session.sessionId) - model.avatar = user?.userInfo?.avatarUrl + + if ChatMessageHelper.isRevokeMessage(message: model.message) { + if let content = ChatMessageHelper.getRevokeMessageContent(message: model.message) { + model.isReedit = true + model.message?.text = content + } + model.isRevoked = true + } + + if let uid = message.senderId { + let fullName = getShowName(uid) + let user = NEFriendUserCache.shared.getFriendInfo(uid) ?? ChatUserCache.shared.getUserInfo(uid) + model.avatar = user?.user?.avatar model.fullName = fullName - model.shortName = ChatUserCache.getShortName(name: user?.showName(false) ?? "", length: 2) - } - model.replyedModel = getReplyMessageWithoutThread(message: message) - if let pin = repo.searchMessagePinHistory(message) { - model.isPined = true - model.pinAccount = pin.accountID - let pinID = pin.accountID ?? NIMSDK.shared().loginManager.currentAccount() - model.pinShowName = ChatUserCache.getShowName(userId: pinID, teamId: session.sessionId) - } else { - model.isPined = false + model.shortName = NEFriendUserCache.getShortName(fullName) + } + + if let replyModel = getReplyMessageWithoutThread(message: message) { + model.replyedModel = replyModel + if replyModel.message == nil { + model.replyText = chatLocalizable("message_not_found") + } } delegate?.getMessageModel?(model: model) return model } - // 查找回复消息 - open func getReplyMessage(message: NIMMessage) -> MessageModel? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - guard let id = message.repliedMessageId, id.count > 0 else { + /// 查找回复消息,优先不使用 thread 方案 (不进行远端拉取) + /// - Parameters: + /// - message: 需要查找回复的消息 + /// - completion: 完成回调 + open func getReplyMessageWithoutThread(message: V2NIMMessage) -> MessageModel? { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + var replyId: String? = message.threadReply?.messageClientId + let replyDic = ChatMessageHelper.getReplyDictionary(message: message) + replyId = replyDic?["idClient"] as? String + guard let replyId = replyId, !replyId.isEmpty else { return nil } - if let m = ConversationProvider.shared.messagesInSession(session, messageIds: [id])? - .first { - let model = modelFromMessage(message: m) - model.isReplay = true - return model + + for model in messages { + if model.message?.messageClientId == replyId, model.isRevoked == false { + model.isReplay = true + return model + } } - let message = NIMMessage() - let model = modelFromMessage(message: message) + + let model = MessageTextModel(message: nil) model.isReplay = true + if let replySenderId = replyDic?["from"] as? String { + model.fullName = replySenderId + } return model } - open func getReplyMessageWithoutThread(message: NIMMessage) -> MessageModel? { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - - var replyId: String? = message.repliedMessageId - if let yxReplyMsg = message.remoteExt?[keyReplyMsgKey] as? [String: Any] { - replyId = yxReplyMsg["idClient"] as? String + /// 查找回复消息,优先不使用 thread 方案,已加载的消息中没有则去远端查 + /// - Parameters: + /// - message: 需要查找回复的消息 + /// - fetch: 是否远端查询 + /// - completion: 完成回调 + open func getReplyMessageWithoutThread(message: V2NIMMessage, + _ completion: @escaping (MessageModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + var replyId: String? = message.threadReply?.messageClientId + let replyDic = ChatMessageHelper.getReplyDictionary(message: message) + replyId = replyDic?["idClient"] as? String + guard let replyId = replyId, !replyId.isEmpty else { + completion(nil) + return } - guard let id = replyId, !id.isEmpty else { - return nil + // 先去已加载的消息中查 + for model in messages { + if model.message?.messageClientId == replyId, !model.isRevoked { + model.isReplay = true + completion(model) + return + } } - if let m = ConversationProvider.shared.messagesInSession(session, messageIds: [id])? - .first { - let model = modelFromMessage(message: m) - model.isReplay = true - return model + // 已加载的消息中没有则去远端查 + let refer = ChatMessageHelper.createMessageRefer(replyDic) + chatRepo.getMessageListByRefers([refer]) { [weak self] messages, error in + if let m = messages?.first { + self?.modelFromMessage(message: m) { model in + model.isReplay = true + completion(model) + } + } else { + completion(nil) + } } - let message = NIMMessage() - let model = modelFromMessage(message: message) - model.isReplay = true - return model } - func deleteMessageUpdateUI(_ message: NIMMessage) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) + @discardableResult + func deleteMessageModel(_ message: V2NIMMessage) -> (deleteIndexs: [Int], reloadIndexs: [Int]) { + var deleteIndexs = [Int]() + var reloadIndexs = [Int]() var index = -1 var replyIndex = [Int]() var hasFind = false + for (i, model) in messages.enumerated() { if hasFind { - var replyId: String? = model.message?.repliedMessageId - if let yxReplyMsg = model.message?.remoteExt?[keyReplyMsgKey] as? [String: Any] { + var replyId: String? = model.message?.threadReply?.messageClientId + if let remoteExt = getDictionaryFromJSONString(model.message?.serverExtension ?? ""), + let yxReplyMsg = remoteExt[keyReplyMsgKey] as? [String: Any] { replyId = yxReplyMsg["idClient"] as? String } - if let id = replyId, !id.isEmpty, id == message.messageId { + if let id = replyId, !id.isEmpty, id == message.messageClientId { messages[i].replyText = chatLocalizable("message_not_found") replyIndex.append(i) } } else { - if model.message?.messageId == message.messageId { + if model.message?.messageClientId == message.messageClientId { index = i hasFind = true } } } - var indexs = [IndexPath]() - var reloadIndexs = [IndexPath]() if index >= 0 { -// remove time tip - let last = index - 1 - if last >= 0, let timeModel = messages[last] as? MessageTipsModel, - timeModel.type == .time { - messages.removeSubrange(last ... index) - indexs.append(IndexPath(row: last, section: 0)) - indexs.append(IndexPath(row: index, section: 0)) - for replyIdx in replyIndex { - reloadIndexs.append(IndexPath(row: replyIdx - 2, section: 0)) - } - } else { - messages.remove(at: index) - indexs.append(IndexPath(row: index, section: 0)) - for replyIdx in replyIndex { - reloadIndexs.append(IndexPath(row: replyIdx - 1, section: 0)) - } + deleteIndexs.append(index) + for replyIdx in replyIndex { + reloadIndexs.append(replyIdx) + } + } + + return (deleteIndexs, reloadIndexs) + } + + /// 删除消息更新UI + /// - Parameter message: 消息 + func deleteMessageUpdateUI(_ messages: [V2NIMMessage]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages count: \(messages.count)") + var deleteIndexs = Set() + var reloadIndexs = Set() + for message in messages { + let indexs = deleteMessageModel(message) + + for index in indexs.deleteIndexs { + deleteIndexs.insert(index) + } + + for index in indexs.reloadIndexs { + reloadIndexs.insert(index) } } - delegate?.onDeleteMessage(message, atIndexs: indexs, reloadIndex: reloadIndexs) + let deleteIndexPaths = deleteIndexs.map { IndexPath(row: $0, section: 0) } + let reloadIndexPaths = reloadIndexs.map { IndexPath(row: $0, section: 0) } + + delegate?.onDeleteMessage(messages, deleteIndexs: deleteIndexPaths, reloadIndex: reloadIndexPaths) } - func revokeMessageUpdateUI(_ message: NIMMessage) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) + /// 撤回消息更新UI + /// - Parameter message: 消息 + func revokeMessageUpdateUI(_ message: V2NIMMessage) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") var index = -1 - var replyIndex = [Int]() + var indexs = [IndexPath]() var hasFind = false + // 遍历查找回复该条消息的消息 for (i, model) in messages.enumerated() { if hasFind { - var replyId: String? = model.message?.repliedMessageId - if let yxReplyMsg = model.message?.remoteExt?[keyReplyMsgKey] as? [String: Any] { + var replyId: String? = model.message?.threadReply?.messageClientId + if let remoteExt = getDictionaryFromJSONString(model.message?.serverExtension ?? ""), + let yxReplyMsg = remoteExt[keyReplyMsgKey] as? [String: Any] { replyId = yxReplyMsg["idClient"] as? String } - if let id = replyId, !id.isEmpty, id == message.messageId { - replyIndex.append(i) + if let id = replyId, !id.isEmpty, id == message.messageClientId { + messages[i].replyText = chatLocalizable("message_not_found") + indexs.append(IndexPath(row: i, section: 0)) } } else { - if model.message?.serverID == message.serverID { + if model.message?.messageServerId == message.messageServerId { index = i hasFind = true } } } - var indexs = [IndexPath]() if index >= 0 { messages[index].isRevoked = true messages[index].replyedModel = nil messages[index].isPined = false - indexs.append(IndexPath(row: index, section: 0)) - } - for replyIdx in replyIndex { - messages[replyIdx].replyText = chatLocalizable("message_not_found") - indexs.append(IndexPath(row: replyIdx, section: 0)) + // 是否可以重新编辑 + if let content = ChatMessageHelper.getRevokeMessageContent(message: messages[index].message) { + messages[index].isReedit = true + messages[index].message?.text = content + } + + indexs.append(IndexPath(row: index, section: 0)) } delegate?.onRevokeMessage(message, atIndexs: indexs) } - open func fetchMessageAttachment(_ message: NIMMessage, didCompleteWithError error: Error?) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - } - - open func fetchMessageAttachment(_ message: NIMMessage, progress: Float) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - } + /// 下载附件 + /// - Parameters: + /// - urlString: 远端 url + /// - filePath: 本地路径 + /// - progress: 下载进度回调 + /// - completion: 完成回调 + open func downLoad(_ urlString: String, + _ filePath: String, + _ progress: ((UInt) -> Void)?, + _ completion: ((String?, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: " + urlString) + ResourceRepo.shared.downLoad(urlString, filePath, progress, completion) + } + + /// 转发消息 + /// - Parameters: + /// - conversationIds: 会话 id 列表 + /// - isMultiForward: 是否是合并转发 + /// - depth: 合并转发深度 + /// - comment: 留言 + /// - completion: 完成回调 + open func forwardMessages(_ conversationIds: [String], + _ isMultiForward: Bool, + _ depth: Int, + _ comment: String?, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(selectedMessages.count)") - open func fetchMessageAttachment(_ message: NIMMessage, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - repo.downloadMessageAttachment(message, completion) - } + // 排序(发送时间正序) + let forwardMessages = selectedMessages.sorted { msg1, msg2 in + msg1.createTime < msg2.createTime + } - 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) + if isMultiForward { + forwardMultiMessage(forwardMessages: forwardMessages, + conversationIds: conversationIds, + depth: depth, + comment: comment, + completion) + } else { + forwardMessage(selectedMessages, conversationIds, comment, completion) + } } - // 转发消息 - open func forwardMessage(_ forwardMessages: [NIMMessage], - _ session: NIMSession, + /// 逐条转发消息 + /// - Parameters: + /// - forwardMessages: 需要逐条转发的消息列表 + /// - conversationId: 转发的会话 id + /// - comment: 留言 + /// - completion: 完成回调 + open func forwardMessage(_ forwardMessages: [V2NIMMessage], + _ conversationIds: [String], _ comment: String?, _ completion: @escaping (Error?) -> Void) { - for message in forwardMessages { - if let forwardMessage = repo.makeForwardMessage(message) { + for conversationId in conversationIds { + for message in forwardMessages { + let forwardMessage = MessageUtils.forwardMessage(message: message) ChatMessageHelper.clearForwardAtMark(forwardMessage) - repo.sendForwardMessage(forwardMessage, session) + + chatRepo.sendMessage(message: forwardMessage, conversationId: conversationId) { result, error, pro in + } + } + + // 发送留言 + if let text = comment, !text.isEmpty { + // 延迟 0.2s 发送,确保留言位置在最后 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: DispatchWorkItem(block: { [weak self] in + self?.sendTextMessage(text: text, conversationId: conversationId, remoteExt: nil) { _, error in + completion(error) + } + })) + } else { + completion(nil) } - } - 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?, + /// 合并转发消息 + /// - Parameters: + /// - forwardMessages: 需要合并的消息列表 + /// - toconversationId: 转发的会话 id + /// - users: 需要转发的好友列表 + /// - depth: 合并转发消息的深度 + /// - comment: 留言 + /// - completion: 完成回调 + open func forwardMultiMessage(forwardMessages: [V2NIMMessage], + conversationIds: [String], + 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) + for conversationId in conversationIds { + sendTextMessage(text: text, conversationId: conversationId, remoteExt: nil) { _, error in + completion(error) + } + } } else { completion(nil) } return } - let fromSession = session + let fromSession = conversationId let header = ChatMessageHelper.buildHeader(messageCount: forwardMessages.count) ChatMessageHelper.buildBody(messages: forwardMessages) { body, abstracts in let multiForwardMsg = header + body let fileName = multiForwardFileName + "\(Int(Date().timeIntervalSince1970))" - if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(fileName)") - if let multiForwardMsgData = multiForwardMsg.data(using: .utf8) { - do { - try multiForwardMsgData.write(to: filePath) - } catch { - completion(NSError(domain: chatLocalizable("forward_failed"), code: 414)) - print("Error writing string to file: \(error)") - return - } + if var filePath = NEPathUtils.getDirectoryForDocuments(dir: "\(imkitDir)file/") { + filePath += fileName + + do { + try multiForwardMsg.write(toFile: filePath, atomically: true, encoding: .utf8) + } catch { + completion(NSError(domain: chatLocalizable("forward_failed"), code: 414)) + print("Error writing string to file: \(error)") + return } - NIMSDK.shared().resourceManager.upload(filePath.path, progress: nil) { [weak self] url, error in + let fileTask = ResourceRepo.shared.createUploadFileTask(filePath) + ResourceRepo.shared.upload(fileTask, nil) { [weak self] url, error in if let err = error { completion(err) - } else if let url = url { + } else if let url = url, let filePath = URL(string: filePath) { let md5 = ChatMessageHelper.getFileChecksum(fileURL: filePath) // 删除本地文件 @@ -1569,33 +1488,34 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg } var data = [String: Any]() - data["sessionId"] = toSession.sessionId - data["sessionName"] = ChatMessageHelper.getSessionName(session: fromSession, showAlias: false) + data["sessionId"] = V2NIMConversationIdUtil.conversationTargetId(fromSession) data["url"] = url data["md5"] = md5 data["depth"] = depth data["abstracts"] = abstracts + data["sessionName"] = ChatMessageHelper.getSessionName(conversationId: fromSession, showAlias: false) var jsonData = [String: Any]() jsonData["data"] = data jsonData["messageType"] = "custom" jsonData["type"] = customMultiForwardType - let attah = NECustomAttachment(customType: customMultiForwardType, - cellHeight: customMultiForwardCellHeight, - data: jsonData) - self?.sendCustomMessage(attachment: attah, - remoteExt: nil, - apnsConstent: "[\(chatLocalizable("chat_history"))]", - session: toSession) { error in - if let err = error { - completion(err) + // 转发到会话 + for conversationId in conversationIds { + self?.sendCustomMessage(text: "[\(chatLocalizable("chat_history"))]", + rawAttachment: getJSONStringFromDictionary(jsonData), conversationId: conversationId) { error in + } + + // 发送留言 + if let text = comment, !text.isEmpty { + // 延迟 0.2s 发送,确保留言位置在最后 + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: DispatchWorkItem(block: { [weak self] in + self?.sendTextMessage(text: text, conversationId: conversationId, remoteExt: nil) { _, error in + completion(error) + } + })) } else { - if let text = comment, !text.isEmpty { - self?.sendTextMessage(text: text, session: toSession, completion) - } else { - completion(nil) - } + completion(nil) } } } @@ -1604,253 +1524,545 @@ open class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDeleg } } - 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 isMultiForward { - forwardMultiMessage(forwardMessages, session, depth, comment, completion) - } else { - forwardMessage(forwardMessages, session, comment, completion) + /// 标记消息 + /// - Parameters: + /// - message: 消息 + /// - completion: 完成回调 + open func addPinMessage(message: V2NIMMessage, + _ completion: @escaping (Error?, Int) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + chatRepo.pinMessage(message: message, serverExtension: "") { [weak self] error in + var index = -1 + if error != nil { + completion(error, index) + } else { + for (i, model) in (self?.messages ?? []).enumerated() { + if message.messageClientId == model.message?.messageClientId, !(self?.messages[i].isPined == true) { + self?.messages[i].isPined = true + self?.messages[i].pinAccount = IMKitClient.instance.account() + self?.messages[i].pinShowName = self?.getShowName(IMKitClient.instance.account()) + index = i + break + } } + completion(nil, index) } } } - 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 isMultiForward { - forwardMultiMessage(selectedMessages, session, depth, comment, completion) + /// 取消消息标记 + /// - Parameters: + /// - message: 消息 + /// - completion: 完成回调 + open func removePinMessage(message: V2NIMMessage, + _ completion: @escaping (Error?, Int) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + chatRepo.unpinMessage(messageRefer: message, serverExtension: "") { [weak self] error in + if error != nil { + completion(error, -1) } else { - forwardMessage(selectedMessages, session, comment, completion) + let index = self?.removeLocalPinMessage(message) ?? -1 + completion(nil, index) } } } - // 标记消息 - 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 { + /// 置顶消息 + /// - Parameter completion: 回调 + open func topMessage(_ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: operationModel?.message?.messageClientId))") + } + + /// 取消置顶消息 + /// - Parameter completion: 回调 + open func untopMessage(_ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId \(String(describing: topMessage?.messageClientId))") + } + + /// 收藏消息 + /// - Parameter model: UI使用消息体 + /// - Parameter conversationName: 会话名 + /// - Parameter completion: 回调 + open func collectMessage(_ model: MessageContentModel, _ conversationName: String, _ completion: @escaping (NSError?) -> Void) { + guard let message = model.message else { return } + let collectionType = message.messageType.rawValue + collectionTypeOffset + var collectionDic = [String: Any]() - repo.addMessagePin(item) { [weak self] error, pinItem in - if error != nil { - completion(error, nil, -1) + if let messageString = V2NIMMessageConverter.messageSerialization(message) { + collectionDic["message"] = messageString + } + collectionDic["conversationName"] = conversationName + + if let senderName = model.fullName { + collectionDic["senderName"] = senderName + } + + if let avatar = model.avatar { + collectionDic["avatar"] = avatar + } + + let content = getJSONStringFromDictionary(collectionDic) + + let params = V2NIMAddCollectionParams() + params.collectionType = Int32(collectionType) + params.collectionData = content + params.uniqueId = message.messageClientId + + chatRepo.addCollection(params) { collection, error in + if let err = error { + completion(err) } else { - var index = -1 - if let messages = self?.messages { - for (i, model) in messages.enumerated() { - if message.messageId == model.message?.messageId, !messages[i].isPined { - messages[i].isPined = true - messages[i].pinAccount = NIMSDK.shared().loginManager.currentAccount() - messages[i].pinShowName = ChatUserCache.getShowName( - userId: NIMSDK.shared().loginManager.currentAccount(), - teamId: message.session?.sessionId - ) - self?.messages = messages - index = i + completion(nil) + } + } + } + + /// 获取听筒模式 + /// - Returns: 听筒模式 + open func getHandSetEnable() -> Bool { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + return SettingRepo.shared.getHandsetMode() + } + + @discardableResult + /// 移除缓存数据的标记状态 + /// - Parameter message: 消息 + /// - Returns: 消息下标 + private func removeLocalPinMessage(_ message: V2NIMMessage) -> Int { + var index = -1 + + for (i, model) in messages.enumerated() { + if message.messageClientId == model.message?.messageClientId, messages[i].isPined { + messages[i].isPined = false + messages[i].pinAccount = nil + index = i + break + } + } + return index + } + + /// 消息即将发送 + /// - Parameter message: 消息 + open func sendingMsg(_ message: V2NIMMessage) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId:\(String(describing: message.messageClientId))") + print("\(#function)") + + // 消息不是当前会话的消息,不处理(转发) + if message.conversationId != conversationId { + return + } + + // 消息已存在则更新消息状态 + for (i, model) in messages.enumerated() { + if message.messageClientId == model.message?.messageClientId { + messages[i].message = message + delegate?.sendSuccess(message, IndexPath(row: i, section: 0)) + return + } + } + + // 避免重复发送 + if ChatDeduplicationHelper.instance.isMessageSended(messageId: message.messageClientId ?? "") { + return + } + + // 自定义消息发送之前的处理 + if newMsg == nil { + newMsg = message + } + + // 插入一条消息 + let model = modelFromMessage(message: message) + ChatMessageHelper.addTimeMessage(model, messages.last) + let index = insertToMessages(model) + + delegate?.sending(message, IndexPath(row: index, section: 0)) + } + + /// 消息发送完成 + /// - Parameters: + /// - message: 消息 + /// - error: 错误信息 + @nonobjc open func sendMsgSuccess(_ message: V2NIMMessage) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: message.messageClientId))") + + if message.conversationId != conversationId { + return + } + + var index = -1 + for (i, msg) in messages.enumerated() { + if message.messageClientId == msg.message?.messageClientId { + if messages[i].message?.sendingState != .MESSAGE_SENDING_STATE_SUCCEEDED { + index = i + } + messages[i].message = message + break + } + } + + if index > 0 { + let indexPath = IndexPath(row: index, section: 0) + + // 重发消息位置替换 + if index != messages.count - 1 { + for (i, model) in messages.enumerated() { + if message.createTime < (model.message?.createTime ?? 0) { + if i - 1 != index { + exchangeMessageModel(index, i) + return + } else { break } + } else if i == messages.count - 1 { + if i != index { + exchangeMessageModel(index, i) + return + } } } - completion(nil, pinItem, index) } + + delegate?.sendSuccess(message, indexPath) } } - // 取消消息标记 - 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 { + /// 消息发送失败 + /// - Parameters: + /// - message: 消息 + /// - error: 错误信息 + open func sendMsgFailed(_ message: V2NIMMessage, _ error: Error?) { + guard let messageClientId = message.messageClientId else { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", error: \(String(describing: error))") return } - let item = NIMMessagePinItem(message: message) - weak var weakSelf = self - repo.removeMessagePin(item) { error, pinItem in - if error != nil { - completion(error, nil, -1) - } else { - let index = weakSelf?.removeLocalPinMessage(message) ?? -1 - completion(nil, pinItem, index) + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "messageClientId:\(messageClientId), error: \(String(describing: error))") + + // 判断发送失败原因是否是因为在黑名单中 + if error != nil { + if let err = error as NSError? { + if err.code == inBlackListCode, let conversationId = message.conversationId { + // 防重 + if ChatDeduplicationHelper.instance.isBlackTipSended(messageId: messageClientId) { + return + } + + DispatchQueue.main.async { [weak self] in + self?.insertTipMessage(chatLocalizable("black_list_tip"), conversationId) + if conversationId == self?.conversationId { + self?.sendMsgSuccess(message) + } + } + } } + } else { + sendMsgSuccess(message) } } - // 发送正在输入中状态 - open func sendInputTypingState() { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - if session.sessionType == .P2P { - setTypingCustom(1) + /// 交换消息位置 + /// - Parameters: + /// - fromIndex: 原始位置 + /// - toIndex: 新位置 + func exchangeMessageModel(_ fromIndex: Int, _ toIndex: Int) { + let resendModel = messages[fromIndex] + // 更新旧位置下一条消息的时间 + if fromIndex + 1 < messages.count { + ChatMessageHelper.addTimeMessage(messages[fromIndex + 1], messages[fromIndex - 1]) } - } - // 发送结束输入中状态 - open func sendInputTypingEndState() { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - if session.sessionType == .P2P { - setTypingCustom(0) + // 更新新位置当前消息的时间 + if toIndex > 0 { + ChatMessageHelper.addTimeMessage(resendModel, messages[toIndex]) } - } - func setTypingCustom(_ typing: Int) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", typing: \(typing)") - let message = NIMMessage() - if message.setting == nil { - message.setting = NIMMessageSetting() + // 更新新位置下一条消息的时间 + if toIndex + 1 < messages.count { + ChatMessageHelper.addTimeMessage(messages[toIndex + 1], resendModel) } - message.setting?.apnsEnabled = false - message.setting?.shouldBeCounted = false - let noti = - NIMCustomSystemNotification(content: getJSONStringFromDictionary(["typing": typing])) - repo.sendCustomNotification(noti, session) { error in - if let err = error { - print("send noti success :", err) - } - } + messages.remove(at: fromIndex) + messages.insert(resendModel, at: toIndex) + delegate?.onResendSuccess(IndexPath(row: fromIndex, section: 0), IndexPath(row: toIndex, section: 0)) } +} - open func getHandSetEnable() -> Bool { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - return repo.getHandsetMode() +// MARK: - NEChatListener + +extension ChatViewModel: NEChatListener { + /// 本端即将发送消息状态回调,此时消息还未发送,可对消息进行修改或者拦截发送 + /// 来源: 发送消息, 插入消息 + /// - Parameter message: 消息 + /// - Parameter completion: 是否继续发送消息 + public func readySendMessage(_ message: V2NIMMessage, _ completion: @escaping (Bool) -> Void) { + delegate?.readySendMessage?(message, completion) + } + + /// 本端发送消息状态回调 + /// 来源: 发送消息, 插入消息 + /// - Parameter message: 消息 + public func onSendMessage(_ message: V2NIMMessage) { + switch message.sendingState { + case .MESSAGE_SENDING_STATE_SENDING: + sendingMsg(message) + case .MESSAGE_SENDING_STATE_FAILED: + sendMsgFailed(message, nil) + case .MESSAGE_SENDING_STATE_SUCCEEDED: + sendMsgSuccess(message) + default: + break + } } - open func getMessageRead() -> Bool { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - return repo.getMessageRead() + /// 消息发送失败 + /// - Parameters: + /// - message: 消息 + /// - error: 错误信息 + public func sendMessageFailed(_ message: V2NIMMessage, _ error: NSError) { + sendMsgFailed(message, error) } - // 本地保存撤回消息 - open func saveRevokeMessage(_ message: NIMMessage, _ completion: @escaping (Error?) -> Void) { - let messageNew = NIMMessage() - messageNew.text = chatLocalizable("message_recalled") - var muta = [String: Any]() - muta[revokeLocalMessage] = true - if message.messageType == .text { - muta[revokeLocalMessageContent] = message.text - } - if message.messageType == .custom { - if let title = NECustomAttachment.titleOfRichText(message: message), !title.isEmpty { - muta[revokeLocalMessageContent] = title + /// 收到消息 + /// - Parameter messages: 消息列表 + public func onReceiveMessages(_ messages: [V2NIMMessage]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count), first.messageID: \(messages.first?.messageClientId ?? "")") + + for msg in messages { + guard V2NIMConversationIdUtil.conversationTargetId(msg.conversationId ?? "") == sessionId else { + return } - if let body = NECustomAttachment.bodyOfRichText(message: message), !body.isEmpty { - muta[revokeLocalMessageContent] = body + + if !(msg.messageServerId?.isEmpty == false), msg.messageType != .MESSAGE_TYPE_CUSTOM { + continue + } + newMsg = msg + + if isHistoryChat { + delegate?.dataReload?() + return + } + + modelFromMessage(message: msg) { [weak self] model in + ChatMessageHelper.addTimeMessage(model, self?.messages.last) + self?.downloadAudioFile([model]) + self?.loadReply(model) { + if let index = self?.insertToMessages(model) { + self?.delegate?.onRecvMessages(messages, [IndexPath(row: index, section: 0)]) + } + } } } - messageNew.timestamp = message.timestamp - messageNew.from = message.from - messageNew.localExt = muta - messageNew.remoteExt = message.remoteExt - let setting = NIMMessageSetting() - setting.shouldBeCounted = false - setting.isSessionUpdate = false - messageNew.setting = setting - repo.saveMessageToDB(messageNew, session, completion) } - open func filterRevokeMessage(_ messages: [MessageModel]) { - messages.forEach { model in - if let isRevoke = model.message?.localExt?[revokeLocalMessage] as? Bool, isRevoke == true { - if let content = model.message?.localExt?[revokeLocalMessageContent] as? String, content.count > 0 { - model.isRevokedText = true - model.message?.text = content + /// 消息撤回回调 + /// - Parameter revokeNotifications: 撤回通知 + public func onMessageRevokeNotifications(_ revokeNotifications: [V2NIMMessageRevokeNotification]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", revokeNotifications.count: \(revokeNotifications.count)") + for revokeNoti in revokeNotifications { + if revokeNoti.messageRefer?.conversationId != conversationId { + continue + } + if revokeNoti.messageRefer?.messageClientId?.isEmpty == true { + continue + } + + // 移除置顶效果 + if topMessage?.messageClientId == revokeNoti.messageRefer?.messageClientId { + topMessage = nil + delegate?.setTopValue(name: nil, content: nil, url: nil, isVideo: false, hideClose: false) + } + + for model in messages { + if let msg = model.message, msg.messageClientId == revokeNoti.messageRefer?.messageClientId { + msg.localExtension = revokeNoti.serverExtension + revokeMessageUpdateUI(msg) + break } - model.isRevoked = true } } } - // 刷新已读回执 - open func refreshReceipts(messages: [NIMMessage]) { - if session.sessionType != .team { - return - } - if repo.settingProvider.getMessageRead() == false { - return - } - print("refresh team id : ", session.sessionId) - var receiptsMessages = [NIMMessage]() - messages.forEach { message in - if message.setting?.teamReceiptEnabled == true { - receiptsMessages.append(message) + /// 消息删除成功回调。当本地端或多端同步删除消息成功时会触发该回调。 + /// - Parameter messageDeletedNotification: 删除通知 + public func onMessageDeletedNotifications(_ messageDeletedNotification: [V2NIMMessageDeletedNotification]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageDeletedNotification.count: \(messageDeletedNotification.count)") + + var deleteMessages = [V2NIMMessage]() + for message in messageDeletedNotification { + if message.messageRefer.conversationId != conversationId { + continue + } + if message.messageRefer.messageClientId?.isEmpty == true { + continue + } + + // 移除置顶效果 + if topMessage?.messageClientId == message.messageRefer.messageClientId { + topMessage = nil + delegate?.setTopValue(name: nil, content: nil, url: nil, isVideo: false, hideClose: false) + } + + for model in messages { + if let msg = model.message, msg.messageClientId == message.messageRefer.messageClientId { + deleteMessages.append(msg) + } } } - for receipt in receiptsMessages.chunk(50) { - repo.refreshReceipts(receipt) - } + + deleteMessageUpdateUI(deleteMessages) } - @discardableResult - private func removeLocalPinMessage(_ message: NIMMessage) -> Int { - var index = -1 + /// 消息清空成功回调。当本地端或多端同步清空消息成功时会触发该回调。 + /// - Parameter clearHistoryNotification: 清空通知 + public func onClearHistoryNotifications(_ clearHistoryNotification: [V2NIMClearHistoryNotification]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", clearHistoryNotification.count: \(clearHistoryNotification.count)") + } - for (i, model) in messages.enumerated() { - if message.messageId == model.message?.messageId, messages[i].isPined { - messages[i].isPined = false - messages[i].pinAccount = nil - index = i - break + /// 消息pin状态回调通知 + /// - Parameter pinNotification: 消息pin状态变化通知数据 + public func onMessagePinNotification(_ pinNotification: V2NIMMessagePinNotification) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId:\(String(describing: pinNotification.pin?.messageRefer?.messageClientId)), pinStatus:\(pinNotification.pinState.rawValue)") + if pinNotification.pinState == .MESSAGE_PIN_STEATE_PINNED { + // 置顶 + var index = -1 + for (i, model) in messages.enumerated() { + if pinNotification.pin?.messageRefer?.messageServerId == model.message?.messageServerId { + messages[i].isPined = true + let pinID = pinNotification.pin?.operatorId ?? IMKitClient.instance.account() + messages[i].pinAccount = pinID + + if let _ = NEFriendUserCache.shared.getFriendInfo(pinID) ?? ChatUserCache.shared.getUserInfo(pinID) { + messages[i].pinShowName = getShowName(pinID) + } else { + loadShowName([pinID], sessionId) { [weak self] in + self?.messages[i].pinShowName = self?.getShowName(pinID) + if let msg = self?.messages[i].message { + self?.delegate?.onMessagePinStatusChange(msg, atIndexs: [IndexPath(row: i, section: 0)]) + } + } + } + index = i + break + } + } + if index >= 0, let msg = messages[index].message { + delegate?.onMessagePinStatusChange(msg, atIndexs: [IndexPath(row: index, section: 0)]) + } + } else if pinNotification.pinState == .MESSAGE_PIN_STEATE_NOT_PINNED { + // 取消置顶 + var index = -1 + for (i, model) in messages.enumerated() { + if pinNotification.pin?.messageRefer?.messageServerId == model.message?.messageServerId { + if !messages[i].isPined { + return + } + messages[i].isPined = false + messages[i].pinAccount = nil + messages[i].pinShowName = nil + index = i + break + } + } + if index >= 0, let msg = messages[index].message { + delegate?.onMessagePinStatusChange(msg, atIndexs: [IndexPath(row: index, section: 0)]) } } - return index } -// MARK: NIMConversationManagerDelegate + /// 消息评论状态回调 + /// - Parameter notification: 快捷评论通知数据 + public func onMessageQuickCommentNotification(_ notification: V2NIMMessageQuickCommentNotification) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", quickComment.index: \(notification.quickComment.index)") + } - // 多端登录删除消息 - open func onRecvMessagesDeleted(_ messages: [NIMMessage], exts: [String: String]?) { - messages.forEach { message in - if message.session?.sessionId != session.sessionId { - return + /// 收到点对点已读回执 + /// - Parameter readReceipts: 已读回执 + public func onReceiveP2PMessageReadReceipts(_ readReceipts: [V2NIMP2PMessageReadReceipt]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", readReceipts.count: \(readReceipts.count)") + var reloadIndexPaths: [IndexPath] = [] + for readReceipt in readReceipts { + if readReceipt.conversationId != conversationId { + continue } - if message.messageId.count <= 0 { - return + + for (i, model) in messages.enumerated() { + if model.message?.isSelf == false { + continue + } + + if model.message?.messageConfig?.readReceiptEnabled == false { + continue + } + + if let msgCreateTime = model.message?.createTime, msgCreateTime <= readReceipt.timestamp { + if model.readCount == 1, model.unreadCount == 0 { + continue + } + + model.readCount = 1 + model.unreadCount = 0 + reloadIndexPaths.append(IndexPath(row: i, section: 0)) + } } - deleteMessageUpdateUI(message) } + delegate?.onLoadMoreWithMessage(reloadIndexPaths) } - func fetchPinMessage(_ completion: @escaping () -> Void) { - repo.fetchPinMessage(session.sessionId, session.sessionType) { error, items in - completion() + /// 收到群已读回执 + /// - Parameter readReceipts: 已读回执 + public func onReceiveTeamMessageReadReceipts(_ readReceipts: [V2NIMTeamMessageReadReceipt]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", readReceipts.count: \(readReceipts.count)") + var reloadIndexPaths: [IndexPath] = [] + for readReceipt in readReceipts { + if readReceipt.conversationId != conversationId { + continue + } + + for (i, model) in messages.enumerated() { + if model.message?.isSelf == false { + continue + } + + if model.message?.messageConfig?.readReceiptEnabled == false { + continue + } + + if model.message?.messageClientId == readReceipt.messageClientId { + model.readCount = readReceipt.readCount + model.unreadCount = readReceipt.unreadCount + reloadIndexPaths.append(IndexPath(row: i, section: 0)) + } + } } + delegate?.onLoadMoreWithMessage(reloadIndexPaths) } +} - // 检查音频消息是否有附件 - open func checkAudioFile(messages: [MessageModel]?) { - messages?.forEach { model in - if let message = model.message { - ChatMessageHelper.downloadAudioFile(message: message) +// MARK: - NEContactListener + +extension ChatViewModel: NEContactListener { + /// 用户信息变更回调 + /// - Parameter users: 用户列表 + public func onUserProfileChanged(_ users: [V2NIMUser]) { + for user in users { + guard let accountId = user.accountId else { + return } + + if !NEFriendUserCache.shared.isFriend(accountId) { + ChatUserCache.shared.updateUserInfo(user) + } + + updateMessageInfo(accountId) } } - open func onTeamMemberChanged(_ team: NIMTeam) { - if session.sessionType == .team, session.sessionId == team.teamId { - self.team = team - delegate?.onTeamMemberChange(team: team) - } + /// 好友信息变更回调 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + updateMessageInfo(friendInfo.accountId) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/CollectionMessageViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/CollectionMessageViewModel.swift new file mode 100644 index 00000000..82bf016c --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/CollectionMessageViewModel.swift @@ -0,0 +1,147 @@ +//// 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 CollectionMessageViewModel: NSObject { + public var collectionDatas = [CollectionMessageModel]() + + /// 消息API 单例 + let chatRepo = ChatRepo.shared + + /// 加载收藏数据 + /// - Parameter completion: 完成回调 + public func loadData(_ completion: @escaping (NSError?, Bool) -> Void) { + let option = V2NIMCollectionOption() + + if let model = collectionDatas.last, let anchor = model.collection { + option.anchorCollection = anchor + } else { + option.endTime = Date().timeIntervalSince1970 + let currentDate = Date() + let tenYearsAgo = Calendar.current.date(byAdding: .year, value: -10, to: currentDate) + let timeInterval = tenYearsAgo!.timeIntervalSince1970 + option.beginTime = timeInterval + } + option.limit = 100 + option.direction = .QUERY_DIRECTION_DESC + chatRepo.getCollections(option) { [weak self] collections, error in + if let error = error { + completion(error, false) + } else { + if let v2Collections = collections, let models = self?.parseMessage(v2Collections) { + self?.collectionDatas.append(contentsOf: models) + if models.count > 0 { + completion(nil, false) + } else { + completion(nil, true) + } + } else { + completion(nil, true) + } + } + } + } + + /// 反序列化收藏的消息 + /// - Parameter collections: 收藏内容列表 + func parseMessage(_ collections: [V2NIMCollection]) -> [CollectionMessageModel] { + var retArray = [CollectionMessageModel]() + for collection in collections { + if let dataString = collection.collectionData { + if let dataDic = getDictionaryFromJSONString(dataString) { + if dataDic["message"] != nil, dataDic["conversationName"] != nil { + let model = CollectionMessageModel() + if let messageString = dataDic["message"] as? String { + if let message = V2NIMMessageConverter.messageDeserialization(messageString) { + model.message = message + } + } + if let conversationName = dataDic["conversationName"] as? String { + model.conversationName = conversationName + } + if let avatar = dataDic["avatar"] as? String { + model.avatar = avatar + model.chatmodel.avatar = avatar + } + if let senderName = dataDic["senderName"] as? String { + model.senderName = senderName + model.chatmodel.fullName = senderName + } + model.collection = collection + retArray.append(model) + } + } + } + } + return retArray + } + + /// 发送文本消息 + /// - Parameter text: 文本内容 + /// - Parameter conversationId: 会话ID + /// - Parameter completion: 完成回调 + open func sendTextMessage(_ text: String, _ conversationId: String, _ completion: @escaping (V2NIMSendMessageResult?, Error?, UInt) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") + if text.count <= 0 { + return + } + chatRepo.sendMessage( + message: MessageUtils.textMessage(text: text), + conversationId: conversationId, + completion + ) + } + + /// 转发消息 + /// - Parameters: + /// - conversationIds: 会话 id 列表 + /// - comment: 留言 + /// - completion: 完成回调 + open func forwardCollectionMessages(_ message: V2NIMMessage, + _ conversationIds: [String], + _ comment: String?, + _ completion: @escaping (V2NIMSendMessageResult?, Error?, UInt) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: \(String(describing: message.messageClientId))") + for conversationId in conversationIds { + let forwardMessage = MessageUtils.forwardMessage(message: message) + ChatMessageHelper.clearForwardAtMark(forwardMessage) + chatRepo.sendMessage(message: forwardMessage, conversationId: conversationId, completion) + if let text = comment, !text.isEmpty { + sendTextMessage(text, conversationId, completion) + } + } + } + + /// 下载文件 + /// - Parameter urlString: 文件URL + /// - Parameter filePath: 文件路径 + /// - Parameter progress: 进度回调 + /// - Parameter completion: 完成回调 + open func downloadFile(_ urlString: String, + _ filePath: String, + _ progress: ((UInt) -> Void)?, + _ completion: ((String?, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) + ResourceRepo.shared.downLoad(urlString, filePath, progress, completion) + } + + /// 删除收藏 + /// - Parameter collection: 收藏对象 + /// - Parameter completion: 完成回调 + open func removeCollection(_ collection: V2NIMCollection, _ completion: @escaping (NSError?) -> Void) { + chatRepo.removeCollections([collection]) { ret, error in + completion(error) + } + } + + open func getHandSetEnable() -> Bool { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + return SettingRepo.shared.getHandsetMode() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift index 38b018c0..fdc9b65b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/MultiForwardViewModel.swift @@ -24,7 +24,7 @@ open class MultiForwardViewModel: NSObject { if FileManager.default.fileExists(atPath: filePath) { decodeMesssage(filePath: filePath, md5: md5, completion) } else if let urlString = messageAttachmentUrl { - downLoad(urlString, filePath, nil) { [weak self] error in + downLoad(urlString, filePath, nil) { [weak self] _, error in self?.decodeMesssage(filePath: filePath, md5: md5, completion) } } @@ -48,8 +48,7 @@ open class MultiForwardViewModel: NSObject { let subStringData = strData?.components(separatedBy: "\n") if let msgCount = subStringData?.count, msgCount > 1 { for i in 1 ..< msgCount { - if let msgData = subStringData?[i].data(using: .utf8) { - let msg = NIMSDK.shared().conversationManager.decodeMessage(from: msgData) + if let msgString = subStringData?[i], let msg = V2NIMMessageConverter.messageDeserialization(msgString) { let model = modelFromMessage(message: msg) ChatMessageHelper.addTimeMessage(model, messages.last) messages.append(model) @@ -62,33 +61,40 @@ open class MultiForwardViewModel: NSObject { } } - open func modelFromMessage(message: NIMMessage) -> MessageModel { + open func modelFromMessage(message: V2NIMMessage) -> MessageModel { var model: MessageModel switch message.messageType { - case .audio: + case .MESSAGE_TYPE_AUDIO: message.text = chatLocalizable("msg_audio") model = MessageTextModel(message: message) - case .rtcCallRecord: + case .MESSAGE_TYPE_CALL: message.text = chatLocalizable("msg_rtc_call") - if let object = message.messageObject as? NIMRtcCallRecordObject { - message.text = object.callType == .audio ? chatLocalizable("msg_rtc_audio") : chatLocalizable("msg_rtc_video") + if let attachment = message.attachment as? V2NIMMessageCallAttachment { + message.text = attachment.type == 1 ? chatLocalizable("msg_rtc_audio") : chatLocalizable("msg_rtc_video") } model = MessageTextModel(message: message) default: model = ChatMessageHelper.modelFromMessage(message: message) } - model.fullName = message.remoteExt?[mergedMessageNickKey] as? String - model.shortName = ChatUserCache.getShortName(name: model.fullName ?? "", length: 2) - model.avatar = message.remoteExt?[mergedMessageAvatarKey] as? String + if let remoteExt = getDictionaryFromJSONString(message.serverExtension ?? "") { + model.fullName = remoteExt[mergedMessageNickKey] as? String + model.shortName = NEFriendUserCache.getShortName(model.fullName ?? "") + model.avatar = remoteExt[mergedMessageAvatarKey] as? String + } else { + model.fullName = message.senderId + model.shortName = NEFriendUserCache.getShortName(model.fullName ?? "") + } delegate?.getMessageModel?(model: model) return model } - open func downLoad(_ urlString: String, _ filePath: String, _ progress: NIMHttpProgressBlock?, - _ completion: NIMDownloadCompleteBlock?) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) - repo.downloadSource(urlString, filePath, progress, completion) + open func downLoad(_ urlString: String, + _ filePath: String, + _ progress: ((UInt) -> Void)?, + _ completion: ((String?, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) + ResourceRepo.shared.downLoad(urlString, filePath, progress, completion) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/P2PChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/P2PChatViewModel.swift new file mode 100644 index 00000000..3bab0d4b --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/P2PChatViewModel.swift @@ -0,0 +1,196 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import CoreText +import Foundation +import NEChatKit +import NECoreIM2Kit +import NIMSDK + +@objcMembers +open class P2PChatViewModel: ChatViewModel { + /// 重写初始化方法 + override init(conversationId: String) { + super.init(conversationId: conversationId) + chatRepo.addNotiListener(self) + } + + /// 重写初始化方法 + override init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId, anchor: anchor) + chatRepo.addNotiListener(self) + } + + /// 重写 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - Returns: 名称和好友信息 + override open func getShowName(_ accountId: String, + _ showAlias: Bool = true) -> String { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:" + accountId) + if NEFriendUserCache.shared.isFriend(accountId) { + return NEFriendUserCache.shared.getShowName(accountId, showAlias) + } else { + return ChatUserCache.shared.getShowName(accountId, showAlias) + } + } + + /// 重写 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - completion: 完成回调 + override open func loadShowName(_ accountIds: [String], + _ teamId: String? = nil, + _ completion: @escaping () -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", count: \(accountIds.count)") + NEFriendUserCache.shared.loadShowName(accountIds) { users in + for user in users ?? [] { + // 非好友,单独缓存 + if let uid = user.user?.accountId, !NEFriendUserCache.shared.isFriend(uid) { + ChatUserCache.shared.updateUserInfo(user) + } + } + completion() + } + } + + /// 重写 发送消息已读回执 + /// - Parameters: + /// - messages: 需要发送已读回执的消息 + /// - completion: 完成回调 + override open func markRead(messages: [V2NIMMessage], _ completion: @escaping ((any Error)?) -> Void) { + markReadInP2P(messages: messages, completion) + } + + /// 单人会话消息发送已读回执 + /// - Parameters: + /// - messages: 需要发送已读回执的消息 + /// - completion: 完成回调 + private func markReadInP2P(messages: [V2NIMMessage], _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") + + let messages = messages.sorted { msg1, msg2 in + msg1.createTime > msg2.createTime + } + for message in messages { + if !message.isSelf { + chatRepo.markP2PMessageRead(message: message, completion) + return + } + } + completion(nil) + } + + /// 重写获取消息已读未读回执 + /// - Parameters: + /// - messages: 消息列表 + /// - completion: 完成回调 + override open func getMessageReceipts(messages: [V2NIMMessage], + _ completion: @escaping ([IndexPath], Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") + getP2PMessageReceipt(messages: messages, completion) + } + + /// 获取 P2P 消息已读未读回执 + /// - Parameters: + /// - messages: 消息列表 + /// - completion: 完成回调 + func getP2PMessageReceipt(messages: [V2NIMMessage], _ completion: @escaping ([IndexPath], Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + chatRepo.getP2PMessageReceipt(conversationId: conversationId) { readReceipt, error in + if let readReceipt = readReceipt { + var reloadIndexs = [IndexPath]() + for (i, model) in self.messages.enumerated() { + if model.message?.isSelf == false { + continue + } + + if model.message?.messageConfig?.readReceiptEnabled == false { + continue + } + + if let msgCreateTime = model.message?.createTime, msgCreateTime <= readReceipt.timestamp { + if model.readCount == 1, model.unreadCount == 0 { + continue + } + + model.readCount = 1 + model.unreadCount = 0 + reloadIndexs.append(IndexPath(row: i, section: 0)) + } + } + completion(reloadIndexs, error) + } else { + completion([], error) + } + } + } + + /// 发送正在输入中状态 + open func sendInputTypingState() { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + if V2NIMConversationIdUtil.conversationType(conversationId) == .CONVERSATION_TYPE_P2P { + setTypingCustom(1) + } + } + + /// 发送结束输入中状态 + open func sendInputTypingEndState() { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + if V2NIMConversationIdUtil.conversationType(conversationId) == .CONVERSATION_TYPE_P2P { + setTypingCustom(0) + } + } + + /// 发送输入状态 + /// - Parameter typing: 输入状态: 1-正在输入, 0-结束输入 + func setTypingCustom(_ typing: Int) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", typing: \(typing)") + let content = getJSONStringFromDictionary(["typing": typing]) + let param = V2NIMSendCustomNotificationParams() + chatRepo.sendCustomNotification(converstaionId: conversationId, content: content, params: param) { error in + if let err = error { + print("send noti success :", err) + } + } + } +} + +// MARK: - NENotiListener + +extension P2PChatViewModel: NENotiListener { + /// 收到自定义系统通知回调 + /// 用于展示对方输入状态 + /// - Parameter customNotifications: 自定义系统通知 + public func onReceiveCustomNotifications(_ customNotifications: [V2NIMCustomNotification]) { + NEALog.infoLog( + ModuleName + " " + className(), + desc: #function + ", customNotifications.count:\(customNotifications.count)" + ) + + // 只处理单聊的输入状态 + if V2NIMConversationIdUtil.conversationType(conversationId) != .CONVERSATION_TYPE_P2P { + return + } + + for notification in customNotifications { + // 只处理当前会话的输入状态 + if sessionId != notification.senderId { + continue + } + + if let content = notification.content, + let dic = getDictionaryFromJSONString(content) as? [String: Any], + let typing = dic["typing"] as? Int { + if typing == 1 { + delegate?.remoteUserEditing() + } else { + delegate?.remoteUserEndEditing() + } + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift index 4890d90d..ed18cd38 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift @@ -8,177 +8,185 @@ import UIKit @objc public protocol PinMessageViewModelDelegate: NSObjectProtocol { - func didNeedRefreshUI() + func tableViewReload(needLoad: Bool) + func tableViewReload(_ indexPaths: [IndexPath]) + func tableViewDelete(_ indexPaths: [IndexPath]) + func refreshModel(_ model: NEPinMessageModel) } @objcMembers -open class PinMessageViewModel: NSObject, ChatExtendProviderDelegate, NIMChatManagerDelegate, NIMConversationManagerDelegate { +open class PinMessageViewModel: NSObject, NEChatListener { public let chatRepo = ChatRepo.shared - public var items = [PinMessageModel]() + public var items = [NEPinMessageModel]() public var delegate: PinMessageViewModelDelegate? - public var session: NIMSession? + public var conversationId: String? override public init() { super.init() - chatRepo.addChatDelegate(delegate: self) - chatRepo.addChatExtendDelegate(delegate: self) - NIMSDK.shared().conversationManager.add(self) + chatRepo.addChatListener(self) } - open func onRecvMessagesDeleted(_ messages: [NIMMessage], exts: [String: String]?) { - for message in messages { - if message.session?.sessionId == session?.sessionId { - delegate?.didNeedRefreshUI() - break - } - } - } - - open func getPinitems(session: NIMSession, _ completion: @escaping (Error?) -> Void) { + open func getPinitems(conversationId: String, _ completion: @escaping (Error?) -> Void) { + let group = DispatchGroup() weak var weakSelf = self - self.session = session - chatRepo.fetchPinMessage(session.sessionId, session.sessionType) { error, pinItems in + self.conversationId = conversationId + chatRepo.getPinnedMessageList(conversationId: conversationId) { pinItems, error in if let pins = pinItems { - if error == nil { - weakSelf?.items.removeAll() - } - var remoteMessages = [NIMMessagePinItem]() - var pinDic = [String: NIMMessagePinItem]() - pins.forEach { item in - if let message = ConversationProvider.shared.messagesInSession(item.session, messageIds: [item.messageId])?.first { - let pinModel = PinMessageModel(message: message, item: item) - weakSelf?.items.append(pinModel) - weakSelf?.items.sort { model1, model2 in - model1.message.timestamp > model2.message.timestamp + let messageRefers = pins.compactMap(\.messageRefer) + group.enter() + weakSelf?.chatRepo.getMessageListByRefers(messageRefers) { messages, error in + if let messages = messages { + weakSelf?.items.removeAll() + for message in messages { + for item in pins { + if message.messageClientId == item.messageRefer?.messageClientId { + let pinModel = NEPinMessageModel(message: message, item: item) + weakSelf?.items.append(pinModel) + } + } } } else { - remoteMessages.append(item) - pinDic[item.messageServerID] = item + completion(error) } + group.leave() } - if remoteMessages.count <= 0 { - completion(error) - } else { - var infos = [NIMChatExtendBasicInfo]() - remoteMessages.forEach { item in - let info = NIMChatExtendBasicInfo() - info.type = session.sessionType - info.fromAccount = item.messageFromAccount - info.toAccount = item.messageToAccount - info.messageID = item.messageId - info.serverID = item.messageServerID - info.timestamp = item.messageTime - infos.append(info) + + group.notify(queue: .main) { + weakSelf?.items.sort { model1, model2 in + model1.message.createTime > model2.message.createTime } - weakSelf?.chatRepo.fetchHistoryMessages(infos, false) { err, mapTable in - let enums = mapTable?.objectEnumerator() - while let message = enums?.nextObject() as? NIMMessage { - print("fetchHistoryMessages ", message.messageId) - if let item = pinDic[message.serverID] { - let pinModel = PinMessageModel(message: message, item: item) - weakSelf?.items.append(pinModel) - weakSelf?.items.sort { model1, model2 in - model1.message.timestamp > model2.message.timestamp - } - } - } - completion(err) + completion(error) + weakSelf?.loadMoreWithModel(items: weakSelf?.items) { + weakSelf?.delegate?.tableViewReload(needLoad: false) } } - } else { completion(error) } } } - open func removePinMessage(_ message: NIMMessage, - _ completion: @escaping (Error?, NIMMessagePinItem?) + open func loadMoreWithModel(items: [NEPinMessageModel]?, _ completion: @escaping () -> Void) { + guard let items = items else { + completion() + return + } + + let userIds = items.compactMap(\.message.senderId) + if let conversationId = conversationId, let teamId = V2NIMConversationIdUtil.conversationTargetId(conversationId) { + ChatTeamCache.shared.loadShowName(userIds: userIds, teamId: teamId) { + for item in items { + let senderId = item.chatmodel.message?.senderId ?? "" + let name = ChatTeamCache.shared.getShowName(senderId) + let user = NEFriendUserCache.shared.getFriendInfo(senderId) ?? ChatUserCache.shared.getUserInfo(senderId) + item.chatmodel.avatar = user?.user?.avatar + item.chatmodel.fullName = name + item.chatmodel.shortName = NEFriendUserCache.getShortName(user?.showName() ?? "") + } + completion() + } + } + } + + open func removePinMessage(_ message: V2NIMMessage, + _ completion: @escaping (Error?) -> Void) { - NELog.infoLog("PinMessageViewModel", desc: #function + ", messageId: " + message.messageId) - let item = NIMMessagePinItem(message: message) - chatRepo.removeMessagePin(item) { error, pinItem in - completion(error, pinItem) + NEALog.infoLog("PinMessageViewModel", desc: #function + ", messageId: \(String(describing: message.messageClientId))") + chatRepo.unpinMessage(messageRefer: message, serverExtension: "") { error in + completion(error) } } - open func sendTextMessage(text: String, session: NIMSession, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") + open func sendTextMessage(text: String, conversationId: String, _ completion: @escaping (V2NIMSendMessageResult?, Error?, UInt) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", text.count: \(text.count)") if text.count <= 0 { return } chatRepo.sendMessage( message: MessageUtils.textMessage(text: text), - session: session, + conversationId: conversationId, completion ) } - open func forwardUserMessage(_ message: NIMMessage, - _ users: [NIMUser], - _ comment: String?, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) - users.forEach { user in - if let uid = user.userId { - let session = NIMSession(uid, type: .P2P) - if let forwardMessage = chatRepo.makeForwardMessage(message) { - ChatMessageHelper.clearForwardAtMark(forwardMessage) - chatRepo.sendForwardMessage(forwardMessage, session) - } - if let text = comment { - sendTextMessage(text: text, session: session) { error in - print("sendTextMessage error: \(String(describing: error))") - } - } + /// 转发消息 + /// - Parameters: + /// - conversationIds: 会话 id 列表 + /// - comment: 留言 + /// - completion: 完成回调 + open func forwardMessages(_ message: V2NIMMessage, + _ conversationIds: [String], + _ comment: String?, + _ completion: @escaping (V2NIMSendMessageResult?, Error?, UInt) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: \(String(describing: message.messageClientId))") + for conversationId in conversationIds { + let forwardMessage = MessageUtils.forwardMessage(message: message) + ChatMessageHelper.clearForwardAtMark(forwardMessage) + chatRepo.sendMessage(message: forwardMessage, conversationId: conversationId, completion) + if let text = comment, !text.isEmpty { + sendTextMessage(text: text, conversationId: conversationId, completion) } } } - 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) { - ChatMessageHelper.clearForwardAtMark(forwardMessage) - chatRepo.sendForwardMessage(forwardMessage, session) - } - if let text = comment { - sendTextMessage(text: text, session: session, completion) - } - } + open func downLoad(_ urlString: String, + _ filePath: String, + _ progress: ((UInt) -> Void)?, + _ completion: ((String?, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) + ResourceRepo.shared.downLoad(urlString, filePath, progress, completion) } - // MARK: NIMChatManagerDelegate - - open func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { -// items = [PinMessageModel]() - delegate?.didNeedRefreshUI() + open func getHandSetEnable() -> Bool { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + return SettingRepo.shared.getHandsetMode() } - // MARK: ChatExtendProviderDelegate + func onDeleteIndexPath(_ messageRefers: [V2NIMMessageRefer?]) { + var indexs = [IndexPath]() + for messageRefer in messageRefers { + for (i, model) in items.enumerated() { + if model.message.messageClientId == messageRefer?.messageClientId { + indexs.append(IndexPath(row: i, section: 0)) + } + } + } - open func onNotifyAddMessagePin(pinItem: NIMMessagePinItem) { -// items = [PinMessageModel]() - delegate?.didNeedRefreshUI() + for messageRefer in messageRefers { + items.removeAll { $0.message.messageClientId == messageRefer?.messageClientId } + } + + delegate?.tableViewDelete(indexs) } - open func onNotifyRemoveMessagePin(pinItem: NIMMessagePinItem) { -// items = [PinMessageModel]() - delegate?.didNeedRefreshUI() + // MARK: - NEChatListener + + /// 收到消息撤回回调 + /// - Parameter revokeNotifications: 消息撤回通知数据 + public func onMessageRevokeNotifications(_ revokeNotifications: [V2NIMMessageRevokeNotification]) { + delegate?.tableViewReload(needLoad: true) } - open func downLoad(_ urlString: String, _ filePath: String, _ progress: NIMHttpProgressBlock?, - _ completion: NIMDownloadCompleteBlock?) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + urlString) - chatRepo.downloadSource(urlString, filePath, progress, completion) + /// 消息pin状态回调通知 + /// - Parameter pinNotification: 消息pin状态变化通知数据 + public func onMessagePinNotification(_ pinNotification: V2NIMMessagePinNotification) { + switch pinNotification.pinState { + case .MESSAGE_PIN_STEATE_NOT_PINNED: + let messageRefer = pinNotification.pin?.messageRefer + onDeleteIndexPath([messageRefer]) + case .MESSAGE_PIN_STEATE_PINNED: + delegate?.tableViewReload(needLoad: true) + case .MESSAGE_PIN_STEATE_UPDATED: + delegate?.tableViewReload(needLoad: true) + default: + break + } } - open func getHandSetEnable() -> Bool { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - return chatRepo.getHandsetMode() + /// 消息被删除通知 + /// - Parameter messageDeletedNotification: 被删除的消息列表 + public func onMessageDeletedNotifications(_ messageDeletedNotification: [V2NIMMessageDeletedNotification]) { + let messageRefers = messageDeletedNotification.map(\.messageRefer) + onDeleteIndexPath(messageRefers) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ReadViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ReadViewModel.swift new file mode 100644 index 00000000..b5094622 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ReadViewModel.swift @@ -0,0 +1,62 @@ +// 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 + +/// 已读未读页面 - ViewModel +@objcMembers +open class ReadViewModel: NSObject { + public let chatRepo = ChatRepo.shared + public var readUsers = [NETeamMemberInfoModel]() + public var unReadUsers = [NETeamMemberInfoModel]() + + override public init() { + super.init() + } + + /// 获取群消息已读回执状态详情 + /// - Parameters: + /// - message: 群消息 + /// - teamId: 群 id + /// - completion: 完成回调 + public func getTeamMessageReceiptDetail(_ message: V2NIMMessage, _ teamId: String, _ completion: @escaping (Error?) -> Void) { + chatRepo.getTeamMessageReceiptDetail(message: message, memberAccountIds: []) { [weak self] readReceiptDetail, error in + guard let readReceiptDetail = readReceiptDetail else { return } + let group = DispatchGroup() + if let error = error as? NSError { + completion(error) + return + } + + // 加载用户信息 + let loadUserIds = readReceiptDetail.readAccountList + readReceiptDetail.unreadAccountList + group.enter() + ChatTeamCache.shared.loadShowName(userIds: loadUserIds, teamId: teamId) { + // 已读用户 + for userId in readReceiptDetail.readAccountList { + let memberInfo = NETeamMemberInfoModel() + memberInfo.teamMember = ChatTeamCache.shared.getTeamMemberInfo(accountId: userId) + memberInfo.nimUser = NEFriendUserCache.shared.getFriendInfo(userId) ?? ChatUserCache.shared.getUserInfo(userId) + self?.readUsers.append(memberInfo) + } + + // 未读用户 + for userId in readReceiptDetail.unreadAccountList { + let memberInfo = NETeamMemberInfoModel() + memberInfo.teamMember = ChatTeamCache.shared.getTeamMemberInfo(accountId: userId) + memberInfo.nimUser = NEFriendUserCache.shared.getFriendInfo(userId) ?? ChatUserCache.shared.getUserInfo(userId) + self?.unReadUsers.append(memberInfo) + } + + group.leave() + } + + group.notify(queue: .main) { + completion(nil) + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift index 22885f04..bf42339a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift @@ -5,36 +5,255 @@ import CoreText import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK @objc public protocol TeamChatViewModelDelegate: ChatViewModelDelegate { - func onTeamRemoved(team: NIMTeam) - func onTeamUpdate(team: NIMTeam) - func onTeamMemberUpdate(team: NIMTeam) + @objc optional func onTeamRemoved(team: V2NIMTeam) + @objc optional func onTeamUpdate(team: V2NIMTeam) + @objc optional func onTeamMemberUpdate(_ teamMembers: [V2NIMTeamMember]) } @objcMembers -open class TeamChatViewModel: ChatViewModel { - private let className = "TeamChatViewModel" +open class TeamChatViewModel: ChatViewModel, NETeamListener { + public let teamRepo = TeamRepo.shared + public var team: V2NIMTeam? + /// 当前成员的群成员对象类 + public var teamMember: V2NIMTeamMember? - override init(session: NIMSession, anchor: NIMMessage?) { - super.init(session: session, anchor: anchor) - NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId: " + session.sessionId) - repo.addTeamDelegate(delegate: self) - getTeamMember() + override init(conversationId: String) { + super.init(conversationId: conversationId) + teamRepo.addTeamListener(self) } - open func getTeam(teamId: String) -> NIMTeam? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + teamId) - return repo.getTeamInfo(teamId: teamId) + override init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId, anchor: anchor) + teamRepo.addTeamListener(self) + getTeamMember {} } - open func fetchTeamInfo(teamId: String, - _ completion: @escaping (NSError?, NIMTeam?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + teamId) - repo.getTeamInfo(teamId: teamId) { [weak self] error, team in + /// 重写 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - Returns: 名称和好友信息 + override open func getShowName(_ accountId: String, _ showAlias: Bool = true) -> String { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", accountId:" + accountId) + return ChatTeamCache.shared.getShowName(accountId, showAlias) + } + + /// 重写 获取用户展示名称 + /// - Parameters: + /// - accountId: 用户 accountId + /// - showAlias: 是否展示备注 + /// - completion: 完成回调 + override open func loadShowName(_ accountIds: [String], + _ teamId: String?, + _ completion: @escaping () -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId:\(String(describing: teamId))") + guard let teamId = teamId else { + return + } + + ChatTeamCache.shared.loadShowName(userIds: accountIds, teamId: teamId, completion) + } + + /// 加载置顶消息 + override open func loadTopMessage() { + // 校验配置项 + if !IMKitConfigCenter.shared.topEnable { + return + } + + if let serverJson = team?.serverExtension, let extDic = getDictionaryFromJSONString(serverJson) { + if let topInfo = extDic[keyTopMessage] as? [String: Any] { + if let type = topInfo["operation"] as? Int, type == 0 { + let refer = ChatMessageHelper.createMessageRefer(topInfo) + chatRepo.getMessageListByRefers([refer]) { [weak self] messages, error in + // 这里查询只是为了校验消息是否还存在(未被删除或撤回) + if let topMessage = messages?.first, let senderId = topMessage.senderId { + var senderName = self?.getShowName(senderId) ?? "" + let group = DispatchGroup() + + if senderName == senderId { + group.enter() + self?.loadShowName([senderId], self?.sessionId) { + senderName = self?.getShowName(senderId) ?? "" + group.leave() + } + } + + group.notify(queue: .main) { + let content = ChatMessageHelper.contentOfMessage(topMessage) + var thumbUrl: String? + var isVideo = false + var hideClose = false + + // 获取图片缩略图 + if let attach = topMessage.attachment as? V2NIMMessageFileAttachment, let imageUrl = attach.url { + thumbUrl = ResourceRepo.shared.imageThumbnailURL(imageUrl) + } + + // 获取视频首帧 + if let attach = topMessage.attachment as? V2NIMMessageVideoAttachment, let videoUrl = attach.url { + thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + isVideo = true + } + + // 是否隐藏移除置顶按钮 + if self?.teamMember?.memberRole == .TEAM_MEMBER_ROLE_NORMAL, + let topAllow = extDic[keyAllowTopMessage] as? String, + topAllow == allowAtManagerValue { + hideClose = true + } + + self?.delegate?.setTopValue(name: senderName, + content: content, + url: thumbUrl, + isVideo: isVideo, + hideClose: hideClose) + self?.topMessage = topMessage + } + } + } + } else { + topMessage = nil + delegate?.setTopValue(name: nil, content: nil, url: nil, isVideo: false, hideClose: false) + } + } + } + } + + /// 校验置顶消息权限 + /// - Returns: 是否具有置顶消息权限 + func hasTopMessagePremission() -> Bool { + // 讨论组所有人都有权限 + if team?.isDisscuss() == true { + return true + } + + // 高级群 + if teamMember?.memberRole == .TEAM_MEMBER_ROLE_OWNER || teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { + // 群主和管理员都有权限 + return true + } else if teamMember?.memberRole == .TEAM_MEMBER_ROLE_NORMAL { + if let custom = team?.serverExtension, + let json = getDictionaryFromJSONString(custom) { + if let atValue = json[keyAllowTopMessage] as? String, atValue == allowAtAllValue { + return true + } else { + return false + } + } else { + return false + } + } + + return false + } + + /// 置顶消息 + /// - Parameter completion: 回调 + override open func topMessage(_ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId: \(String(describing: operationModel?.message?.messageClientId))") + guard let message = operationModel?.message else { return } + + let topMessageDic: [String: Any] = [ + "idClient": message.messageClientId as Any, + "scene": message.conversationType.rawValue, + "from": message.senderId as Any, + "to": message.conversationId as Any, + "idServer": message.messageServerId as Any, + "time": Int(message.createTime * 1000), + "operator": IMKitClient.instance.account(), // 操作者 + "operation": 0, // 操作: 0 - "add"; 1 - "remove"; + ] + + // 更新群扩展 + TeamRepo.shared.getTeamInfo(sessionId) { [weak self] team, error in + if let err = error { + print("getTeamInfo error: \(String(describing: err))") + completion(err) + return + } + + guard let tid = self?.sessionId else { return } + + // 校验权限 + if self?.hasTopMessagePremission() == false { + let error = NSError(domain: chatLocalizable("no_permission_tip"), code: noPermissionOperationCode) + completion(error) + return + } + + var serverExtension = [String: Any]() + if let serverJson = team?.serverExtension, let serverExt = NECommonUtil.getDictionaryFromJSONString(serverJson) as? [String: Any] { + serverExtension = serverExt + } + + serverExtension[keyTopMessage] = topMessageDic + serverExtension["lastOpt"] = keyTopMessage + TeamRepo.shared.updateTeamExtension(tid, .TEAM_TYPE_NORMAL, NECommonUtil.getJSONStringFromDictionary(serverExtension)) { error in + print("updateTeamExtension error: \(String(describing: error))") + completion(error) + } + } + } + + /// 取消置顶消息 + /// - Parameter completion: 回调 + override open func untopMessage(_ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messageClientId \(String(describing: topMessage?.messageClientId))") + + guard let _ = topMessage?.messageClientId else { + let error = NSError(domain: chatLocalizable("failed_operation"), code: failedOperation) + completion(error) + return + } + + let topMessageDic: [String: Any] = [ + "idClient": topMessage?.messageClientId as Any, + "operator": IMKitClient.instance.account(), // 操作者 + "operation": 1, // 操作: 0 - "add"; 1 - "remove"; + ] + + // 更新群扩展 + TeamRepo.shared.getTeamInfo(sessionId) { [weak self] team, error in + if let err = error { + print("getTeamInfo error: \(String(describing: err))") + completion(err) + return + } + + guard let tid = self?.sessionId else { return } + + // 校验权限 + if self?.hasTopMessagePremission() == false { + let error = NSError(domain: chatLocalizable("no_permission_tip"), code: noPermissionOperationCode) + completion(error) + return + } + + if let serverJson = team?.serverExtension, + var serverExtension = NECommonUtil.getDictionaryFromJSONString(serverJson) as? [String: Any], + serverExtension[keyTopMessage] != nil { + serverExtension[keyTopMessage] = topMessageDic + serverExtension["lastOpt"] = keyTopMessage + + self?.topMessage = nil + TeamRepo.shared.updateTeamExtension(tid, .TEAM_TYPE_NORMAL, NECommonUtil.getJSONStringFromDictionary(serverExtension)) { error in + print("updateTeamExtension error: \(String(describing: error))") + completion(error) + } + } + } + } + + open func getTeamInfo(teamId: String, + _ completion: @escaping (Error?, V2NIMTeam?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: " + teamId) + teamRepo.getTeamInfo(teamId) { [weak self] team, error in if error == nil { self?.team = team } @@ -42,42 +261,140 @@ open class TeamChatViewModel: ChatViewModel { } } -// MARK: NIMTeamManagerDelegate + /// 获取自己的群成员信息 + public func getTeamMember(_ completion: @escaping () -> Void) { + teamRepo.getTeamMember(sessionId, .TEAM_TYPE_NORMAL, IMKitClient.instance.account()) { [weak self] member, error in + self?.teamMember = member + completion() + } + } + + /// 重写发送消息已读回执 + /// - Parameters: + /// - messages: 需要发送已读回执的消息 + /// - completion: 完成回调 + override open func markRead(messages: [V2NIMMessage], _ completion: @escaping ((any Error)?) -> Void) { + markReadInTeam(messages: messages, completion) + } + + /// 群消息发送已读回执 + /// - Parameters: + /// - messages: 需要发送已读回执的消息 + /// - completion: 完成回调 + private func markReadInTeam(messages: [V2NIMMessage], _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") + + var markMessages = [V2NIMMessage]() + for message in messages { + if message.messageServerId != nil, !message.isSelf, message.messageConfig?.readReceiptEnabled == true { + markMessages.append(message) + } + } + chatRepo.markTeamMessagesRead(messages: markMessages, completion) + } + + /// 重写获取消息已读未读回执 + /// - Parameters: + /// - messages: 消息列表 + /// - completion: 完成回调 + override open func getMessageReceipts(messages: [V2NIMMessage], + _ completion: @escaping ([IndexPath], Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", messages.count: \(messages.count)") + getTeamMessageReceipts(messages: messages, completion) + } + + /// 获取群消息已读未读回执 + /// - Parameters: + /// - messages: 消息列表 + /// - completion: 完成回调 + func getTeamMessageReceipts(messages: [V2NIMMessage], _ completion: @escaping ([IndexPath], Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + let sendMessages = messages.filter { msg in + msg.messageServerId != nil && msg.isSelf && msg.messageType != .MESSAGE_TYPE_NOTIFICATION && msg.messageType != .MESSAGE_TYPE_TIP + } + + if sendMessages.isEmpty { + completion([], nil) + return + } + + chatRepo.getTeamMessageReceipts(messages: sendMessages) { readReceipts, error in + var reloadIndexs = [IndexPath]() + readReceipts?.forEach { readReceipt in + for (i, model) in self.messages.enumerated() { + if model.message?.isSelf == false { + continue + } + + if model.message?.messageConfig?.readReceiptEnabled == false { + continue + } + + if model.message?.messageClientId == readReceipt.messageClientId { + if model.readCount == readReceipt.readCount, + model.unreadCount == readReceipt.unreadCount { + continue + } + + model.readCount = readReceipt.readCount + model.unreadCount = readReceipt.unreadCount + reloadIndexs.append(IndexPath(row: i, section: 0)) + } + } + } + completion(reloadIndexs, error) + } + } + + // MARK: - NETeamListener - open func onTeamRemoved(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + (team.teamId ?? "nil")) - if session.sessionId == team.teamId { + public func onTeamDismissed(_ team: V2NIMTeam) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: " + (team.teamId)) + if sessionId == team.teamId { if let delegate = delegate as? TeamChatViewModelDelegate { - delegate.onTeamRemoved(team: team) + delegate.onTeamRemoved?(team: team) } } } - open func onTeamUpdated(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + (team.teamId ?? "nil")) - if session.sessionId == team.teamId { + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId: " + (team.teamId)) + if sessionId == team.teamId { self.team = team if let delegate = delegate as? TeamChatViewModelDelegate { - delegate.onTeamUpdate(team: team) + delegate.onTeamUpdate?(team: team) } } + + loadTopMessage() } - open func onTeamMemberUpdated(_ team: NIMTeam, withMembers memberIDs: [String]?) { - guard let membersIds = memberIDs else { - return + /// 群成员加入回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { + for teamMember in teamMembers { + guard teamMember.teamId == team?.teamId else { break } + + ChatTeamCache.shared.updateTeamMemberInfo(teamMember) + updateMessageInfo(teamMember.accountId) } - for memberId in membersIds { - if let user = UserInfoProvider.shared.getUserInfo(userId: memberId) { - ChatUserCache.updateUserInfo(user) + } + + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + for teamMember in teamMembers { + guard teamMember.teamId == team?.teamId else { break } + + if teamMember.accountId == self.teamMember?.accountId { + self.teamMember = teamMember + loadTopMessage() } + + ChatTeamCache.shared.updateTeamMemberInfo(teamMember) + updateMessageInfo(teamMember.accountId) } + if let delegate = delegate as? TeamChatViewModelDelegate { - delegate.onTeamMemberUpdate(team: team) + delegate.onTeamMemberUpdate?(teamMembers) } } - - public func getTeamMember() { - teamMember = getTeamMember(userId: IMKitClient.instance.imAccid(), teamId: session.sessionId) - } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift index 6eb70637..b0e887d2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamMemberSelectVM.swift @@ -4,17 +4,139 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK @objcMembers open class TeamMemberSelectVM: NSObject { - public var chatRepo = ChatRepo.shared + public var teamRepo = TeamRepo.shared private let className = "TeamMemberSelectVM" - open func fetchTeamMembers(sessionId: String, - _ completion: @escaping (Error?, ChatTeamInfoModel?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId: " + sessionId) - chatRepo.getTeamInfo(sessionId, completion) + let teamProvider = TeamProvider.shared + + open func fetchTeamMembers(_ teamId: String, + _ completion: @escaping (Error?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", teamId: " + teamId) + getTeamWithMembers(teamId, .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL, completion) + } + + /// 分页查询群成员信息 + /// - Parameter members: 要查询的群成员列表 + /// - Parameter model : 群信息 + /// - Parameter maxSizeByPage: 单页最大查询数量 + /// - Parameter completion: 完成后的回调 + public func splitTeamMember(_ members: [V2NIMTeamMember], + _ model: NETeamInfoModel, + _ maxSizeByPage: Int = 150, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", members.count:\(members.count)") + var remaind = [[V2NIMTeamMember]]() + remaind.append(contentsOf: members.chunk(maxSizeByPage)) + fetchAtListUserInfo(&remaind, model, completion) + } + + /// 获取群信息 + /// - Parameter teamId: 群id + /// - Parameter queryType: 查询类型 + /// - Parameter completion: 完成后的回调 + public func getTeamWithMembers(_ teamId: String, + _ queryType: V2NIMTeamMemberRoleQueryType, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamid:\(teamId)") + weak var weakSelf = self + + teamRepo.getTeamInfo(teamId) { team, error in + if let err = error { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "CALLBACK fetchTeamInfo \(String(describing: error))") + completion(err, nil) + } else { + var memberLists = [V2NIMTeamMember]() + + weakSelf?.getAllTeamMemberList(teamId, nil, &memberLists, queryType) { ms, error in + if let e = error { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "CALLBACK fetchTeamMember \(String(describing: error))") + completion(e, nil) + } else { + let model = NETeamInfoModel() + model.team = team + if let members = ms { + weakSelf?.splitTeamMember(members, model, 150, completion) + } else { + completion(error, model) + } + } + } + } + } + } + + /// 从云信服务器批量获取用户资料 + /// - Parameter remainUserIds: 用户集合 + /// - Parameter completion: 成功回调 + private func fetchAtListUserInfo(_ remainUserIds: inout [[V2NIMTeamMember]], + _ model: NETeamInfoModel, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", remainUserIds.count:\(remainUserIds.count)") + guard let members = remainUserIds.first else { + completion(nil, model) + return + } + + let accids = members.map(\.accountId) + var temArray = remainUserIds + weak var weakSelf = self + + ContactRepo.shared.getUserWithFriend(accountIds: accids) { users, v2Error in + if let err = v2Error { + completion(err as NSError, model) + } else { + if let us = users { + for index in 0 ..< members.count { + let memberInfoModel = NETeamMemberInfoModel() + memberInfoModel.teamMember = members[index] + if us.count > index { + let user = us[index] + memberInfoModel.nimUser = user + } + model.users.append(memberInfoModel) + } + } + temArray.removeFirst() + weakSelf?.fetchAtListUserInfo(&temArray, model, completion) + } + } + } + + /// 获取群成员 + /// - Parameter teamId: 群ID + /// - Parameter completion: 完成回调 + public func getAllTeamMemberList(_ teamId: String, _ nextToken: String? = nil, _ memberList: inout [V2NIMTeamMember], _ queryType: V2NIMTeamMemberRoleQueryType, _ completion: @escaping ([V2NIMTeamMember]?, NSError?) -> Void) { + let option = V2NIMTeamMemberQueryOption() + if let token = nextToken { + option.nextToken = token + } else { + option.nextToken = "" + } + option.limit = 1000 + option.direction = .QUERY_DIRECTION_ASC + option.onlyChatBanned = false + option.roleQueryType = queryType + var temMemberLists = memberList + teamProvider.getTeamMemberList(teamId: teamId, teamType: .TEAM_TYPE_NORMAL, queryOption: option) { result, error in + if let err = error { + completion(nil, err) + } else { + if let members = result?.memberList { + temMemberLists.append(contentsOf: members) + } + if let finished = result?.finished { + if finished == true { + completion(temMemberLists, nil) + } else { + self.getAllTeamMemberList(teamId, result?.nextToken, &temMemberLists, queryType, completion) + } + } + } + } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift index 2cf6919b..3dee4b48 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift @@ -4,7 +4,7 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK protocol UserSettingViewModelDelegate: NSObjectProtocol { @@ -13,39 +13,82 @@ protocol UserSettingViewModelDelegate: NSObjectProtocol { } @objcMembers -open class UserSettingViewModel: NSObject { - var repo = ChatRepo.shared +open class UserSettingViewModel: NSObject, NEConversationListener { + var chatRepo = ChatRepo.shared + var contactRepo = ContactRepo.shared + var conversationRepo = ConversationRepo.shared + var settingRepo = SettingRepo.shared - var userInfo: NEKitUser? + var userInfo: NEUserWithFriend? var cellDatas = [UserSettingCellModel]() var delegate: UserSettingViewModelDelegate? + public var conversation: V2NIMConversation? + + override public init() { + super.init() + conversationRepo.addListener(self) + } + + deinit { + conversationRepo.removeListener(self) + } + private let className = "UserSettingViewModel" - func getUserSettingModel(_ userId: String) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + userId) - guard let user = repo.getUserInfo(userId: userId) else { - return + /// 回去单聊会话 + /// - Parameter userId: 用户id + /// - Parameter completion: 完成回调 + func getConversation(_ userId: String, _ completion: @escaping (NSError?) -> Void) { + if let cid = V2NIMConversationIdUtil.p2pConversationId(userId) { + weak var weakSelf = self + conversationRepo.getConversation(cid) { conversation, error in + if conversation != nil { + weakSelf?.conversation = conversation + } + completion(error) + } } - userInfo = user - weak var weakSelf = self + } + + /// 获取用户设置UI显示数据模型 + /// - Parameter userId: 用户id + /// - Parameter completion: 完成回调 + func getUserSettingModel(_ userId: String, _ completion: @escaping () -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + userId) + contactRepo.getUserWithFriend(accountIds: [userId]) { [weak self] userfriends, error in + self?.userInfo = userfriends?.first + + self?.getSectionDatas() + + self?.delegate?.didNeedRefreshUI() + + completion() + } + } + + /// 拼装UI显示数据模型 + func getSectionDatas() { + cellDatas.removeAll() let mark = UserSettingCellModel() mark.cellName = chatLocalizable("operation_pin") mark.type = UserSettingType.SelectType.rawValue - mark.cornerType = .topLeft.union(.topRight) +// mark.cornerType = .topLeft.union(.topRight) let remind = UserSettingCellModel() remind.cellName = chatLocalizable("message_remind") - if let isNotiMsg = user.imUser?.notifyForNewMsg() { - remind.switchOpen = isNotiMsg + if let userId = userInfo?.user?.accountId { + remind.switchOpen = settingRepo.getP2PMessageMuteMode(accountId: userId) == .NIM_P2P_MESSAGE_MUTE_MODE_OFF } + weak var weakSelf = self remind.swichChange = { isOpen in - if let uid = weakSelf?.userInfo?.userId { - weakSelf?.repo.setNotify(uid, isOpen) { error in + if let uid = weakSelf?.userInfo?.user?.accountId { + let muteMode: V2NIMP2PMessageMuteMode = isOpen ? .NIM_P2P_MESSAGE_MUTE_MODE_OFF : .NIM_P2P_MESSAGE_MUTE_MODE_ON + weakSelf?.settingRepo.setP2PMessageMuteMode(accountId: uid, muteMode: muteMode) { error in if let err = error { weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) @@ -58,62 +101,79 @@ open class UserSettingViewModel: NSObject { let setTop = UserSettingCellModel() setTop.cellName = chatLocalizable("session_set_top") - setTop.cornerType = .bottomRight.union(.bottomLeft) +// setTop.cornerType = .bottomRight.union(.bottomLeft) - if let uid = user.userId { - let session = NIMSession(uid, type: .P2P) - setTop.switchOpen = repo.isStickTop(session) + if let currentConversation = conversation { + setTop.switchOpen = currentConversation.stickTop } setTop.swichChange = { isOpen in - if let uid = weakSelf?.userInfo?.userId { - let session = NIMSession(uid, type: .P2P) + if let uid = weakSelf?.userInfo?.user?.accountId, let cid = V2NIMConversationIdUtil.p2pConversationId(uid) { if isOpen { - if weakSelf?.getRecenterSession() == nil { - weakSelf?.addRecentetSession() - } - let params = NIMAddStickTopSessionParams(session: session) - weakSelf?.repo.chatExtendProvider - .addStickTopSession(params: params) { error, info in - print("add stick : ", error as Any) - if let err = error { - weakSelf?.delegate?.didNeedRefreshUI() - weakSelf?.delegate?.didError(err) - } else { - setTop.switchOpen = false - } + weakSelf?.conversationRepo.setStickTop(cid, true) { error in + print("add stick : ", error as Any) + if let err = error { + weakSelf?.delegate?.didNeedRefreshUI() + weakSelf?.delegate?.didError(err) + } else { + setTop.switchOpen = false } + } + } else { - if let info = weakSelf?.repo.chatExtendProvider.getTopSessionInfo(session) { - weakSelf?.repo.chatExtendProvider - .removeStickTopSession(params: info) { error, info in - print("remote stick : ", error as Any) - if let err = error { - weakSelf?.delegate?.didNeedRefreshUI() - weakSelf?.delegate?.didError(err) - } else { - setTop.switchOpen = true - } - } + weakSelf?.conversationRepo.setStickTop(cid, false) { error in + print("remote stick : ", error as Any) + if let err = error { + weakSelf?.delegate?.didNeedRefreshUI() + weakSelf?.delegate?.didError(err) + } else { + setTop.switchOpen = true + } } } } } - cellDatas.append(contentsOf: [mark, remind, setTop]) + if IMKitConfigCenter.shared.pinEnable { + cellDatas.append(mark) + } + cellDatas.append(remind) + cellDatas.append(setTop) + + setAudoType() + } + + // 设置圆角 + open func setAudoType() { + for model in cellDatas { + if model == cellDatas.first { + model.cornerType = .topLeft.union(.topRight) + if model == cellDatas.last { + model.cornerType = .topLeft.union(.topRight).union(.bottomLeft).union(.bottomRight) + } + } else if model == cellDatas.last { + model.cornerType = .bottomLeft.union(.bottomRight) + } else { + model.cornerType = .none + } + } } - public func addRecentetSession() { - if let uid = userInfo?.userId { - let currentSession = NIMSession(uid, type: .P2P) - repo.addRecentSession(currentSession) + open func setFunType() { + for model in cellDatas { + model.cornerType = .none } } - public func getRecenterSession() -> NIMRecentSession? { - if let uid = userInfo?.userId { - let currentSession = NIMSession(uid, type: .P2P) - return repo.getRecentSession(currentSession) + /// 会话变更回调 + /// - Parameter conversations: 会话列表 + public func onConversationChanged(_ conversations: [V2NIMConversation]) { + for changeConversation in conversations { + if let currentConversation = conversation, currentConversation.conversationId == changeConversation.conversationId { + conversation = changeConversation + getSectionDatas() + delegate?.didNeedRefreshUI() + continue + } } - return nil } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift b/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift index a891a613..ebf8f7be 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import NECommonKit +import NIMSDK import UIKit /// 头像枚举类型 @@ -35,6 +36,11 @@ public class ChatUIConfig: NSObject { /// 消息列表的视图控制器回调,回调中会返回消息列表的视图控制器 public var customController: ((ChatViewController) -> Void)? + /// 消息列表发送消息时的视图控制器回调 + /// 回调参数:消息体和消息列表的视图控制器 + /// 返回值:是否继续发送消息 + public var onSendMessage: ((V2NIMMessage, ChatViewController) -> Bool)? + /// 用户可自定义参数 // 发送文件大小限制(单位:MB) diff --git a/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift b/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift index 44f6c34e..2308d614 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/NEBaseChatRouter.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import Foundation +import NEChatKit import NECommonKit import NIMSDK import SDWebImage @@ -14,10 +15,23 @@ import SDWebImageWebPCoder open class ChatRouter: NSObject { public static func setupInit() { NIMKitFileLocationHelper.setStaticAppkey(NIMSDK.shared().appKey()) - NIMKitFileLocationHelper.setStaticUserId(NIMSDK.shared().loginManager.currentAccount()) + NIMKitFileLocationHelper.setStaticUserId(IMKitClient.instance.account()) let webpCoder = SDImageWebPCoder() SDImageCodersManager.shared.addCoder(webpCoder) let svgCoder = SDImageSVGKCoder.shared SDImageCodersManager.shared.addCoder(svgCoder) } + + public static func registerCommon() { + // sendMessage + Router.shared.register(ChatAddFriendRouter) { param in + if let text = param["text"] as? String, + let cid = param["conversationId"] as? String { + let helloMessage = MessageUtils.textMessage(text: text) + ChatRepo.shared.sendMessage(message: helloMessage, conversationId: cid) { result, error, pro in + NEALog.errorLog("ChatAddFriendRouter", desc: "send hello message error:\(error?.localizedDescription ?? "nil")") + } + } + } + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift index d38eb966..c79789dd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatCellConstantValue.swift @@ -28,7 +28,7 @@ public let chat_min_h: CGFloat = 40.0 public let chat_reply_height: CGFloat = 16.0 // 气泡最大宽度 -public let chat_content_maxW: CGFloat = (kScreenWidth - 136) +public let chat_content_maxW: CGFloat = (kScreenWidth - 156) // 文本内容最大宽度 public let chat_text_maxW: CGFloat = chat_content_maxW - 2 * chat_content_margin diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift index 6a473b6c..1f9a30c4 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift @@ -7,10 +7,10 @@ import Foundation import NEChatKit @_exported import NECommonKit @_exported import NECommonUIKit -@_exported import NECoreIMKit +@_exported import NECoreIM2Kit @_exported import NECoreKit -let coreLoader = CoreLoader() +let coreLoader = CoreLoader() func chatLocalizable(_ key: String) -> String { coreLoader.localizable(key) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift index 18a155ad..bb7f5fa7 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift @@ -43,12 +43,12 @@ open class NEChatUIKitClient: NSObject { /// 获取更多面板数据 /// - Returns: 返回更多操作数据 - open func getMoreActionData(sessionType: NIMSessionType) -> [NEMoreItemModel] { + open func getMoreActionData(sessionType: V2NIMConversationType) -> [NEMoreItemModel] { var more = [NEMoreItemModel]() - moreAction.forEach { model in + for model in moreAction { if model.type != .rtc { more.append(model) - } else if sessionType == .P2P { + } else if sessionType == .CONVERSATION_TYPE_P2P { more.append(model) } } @@ -62,7 +62,7 @@ open class NEChatUIKitClient: NSObject { /// 新增聊天页针对自定义消息的cell扩展,以及现有cell样式覆盖 open func regsiterCustomCell(_ registerDic: [String: UITableViewCell.Type]) { - registerDic.forEach { (key: String, value: UITableViewCell.Type) in + for (key, value) in registerDic { customRegisterDic[key] = value } } @@ -70,4 +70,18 @@ open class NEChatUIKitClient: NSObject { open func getRegisterCustomCell() -> [String: UITableViewCell.Type] { customRegisterDic } + + /// 获取图片资源 + /// - Parameter imageName 图片名称 + /// - Returns 图片资源 + open func getImageSource(imageName: String) -> UIImage? { + coreLoader.loadImage(imageName) + } + + /// 获取多语言 + /// - Parameter key 多语言key + /// - Returns 多语言 + open func getLanguage(key: String) -> String? { + coreLoader.localizable(key) + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift b/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift index d27738a2..b7f528c5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Extension/AlertVCExtention.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import Foundation + extension UIAlertController { class func reconfimAlertView(title: String?, message: String?, confirm: @escaping () -> Void) -> UIAlertController { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatImageExtension.swift b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatImageExtension.swift index be05242b..1e0d0ee5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatImageExtension.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatImageExtension.swift @@ -6,6 +6,7 @@ import CoreGraphics import Foundation import UIKit + public extension UIImage { class func ne_imageNamed(name: String?) -> UIImage? { guard let imageName = name else { @@ -23,3 +24,67 @@ public extension UIImage { return UIImage() } } + +public extension UIImage { + /// 修复图片旋转 + func fixOrientation() -> UIImage { + // 默认方向无需旋转 + if imageOrientation == .up { + return self + } + + var transform = CGAffineTransform.identity + + switch imageOrientation { + // 默认方向旋转180度、镜像旋转180度 + case .down, .downMirrored: + transform = transform.translatedBy(x: size.width, y: size.height) + transform = transform.rotated(by: .pi) + + // 默认方向逆时针旋转90度、镜像逆时针旋转90度 + case .left, .leftMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.rotated(by: .pi / 2) + + // 默认方向顺时针旋转90度、镜像顺时针旋转90度 + case .right, .rightMirrored: + transform = transform.translatedBy(x: 0, y: size.height) + transform = transform.rotated(by: -.pi / 2) + + default: + break + } + + switch imageOrientation { + // 默认方向的竖线镜像、镜像旋转180度 + case .upMirrored, .downMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + + // 镜像逆时针旋转90度、镜像顺时针旋转90度 + case .leftMirrored, .rightMirrored: + transform = transform.translatedBy(x: size.height, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + + default: + break + } + + let ctx = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: cgImage!.bitsPerComponent, bytesPerRow: 0, space: cgImage!.colorSpace!, bitmapInfo: cgImage!.bitmapInfo.rawValue) + ctx?.concatenate(transform) + + // 重新绘制 + switch imageOrientation { + case .left, .leftMirrored, .right, .rightMirrored: + ctx?.draw(cgImage!, in: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.height), height: CGFloat(size.width))) + + default: + ctx?.draw(cgImage!, in: CGRect(x: CGFloat(0), y: CGFloat(0), width: CGFloat(size.width), height: CGFloat(size.height))) + } + + let cgimg: CGImage = (ctx?.makeImage())! + let img = UIImage(cgImage: cgimg) + + return img + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift index 1bca66c5..ea4ff441 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift @@ -5,28 +5,12 @@ import Foundation -// 缓存的用于计算高度的Label -var tempLabelForCalc: UILabel = { - let label = UILabel() - label.numberOfLines = 0 - return label -}() - extension String { - /// 计算 string 的 size - static func getTextRectSize(_ text: String, font: UIFont, size: CGSize) -> CGSize { - let attributes = [NSAttributedString.Key.font: font] - let option = NSStringDrawingOptions.usesLineFragmentOrigin - let rect: CGRect = text.boundingRect(with: size, options: option, - attributes: attributes, context: nil) - return CGSize(width: ceil(rect.width), height: ceil(rect.height)) - } - /// 计算 string 的行数,使用 font 的 lineHeight static func calculateMaxLines(width: CGFloat, string: String?, font: UIFont) -> Int { let maxSize = CGSize(width: width, height: CGFloat(Float.infinity)) let charSize = font.lineHeight - let textSize = string?.finalSize(font, maxSize) ?? .zero + let textSize = String.getRealSize(string, font, maxSize) let lines = Int(textSize.height / charSize) return lines } @@ -35,7 +19,7 @@ extension String { static func calculateMaxLines(width: CGFloat, attributeString: NSAttributedString?, font: UIFont) -> Int { let maxSize = CGSize(width: width, height: CGFloat(Float.infinity)) let charSize = font.lineHeight - let textSize = attributeString?.finalSize(font, maxSize) ?? .zero + let textSize = NSAttributedString.getRealSize(attributeString, font, maxSize) let lines = Int(textSize.height / charSize) return lines } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Extension/NEErrorExtension.swift b/NEChatUIKit/NEChatUIKit/Classes/Extension/NEErrorExtension.swift index 1304ab31..1cc6d102 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Extension/NEErrorExtension.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Extension/NEErrorExtension.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import Foundation + extension NSError { class func paramError() -> NSError { NSError( diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageAudioCell.swift new file mode 100644 index 00000000..25ac2849 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageAudioCell.swift @@ -0,0 +1,38 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class FunCollectionMessageAudioCell: NEBaseCollectionMessageAudioCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + setFunStyle() + let image = NEKitChatConfig.shared.ui.messageProperties.leftBubbleBg ?? UIImage.ne_imageNamed(name: "fun_pin_message_audio_bg") + bubbleImage.image = image? + .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageDefaultCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageDefaultCell.swift new file mode 100644 index 00000000..8f687685 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageDefaultCell.swift @@ -0,0 +1,35 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class FunCollectionMessageDefaultCell: NEBaseCollectionMessageDefaultCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + setFunStyle() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageFileCell.swift new file mode 100644 index 00000000..21d6ab6d --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageFileCell.swift @@ -0,0 +1,35 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class FunCollectionMessageFileCell: NEBaseCollectionMessageFileCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + setFunStyle() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageImageCell.swift new file mode 100644 index 00000000..57abf5e9 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageImageCell.swift @@ -0,0 +1,35 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class FunCollectionMessageImageCell: NEBaseCollectionMessageImageCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + setFunStyle() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageLocationCell.swift new file mode 100644 index 00000000..029e7b11 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageLocationCell.swift @@ -0,0 +1,35 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class FunCollectionMessageLocationCell: NEBaseCollectionMessageLocationCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + setFunStyle() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageMultiForwardCell.swift new file mode 100644 index 00000000..6588ac60 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageMultiForwardCell.swift @@ -0,0 +1,79 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class FunCollectionMessageMultiForwardCell: NEBaseCollectionMessageMultiForwardCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + setFunStyle() + + titleLabelFontSize = 16 + contentLabelFontSize = 12 + contentLabelColor = .funChatMultiForwardContentColor + + backImageViewLeft.addSubview(titleLabelLeft1) + NSLayoutConstraint.activate([ + titleLabelLeft1.leftAnchor.constraint(equalTo: backImageViewLeft.leftAnchor, constant: 12 + funMargin), + titleLabelLeft1.rightAnchor.constraint(lessThanOrEqualTo: backImageViewLeft.rightAnchor, constant: -102), + titleLabelLeft1.topAnchor.constraint(equalTo: backImageViewLeft.topAnchor, constant: 12), + titleLabelLeft1.heightAnchor.constraint(equalToConstant: 22), + ]) + + backImageViewLeft.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), + ]) + + backImageViewLeft.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), + ]) + + backImageViewLeft.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), + ]) + + backImageViewLeft.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), + ]) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageRichTextCell.swift new file mode 100644 index 00000000..b8f99736 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageRichTextCell.swift @@ -0,0 +1,35 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class FunCollectionMessageRichTextCell: NEBaseCollectionMessageRichTextCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + setFunStyle() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageTextCell.swift new file mode 100644 index 00000000..b09eeeb0 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageTextCell.swift @@ -0,0 +1,35 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class FunCollectionMessageTextCell: NEBaseCollectionMessageTextCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + setFunStyle() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageVideoCell.swift new file mode 100644 index 00000000..180a6766 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/CollectionCell/FunCollectionMessageVideoCell.swift @@ -0,0 +1,35 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class FunCollectionMessageVideoCell: NEBaseCollectionMessageVideoCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + setFunStyle() + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift index f09afd29..722cffeb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageAudioCell.swift @@ -21,7 +21,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -32,6 +32,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco open func commonUILeft() { audioImageViewLeft.contentMode = .center audioImageViewLeft.translatesAutoresizingMaskIntoConstraints = false + audioImageViewLeft.accessibilityIdentifier = "id.animation" bubbleImageLeft.addSubview(audioImageViewLeft) NSLayoutConstraint.activate([ audioImageViewLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: 16), @@ -44,6 +45,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco timeLabelLeft.textColor = UIColor.ne_darkText timeLabelLeft.textAlignment = .left timeLabelLeft.translatesAutoresizingMaskIntoConstraints = false + timeLabelLeft.accessibilityIdentifier = "id.time" bubbleImageLeft.addSubview(timeLabelLeft) NSLayoutConstraint.activate([ timeLabelLeft.leftAnchor.constraint(equalTo: audioImageViewLeft.rightAnchor, constant: 12), @@ -62,6 +64,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco open func commonUIRight() { audioImageViewRight.contentMode = .center audioImageViewRight.translatesAutoresizingMaskIntoConstraints = false + audioImageViewRight.accessibilityIdentifier = "id.animation" bubbleImageRight.addSubview(audioImageViewRight) NSLayoutConstraint.activate([ audioImageViewRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -16), @@ -74,6 +77,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco timeLabelRight.textColor = UIColor.ne_darkText timeLabelRight.textAlignment = .right timeLabelRight.translatesAutoresizingMaskIntoConstraints = false + timeLabelRight.accessibilityIdentifier = "id.time" bubbleImageRight.addSubview(timeLabelRight) NSLayoutConstraint.activate([ timeLabelRight.rightAnchor.constraint(equalTo: audioImageViewRight.leftAnchor, constant: -12), @@ -90,13 +94,11 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco } open func startAnimation(byRight: Bool) { - if byRight { - if !audioImageViewRight.isAnimating { - audioImageViewRight.startAnimating() - } - } else if !audioImageViewLeft.isAnimating { - audioImageViewLeft.startAnimating() + let audioImageView = byRight ? audioImageViewRight : audioImageViewLeft + if !audioImageView.isAnimating { + audioImageView.startAnimating() } + if let m = contentModel as? MessageAudioModel { m.isPlaying = true isPlaying = true @@ -104,13 +106,11 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco } open func stopAnimation(byRight: Bool) { - if byRight { - if audioImageViewRight.isAnimating { - audioImageViewRight.stopAnimating() - } - } else if audioImageViewLeft.isAnimating { - audioImageViewLeft.stopAnimating() + let audioImageView = byRight ? audioImageViewRight : audioImageViewLeft + if audioImageView.isAnimating { + audioImageView.stopAnimating() } + if let m = contentModel as? MessageAudioModel { m.isPlaying = false isPlaying = false @@ -135,7 +135,7 @@ open class FunChatMessageAudioCell: FunChatMessageBaseCell, ChatAudioCellProtoco timeLabelLeft.text = "\(m.duration)" + "″" } m.isPlaying ? startAnimation(byRight: isSend) : stopAnimation(byRight: isSend) - messageId = m.message?.messageId + messageId = m.message?.messageClientId } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift index f586fed8..13d784dd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageBaseCell.swift @@ -22,7 +22,7 @@ open class FunChatMessageBaseCell: NEBaseChatMessageCell { bubbleImageRight.image = image? .resizableImage(withCapInsets: NEKitChatConfig.shared.ui.messageProperties.backgroundImageCapInsets) - seletedBtn.setImage(.ne_imageNamed(name: "fun_select"), for: .selected) + selectedButton.setImage(.ne_imageNamed(name: "fun_select"), for: .selected) } override open func baseCommonUI() { @@ -31,6 +31,9 @@ open class FunChatMessageBaseCell: NEBaseChatMessageCell { contentView.updateLayoutConstraint(firstItem: fullNameLabel, seconedItem: avatarImageLeft, attribute: .left, constant: 8 + funMargin) contentView.updateLayoutConstraint(firstItem: fullNameLabel, seconedItem: avatarImageLeft, attribute: .top, constant: -4) + + contentView.updateLayoutConstraint(firstItem: pinLabelLeft, seconedItem: bubbleImageLeft, attribute: .left, constant: 14 + funMargin) + contentView.updateLayoutConstraint(firstItem: pinLabelRight, seconedItem: bubbleImageRight, attribute: .right, constant: -funMargin) } override open func initSubviewsLayout() { diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift index ee526efc..46bbc2b2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageCallCell.swift @@ -27,9 +27,10 @@ open class FunChatMessageCallCell: FunChatMessageBaseCell { contentLabelLeft.isEnabled = false contentLabelLeft.numberOfLines = 0 contentLabelLeft.isUserInteractionEnabled = false - contentLabelLeft.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + contentLabelLeft.font = messageTextFont contentLabelLeft.textAlignment = .center contentLabelLeft.backgroundColor = .clear + contentLabelLeft.accessibilityIdentifier = "id.chatMessageCallText" bubbleImageLeft.addSubview(contentLabelLeft) NSLayoutConstraint.activate([ contentLabelLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor, constant: -chat_content_margin), @@ -43,9 +44,10 @@ open class FunChatMessageCallCell: FunChatMessageBaseCell { contentLabelRight.isEnabled = false contentLabelRight.numberOfLines = 0 contentLabelRight.isUserInteractionEnabled = false - contentLabelRight.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + contentLabelRight.font = messageTextFont contentLabelRight.textAlignment = .center contentLabelRight.backgroundColor = .clear + contentLabelRight.accessibilityIdentifier = "id.chatMessageCallText" bubbleImageRight.addSubview(contentLabelRight) NSLayoutConstraint.activate([ contentLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -(chat_content_margin + funMargin)), diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift index 5dedf7ea..666772db 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageFileCell.swift @@ -11,19 +11,19 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { weak var weakModel: MessageFileModel? public lazy var imgViewLeft: UIImageView = { - let view_img = UIImageView() - view_img.translatesAutoresizingMaskIntoConstraints = false - view_img.backgroundColor = .clear - view_img.accessibilityIdentifier = "id.fileType" - return view_img + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = .clear + imageView.accessibilityIdentifier = "id.fileType" + return imageView }() public lazy var stateViewLeft: FileStateView = { - let state = FileStateView() - state.translatesAutoresizingMaskIntoConstraints = false - state.backgroundColor = .clear - state.accessibilityIdentifier = "id.fileStatus" - return state + let stateView = FileStateView() + stateView.translatesAutoresizingMaskIntoConstraints = false + stateView.backgroundColor = .clear + stateView.accessibilityIdentifier = "id.fileStatus" + return stateView }() public lazy var titleLabelLeft: UILabel = { @@ -34,6 +34,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { label.lineBreakMode = .byTruncatingMiddle label.font = DefaultTextFont(14) label.textAlignment = .left + label.accessibilityIdentifier = "id.displayName" return label }() @@ -43,6 +44,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { label.textColor = UIColor(hexString: "#999999") label.font = NEConstant.defaultTextFont(10.0) label.textAlignment = .left + label.accessibilityIdentifier = "id.displaySize" return label }() @@ -68,19 +70,19 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { }() public lazy var imgViewRight: UIImageView = { - let view_img = UIImageView() - view_img.translatesAutoresizingMaskIntoConstraints = false - view_img.backgroundColor = .clear - view_img.accessibilityIdentifier = "id.fileType" - return view_img + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = .clear + imageView.accessibilityIdentifier = "id.fileType" + return imageView }() public lazy var stateViewRight: FileStateView = { - let state = FileStateView() - state.translatesAutoresizingMaskIntoConstraints = false - state.backgroundColor = .clear - state.accessibilityIdentifier = "id.fileStatus" - return state + let stateView = FileStateView() + stateView.translatesAutoresizingMaskIntoConstraints = false + stateView.backgroundColor = .clear + stateView.accessibilityIdentifier = "id.fileStatus" + return stateView }() public lazy var titleLabelRight: UILabel = { @@ -91,6 +93,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { label.lineBreakMode = .byTruncatingMiddle label.font = DefaultTextFont(14) label.textAlignment = .left + label.accessibilityIdentifier = "id.displayName" return label }() @@ -100,6 +103,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { label.textColor = UIColor(hexString: "#999999") label.font = NEConstant.defaultTextFont(10.0) label.textAlignment = .left + label.accessibilityIdentifier = "id.displaySize" return label }() @@ -130,7 +134,7 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -223,56 +227,55 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { bubbleW?.constant = kScreenWidth <= 320 ? 222 : 242 // 适配小屏幕 - if let fileObject = model.message?.messageObject as? NIMFileObject { + if let fileObject = model.message?.attachment as? V2NIMMessageFileAttachment { if let fileModel = model as? MessageFileModel { weakModel?.cell = nil weakModel = fileModel fileModel.cell = self - fileModel.size = Float(fileObject.fileLength) + fileModel.size = Float(fileObject.size) if fileModel.state == .Success { stateView.state = .FileOpen } else { stateView.state = .FileDownload - stateView.setProgress(fileModel.progress) - if fileModel.progress >= 1 { + stateView.setProgress(Float(fileModel.progress / 100)) + if fileModel.progress >= 100 { fileModel.state = .Success } } } var imageName = "file_unknown" - var displayName = "未知文件" - if let filePath = fileObject.path as? NSString { - displayName = filePath.lastPathComponent - switch filePath.pathExtension.lowercased() { - case file_doc_support: - imageName = "file_doc" - case file_xls_support: - imageName = "file_xls" - case file_img_support: - imageName = "file_img" - case file_ppt_support: - imageName = "file_ppt" - case file_txt_support: - imageName = "file_txt" - case file_audio_support: - imageName = "file_audio" - case file_vedio_support: - imageName = "file_vedio" - case file_zip_support: - imageName = "file_zip" - case file_pdf_support: - imageName = "file_pdf" - case file_html_support: - imageName = "file_html" - case "key", "keynote": - imageName = "file_keynote" - default: - imageName = "file_unknown" - } + let suffix = (fileObject.name as NSString).pathExtension.lowercased() + switch suffix { + case file_doc_support: + imageName = "file_doc" + case file_xls_support: + imageName = "file_xls" + case file_img_support: + imageName = "file_img" + case file_ppt_support: + imageName = "file_ppt" + case file_txt_support: + imageName = "file_txt" + case file_audio_support: + imageName = "file_audio" + case file_vedio_support: + imageName = "file_vedio" + case file_zip_support: + imageName = "file_zip" + case file_pdf_support: + imageName = "file_pdf" + case file_html_support: + imageName = "file_html" + case "key", "keynote": + imageName = "file_keynote" + default: + imageName = "file_unknown" } + imgView.image = UIImage.ne_imageNamed(name: imageName) - titleLabel.text = fileObject.displayName ?? displayName - let size_B = Double(fileObject.fileLength) + titleLabel.text = fileObject.name + + let size_B = Double(fileObject.size) var size_str = String(format: "%.1f B", size_B) if size_B > 1e3 { let size_KB = size_B / 1e3 @@ -290,8 +293,8 @@ open class FunChatMessageFileCell: FunChatMessageBaseCell { } } - override open func uploadProgress(byRight: Bool, _ progress: Float) { + override open func uploadProgress(byRight: Bool, _ progress: UInt) { let stateView = byRight ? stateViewRight : stateViewLeft - stateView.setProgress(progress) + stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift index 0c5e1790..b1a36c7b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageImageCell.swift @@ -3,6 +3,7 @@ // found in the LICENSE file. import NIMSDK +import SDWebImage import UIKit @objcMembers @@ -15,7 +16,7 @@ open class FunChatMessageImageCell: FunChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -28,6 +29,7 @@ open class FunChatMessageImageCell: FunChatMessageBaseCell { contentImageViewLeft.contentMode = .scaleAspectFill contentImageViewLeft.clipsToBounds = true contentImageViewLeft.layer.cornerRadius = 4 + contentImageViewLeft.accessibilityIdentifier = "id.thumbnail" bubbleImageLeft.image = nil bubbleImageLeft.addSubview(contentImageViewLeft) NSLayoutConstraint.activate([ @@ -43,6 +45,7 @@ open class FunChatMessageImageCell: FunChatMessageBaseCell { contentImageViewRight.contentMode = .scaleAspectFill contentImageViewRight.clipsToBounds = true contentImageViewRight.layer.cornerRadius = 4 + contentImageViewRight.accessibilityIdentifier = "id.thumbnail" bubbleImageRight.image = nil bubbleImageRight.addSubview(contentImageViewRight) NSLayoutConstraint.activate([ @@ -63,18 +66,19 @@ open class FunChatMessageImageCell: FunChatMessageBaseCell { super.setModel(model, isSend) let contentImageView = isSend ? contentImageViewRight : contentImageViewLeft - if let m = model as? MessageImageModel, let imageUrl = m.imageUrl { + if let m = model as? MessageImageModel, let imageUrl = m.urlString { + var options: SDWebImageOptions = [.retryFailed] + if let imageObject = model.message?.attachment as? V2NIMMessageImageAttachment, imageObject.ext != ".gif" { + options = [.retryFailed, .progressiveLoad] + } + + let context: [SDWebImageContextOption: Any] = [.imageThumbnailPixelSize: CGSize(width: 1000, height: 1000)] if imageUrl.hasPrefix("http") { - contentImageView.sd_setImage( - with: URL(string: imageUrl), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) + let url = URL(string: imageUrl) + contentImageView.sd_setImage(with: url, placeholderImage: nil, options: options, context: context) } else { let url = URL(fileURLWithPath: imageUrl) - contentImageView.sd_setImage(with: url) + contentImageView.sd_setImage(with: url, placeholderImage: nil, options: options, context: context) } } else { contentImageView.image = nil diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift index 8e2c9509..a04c0157 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageLocationCell.swift @@ -12,6 +12,7 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_darkText label.font = UIFont.systemFont(ofSize: 16.0) + label.accessibilityIdentifier = "id.locationItemTitle" return label }() @@ -20,6 +21,7 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_lightText label.font = UIFont.systemFont(ofSize: 12.0) + label.accessibilityIdentifier = "id.locationItemAddress" return label }() @@ -34,6 +36,15 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { }() public var mapViewLeft: UIView? + + public var mapImageViewLeft: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + return imageView + }() + let backgroundViewLeft = UIView() // Right @@ -42,6 +53,7 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_darkText label.font = UIFont.systemFont(ofSize: 16.0) + label.accessibilityIdentifier = "id.locationItemTitle" return label }() @@ -50,6 +62,7 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_lightText label.font = UIFont.systemFont(ofSize: 12.0) + label.accessibilityIdentifier = "id.locationItemAddress" return label }() @@ -64,15 +77,38 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { }() public var mapViewRight: UIView? + + public var mapImageViewRight: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + return imageView + }() + let backgroundViewRight = UIView() + lazy var pointImageRight: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = coreLoader.loadImage("location_point") + return imageView + }() + + lazy var pointImageLeft: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = coreLoader.loadImage("location_point") + return imageView + }() + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonUI() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -122,33 +158,26 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { subTitleLabelLeft.topAnchor.constraint(equalTo: titleLabelLeft.bottomAnchor, constant: 4), ]) - if let map = NEChatKitClient.instance.delegate?.getCellMapView?() as? UIView { - mapViewLeft = map - backgroundViewLeft.addSubview(map) - map.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - map.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), - map.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor), - map.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), - map.topAnchor.constraint(equalTo: subTitleLabelLeft.bottomAnchor, constant: 4), - ]) - - let pointImage = UIImageView() - pointImage.translatesAutoresizingMaskIntoConstraints = false - pointImage.image = coreLoader.loadImage("location_point") - map.addSubview(pointImage) - NSLayoutConstraint.activate([ - pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), - pointImage.bottomAnchor.constraint(equalTo: map.bottomAnchor, constant: -30), - ]) - } else { - backgroundViewLeft.addSubview(emptyLabelLeft) - NSLayoutConstraint.activate([ - emptyLabelLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), - emptyLabelLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), - emptyLabelLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor, constant: -40), - ]) - } + backgroundViewLeft.addSubview(mapImageViewLeft) + NSLayoutConstraint.activate([ + mapImageViewLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), + mapImageViewLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor), + mapImageViewLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), + mapImageViewLeft.heightAnchor.constraint(equalToConstant: 86), + ]) + + mapImageViewLeft.addSubview(pointImageLeft) + NSLayoutConstraint.activate([ + pointImageLeft.centerXAnchor.constraint(equalTo: mapImageViewLeft.centerXAnchor), + pointImageLeft.bottomAnchor.constraint(equalTo: mapImageViewLeft.bottomAnchor, constant: -30), + ]) + + backgroundViewLeft.addSubview(emptyLabelLeft) + NSLayoutConstraint.activate([ + emptyLabelLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), + emptyLabelLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), + emptyLabelLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor, constant: -40), + ]) } open func commonUIRight() { @@ -192,33 +221,26 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { subTitleLabelRight.topAnchor.constraint(equalTo: titleLabelRight.bottomAnchor, constant: 4), ]) - if let map = NEChatKitClient.instance.delegate?.getCellMapView?() as? UIView { - mapViewRight = map - backgroundViewRight.addSubview(map) - map.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - map.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), - map.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor), - map.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), - map.topAnchor.constraint(equalTo: subTitleLabelRight.bottomAnchor, constant: 4), - ]) - - let pointImage = UIImageView() - pointImage.translatesAutoresizingMaskIntoConstraints = false - pointImage.image = coreLoader.loadImage("location_point") - map.addSubview(pointImage) - NSLayoutConstraint.activate([ - pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), - pointImage.bottomAnchor.constraint(equalTo: map.bottomAnchor, constant: -30), - ]) - } else { - backgroundViewRight.addSubview(emptyLabelRight) - NSLayoutConstraint.activate([ - emptyLabelRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), - emptyLabelRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), - emptyLabelRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor, constant: -40), - ]) - } + backgroundViewRight.addSubview(mapImageViewRight) + NSLayoutConstraint.activate([ + mapImageViewRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), + mapImageViewRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor), + mapImageViewRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), + mapImageViewRight.heightAnchor.constraint(equalToConstant: 86), + ]) + + mapImageViewRight.addSubview(pointImageRight) + NSLayoutConstraint.activate([ + pointImageRight.centerXAnchor.constraint(equalTo: mapImageViewRight.centerXAnchor), + pointImageRight.bottomAnchor.constraint(equalTo: mapImageViewRight.bottomAnchor, constant: -30), + ]) + + backgroundViewRight.addSubview(emptyLabelRight) + NSLayoutConstraint.activate([ + emptyLabelRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), + emptyLabelRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), + emptyLabelRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor, constant: -40), + ]) } override open func showLeftOrRight(showRight: Bool) { @@ -233,14 +255,24 @@ open class FunChatMessageLocationCell: FunChatMessageBaseCell { let subTitleLabel = isSend ? subTitleLabelRight : subTitleLabelLeft let mapView = isSend ? mapViewRight : mapViewLeft let bubbleW = isSend ? bubbleWRight : bubbleWLeft - - bubbleW?.constant = kScreenWidth <= 320 ? 222 : 242 // 适配小屏幕 + let mapImageView = isSend ? mapImageViewRight : mapImageViewLeft + let emptyLabel = isSend ? emptyLabelRight : emptyLabelLeft + let pointImage = isSend ? pointImageRight : pointImageLeft if let m = model as? MessageLocationModel { titleLabel.text = m.title subTitleLabel.text = m.subTitle - if let lat = m.lat, let lng = m.lng, let map = mapView { - NEChatKitClient.instance.delegate?.setMapviewLocation?(lat: lat, lng: lng, mapview: map) + if let lat = m.lat, let lng = m.lng { + if let url = NEChatKitClient.instance.delegate?.getMapImageUrl?(lat: lat, lng: lng) { + NEALog.infoLog(className(), desc: #function + "location image url = \(url)") + mapImageView.sd_setImage(with: URL(string: url), placeholderImage: coreLoader.loadImage("chat_map_default")) + emptyLabel.isHidden = true + pointImage.isHidden = false + } else { + mapImageView.image = UIImage.ne_imageNamed(name: "map_placeholder_image") + emptyLabel.isHidden = false + pointImage.isHidden = true + } } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift index 15ba90b3..e46dd92f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageMultiForwardCell.swift @@ -3,6 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECommonKit import NIMSDK import UIKit @@ -19,7 +20,7 @@ open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -175,7 +176,7 @@ open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { super.setModel(model, isSend) - guard let data = NECustomAttachment.dataOfCustomMessage(message: model.message) else { + guard let data = NECustomAttachment.dataOfCustomMessage(model.message?.attachment) else { return } @@ -214,13 +215,7 @@ open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { var contentText = "" if var senderNick = abstracts[i]["senderNick"] as? String { - if senderNick.count > 5 { - // 截取字符串 abcdefg -> ab...fg - let leftEndIndex = senderNick.index(senderNick.startIndex, offsetBy: 2) - let rightStartIndex = senderNick.index(senderNick.endIndex, offsetBy: -2) - senderNick = senderNick[senderNick.startIndex ..< leftEndIndex] + "..." + senderNick[rightStartIndex ..< senderNick.endIndex] - } - contentText = senderNick + contentText = NEFriendUserCache.getCutName(senderNick) if let content = abstracts[i]["content"] as? String { contentText += ":" + content } @@ -259,9 +254,9 @@ open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { // MARK: - lazy load public lazy var backViewLeft: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - return view + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView }() public lazy var titleLabelLeft1: UILabel = { @@ -321,9 +316,9 @@ open class FunChatMessageMultiForwardCell: FunChatMessageBaseCell { }() public lazy var backViewRight: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - return view + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView }() public lazy var titleLabelRight1: UILabel = { diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift index 57cd1925..7d5a0d5d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageReplyCell.swift @@ -12,6 +12,7 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyLabelLeft.textColor = .ne_greyText replyLabelLeft.translatesAutoresizingMaskIntoConstraints = false replyLabelLeft.font = UIFont.systemFont(ofSize: 13) + replyLabelLeft.accessibilityIdentifier = "id.messageReply" return replyLabelLeft }() @@ -32,6 +33,7 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyLabelRight.textColor = .ne_greyText replyLabelRight.translatesAutoresizingMaskIntoConstraints = false replyLabelRight.font = UIFont.systemFont(ofSize: 13) + replyLabelRight.accessibilityIdentifier = "id.messageReply" return replyLabelRight }() @@ -44,9 +46,6 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { return replyTextView }() - public var funPinLabelLeftTopAnchor: NSLayoutConstraint? // 左侧标记文案顶部布局约束 - public var funPinLabelRightTopAnchor: NSLayoutConstraint? // 右侧标记文案顶部布局约束 - override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonUI() @@ -54,7 +53,7 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func commonUI() { @@ -72,11 +71,6 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyTextViewLeft.widthAnchor.constraint(lessThanOrEqualToConstant: chat_content_maxW - funMargin), ]) - contentView.updateLayoutConstraint(firstItem: pinLabelLeft, seconedItem: bubbleImageLeft, attribute: .left, constant: 14 + funMargin) - pinLabelLeftTopAnchor?.isActive = false - funPinLabelLeftTopAnchor = pinLabelLeft.topAnchor.constraint(equalTo: replyTextViewLeft.bottomAnchor, constant: 4) - funPinLabelLeftTopAnchor?.isActive = true - replyTextViewLeft.addSubview(replyLabelLeft) NSLayoutConstraint.activate([ replyLabelLeft.topAnchor.constraint(equalTo: replyTextViewLeft.topAnchor, constant: 4), @@ -95,11 +89,6 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyTextViewRight.widthAnchor.constraint(lessThanOrEqualToConstant: chat_content_maxW - funMargin), ]) - contentView.updateLayoutConstraint(firstItem: pinLabelRight, seconedItem: bubbleImageRight, attribute: .right, constant: -funMargin) - pinLabelRightTopAnchor?.isActive = false - funPinLabelRightTopAnchor = pinLabelRight.topAnchor.constraint(equalTo: replyTextViewRight.bottomAnchor, constant: 4) - funPinLabelRightTopAnchor?.isActive = true - replyTextViewRight.addSubview(replyLabelRight) NSLayoutConstraint.activate([ replyLabelRight.topAnchor.constraint(equalTo: replyTextViewRight.topAnchor, constant: 4), @@ -112,7 +101,9 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { override open func showLeftOrRight(showRight: Bool) { super.showLeftOrRight(showRight: showRight) replyTextViewLeft.isHidden = showRight + replyLabelLeft.isHidden = showRight replyTextViewRight.isHidden = !showRight + replyLabelRight.isHidden = !showRight } open func addReplyGesture() { @@ -126,8 +117,7 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { } open func tapReplyView(tap: UITapGestureRecognizer) { - print(#function) - delegate?.didTapMessageView(self, contentModel) + delegate?.didTapMessageView(self, contentModel, contentModel?.replyedModel) } override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { @@ -139,6 +129,7 @@ open class FunChatMessageReplyCell: FunChatMessageTextCell { replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, font: font, color: replyLabel.textColor) + replyLabel.accessibilityValue = text } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift index 33dd595e..645d1e5e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRevokeCell.swift @@ -17,7 +17,7 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -31,6 +31,7 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { revokeLabelLeft.textAlignment = .center revokeLabelLeft.lineBreakMode = .byTruncatingMiddle revokeLabelLeft.font = UIFont.systemFont(ofSize: 14.0) + revokeLabelLeft.accessibilityIdentifier = "id.messageText" contentView.addSubview(revokeLabelLeft) NSLayoutConstraint.activate([ revokeLabelLeft.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), @@ -45,6 +46,7 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { revokeLabelRight.textColor = UIColor.ne_greyText revokeLabelRight.textAlignment = .center revokeLabelRight.font = UIFont.systemFont(ofSize: 14.0) + revokeLabelRight.accessibilityIdentifier = "id.messageText" contentView.addSubview(revokeLabelRight) revokeLabelRightXAnchor = revokeLabelRight.centerXAnchor.constraint(equalTo: contentView.centerXAnchor, constant: 0) revokeLabelRightXAnchor?.isActive = true @@ -56,7 +58,8 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { reeditButton.translatesAutoresizingMaskIntoConstraints = false reeditButton.titleLabel?.font = UIFont.systemFont(ofSize: 14.0) - reeditButton.setTitleColor(UIColor.ne_blueText, for: .normal) + reeditButton.setTitleColor(UIColor.ne_normalTheme, for: .normal) + reeditButton.accessibilityIdentifier = "id.reeditButton" contentView.addSubview(reeditButton) NSLayoutConstraint.activate([ @@ -83,14 +86,17 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { pinLabelRight.isHidden = true activityView.isHidden = true readView.isHidden = true - seletedBtn.isHidden = true + selectedButton.isHidden = true revokeLabelLeft.isHidden = showRight revokeLabelRight.isHidden = !showRight } override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { - if let time = model.message?.timestamp { + let isSend = IMKitClient.instance.isMe(model.message?.senderId) + let revokeLabel = isSend ? revokeLabelRight : revokeLabelLeft + + if let time = model.message?.createTime { let date = Date() let currentTime = date.timeIntervalSince1970 if currentTime - time >= 60 * 2 { @@ -98,8 +104,6 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { } } - let revokeLabel = isSend ? revokeLabelRight : revokeLabelLeft - model.contentSize = CGSize(width: kScreenWidth, height: 0) super.setModel(model, isSend) showLeftOrRight(showRight: isSend) @@ -111,7 +115,7 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { revokeLabel.text = (model.fullName ?? "") + " " + chatLocalizable("withdrew_message") } - if isSend, model.isRevokedText == true { + if isSend, model.isReedit == true { if model.timeOut == true { reeditButton.isHidden = true revokeLabelRightXAnchor?.constant = 0 @@ -127,7 +131,6 @@ open class FunChatMessageRevokeCell: FunChatMessageBaseCell { } func reeditEvent(button: UIButton) { - print(#function) delegate?.didTapReeditButton(self, contentModel) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift index 90300fee..32815a98 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageRichTextCell.swift @@ -14,6 +14,7 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) label.backgroundColor = .clear + label.accessibilityIdentifier = "id.messageTitle" return label }() @@ -25,6 +26,7 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { label.isUserInteractionEnabled = false label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize, weight: .semibold) label.backgroundColor = .clear + label.accessibilityIdentifier = "id.messageTitle" return label }() @@ -37,20 +39,22 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { /// left bubbleImageLeft.addSubview(titleLabelLeft) titleLabelLeftHeightAnchor = titleLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + titleLabelLeftHeightAnchor?.priority = .fittingSizeLevel + titleLabelLeftHeightAnchor?.isActive = true NSLayoutConstraint.activate([ titleLabelLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor, constant: -chat_content_margin), titleLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: chat_content_margin + funMargin), titleLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: chat_content_margin), - titleLabelLeftHeightAnchor!, ]) bubbleImageLeft.addSubview(contentLabelLeft) contentLabelLeftHeightAnchor = contentLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + contentLabelLeftHeightAnchor?.priority = .fittingSizeLevel + contentLabelLeftHeightAnchor?.isActive = true NSLayoutConstraint.activate([ contentLabelLeft.rightAnchor.constraint(equalTo: titleLabelLeft.rightAnchor, constant: -0), contentLabelLeft.leftAnchor.constraint(equalTo: titleLabelLeft.leftAnchor, constant: 0), contentLabelLeft.topAnchor.constraint(equalTo: titleLabelLeft.bottomAnchor, constant: chat_content_margin), - contentLabelLeftHeightAnchor!, ]) commonUILeft() @@ -58,20 +62,22 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { /// right bubbleImageRight.addSubview(titleLabelRight) titleLabelRightHeightAnchor = titleLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + titleLabelRightHeightAnchor?.priority = .fittingSizeLevel + titleLabelRightHeightAnchor?.isActive = true NSLayoutConstraint.activate([ titleLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -chat_content_margin - funMargin), titleLabelRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: chat_content_margin), titleLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: chat_content_margin), - titleLabelRightHeightAnchor!, ]) bubbleImageRight.addSubview(contentLabelRight) contentLabelRightHeightAnchor = contentLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + contentLabelRightHeightAnchor?.priority = .fittingSizeLevel + contentLabelRightHeightAnchor?.isActive = true NSLayoutConstraint.activate([ contentLabelRight.rightAnchor.constraint(equalTo: titleLabelRight.rightAnchor, constant: -0), contentLabelRight.leftAnchor.constraint(equalTo: titleLabelRight.leftAnchor, constant: 0), contentLabelRight.topAnchor.constraint(equalTo: titleLabelRight.bottomAnchor, constant: chat_content_margin), - contentLabelRightHeightAnchor!, ]) commonUIRight() @@ -89,17 +95,11 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { let titleLabel = isSend ? titleLabelRight : titleLabelLeft let titleLabelHeightAnchor = isSend ? titleLabelRightHeightAnchor : titleLabelLeftHeightAnchor let contentLabelHeightAnchor = isSend ? contentLabelRightHeightAnchor : contentLabelLeftHeightAnchor - let pinLabelTopAnchor = isSend ? pinLabelRightTopAnchor : pinLabelLeftTopAnchor - let funPinLabelTopAnchor = isSend ? funPinLabelRightTopAnchor : funPinLabelLeftTopAnchor if model.replyText == nil || model.replyText!.isEmpty { replyView.isHidden = true - funPinLabelTopAnchor?.isActive = false - pinLabelTopAnchor?.isActive = true } else { replyView.isHidden = false - funPinLabelTopAnchor?.isActive = true - pinLabelTopAnchor?.isActive = false } if let m = model as? MessageTextModel { @@ -109,9 +109,6 @@ open class FunChatMessageRichTextCell: FunChatMessageReplyCell { if let m = model as? MessageRichTextModel { titleLabel.attributedText = m.titleAttributeStr titleLabelHeightAnchor?.constant = m.titleTextHeight - if m.message?.text?.isEmpty == true { - titleLabelHeightAnchor?.constant = 26 - } } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift index 19656f6e..5daad0d6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageTextCell.swift @@ -12,8 +12,9 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { label.isEnabled = false label.numberOfLines = 0 label.isUserInteractionEnabled = false - label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + label.font = messageTextFont label.backgroundColor = .clear + label.accessibilityIdentifier = "id.messageText" return label }() @@ -23,8 +24,9 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { label.isEnabled = false label.numberOfLines = 0 label.isUserInteractionEnabled = false - label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + label.font = messageTextFont label.backgroundColor = .clear + label.accessibilityIdentifier = "id.messageText" return label }() @@ -34,7 +36,7 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -45,7 +47,6 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { contentLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: chat_content_margin), contentLabelLeft.bottomAnchor.constraint(equalTo: bubbleImageLeft.bottomAnchor, constant: -chat_content_margin), ]) - contentView.updateLayoutConstraint(firstItem: pinLabelLeft, seconedItem: bubbleImageLeft, attribute: .left, constant: 14 + funMargin) bubbleImageRight.addSubview(contentLabelRight) NSLayoutConstraint.activate([ @@ -54,7 +55,6 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { contentLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: chat_content_margin), contentLabelRight.bottomAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: -chat_content_margin), ]) - contentView.updateLayoutConstraint(firstItem: pinLabelRight, seconedItem: bubbleImageRight, attribute: .right, constant: -funMargin) } override open func showLeftOrRight(showRight: Bool) { @@ -70,6 +70,7 @@ open class FunChatMessageTextCell: FunChatMessageBaseCell { if let m = model as? MessageTextModel { contentLabel.attributedText = m.attributeStr + contentLabel.accessibilityValue = m.message?.text } bubbleW?.constant += funMargin } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift index c5b900dc..74dc0b7e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunChatMessageVideoCell.swift @@ -2,6 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECommonKit import NIMSDK import UIKit @@ -80,7 +81,7 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -127,24 +128,17 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { let timeLabel = isSend ? timeLabelRight : timeLabelLeft let stateView = isSend ? stateViewRight : stateViewLeft - if let videoObject = model.message?.messageObject as? NIMVideoObject { - if let path = videoObject.coverPath, FileManager.default.fileExists(atPath: path) { - contentImageView.sd_setImage( - with: URL(fileURLWithPath: path), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } else { - contentImageView.sd_setImage( - with: URL(string: videoObject.coverUrl ?? ""), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } + if let videoObject = model.message?.attachment as? V2NIMMessageVideoAttachment { + // 获取首帧 + let videoUrl = videoObject.url ?? "" + let thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + contentImageView.sd_setImage( + with: URL(string: thumbUrl), + placeholderImage: nil, + options: .retryFailed, + progress: nil, + completed: nil + ) if videoObject.duration > 0 { timeView.isHidden = false @@ -161,8 +155,8 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { stateView.state = .VideoPlay } else { stateView.state = .VideoDownload - stateView.setProgress(videoModel.progress) - if videoModel.progress >= 1 { + stateView.setProgress(Float(videoModel.progress / 100)) + if videoModel.progress >= 100 { videoModel.state = .Success } } @@ -170,8 +164,8 @@ open class FunChatMessageVideoCell: FunChatMessageImageCell { } } - override open func uploadProgress(byRight: Bool, _ progress: Float) { + override open func uploadProgress(byRight: Bool, _ progress: UInt) { let stateView = byRight ? stateViewRight : stateViewLeft - stateView.setProgress(progress) + stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserSettingSelectCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserSettingSelectCell.swift index c0cb12da..84f773d9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserSettingSelectCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserSettingSelectCell.swift @@ -27,8 +27,8 @@ open class FunUserSettingSelectCell: NEBaseUserSettingSelectCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), ]) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserTableViewCell.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserTableViewCell.swift index 2e4876c7..a8eb189d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserTableViewCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Cell/FunUserTableViewCell.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -10,17 +10,17 @@ open class FunUserTableViewCell: UserBaseTableViewCell { override open func baseCommonUI() { super.baseCommonUI() // avatar - avatarImage.layer.cornerRadius = 4 + avatarImageView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), - avatarImage.widthAnchor.constraint(equalToConstant: 40), - avatarImage.heightAnchor.constraint(equalToConstant: 40), - avatarImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), ]) titleLabel.textColor = .ne_darkText NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 11), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 11), titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -29), titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) @@ -30,7 +30,7 @@ open class FunUserTableViewCell: UserBaseTableViewCell { line.backgroundColor = .funChatLineBorderColor contentView.addSubview(line) NSLayoutConstraint.activate([ - line.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), + line.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), line.rightAnchor.constraint(equalTo: contentView.rightAnchor), line.heightAnchor.constraint(equalToConstant: 0.6), line.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift index a95e4c6c..050ff5a3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunChatViewController.swift @@ -8,29 +8,31 @@ import NIMSDK import UIKit @objcMembers -open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, NIMUserManagerDelegate, FunChatRecordViewDelegate { +open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, FunChatRecordViewDelegate { public weak var recordView: FunRecordAudioView? - override public init(session: NIMSession) { - super.init(session: session) + override public init(conversationId: String) { + super.init(conversationId: conversationId) cellRegisterDic = ChatMessageHelper.getChatCellRegisterDic(isFun: true) normalInputHeight = 90 brokenNetworkViewHeight = 48 navigationView.titleBarBottomLine.backgroundColor = .funChatNavigationBottomLineColor + + topMessageView.topImageView.image = UIImage.ne_imageNamed(name: "top_message_image") } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funChatBackgroundColor // 换肤颜色提取 view.bringSubviewToFront(chatInputView) - brokenNetworkView.errorIcon.isHidden = false + brokenNetworkView.errorIconView.isHidden = false brokenNetworkView.backgroundColor = .funChatNetworkBrokenBackgroundColor - brokenNetworkView.content.textColor = .funChatNetworkBrokenTitleColor + brokenNetworkView.contentLabel.textColor = .funChatNetworkBrokenTitleColor getFunInputView()?.funDelegate = self } @@ -42,21 +44,25 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, return input } + /// 获取转发确认弹窗 - 通用版 override open func getForwardAlertController() -> NEBaseForwardAlertViewController { 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) + FunSelectUserViewController(sessionId: viewModel.sessionId, showSelf: false) } - override func getTextViewController(title: String?, body: String?) -> TextViewController { + /// 获取文本详情页视图控制器 - 通用版 + override func getTextViewController(title: String?, body: NSAttributedString?) -> TextViewController { let textViewController = super.getTextViewController(title: title, body: body) textViewController.view.backgroundColor = .funChatBackgroundColor return textViewController @@ -87,7 +93,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } open func didHideReplyMode() { - viewmodel.isReplying = false + viewModel.isReplying = false if currentKeyboardHeight > 0 { normalOffset = 30 @@ -98,12 +104,20 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } open func didShowReplyMode() { - viewmodel.isReplying = true + viewModel.isReplying = true chatInputView.textView.becomeFirstResponder() } override open func expandMoreAction() { - var items = NEChatUIKitClient.instance.getMoreActionData(sessionType: viewmodel.session.sessionType) + var items = NEChatUIKitClient.instance.getMoreActionData(sessionType: V2NIMConversationIdUtil.conversationType(viewModel.conversationId)) + if NEChatKitClient.instance.delegate == nil { + items = items.filter { item in + if item.type == .location { + return false + } + return true + } + } let photo = NEMoreItemModel() photo.image = UIImage.ne_imageNamed(name: "fun_chat_photo") photo.title = chatLocalizable("chat_photo") @@ -115,37 +129,29 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } func openPhoto() { - NELog.infoLog(className(), desc: "open photo") + NEALog.infoLog(className(), desc: "open photo") willSelectItem(button: chatInputView.currentButton, index: showPhotoTag) } override open func showRtcCallAction() { - var param = [String: AnyObject]() - param["remoteUserAccid"] = viewmodel.session.sessionId as AnyObject - param["currentUserAccid"] = NIMSDK.shared().loginManager.currentAccount() as AnyObject - param["remoteShowName"] = titleContent as AnyObject - if let user = viewmodel.repo.getUserInfo(userId: viewmodel.session.sessionId), let avatar = user.userInfo?.avatarUrl { - param["remoteAvatar"] = avatar as AnyObject + let videoCallAction = NECustomAlertAction(title: chatLocalizable("video_call")) { [weak self] in + self?.useToCallViewRouter(2) } - let videoCallAction = NECustomAlertAction(title: chatLocalizable("video_call")) { - param["type"] = NSNumber(integerLiteral: 2) as AnyObject - Router.shared.use(CallViewRouter, parameters: param) - } - let audioCallAction = NECustomAlertAction(title: chatLocalizable("audio_call")) { - param["type"] = NSNumber(integerLiteral: 1) as AnyObject - Router.shared.use(CallViewRouter, parameters: param) + let audioCallAction = NECustomAlertAction(title: chatLocalizable("audio_call")) { [weak self] in + self?.useToCallViewRouter(1) } + showCustomActionSheet([videoCallAction, audioCallAction]) } override func getUserSettingViewController() -> NEBaseUserSettingViewController { - FunUserSettingViewController(userId: viewmodel.session.sessionId) + FunUserSettingViewController(userId: viewModel.sessionId) } override open func keyBoardWillShow(_ notification: Notification) { if chatInputView.chatInpuMode == .normal || chatInputView.chatInpuMode == .multipleSend { - if viewmodel.isReplying { + if viewModel.isReplying { normalOffset = -10 } else { normalOffset = 30 @@ -160,7 +166,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, override open func keyBoardWillHide(_ notification: Notification) { if chatInputView.chatInpuMode == .normal || chatInputView.chatInpuMode == .multipleSend { - if viewmodel.isReplying { + if viewModel.isReplying { normalOffset = -30 } else { normalOffset = 0 @@ -274,41 +280,41 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } override open func closeReply(button: UIButton?) { - viewmodel.isReplying = false + viewModel.isReplying = false getFunInputView()?.hideReplyMode() getFunInputView()?.replyLabel.attributedText = nil } override open func showReplyMessageView(isReEdit: Bool = false) { - viewmodel.isReplying = true + viewModel.isReplying = true guard let replyView = getFunInputView() else { return } replyView.showReplyMode() - if let message = viewmodel.operationModel?.message { + if let message = viewModel.operationModel?.message { if isReEdit { - replyView.replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: viewmodel.operationModel?.replyText ?? "", + replyView.replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: viewModel.operationModel?.replyText ?? "", font: .systemFont(ofSize: 13), color: .ne_greyText) - if let replyMessage = viewmodel.getReplyMessageWithoutThread(message: message) as? MessageContentModel { - viewmodel.operationModel = replyMessage + viewModel.getReplyMessageWithoutThread(message: message) { model in + if let replyMessage = model as? MessageContentModel { + self.viewModel.operationModel = replyMessage + } } } else { var text = chatLocalizable("msg_reply") - if let uid = message.from { - var showName = ChatUserCache.getShowName(userId: uid, teamId: viewmodel.session.sessionId, false) - if viewmodel.session.sessionType != .P2P, - !IMKitClient.instance.isMySelf(uid) { + if let uid = message.senderId { + var showName = ChatTeamCache.shared.getShowName(uid, false) + if V2NIMConversationIdUtil.conversationType(viewModel.conversationId) != .CONVERSATION_TYPE_P2P, + !IMKitClient.instance.isMe(uid) { addToAtUsers(addText: "@" + showName + "", isReply: true, accid: uid) } - let user = viewmodel.getUserInfo(userId: uid) - if let alias = user?.alias, !alias.isEmpty { - showName = alias - } + + showName = ChatTeamCache.shared.getShowName(uid) text += " " + showName + text += ": \(ChatMessageHelper.contentOfMessage(message))" + getFunInputView()?.replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, + font: .systemFont(ofSize: 13), + color: .ne_greyText) } - text += ": \(ChatMessageHelper.contentOfMessage(message))" - getFunInputView()?.replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, - font: .systemFont(ofSize: 13), - color: .ne_greyText) } if chatInputView.textView.isFirstResponder { normalOffset = -10 @@ -319,19 +325,15 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } } - override open func getReadView(_ message: NIMMessage) -> NEBaseReadViewController { - FunReadViewController(message: message) + override open func getReadView(_ message: V2NIMMessage, _ teamId: String) -> NEBaseReadViewController { + FunReadViewController(message: message, teamId: teamId) } override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.messages[indexPath.row] + guard indexPath.row < viewModel.messages.count else { return 0 } + let model = viewModel.messages[indexPath.row] if let contentModel = model as? MessageContentModel { - if let tipModel = model as? MessageTipsModel { - tipModel.commonInit() - return tipModel.cellHeight() + chat_content_margin - } - if contentModel.type == .revoke { if let time = contentModel.timeContent, !time.isEmpty { return 28 + chat_timeCellH @@ -344,15 +346,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, } open func getMessageModel(model: MessageModel) { - if model.type == .tip || - model.type == .notification || - model.type == .time { - if let tipModel = model as? MessageTipsModel { - tipModel.contentSize = String.getTextRectSize(tipModel.text ?? "", - font: .systemFont(ofSize: 14), - size: CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) - tipModel.height = max(tipModel.contentSize.height + chat_content_margin, 28) - } + if model.type == .tip || model.type == .notification { return } @@ -384,52 +378,6 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, // 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 ") @@ -461,7 +409,7 @@ open class FunChatViewController: ChatViewController, FunChatInputViewDelegate, normalInputHeight = 130 } - if viewmodel.isReplying { + if viewModel.isReplying { normalOffset = -30 } else { normalOffset = 0 diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunCollectionMessageController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunCollectionMessageController.swift new file mode 100644 index 00000000..d964ca0d --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunCollectionMessageController.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 FunCollectionMessageController: NEBaseCollectionMessageController { + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + collection_content_maxW = (kScreenWidth - 32) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .funChatBackgroundColor + collectionEmptyView.setEmptyImage(name: "fun_user_empty") + } + + /// 获取娱乐版皮肤样式注册表 + override open func getRegisterDic() -> [String: NEBaseCollectionMessageCell.Type] { + ChatMessageHelper.getCollectionCellRegisterDic(isFun: true) + } + + override open func showActions(_ model: CollectionMessageModel) { + guard let message = model.message else { + return + } + var actions = [NECustomAlertAction]() + weak var weakSelf = self + + let deleteCollectionAction = NECustomAlertAction(title: chatLocalizable("operation_delete_collection")) { + weakSelf?.removeCollectionActionClicked(model) + } + actions.append(deleteCollectionAction) + + if message.messageType == .MESSAGE_TYPE_TEXT { + let copyAction = NECustomAlertAction(title: chatLocalizable("operation_copy")) { + weakSelf?.copyCollectionActionClicked(model) + } + actions.append(copyAction) + } + + if message.messageType != .MESSAGE_TYPE_AUDIO { + let forwardAction = NECustomAlertAction(title: chatLocalizable("operation_forward")) { + weakSelf?.forwardCollectionMessage(message, model.senderName ?? "") + } + actions.append(forwardAction) + } + + showCustomActionSheet(actions) + } + + override open func getCollectionForwardAlertController() -> NEBaseForwardAlertViewController { + FunForwardAlertViewController() + } + + override open func getMultiForwardViewController(_ messageAttachmentUrl: String?, + _ messageAttachmentFilePath: String, + _ messageAttachmentMD5: String?) -> MultiForwardViewController { + FunMultiForwardViewController(messageAttachmentUrl, messageAttachmentFilePath, messageAttachmentMD5) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift index 0d583f0d..a0b3f819 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunForwardAlertViewController.swift @@ -8,10 +8,10 @@ import NECommonUIKit import UIKit @objcMembers -open class FunForwardUserCell: NEBaseForwardUserCell { +open class FunForwardSessionCell: NEBaseForwardSessionCell { override func setupUI() { super.setupUI() - userHeader.layer.cornerRadius = 4 + sessionHeaderView.layer.cornerRadius = 4 } } @@ -19,20 +19,20 @@ open class FunForwardUserCell: NEBaseForwardUserCell { open class FunForwardAlertViewController: NEBaseForwardAlertViewController { override open func setupUI() { super.setupUI() - tip.font = .systemFont(ofSize: 16, weight: .semibold) - oneUserHead.layer.cornerRadius = 4.0 - sureBtn.setTitleColor(.funChatThemeColor, for: .normal) - userCollection.register( - FunForwardUserCell.self, - forCellWithReuseIdentifier: "\(FunForwardUserCell.self)" + tipLabel.font = .systemFont(ofSize: 16, weight: .semibold) + oneSessionHeadView.layer.cornerRadius = 4.0 + sureButton.setTitleColor(.funChatThemeColor, for: .normal) + sessionCollectionView.register( + FunForwardSessionCell.self, + forCellWithReuseIdentifier: "\(FunForwardSessionCell.self)" ) } override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { if let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: "\(FunForwardUserCell.self)", + withReuseIdentifier: "\(FunForwardSessionCell.self)", for: indexPath - ) as? FunForwardUserCell { + ) as? FunForwardSessionCell { return setCellModel(cell: cell, indexPath: indexPath) } return UICollectionViewCell() diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift index 390085f0..a78acf2d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunMultiForwardViewController.swift @@ -17,15 +17,15 @@ open class FunMultiForwardViewController: MultiForwardViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funChatBackgroundColor // 换肤颜色提取 - brokenNetworkView.errorIcon.isHidden = false + brokenNetworkView.errorIconView.isHidden = false brokenNetworkView.backgroundColor = .funChatNetworkBrokenBackgroundColor - brokenNetworkView.content.textColor = .funChatNetworkBrokenTitleColor + brokenNetworkView.contentLabel.textColor = .funChatNetworkBrokenTitleColor navigationView.backgroundColor = .funChatBackgroundColor navigationView.titleBarBottomLine.backgroundColor = .funChatNavigationBottomLineColor } @@ -37,17 +37,13 @@ open class FunMultiForwardViewController: MultiForwardViewController { } override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - viewmodel.messages[indexPath.row].cellHeight() + viewModel.messages[indexPath.row].cellHeight() } open func getMessageModel(model: MessageModel) { - if model.type == .tip || - model.type == .notification || - model.type == .time { - if let tipModel = model as? MessageTipsModel { - tipModel.contentSize = String.getTextRectSize(tipModel.text ?? "", - font: .systemFont(ofSize: 14), - size: CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) + if model.type == .tip || model.type == .notification { + if let tipModel = model as? MessageTipsModel, let text = tipModel.text { + tipModel.contentSize = String.getRealSize(text, .systemFont(ofSize: 14), CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) tipModel.height = max(tipModel.contentSize.height + chat_content_margin, 28) } return diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift index 733572a6..43a7122d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunP2PChatViewController.swift @@ -8,53 +8,88 @@ import UIKit @objcMembers open class FunP2PChatViewController: FunChatViewController { - public init(session: NIMSession, anchor: NIMMessage?) { - super.init(session: session) - viewmodel = ChatViewModel(session: session, anchor: anchor) + /// 重写父类的构造方法 + /// - Parameter conversationId: 会话id + override public init(conversationId: String) { + super.init(conversationId: conversationId) + viewModel = P2PChatViewModel(conversationId: conversationId, anchor: nil) } - override open func viewDidLoad() { - super.viewDidLoad() + /// 重写父类的构造方法 + /// - Parameter conversationId: 会话id + /// - Parameter anchor: 锚点消息 + public init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId) + viewModel = P2PChatViewModel(conversationId: conversationId, anchor: anchor) + } - // Do any additional setup after loading the view. + public required init?(coder: NSCoder) { + super.init(coder: coder) } - override open func getSessionInfo(session: NIMSession) { - var showName = session.sessionId - ChatUserCache.getUserInfo(session.sessionId) { [weak self] user, error in - if let name = user?.showName() { - showName = name + override open var title: String? { + didSet { + super.title = title + if title != nil { + let text = chatLocalizable("fun_chat_input_placeholder") + let attribute = NSMutableAttributedString(string: text) + let style = NSMutableParagraphStyle() + style.lineBreakMode = .byTruncatingTail + style.alignment = .left + attribute.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: NSMakeRange(0, text.utf16.count)) + attribute.addAttribute(.foregroundColor, value: UIColor.funChatInputViewPlaceholderTextColor, range: NSMakeRange(0, text.utf16.count)) + attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.utf16.count)) + chatInputView.textView.attributedPlaceholder = attribute + chatInputView.textView.setNeedsLayout() } - - self?.title = showName - self?.titleContent = showName - let text = chatLocalizable("fun_chat_input_placeholder") - let attribute = NSMutableAttributedString(string: text) - let style = NSMutableParagraphStyle() - style.lineBreakMode = .byTruncatingTail - style.alignment = .left - attribute.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: NSMakeRange(0, text.utf16.count)) - attribute.addAttribute(.foregroundColor, value: UIColor.funChatInputViewPlaceholderTextColor, range: NSMakeRange(0, text.utf16.count)) - attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.utf16.count)) - self?.chatInputView.textView.attributedPlaceholder = attribute - self?.chatInputView.textView.setNeedsLayout() } } - /// 创建个人聊天页构造方法 - /// - Parameter sessionId: 会话id - public init(sessionId: String) { - let session = NIMSession(sessionId, type: .P2P) - super.init(session: session) + override open func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { + super.getSessionInfo(sessionId: sessionId) { [weak self] in + self?.viewModel.loadShowName([sessionId]) { + let name = self?.viewModel.getShowName(sessionId) ?? sessionId + self?.title = name + self?.titleContent = name + } + completion() + } } - /// 重写父类的构造方法 - /// - Parameter session: sessionId - override public init(session: NIMSession) { - super.init(session: session) - } + /// 重写检查并发送正在输入状态 + /// - Parameter endEdit: 是否停止输入 + override open func checkAndSendTypingState(endEdit: Bool = false) { + guard let viewModel = viewModel as? P2PChatViewModel else { + return + } - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + if endEdit { + viewModel.sendInputTypingEndState() + return + } + + if chatInputView.chatInpuMode == .normal { + if let content = chatInputView.textView.text, content.count > 0 { + viewModel.sendInputTypingState() + } else { + viewModel.sendInputTypingEndState() + } + } else { + var title = "" + var content = "" + + if let titleText = chatInputView.titleField.text { + title = titleText + } + + if let contentText = chatInputView.textView.text { + content = contentText + } + if title.count <= 0, content.count <= 0 { + viewModel.sendInputTypingEndState() + } else { + viewModel.sendInputTypingState() + } + } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift index 1a942c49..f6ed6816 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunPinMessageViewController.swift @@ -7,13 +7,13 @@ import UIKit @objcMembers open class FunPinMessageViewController: NEBasePinMessageViewController { - override public init(session: NIMSession) { - super.init(session: session) + override public init(conversationId: String) { + super.init(conversationId: conversationId) pin_content_maxW = (kScreenWidth - 32) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -26,7 +26,7 @@ open class FunPinMessageViewController: NEBasePinMessageViewController { ChatMessageHelper.getPinCellRegisterDic(isFun: true) } - override open func showAction(item: PinMessageModel) { + override open func showAction(item: NEPinMessageModel) { var actions = [NECustomAlertAction]() weak var weakSelf = self @@ -35,14 +35,14 @@ open class FunPinMessageViewController: NEBasePinMessageViewController { } actions.append(cancelPinAction) - if item.message.messageType == .text { + if item.message.messageType == .MESSAGE_TYPE_TEXT { let copyAction = NECustomAlertAction(title: chatLocalizable("operation_copy")) { weakSelf?.copyActionClicked(item: item) } actions.append(copyAction) } - if item.message.messageType != .audio { + if item.message.messageType != .MESSAGE_TYPE_AUDIO { let forwardAction = NECustomAlertAction(title: chatLocalizable("operation_forward")) { weakSelf?.forwardActionClicked(item: item) } @@ -52,26 +52,11 @@ open class FunPinMessageViewController: NEBasePinMessageViewController { showCustomActionSheet(actions) } + /// 获取转发确认弹窗 - 通用版 override open func getForwardAlertController() -> NEBaseForwardAlertViewController { FunForwardAlertViewController() } - override open func forwardMessage(_ message: NIMMessage) { - if IMKitClient.instance.getConfigCenter().teamEnable { - let userAction = NECustomAlertAction(title: chatLocalizable("contact_user")) { [weak self] in - self?.forwardMessageToUser(message) - } - - let teamAction = NECustomAlertAction(title: chatLocalizable("team")) { [weak self] in - self?.forwardMessageToTeam(message) - } - - showCustomActionSheet([teamAction, userAction]) - } else { - forwardMessageToUser(message) - } - } - override open func getMultiForwardViewController(_ messageAttachmentUrl: String?, _ messageAttachmentFilePath: String, _ messageAttachmentMD5: String?) -> MultiForwardViewController { diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift index ccd2b5c8..8d5356d3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunReadViewController.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -15,7 +15,7 @@ open class FunReadViewController: NEBaseReadViewController { navigationView.backgroundColor = .white readButton.setTitleColor(UIColor.funChatThemeColor, for: .normal) - line.backgroundColor = UIColor.funChatThemeColor + bottonBottomLine.backgroundColor = UIColor.funChatThemeColor readTableView.register( FunUserTableViewCell.self, @@ -23,15 +23,25 @@ open class FunReadViewController: NEBaseReadViewController { ) readTableView.rowHeight = 64 + unreadTableView.register( + FunUserTableViewCell.self, + forCellReuseIdentifier: "\(UserBaseTableViewCell.self)" + ) + unreadTableView.rowHeight = 64 + emptyView.setEmptyImage(name: "fun_emptyView") } + /// 重写已读按钮点击事件 + /// - Parameter button: 按钮 override open func readButtonEvent(button: UIButton) { super.readButtonEvent(button: button) readButton.setTitleColor(UIColor.funChatThemeColor, for: .normal) unreadButton.setTitleColor(UIColor.ne_darkText, for: .normal) } + /// 重写未读按钮点击事件 + /// - Parameter button: 按钮 override open func unreadButtonEvent(button: UIButton) { super.unreadButtonEvent(button: button) readButton.setTitleColor(UIColor.ne_darkText, for: .normal) diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift index d7816365..d819c0fb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunSelectUserViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -15,7 +15,7 @@ open class FunSelectUserViewController: NEBaseSelectUserViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func commonUI() { diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunGroupChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunTeamChatViewController.swift similarity index 51% rename from NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunGroupChatViewController.swift rename to NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunTeamChatViewController.swift index 33b4726b..1cae4e05 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunGroupChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunTeamChatViewController.swift @@ -7,32 +7,33 @@ import NIMSDK import UIKit @objcMembers -open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelDelegate { +open class FunTeamChatViewController: FunChatViewController, TeamChatViewModelDelegate { private var isLeaveTeamByOther = false // 是否被移出群聊 private var isLeaveTeamBySelf = false // 是否多端登录另一端退出群聊 private var isdismissTeam = false // 群聊是否已解散 private var isdismissDiscuss = false // 讨论组是否已解散 private var onCurrentPage = false // 是否位于聊天详情页 - public init(session: NIMSession, anchor: NIMMessage?) { - super.init(session: session) - viewmodel = TeamChatViewModel(session: session, anchor: anchor) - viewmodel.delegate = self + public init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId) + viewModel = TeamChatViewModel(conversationId: conversationId, anchor: anchor) + viewModel.delegate = self } /// 创建群的构造方法 /// - Parameter sessionId: 会话id public init(sessionId: String) { - let session = NIMSession(sessionId, type: .team) - super.init(session: session) + let conversationId = V2NIMConversationIdUtil.teamConversationId(sessionId) ?? "" + super.init(conversationId: conversationId) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } deinit { NotificationCenter.default.removeObserver(self) + ChatTeamCache.shared.removeAllTeamInfo() } override open func viewWillAppear(_ animated: Bool) { @@ -48,16 +49,12 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD weak var weakSelf = self // 被移除群聊 if isLeaveTeamByOther { - showSingleAlert(message: chatLocalizable("team_has_been_quit")) { - weakSelf?.navigationController?.popViewController(animated: true) - } + showLeaveTeamAlert() } // 解散群聊 if isdismissTeam { - showSingleAlert(message: chatLocalizable("team_has_been_removed")) { - weakSelf?.navigationController?.popViewController(animated: true) - } + showDismissTeamAlert() } } @@ -71,11 +68,24 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD NotificationCenter.default.addObserver(self, selector: #selector(popGroupChatVC), name: NENotificationName.popGroupChatVC, object: nil) } - override open func getSessionInfo(session: NIMSession) { - if let vm = viewmodel as? TeamChatViewModel { - if let t = vm.getTeam(teamId: session.sessionId) { - updateTeamInfo(team: t) + override open func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { + chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("fun_chat_input_placeholder")) + super.getSessionInfo(sessionId: sessionId) { [weak self] in + + if let vm = self?.viewModel as? TeamChatViewModel { + vm.getTeamInfo(teamId: sessionId) { error, team in + if let team = team { + if IMKitConfigCenter.shared.dismissTeamDeleteConversation == true, team.isValidTeam == false { + self?.showSingleAlert(message: coreLoader.localizable("team_not_exist")) { + NotificationCenter.default.post(name: NENotificationName.deleteConversationNotificationName, object: V2NIMConversationIdUtil.teamConversationId(team.teamId)) + self?.popGroupChatVC() + } + } + self?.updateTeamInfo(team: team) + } + } } + completion() } } @@ -110,9 +120,30 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD return attribute } - open func updateTeamInfo(team: NIMTeam) { - title = team.getShowName() - if team.inAllMuteMode(), viewmodel.teamMember?.type != .manager, viewmodel.teamMember?.type != .owner { + open func updateTeamTitle(_ noti: Notification) { + if let tid = noti.userInfo?["teamId"] as? String, + tid == viewModel.sessionId, + let team = ChatTeamCache.shared.getTeamInfo() { + updateTeamInfo(team: team) + } + } + + /// 更新群聊信息(群聊名称、群禁言状态、缓存) + /// - Parameter team: 群聊信息 + open func updateTeamInfo(team: V2NIMTeam) { + title = team.name + ChatTeamCache.shared.updateTeamInfo(team) + setMute(team: team) + } + + /// 设置群禁言/取消群禁言状态 + /// - Parameter team: 群聊信息 + open func setMute(team: V2NIMTeam) { + guard let viewModel = viewModel as? TeamChatViewModel else { + return + } + + if team.chatBannedMode == .TEAM_CHAT_BANNED_MODE_BANNED_ALL || (team.chatBannedMode == .TEAM_CHAT_BANNED_MODE_BANNED_NORMAL && viewModel.teamMember?.memberRole == .TEAM_MEMBER_ROLE_NORMAL) { // 群禁言 isMute = true chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("team_mute")) @@ -138,28 +169,39 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD } } - override open func onRecvMessages(_ messages: [NIMMessage]) { - super.onRecvMessages(messages) + override open func onRecvMessages(_ messages: [V2NIMMessage], _ index: [IndexPath]) { + super.onRecvMessages(messages, index) for message in messages { - if let object = message.messageObject as? NIMNotificationObject, - let content = object.content as? NIMTeamNotificationContent { - if content.operationType == .leave, - IMKitClient.instance.isMySelf(content.sourceID) { + if let content = message.attachment as? V2NIMMessageNotificationAttachment { + if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_UPDATE_TINFO, + let updatedTeamInfo = content.updatedTeamInfo { + if let name = updatedTeamInfo.name { + title = name + onTeamMemberUpdate([]) + } + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE, + let targetIDs = content.targetIds, + targetIDs.contains(IMKitClient.instance.account()) { + // 被重新拉进群聊 + isLeaveTeamByOther = false + if onCurrentPage { + dismissAlert() + } + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE, + message.senderId == IMKitClient.instance.account() { isLeaveTeamBySelf = true if onCurrentPage { popGroupChatVC() } - } else if content.operationType == .kick, - let targetIDs = content.targetIDs, - targetIDs.contains(IMKitClient.instance.imAccid()) { + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_KICK, + let targetIDs = content.targetIds, + targetIDs.contains(IMKitClient.instance.account()) { // 被移出群聊 isLeaveTeamByOther = true if onCurrentPage { - showSingleAlert(message: chatLocalizable("team_has_been_quit")) { [weak self] in - self?.navigationController?.popViewController(animated: true) - } + showLeaveTeamAlert() } - } else if content.operationType == .dismiss { + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_DISMISS { if isdismissDiscuss { return } @@ -167,44 +209,26 @@ open class FunGroupChatViewController: FunChatViewController, TeamChatViewModelD // 解散群聊 isdismissTeam = true if onCurrentPage { - showSingleAlert(message: chatLocalizable("team_has_been_removed")) { [weak self] in - self?.navigationController?.popViewController(animated: true) - } + showDismissTeamAlert() } } } } } - // MARK: TeamChatViewModelDelegate + // MARK: - TeamChatViewModelDelegate - open func onTeamRemoved(team: NIMTeam) { - // 多端登录另一端解散、退出讨论组 - if team.isDisscuss() == true { - isdismissDiscuss = true - if onCurrentPage { - popGroupChatVC() - } - return - } - } - - open func onTeamUpdate(team: NIMTeam) { - if team.teamId != viewmodel.session.sessionId { - return - } + /// 群聊更新回调 + /// - Parameter team: 群聊 + public func onTeamUpdate(team: V2NIMTeam) { updateTeamInfo(team: team) } - open func onTeamMemberUpdate(team: NIMTeam) { - didRefreshTable() - } - - override public func onTeamMemberChange(team: NIMTeam) { - if viewmodel.session.sessionId != team.teamId { - return + /// 群成员更新回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberUpdate(_ teamMembers: [V2NIMTeamMember]) { + if let team = ChatTeamCache.shared.getTeamInfo() { + setMute(team: team) } - (viewmodel as? TeamChatViewModel)?.getTeamMember() - updateTeamInfo(team: team) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift index e53c3c03..36ef1163 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/Controller/FunUserSettingViewController.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 @@ -17,13 +18,13 @@ open class FunUserSettingViewController: NEBaseUserSettingViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funChatBackgroundColor - viewmodel.cellDatas.forEach { cellModel in + for cellModel in viewModel.cellDatas { cellModel.cornerType = .none } } @@ -33,87 +34,87 @@ open class FunUserSettingViewController: NEBaseUserSettingViewController { navigationController?.navigationBar.backgroundColor = .white navigationView.backgroundColor = .white navigationView.titleBarBottomLine.isHidden = false - userHeader.layer.cornerRadius = 4.0 - addBtn.setImage(coreLoader.loadImage("fun_setting_add"), for: .normal) + userHeaderView.layer.cornerRadius = 4.0 + addButton.setImage(coreLoader.loadImage("fun_setting_add"), for: .normal) contentTable.rowHeight = 56 } override open func headerView() -> UIView { - let header = UIView(frame: CGRect(x: 0, y: 0, width: view.width, height: 117)) - header.backgroundColor = .clear - let cornerBack = UIView() - cornerBack.backgroundColor = .white - cornerBack.translatesAutoresizingMaskIntoConstraints = false - header.addSubview(cornerBack) + let headerView = UIView(frame: CGRect(x: 0, y: 0, width: view.width, height: 117)) + headerView.backgroundColor = .clear + let cornerBackView = UIView() + cornerBackView.backgroundColor = .white + cornerBackView.translatesAutoresizingMaskIntoConstraints = false + headerView.addSubview(cornerBackView) NSLayoutConstraint.activate([ - cornerBack.bottomAnchor.constraint(equalTo: header.bottomAnchor, constant: -8), - cornerBack.leftAnchor.constraint(equalTo: header.leftAnchor), - cornerBack.rightAnchor.constraint(equalTo: header.rightAnchor), - cornerBack.heightAnchor.constraint(equalToConstant: 109.0), + cornerBackView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor, constant: -8), + cornerBackView.leftAnchor.constraint(equalTo: headerView.leftAnchor), + cornerBackView.rightAnchor.constraint(equalTo: headerView.rightAnchor), + cornerBackView.heightAnchor.constraint(equalToConstant: 109.0), ]) - cornerBack.addSubview(userHeader) + cornerBackView.addSubview(userHeaderView) let tap = UITapGestureRecognizer() - userHeader.addGestureRecognizer(tap) + userHeaderView.addGestureRecognizer(tap) tap.numberOfTapsRequired = 1 tap.numberOfTouchesRequired = 1 - if let url = viewmodel.userInfo?.userInfo?.avatarUrl, !url.isEmpty { - userHeader.sd_setImage(with: URL(string: url), completed: nil) - userHeader.setTitle("") - userHeader.backgroundColor = .clear - } else if let name = viewmodel.userInfo?.shortName(showAlias: false, count: 2) { - userHeader.sd_setImage(with: nil) - userHeader.setTitle(name) - userHeader.backgroundColor = UIColor.colorWithString(string: viewmodel.userInfo?.userId) + if let url = viewModel.userInfo?.user?.avatar, !url.isEmpty { + userHeaderView.sd_setImage(with: URL(string: url), completed: nil) + userHeaderView.setTitle("") + userHeaderView.backgroundColor = .clear + } else if let name = viewModel.userInfo?.shortName(count: 2) { + userHeaderView.sd_setImage(with: nil) + userHeaderView.setTitle(name) + userHeaderView.backgroundColor = UIColor.colorWithString(string: viewModel.userInfo?.user?.accountId) } - nameLabel.text = viewmodel.userInfo?.showName() - cornerBack.addSubview(nameLabel) + nameLabel.text = viewModel.userInfo?.showName() + cornerBackView.addSubview(nameLabel) - if IMKitClient.instance.getConfigCenter().teamEnable { + if IMKitConfigCenter.shared.teamEnable { NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: cornerBack.leftAnchor, constant: 22), - userHeader.topAnchor.constraint(equalTo: cornerBack.topAnchor, constant: 22), - userHeader.widthAnchor.constraint(equalToConstant: 50), - userHeader.heightAnchor.constraint(equalToConstant: 50), + userHeaderView.leftAnchor.constraint(equalTo: cornerBackView.leftAnchor, constant: 22), + userHeaderView.topAnchor.constraint(equalTo: cornerBackView.topAnchor, constant: 22), + userHeaderView.widthAnchor.constraint(equalToConstant: 50), + userHeaderView.heightAnchor.constraint(equalToConstant: 50), ]) nameLabel.font = NEConstant.defaultTextFont(12) nameLabel.textAlignment = .center NSLayoutConstraint.activate([ - nameLabel.topAnchor.constraint(equalTo: userHeader.bottomAnchor, constant: 3.0), - nameLabel.centerXAnchor.constraint(equalTo: userHeader.centerXAnchor), - nameLabel.widthAnchor.constraint(equalTo: userHeader.widthAnchor), + nameLabel.topAnchor.constraint(equalTo: userHeaderView.bottomAnchor, constant: 3.0), + nameLabel.centerXAnchor.constraint(equalTo: userHeaderView.centerXAnchor), + nameLabel.widthAnchor.constraint(equalTo: userHeaderView.widthAnchor), ]) - addBtn.addTarget(self, action: #selector(createDiscuss), for: .touchUpInside) - cornerBack.addSubview(addBtn) + addButton.addTarget(self, action: #selector(createDiscuss), for: .touchUpInside) + cornerBackView.addSubview(addButton) NSLayoutConstraint.activate([ - addBtn.leftAnchor.constraint(equalTo: userHeader.rightAnchor, constant: 20.0), - addBtn.topAnchor.constraint(equalTo: userHeader.topAnchor), - addBtn.widthAnchor.constraint(equalToConstant: 50.0), - addBtn.heightAnchor.constraint(equalToConstant: 50.0), + addButton.leftAnchor.constraint(equalTo: userHeaderView.rightAnchor, constant: 20.0), + addButton.topAnchor.constraint(equalTo: userHeaderView.topAnchor), + addButton.widthAnchor.constraint(equalToConstant: 50.0), + addButton.heightAnchor.constraint(equalToConstant: 50.0), ]) } else { NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: cornerBack.leftAnchor, constant: 16), - userHeader.centerYAnchor.constraint(equalTo: cornerBack.centerYAnchor), - userHeader.widthAnchor.constraint(equalToConstant: 60), - userHeader.heightAnchor.constraint(equalToConstant: 60), + userHeaderView.leftAnchor.constraint(equalTo: cornerBackView.leftAnchor, constant: 16), + userHeaderView.centerYAnchor.constraint(equalTo: cornerBackView.centerYAnchor), + userHeaderView.widthAnchor.constraint(equalToConstant: 60), + userHeaderView.heightAnchor.constraint(equalToConstant: 60), ]) nameLabel.font = NEConstant.defaultTextFont(16) nameLabel.textAlignment = .left NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: userHeader.rightAnchor, constant: 16.0), - nameLabel.rightAnchor.constraint(equalTo: cornerBack.rightAnchor), - nameLabel.centerYAnchor.constraint(equalTo: userHeader.centerYAnchor), + nameLabel.leftAnchor.constraint(equalTo: userHeaderView.rightAnchor, constant: 16.0), + nameLabel.rightAnchor.constraint(equalTo: cornerBackView.rightAnchor), + nameLabel.centerYAnchor.constraint(equalTo: userHeaderView.centerYAnchor), ]) } - return header + return headerView } override open func filterStackViewController() -> [UIViewController]? { @@ -126,7 +127,11 @@ open class FunUserSettingViewController: NEBaseUserSettingViewController { } } - override func getPinMessageViewController(session: NIMSession) -> NEBasePinMessageViewController { - FunPinMessageViewController(session: session) + override open func didLoadData() { + viewModel.setFunType() + } + + override func getPinMessageViewController(conversationId: String) -> NEBasePinMessageViewController { + FunPinMessageViewController(conversationId: conversationId) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift index bae3339b..032763e8 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatRouter.swift @@ -7,13 +7,15 @@ import NIMSDK public extension ChatRouter { static func registerFun() { + registerCommon() + // pin Router.shared.register(PushPinMessageVCRouter) { param in let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let pin = FunPinMessageViewController(session: session) + let pin = FunPinMessageViewController(conversationId: conversationId) nav?.pushViewController(pin, animated: true) } @@ -21,11 +23,11 @@ public extension ChatRouter { Router.shared.register(PushP2pChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let p2pChatVC = FunP2PChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let p2pChatVC = FunP2PChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { @@ -46,12 +48,12 @@ public extension ChatRouter { Router.shared.register(PushTeamChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let groupVC = FunGroupChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let groupVC = FunTeamChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { nav?.viewControllers[i] = groupVC diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatUIColor.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatUIColor.swift index 08f003f8..4e693d00 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatUIColor.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/FunChatUIColor.swift @@ -18,7 +18,7 @@ public extension UIColor { static let funRecordAudioProgressCancelColor = UIColor(hexString: "#E75D58") static let funRecordAudioLastTimeColor = UIColor(hexString: "#000000", 0.4) - static let funChatThemeColor = UIColor(hexString: "#58BE6B") + static let funChatThemeColor = UIColor.ne_funTheme static let funChatBackgroundColor = UIColor(hexString: "#EDEDED") static let funChatLineBorderColor = UIColor(hexString: "#E5E5E5") static let funChatNavigationBottomLineColor = UIColor(hexString: "#D5D5D5", 0.4) diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift index 38cc86cd..74a253de 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunChatInputView.swift @@ -18,14 +18,6 @@ public protocol FunChatInputViewDelegate: NSObjectProtocol { @objcMembers open class FunChatInputView: NEBaseChatInputView { - /* - // Only override draw() if you perform custom drawing. - // An empty implementation adversely affects performance during animation. - override func draw(_ rect: CGRect) { - // Drawing code - } - */ - var replyViewTopConstraint: NSLayoutConstraint? weak var funDelegate: FunChatInputViewDelegate? @@ -41,12 +33,12 @@ open class FunChatInputView: NEBaseChatInputView { // public var textViewHeight: NSLayoutConstraint? public var replyBackView: UIView = { - let back = UIView() - back.translatesAutoresizingMaskIntoConstraints = false - back.layer.cornerRadius = 8.0 - back.clipsToBounds = true - back.backgroundColor = UIColor.funChatInputReplyBg - return back + let backView = UIView() + backView.translatesAutoresizingMaskIntoConstraints = false + backView.layer.cornerRadius = 8.0 + backView.clipsToBounds = true + backView.backgroundColor = UIColor.funChatInputReplyBg + return backView }() public lazy var replyLabel: UILabel = { @@ -54,6 +46,7 @@ open class FunChatInputView: NEBaseChatInputView { label.translatesAutoresizingMaskIntoConstraints = false label.backgroundColor = UIColor.clear label.numberOfLines = 2 + label.accessibilityIdentifier = "id.messageReplyInput" return label }() @@ -62,6 +55,7 @@ open class FunChatInputView: NEBaseChatInputView { button.translatesAutoresizingMaskIntoConstraints = false button.backgroundColor = UIColor.clear button.setImage(coreLoader.loadImage("fun_chat_input_reply_clear"), for: .normal) + button.accessibilityIdentifier = "id.clear" return button }() @@ -71,6 +65,7 @@ open class FunChatInputView: NEBaseChatInputView { button.backgroundColor = UIColor.clear button.setImage(coreLoader.loadImage("fun_chat_input_change_record"), for: .normal) button.setImage(coreLoader.loadImage("fun_chat_input_keyboard"), for: .selected) + button.accessibilityIdentifier = "id.changeRecordMode" return button }() @@ -80,6 +75,7 @@ open class FunChatInputView: NEBaseChatInputView { button.backgroundColor = UIColor.clear button.tag = addMoreBtnTag button.setImage(coreLoader.loadImage("fun_chat_input_show_more"), for: .normal) + button.accessibilityIdentifier = "id.inputMore" return button }() @@ -89,6 +85,7 @@ open class FunChatInputView: NEBaseChatInputView { button.backgroundColor = UIColor.clear button.tag = addEmojBtnTag button.setImage(coreLoader.loadImage("fun_chat_input_show_emoj"), for: .normal) + button.accessibilityIdentifier = "id.inputEmoji" return button }() diff --git a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift index 8b298f69..623388c4 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/FunUI/View/FunRecordAudioView.swift @@ -37,10 +37,10 @@ open class FunRecordAudioView: UIView { }() lazy var lottieContentView: UIView = { - let content = UIView() - content.translatesAutoresizingMaskIntoConstraints = false - content.backgroundColor = UIColor.clear - return content + let contentView = UIView() + contentView.translatesAutoresizingMaskIntoConstraints = false + contentView.backgroundColor = UIColor.clear + return contentView }() public var triangleView: UIView = { @@ -70,13 +70,13 @@ open class FunRecordAudioView: UIView { }() public let recordCloseImage: UIImageView = { - let close = UIImageView() - close.contentMode = .center - close.translatesAutoresizingMaskIntoConstraints = false - close.image = coreLoader.loadImage("fun_chat_record_close_dark") - close.highlightedImage = coreLoader.loadImage("fun_chat_record_close_light") - close.isHighlighted = false - return close + let imageView = UIImageView() + imageView.contentMode = .center + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = coreLoader.loadImage("fun_chat_record_close_dark") + imageView.highlightedImage = coreLoader.loadImage("fun_chat_record_close_light") + imageView.isHighlighted = false + return imageView }() public let releaseToSendLabel: UILabel = { diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift index 7bf3d4be..5e686a62 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageAudioCell.swift @@ -22,7 +22,7 @@ open class ChatMessageAudioCell: NormalChatMessageBaseCell, ChatAudioCellProtoco } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -95,13 +95,11 @@ open class ChatMessageAudioCell: NormalChatMessageBaseCell, ChatAudioCellProtoco } open func startAnimation(byRight: Bool) { - if byRight { - if !audioImageViewRight.isAnimating { - audioImageViewRight.startAnimating() - } - } else if !audioImageViewLeft.isAnimating { - audioImageViewLeft.startAnimating() + let audioImageView = byRight ? audioImageViewRight : audioImageViewLeft + if !audioImageView.isAnimating { + audioImageView.startAnimating() } + if let m = contentModel as? MessageAudioModel { m.isPlaying = true isPlaying = true @@ -109,13 +107,11 @@ open class ChatMessageAudioCell: NormalChatMessageBaseCell, ChatAudioCellProtoco } open func stopAnimation(byRight: Bool) { - if byRight { - if audioImageViewRight.isAnimating { - audioImageViewRight.stopAnimating() - } - } else if audioImageViewLeft.isAnimating { - audioImageViewLeft.stopAnimating() + let audioImageView = byRight ? audioImageViewRight : audioImageViewLeft + if audioImageView.isAnimating { + audioImageView.stopAnimating() } + if let m = contentModel as? MessageAudioModel { m.isPlaying = false isPlaying = false @@ -140,7 +136,7 @@ open class ChatMessageAudioCell: NormalChatMessageBaseCell, ChatAudioCellProtoco timeLabelLeft.text = "\(m.duration)" + "s" } m.isPlaying ? startAnimation(byRight: isSend) : stopAnimation(byRight: isSend) - messageId = m.message?.messageId + messageId = m.message?.messageClientId } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift index b77bd2ad..a3e5de21 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageCallCell.swift @@ -27,7 +27,7 @@ open class ChatMessageCallCell: NormalChatMessageBaseCell { contentLabelLeft.isEnabled = false contentLabelLeft.numberOfLines = 0 contentLabelLeft.isUserInteractionEnabled = false - contentLabelLeft.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + contentLabelLeft.font = messageTextFont contentLabelLeft.textAlignment = .center contentLabelLeft.backgroundColor = .clear contentLabelLeft.accessibilityIdentifier = "id.chatMessageCallText" @@ -45,7 +45,7 @@ open class ChatMessageCallCell: NormalChatMessageBaseCell { contentLabelRight.isEnabled = false contentLabelRight.numberOfLines = 0 contentLabelRight.isUserInteractionEnabled = false - contentLabelRight.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + contentLabelRight.font = messageTextFont contentLabelRight.textAlignment = .center contentLabelRight.backgroundColor = .clear contentLabelRight.accessibilityIdentifier = "id.chatMessageCallText" diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift index 619af615..f196ed48 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageFileCell.swift @@ -12,19 +12,19 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { weak var weakModel: MessageFileModel? public lazy var imgViewLeft: UIImageView = { - let view_img = UIImageView() - view_img.translatesAutoresizingMaskIntoConstraints = false - view_img.backgroundColor = .clear - view_img.accessibilityIdentifier = "id.fileType" - return view_img + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = .clear + imageView.accessibilityIdentifier = "id.fileType" + return imageView }() public lazy var stateViewLeft: FileStateView = { - let state = FileStateView() - state.translatesAutoresizingMaskIntoConstraints = false - state.backgroundColor = .clear - state.accessibilityIdentifier = "id.fileStatus" - return state + let stateView = FileStateView() + stateView.translatesAutoresizingMaskIntoConstraints = false + stateView.backgroundColor = .clear + stateView.accessibilityIdentifier = "id.fileStatus" + return stateView }() public lazy var titleLabelLeft: UILabel = { @@ -71,11 +71,11 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { }() public lazy var imgViewRight: UIImageView = { - let view_img = UIImageView() - view_img.translatesAutoresizingMaskIntoConstraints = false - view_img.backgroundColor = .clear - view_img.accessibilityIdentifier = "id.fileType" - return view_img + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.backgroundColor = .clear + imageView.accessibilityIdentifier = "id.fileType" + return imageView }() public lazy var stateViewRight: FileStateView = { @@ -135,7 +135,7 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -228,56 +228,55 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { bubbleW?.constant = kScreenWidth <= 320 ? 222 : 242 // 适配小屏幕 - if let fileObject = model.message?.messageObject as? NIMFileObject { + if let fileObject = model.message?.attachment as? V2NIMMessageFileAttachment { if let fileModel = model as? MessageFileModel { weakModel?.cell = nil weakModel = fileModel fileModel.cell = self - fileModel.size = Float(fileObject.fileLength) + fileModel.size = Float(fileObject.size) if fileModel.state == .Success { stateView.state = .FileOpen } else { stateView.state = .FileDownload - stateView.setProgress(fileModel.progress) - if fileModel.progress >= 1 { + stateView.setProgress(Float(fileModel.progress / 100)) + if fileModel.progress >= 100 { fileModel.state = .Success } } } var imageName = "file_unknown" - var displayName = "未知文件" - if let filePath = fileObject.path as? NSString { - displayName = filePath.lastPathComponent - switch filePath.pathExtension.lowercased() { - case file_doc_support: - imageName = "file_doc" - case file_xls_support: - imageName = "file_xls" - case file_img_support: - imageName = "file_img" - case file_ppt_support: - imageName = "file_ppt" - case file_txt_support: - imageName = "file_txt" - case file_audio_support: - imageName = "file_audio" - case file_vedio_support: - imageName = "file_vedio" - case file_zip_support: - imageName = "file_zip" - case file_pdf_support: - imageName = "file_pdf" - case file_html_support: - imageName = "file_html" - case "key", "keynote": - imageName = "file_keynote" - default: - imageName = "file_unknown" - } + let suffix = (fileObject.name as NSString).pathExtension.lowercased() + switch suffix { + case file_doc_support: + imageName = "file_doc" + case file_xls_support: + imageName = "file_xls" + case file_img_support: + imageName = "file_img" + case file_ppt_support: + imageName = "file_ppt" + case file_txt_support: + imageName = "file_txt" + case file_audio_support: + imageName = "file_audio" + case file_vedio_support: + imageName = "file_vedio" + case file_zip_support: + imageName = "file_zip" + case file_pdf_support: + imageName = "file_pdf" + case file_html_support: + imageName = "file_html" + case "key", "keynote": + imageName = "file_keynote" + default: + imageName = "file_unknown" } + imgView.image = UIImage.ne_imageNamed(name: imageName) - titleLabel.text = fileObject.displayName ?? displayName - let size_B = Double(fileObject.fileLength) + titleLabel.text = fileObject.name + + let size_B = Double(fileObject.size) var size_str = String(format: "%.1f B", size_B) if size_B > 1e3 { let size_KB = size_B / 1e3 @@ -295,8 +294,8 @@ open class ChatMessageFileCell: NormalChatMessageBaseCell { } } - override open func uploadProgress(byRight: Bool, _ progress: Float) { + override open func uploadProgress(byRight: Bool, _ progress: UInt) { let stateView = byRight ? stateViewRight : stateViewLeft - stateView.setProgress(progress) + stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift index f1d21e87..070fb508 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageImageCell.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import NIMSDK +import SDWebImage import UIKit @objcMembers @@ -16,7 +17,7 @@ open class ChatMessageImageCell: NormalChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -79,18 +80,19 @@ open class ChatMessageImageCell: NormalChatMessageBaseCell { super.setModel(model, isSend) let contentImageView = isSend ? contentImageViewRight : contentImageViewLeft - if let m = model as? MessageImageModel, let imageUrl = m.imageUrl { + if let m = model as? MessageImageModel, let imageUrl = m.urlString { + var options: SDWebImageOptions = [.retryFailed] + if let imageObject = model.message?.attachment as? V2NIMMessageImageAttachment, imageObject.ext?.lowercased() != ".gif" { + options = [.retryFailed, .progressiveLoad] + } + + let context: [SDWebImageContextOption: Any] = [.imageThumbnailPixelSize: CGSize(width: 1000, height: 1000)] if imageUrl.hasPrefix("http") { - contentImageView.sd_setImage( - with: URL(string: imageUrl), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) + let url = URL(string: imageUrl) + contentImageView.sd_setImage(with: url, placeholderImage: nil, options: options, context: context) } else { let url = URL(fileURLWithPath: imageUrl) - contentImageView.sd_setImage(with: url) + contentImageView.sd_setImage(with: url, placeholderImage: nil, options: options, context: context) } } else { contentImageView.image = nil diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift index b7305f54..1bd20899 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageLocationCell.swift @@ -32,12 +32,20 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { label.text = chatLocalizable("no_map_plugin") label.textAlignment = .center label.textColor = UIColor.ne_greyText + label.isHidden = true return label }() public var mapViewLeft: UIView? let backgroundViewLeft = UIView() + public var mapImageViewLeft: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + return imageView + }() + // Right public lazy var titleLabelRight: UILabel = { let label = UILabel() @@ -64,19 +72,41 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { label.text = chatLocalizable("no_map_plugin") label.textAlignment = .center label.textColor = UIColor.ne_greyText + label.isHidden = true return label }() + lazy var pointImageLeft: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = coreLoader.loadImage("location_point") + return imageView + }() + + lazy var pointImageRight: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = coreLoader.loadImage("location_point") + return imageView + }() + public var mapViewRight: UIView? let backgroundViewRight = UIView() + public var mapImageViewRight: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + return imageView + }() + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonUI() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -126,33 +156,26 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { subTitleLabelLeft.topAnchor.constraint(equalTo: titleLabelLeft.bottomAnchor, constant: 4), ]) - if let map = NEChatKitClient.instance.delegate?.getCellMapView?() as? UIView { - mapViewLeft = map - backgroundViewLeft.addSubview(map) - map.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - map.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), - map.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor), - map.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), - map.topAnchor.constraint(equalTo: subTitleLabelLeft.bottomAnchor, constant: 4), - ]) - - let pointImage = UIImageView() - pointImage.translatesAutoresizingMaskIntoConstraints = false - pointImage.image = coreLoader.loadImage("location_point") - map.addSubview(pointImage) - NSLayoutConstraint.activate([ - pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), - pointImage.bottomAnchor.constraint(equalTo: map.bottomAnchor, constant: -30), - ]) - } else { - backgroundViewLeft.addSubview(emptyLabelLeft) - NSLayoutConstraint.activate([ - emptyLabelLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), - emptyLabelLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), - emptyLabelLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor, constant: -40), - ]) - } + backgroundViewLeft.addSubview(mapImageViewLeft) + NSLayoutConstraint.activate([ + mapImageViewLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), + mapImageViewLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor), + mapImageViewLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), + mapImageViewLeft.heightAnchor.constraint(equalToConstant: 86), + ]) + + mapImageViewLeft.addSubview(pointImageLeft) + NSLayoutConstraint.activate([ + pointImageLeft.centerXAnchor.constraint(equalTo: mapImageViewLeft.centerXAnchor), + pointImageLeft.bottomAnchor.constraint(equalTo: mapImageViewLeft.bottomAnchor, constant: -30), + ]) + + backgroundViewLeft.addSubview(emptyLabelLeft) + NSLayoutConstraint.activate([ + emptyLabelLeft.leftAnchor.constraint(equalTo: backgroundViewLeft.leftAnchor), + emptyLabelLeft.rightAnchor.constraint(equalTo: backgroundViewLeft.rightAnchor), + emptyLabelLeft.bottomAnchor.constraint(equalTo: backgroundViewLeft.bottomAnchor, constant: -40), + ]) } open func commonUIRight() { @@ -196,33 +219,26 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { subTitleLabelRight.topAnchor.constraint(equalTo: titleLabelRight.bottomAnchor, constant: 4), ]) - if let map = NEChatKitClient.instance.delegate?.getCellMapView?() as? UIView { - mapViewRight = map - backgroundViewRight.addSubview(map) - map.translatesAutoresizingMaskIntoConstraints = false - NSLayoutConstraint.activate([ - map.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), - map.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor), - map.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), - map.topAnchor.constraint(equalTo: subTitleLabelRight.bottomAnchor, constant: 4), - ]) - - let pointImage = UIImageView() - pointImage.translatesAutoresizingMaskIntoConstraints = false - pointImage.image = coreLoader.loadImage("location_point") - map.addSubview(pointImage) - NSLayoutConstraint.activate([ - pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), - pointImage.bottomAnchor.constraint(equalTo: map.bottomAnchor, constant: -30), - ]) - } else { - backgroundViewRight.addSubview(emptyLabelRight) - NSLayoutConstraint.activate([ - emptyLabelRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), - emptyLabelRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), - emptyLabelRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor, constant: -40), - ]) - } + backgroundViewRight.addSubview(mapImageViewRight) + NSLayoutConstraint.activate([ + mapImageViewRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), + mapImageViewRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor), + mapImageViewRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), + mapImageViewRight.heightAnchor.constraint(equalToConstant: 86), + ]) + + mapImageViewRight.addSubview(pointImageRight) + NSLayoutConstraint.activate([ + pointImageRight.centerXAnchor.constraint(equalTo: mapImageViewRight.centerXAnchor), + pointImageRight.bottomAnchor.constraint(equalTo: mapImageViewRight.bottomAnchor, constant: -30), + ]) + + backgroundViewRight.addSubview(emptyLabelRight) + NSLayoutConstraint.activate([ + emptyLabelRight.leftAnchor.constraint(equalTo: backgroundViewRight.leftAnchor), + emptyLabelRight.rightAnchor.constraint(equalTo: backgroundViewRight.rightAnchor), + emptyLabelRight.bottomAnchor.constraint(equalTo: backgroundViewRight.bottomAnchor, constant: -40), + ]) } override open func showLeftOrRight(showRight: Bool) { @@ -235,16 +251,25 @@ open class ChatMessageLocationCell: NormalChatMessageBaseCell { super.setModel(model, isSend) let titleLabel = isSend ? titleLabelRight : titleLabelLeft let subTitleLabel = isSend ? subTitleLabelRight : subTitleLabelLeft - let mapView = isSend ? mapViewRight : mapViewLeft let bubbleW = isSend ? bubbleWRight : bubbleWLeft - - bubbleW?.constant = kScreenWidth <= 320 ? 222 : 242 // 适配小屏幕 + let mapImageView = isSend ? mapImageViewRight : mapImageViewLeft + let emptyLabel = isSend ? emptyLabelRight : emptyLabelLeft + let pointImage = isSend ? pointImageRight : pointImageLeft if let m = model as? MessageLocationModel { titleLabel.text = m.title subTitleLabel.text = m.subTitle - if let lat = m.lat, let lng = m.lng, let map = mapView { - NEChatKitClient.instance.delegate?.setMapviewLocation?(lat: lat, lng: lng, mapview: map) + if let lat = m.lat, let lng = m.lng { + if let url = NEChatKitClient.instance.delegate?.getMapImageUrl?(lat: lat, lng: lng) { + NEALog.infoLog(className(), desc: #function + "location image url = \(url)") + mapImageView.sd_setImage(with: URL(string: url)) + emptyLabel.isHidden = true + pointImage.isHidden = false + } else { + mapImageView.image = UIImage.ne_imageNamed(name: "map_placeholder_image") + emptyLabel.isHidden = false + pointImage.isHidden = true + } } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift index 587fd1ff..5f6f720b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageMultiForwardCell.swift @@ -3,6 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECommonKit import NIMSDK import UIKit @@ -17,7 +18,7 @@ open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -171,7 +172,7 @@ open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { super.setModel(model, isSend) - guard let data = NECustomAttachment.dataOfCustomMessage(message: model.message) else { + guard let data = NECustomAttachment.dataOfCustomMessage(model.message?.attachment) else { return } @@ -210,13 +211,7 @@ open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { var contentText = "" if var senderNick = abstracts[i]["senderNick"] as? String { - if senderNick.count > 5 { - // 截取字符串 abcdefg -> ab...fg - let leftEndIndex = senderNick.index(senderNick.startIndex, offsetBy: 2) - let rightStartIndex = senderNick.index(senderNick.endIndex, offsetBy: -2) - senderNick = senderNick[senderNick.startIndex ..< leftEndIndex] + "..." + senderNick[rightStartIndex ..< senderNick.endIndex] - } - contentText = senderNick + contentText = NEFriendUserCache.getCutName(senderNick) if let content = abstracts[i]["content"] as? String { contentText += ":" + content } @@ -255,9 +250,9 @@ open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { // MARK: - lazy load public lazy var backViewLeft: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - return view + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView }() public lazy var titleLabelLeft1: UILabel = { @@ -318,9 +313,9 @@ open class ChatMessageMultiForwardCell: NormalChatMessageBaseCell { }() public lazy var backViewRight: UIImageView = { - let view = UIImageView() - view.translatesAutoresizingMaskIntoConstraints = false - return view + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView }() public lazy var titleLabelRight1: UILabel = { diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift index 7a38dfa1..f42d32f2 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageReplyCell.swift @@ -31,7 +31,7 @@ open class ChatMessageReplyCell: ChatMessageTextCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func commonUI() { @@ -84,14 +84,21 @@ open class ChatMessageReplyCell: ChatMessageTextCell { override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { let replyLabel = isSend ? replyLabelRight : replyLabelLeft - if let text = model.replyText, + if var text = model.replyText, let font = replyLabel.font { - replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: "| " + text, + // 如果有回复的消息,需要在回复的消息前加上“| ” + if text != chatLocalizable("message_not_found") { + text = "| " + text + } + + replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, font: font, color: replyLabel.textColor) - if let attriText = replyLabel.attributedText { - let textSize = attriText.finalSize(font, CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) - model.contentSize.width = max(textSize.width + chat_content_margin * 2, model.contentSize.width) + replyLabel.accessibilityValue = text + + if let attriText = replyLabel.attributedText, let model = model as? MessageTextModel { + let textSize = NSAttributedString.getRealSize(attriText, font, CGSize(width: chat_text_maxW, height: CGFloat.greatestFiniteMagnitude)) + model.contentSize.width = max(textSize.width, model.textWidght) + chat_content_margin * 2 } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift index 0ed2ed6d..8102d941 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRevokeCell.swift @@ -25,7 +25,7 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -64,15 +64,15 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { 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) + reeditButton.setTitleColor(UIColor.ne_normalTheme, for: .normal) reeditButton.titleEdgeInsets = UIEdgeInsets(top: 0, left: -30, bottom: 0, right: 0) reeditButton.imageEdgeInsets = UIEdgeInsets(top: 0, left: 70, bottom: 0, right: 0) bubbleImageRight.addSubview(reeditButton) reeditButtonW = reeditButton.widthAnchor.constraint(equalToConstant: 86) + reeditButtonW?.isActive = true NSLayoutConstraint.activate([ reeditButton.leftAnchor.constraint(equalTo: revokeLabelRight.rightAnchor, constant: 8), - reeditButtonW!, reeditButton.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: 0), reeditButton.bottomAnchor.constraint(equalTo: bubbleImageRight.bottomAnchor, constant: 0), ]) @@ -87,7 +87,7 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { activityView.isHidden = true readView.isHidden = true - seletedBtn.isHidden = true + selectedButton.isHidden = true pinLabelLeft.isHidden = true pinImageLeft.isHidden = true pinLabelRight.isHidden = true @@ -95,15 +95,17 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { } override open func setModel(_ model: MessageContentModel, _ isSend: Bool) { - if let time = model.message?.timestamp { + let isSend = IMKitClient.instance.isMe(model.message?.senderId) + if let time = model.message?.createTime { let date = Date() let currentTime = date.timeIntervalSince1970 if currentTime - time >= 60 * 2 { model.timeOut = true } } + if isSend, - model.isRevokedText == true, + model.isReedit == true, model.timeOut == false { reeditButtonW?.constant = 86 reeditButton.isHidden = false @@ -121,7 +123,6 @@ open class ChatMessageRevokeCell: NormalChatMessageBaseCell { } func reeditEvent(button: UIButton) { - print(#function) delegate?.didTapReeditButton(self, contentModel) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift index b6c4956f..2c9b514f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageRichTextCell.swift @@ -43,60 +43,60 @@ open class ChatMessageRichTextCell: ChatMessageReplyCell { override open func commonUI() { /// left bubbleImageLeft.addSubview(replyLabelLeft) - replyLabelLeftHeightAnchor = replyLabelLeft.heightAnchor.constraint(equalToConstant: 16.0) + replyLabelLeftHeightAnchor = replyLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + replyLabelLeftHeightAnchor?.isActive = true NSLayoutConstraint.activate([ replyLabelLeft.leadingAnchor.constraint(equalTo: bubbleImageLeft.leadingAnchor, constant: chat_content_margin), replyLabelLeft.topAnchor.constraint(equalTo: bubbleImageLeft.topAnchor, constant: chat_content_margin), - replyLabelLeftHeightAnchor!, replyLabelLeft.trailingAnchor.constraint(equalTo: bubbleImageLeft.trailingAnchor, constant: -chat_content_margin), ]) bubbleImageLeft.addSubview(titleLabelLeft) titleLabelLeftTopAnchor = titleLabelLeft.topAnchor.constraint(equalTo: replyLabelLeft.bottomAnchor, constant: chat_content_margin) + titleLabelLeftTopAnchor?.isActive = true titleLabelLeftHeightAnchor = titleLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + titleLabelLeftHeightAnchor?.isActive = true NSLayoutConstraint.activate([ titleLabelLeft.rightAnchor.constraint(equalTo: bubbleImageLeft.rightAnchor, constant: -chat_content_margin), titleLabelLeft.leftAnchor.constraint(equalTo: bubbleImageLeft.leftAnchor, constant: chat_content_margin), - titleLabelLeftTopAnchor!, - titleLabelLeftHeightAnchor!, ]) bubbleImageLeft.addSubview(contentLabelLeft) contentLabelLeftHeightAnchor = contentLabelLeft.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + contentLabelLeftHeightAnchor?.isActive = true NSLayoutConstraint.activate([ contentLabelLeft.rightAnchor.constraint(equalTo: titleLabelLeft.rightAnchor, constant: 0), contentLabelLeft.leftAnchor.constraint(equalTo: titleLabelLeft.leftAnchor, constant: 0), contentLabelLeft.topAnchor.constraint(equalTo: titleLabelLeft.bottomAnchor, constant: chat_content_margin), - contentLabelLeftHeightAnchor!, ]) /// right bubbleImageRight.addSubview(replyLabelRight) - replyLabelRightHeightAnchor = replyLabelRight.heightAnchor.constraint(equalToConstant: 16.0) + replyLabelRightHeightAnchor = replyLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + replyLabelRightHeightAnchor?.isActive = true NSLayoutConstraint.activate([ replyLabelRight.leadingAnchor.constraint(equalTo: bubbleImageRight.leadingAnchor, constant: chat_content_margin), replyLabelRight.topAnchor.constraint(equalTo: bubbleImageRight.topAnchor, constant: chat_content_margin), - replyLabelRightHeightAnchor!, replyLabelRight.trailingAnchor.constraint(equalTo: bubbleImageRight.trailingAnchor, constant: -chat_content_margin), ]) bubbleImageRight.addSubview(titleLabelRight) titleLabelRightTopAnchor = titleLabelRight.topAnchor.constraint(equalTo: replyLabelRight.bottomAnchor, constant: chat_content_margin) + titleLabelRightTopAnchor?.isActive = true titleLabelRightHeightAnchor = titleLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + titleLabelRightHeightAnchor?.isActive = true NSLayoutConstraint.activate([ titleLabelRight.rightAnchor.constraint(equalTo: bubbleImageRight.rightAnchor, constant: -chat_content_margin), titleLabelRight.leftAnchor.constraint(equalTo: bubbleImageRight.leftAnchor, constant: chat_content_margin), - titleLabelRightTopAnchor!, - titleLabelRightHeightAnchor!, ]) bubbleImageRight.addSubview(contentLabelRight) contentLabelRightHeightAnchor = contentLabelRight.heightAnchor.constraint(equalToConstant: CGFloat.greatestFiniteMagnitude) + contentLabelRightHeightAnchor?.isActive = true NSLayoutConstraint.activate([ contentLabelRight.rightAnchor.constraint(equalTo: titleLabelRight.rightAnchor, constant: -0), contentLabelRight.leftAnchor.constraint(equalTo: titleLabelRight.leftAnchor, constant: 0), contentLabelRight.topAnchor.constraint(equalTo: titleLabelRight.bottomAnchor, constant: chat_content_margin), - contentLabelRightHeightAnchor!, ]) } @@ -114,12 +114,12 @@ open class ChatMessageRichTextCell: ChatMessageReplyCell { let titleLabelHeightAnchor = isSend ? titleLabelRightHeightAnchor : titleLabelLeftHeightAnchor let contentLabelHeightAnchor = isSend ? contentLabelRightHeightAnchor : contentLabelLeftHeightAnchor - if let text = model.replyText { - replyLabelHeightAnchor?.constant = 16 - titleLabelTopAnchor?.constant = chat_content_margin - } else { + if model.replyText == nil || model.replyText!.isEmpty { replyLabelHeightAnchor?.constant = 0 titleLabelTopAnchor?.constant = 0 + } else { + replyLabelHeightAnchor?.constant = 16 + titleLabelTopAnchor?.constant = chat_content_margin } if let m = model as? MessageTextModel { diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift index 219e816a..a21ad783 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageTextCell.swift @@ -13,7 +13,7 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { label.isEnabled = false label.numberOfLines = 0 label.isUserInteractionEnabled = false - label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + label.font = messageTextFont label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageText" return label @@ -25,7 +25,7 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { label.isEnabled = false label.numberOfLines = 0 label.isUserInteractionEnabled = false - label.font = .systemFont(ofSize: NEKitChatConfig.shared.ui.messageProperties.messageTextSize) + label.font = messageTextFont label.backgroundColor = .clear label.accessibilityIdentifier = "id.messageText" return label @@ -37,7 +37,7 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -69,6 +69,7 @@ open class ChatMessageTextCell: NormalChatMessageBaseCell { let contentLabel = isSend ? contentLabelRight : contentLabelLeft if let m = model as? MessageTextModel { contentLabel.attributedText = m.attributeStr + contentLabel.accessibilityValue = m.message?.text } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift index 91a481b5..dae2602c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/ChatMessageVideoCell.swift @@ -3,6 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECommonKit import NIMSDK import UIKit @@ -81,7 +82,7 @@ open class ChatMessageVideoCell: ChatMessageImageCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { @@ -130,24 +131,17 @@ open class ChatMessageVideoCell: ChatMessageImageCell { let timeLabel = isSend ? timeLabelRight : timeLabelLeft let stateView = isSend ? stateViewRight : stateViewLeft - if let videoObject = model.message?.messageObject as? NIMVideoObject { - if let path = videoObject.coverPath, FileManager.default.fileExists(atPath: path) { - contentImageView.sd_setImage( - with: URL(fileURLWithPath: path), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } else { - contentImageView.sd_setImage( - with: URL(string: videoObject.coverUrl ?? ""), - placeholderImage: nil, - options: .retryFailed, - progress: nil, - completed: nil - ) - } + if let videoObject = model.message?.attachment as? V2NIMMessageVideoAttachment { + // 获取首帧 + let videoUrl = videoObject.url ?? "" + let thumbUrl = ResourceRepo.shared.videoThumbnailURL(videoUrl) + contentImageView.sd_setImage( + with: URL(string: thumbUrl), + placeholderImage: nil, + options: .retryFailed, + progress: nil, + completed: nil + ) if videoObject.duration > 0 { timeView.isHidden = false @@ -164,8 +158,8 @@ open class ChatMessageVideoCell: ChatMessageImageCell { stateView.state = .VideoPlay } else { stateView.state = .VideoDownload - stateView.setProgress(videoModel.progress) - if videoModel.progress >= 1 { + stateView.setProgress(Float(videoModel.progress / 100)) + if videoModel.progress >= 100 { videoModel.state = .Success } } @@ -173,8 +167,8 @@ open class ChatMessageVideoCell: ChatMessageImageCell { } } - override open func uploadProgress(byRight: Bool, _ progress: Float) { + override open func uploadProgress(byRight: Bool, _ progress: UInt) { let stateView = byRight ? stateViewRight : stateViewLeft - stateView.setProgress(progress) + stateView.setProgress(Float(progress) / 100) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageAudioCell.swift new file mode 100644 index 00000000..26084b66 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageAudioCell.swift @@ -0,0 +1,30 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class CollectionMessageAudioCell: NEBaseCollectionMessageAudioCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageDefaultCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageDefaultCell.swift new file mode 100644 index 00000000..23a5b109 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageDefaultCell.swift @@ -0,0 +1,30 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class CollectionMessageDefaultCell: NEBaseCollectionMessageDefaultCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageFileCell.swift new file mode 100644 index 00000000..d777556d --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageFileCell.swift @@ -0,0 +1,30 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class CollectionMessageFileCell: NEBaseCollectionMessageFileCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageImageCell.swift new file mode 100644 index 00000000..1eeb9291 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageImageCell.swift @@ -0,0 +1,30 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class CollectionMessageImageCell: NEBaseCollectionMessageImageCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageLocationCell.swift new file mode 100644 index 00000000..b8b684b2 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageLocationCell.swift @@ -0,0 +1,30 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class CollectionMessageLocationCell: NEBaseCollectionMessageLocationCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageMultiForwardCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageMultiForwardCell.swift new file mode 100644 index 00000000..b6e9a1a1 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageMultiForwardCell.swift @@ -0,0 +1,73 @@ +//// 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 CollectionMessageMultiForwardCell: NEBaseCollectionMessageMultiForwardCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func setupCommonUI() { + super.setupCommonUI() + backImageViewLeft.addSubview(titleLabelLeft1) + NSLayoutConstraint.activate([ + titleLabelLeft1.leftAnchor.constraint(equalTo: backImageViewLeft.leftAnchor, constant: 16), + titleLabelLeft1.rightAnchor.constraint(lessThanOrEqualTo: backImageViewLeft.rightAnchor, constant: -84), + titleLabelLeft1.topAnchor.constraint(equalTo: backImageViewLeft.topAnchor, constant: 10), + titleLabelLeft1.heightAnchor.constraint(equalToConstant: 22), + ]) + + backImageViewLeft.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), + ]) + + backImageViewLeft.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), + ]) + + backImageViewLeft.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), + ]) + + backImageViewLeft.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), + ]) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageRichTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageRichTextCell.swift new file mode 100644 index 00000000..612b4ad5 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageRichTextCell.swift @@ -0,0 +1,30 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class CollectionMessageRichTextCell: NEBaseCollectionMessageRichTextCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageTextCell.swift new file mode 100644 index 00000000..be3632d1 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageTextCell.swift @@ -0,0 +1,30 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class CollectionMessageTextCell: NEBaseCollectionMessageTextCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageVideoCell.swift new file mode 100644 index 00000000..67eb1a55 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/CollectionCell/CollectionMessageVideoCell.swift @@ -0,0 +1,30 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open +class CollectionMessageVideoCell: NEBaseCollectionMessageVideoCell { + override open func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override open func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + /// 初始化的生命周期 + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + /// 反序列化支持回调 + public required init?(coder: NSCoder) { + super.init(coder: coder) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserSettingSelectCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserSettingSelectCell.swift index 796a974a..8d7802f9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserSettingSelectCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserSettingSelectCell.swift @@ -23,8 +23,8 @@ open class UserSettingSelectCell: NEBaseUserSettingSelectCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + arrowImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), ]) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserTableViewCell.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserTableViewCell.swift index d9f5a332..9c62db07 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserTableViewCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Cell/UserTableViewCell.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -11,12 +11,12 @@ open class UserTableViewCell: UserBaseTableViewCell { override open func baseCommonUI() { super.baseCommonUI() // avatar - avatarImage.layer.cornerRadius = 21 + avatarImageView.layer.cornerRadius = 21 NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), - avatarImage.widthAnchor.constraint(equalToConstant: 42), - avatarImage.heightAnchor.constraint(equalToConstant: 42), - avatarImage.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + avatarImageView.widthAnchor.constraint(equalToConstant: 42), + avatarImageView.heightAnchor.constraint(equalToConstant: 42), + avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 10), ]) titleLabel.font = UIFont.systemFont(ofSize: 16) @@ -27,7 +27,7 @@ open class UserTableViewCell: UserBaseTableViewCell { alpha: 1.0 ) NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 12), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 12), titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35), titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/CollectionMessageController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/CollectionMessageController.swift new file mode 100644 index 00000000..773b689a --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/CollectionMessageController.swift @@ -0,0 +1,30 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +open class CollectionMessageController: NEBaseCollectionMessageController { + override open func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .ne_lightBackgroundColor + navigationView.backgroundColor = .ne_lightBackgroundColor + navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor + } + + /// 获取通用版皮肤样式注册表 + override open func getRegisterDic() -> [String: NEBaseCollectionMessageCell.Type] { + ChatMessageHelper.getCollectionCellRegisterDic(isFun: false) + } + + override open func getCollectionForwardAlertController() -> 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/ForwardAlertViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ForwardAlertViewController.swift index 1df9eb9e..78d46807 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ForwardAlertViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ForwardAlertViewController.swift @@ -8,10 +8,10 @@ import NECommonUIKit import UIKit @objcMembers -open class ForwardUserCell: NEBaseForwardUserCell { +open class ForwardSessionCell: NEBaseForwardSessionCell { override func setupUI() { super.setupUI() - userHeader.layer.cornerRadius = 16 + sessionHeaderView.layer.cornerRadius = 16 } } @@ -19,18 +19,18 @@ open class ForwardUserCell: NEBaseForwardUserCell { open class ForwardAlertViewController: NEBaseForwardAlertViewController { override open func setupUI() { super.setupUI() - oneUserHead.layer.cornerRadius = 16.0 - userCollection.register( - ForwardUserCell.self, - forCellWithReuseIdentifier: "\(ForwardUserCell.self)" + oneSessionHeadView.layer.cornerRadius = 16.0 + sessionCollectionView.register( + ForwardSessionCell.self, + forCellWithReuseIdentifier: "\(ForwardSessionCell.self)" ) } override open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { if let cell = collectionView.dequeueReusableCell( - withReuseIdentifier: "\(ForwardUserCell.self)", + withReuseIdentifier: "\(ForwardSessionCell.self)", for: indexPath - ) as? ForwardUserCell { + ) as? ForwardSessionCell { return setCellModel(cell: cell, indexPath: indexPath) } return UICollectionViewCell() diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift index 1aa5b508..7197e8e3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalChatViewController.swift @@ -7,15 +7,19 @@ import UIKit @objcMembers open class NormalChatViewController: ChatViewController { - override public init(session: NIMSession) { - super.init(session: session) + override public init(conversationId: String) { + super.init(conversationId: conversationId) navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white cellRegisterDic = ChatMessageHelper.getChatCellRegisterDic(isFun: false) + + topMessageView.topImageView.image = UIImage.ne_imageNamed(name: "top_message_image") + topMessageView.layer.borderColor = UIColor(hexString: "#E8EAED").cgColor + topMessageView.layer.borderWidth = 1 } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -28,18 +32,21 @@ open class NormalChatViewController: ChatViewController { 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) + SelectUserViewController(sessionId: viewModel.sessionId, showSelf: false) } open func getMessageModel(model: MessageModel) { @@ -72,7 +79,8 @@ open class NormalChatViewController: ChatViewController { } else { normalInputHeight = 150 } - bottomViewTopAnchor?.constant = -normalInputHeight + + layoutInputViewWithAnimation(offset: 0) checkAndRestoreReplyView() } @@ -87,7 +95,7 @@ open class NormalChatViewController: ChatViewController { // 切换到单行输入框如果有回复显示回复视图 func checkAndRestoreReplyView() { - if viewmodel.isReplying == true, replyView.superview == nil { + if viewModel.isReplying == true, replyView.superview == nil { view.addSubview(replyView) replyView.closeButton.addTarget(self, action: #selector(closeReply), for: .touchUpInside) replyView.translatesAutoresizingMaskIntoConstraints = false diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift index ff2087df..288909d5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/NormalMultiForwardViewController.swift @@ -17,7 +17,7 @@ open class NormalMultiForwardViewController: MultiForwardViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func getMultiForwardViewController(_ messageAttachmentUrl: String?, diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift index eb1b6ad1..3ad97b99 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/P2PChatViewController.swift @@ -9,53 +9,92 @@ import UIKit @objcMembers open class P2PChatViewController: NormalChatViewController { - public init(session: NIMSession, anchor: NIMMessage?) { - super.init(session: session) - viewmodel = ChatViewModel(session: session, anchor: anchor) + /// 重写父类的构造方法 + /// - Parameter conversationId: 会话id + override public init(conversationId: String) { + super.init(conversationId: conversationId) + viewModel = P2PChatViewModel(conversationId: conversationId, anchor: nil) } - override open func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. + /// 重写父类的构造方法 + /// - Parameter conversationId: 会话id + /// - Parameter anchor: 锚点消息 + public init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId) + viewModel = P2PChatViewModel(conversationId: conversationId, anchor: anchor) } - override open func getSessionInfo(session: NIMSession) { - var showName = session.sessionId - ChatUserCache.getUserInfo(session.sessionId) { [weak self] user, error in - if let name = user?.showName() { - showName = name - } + public required init?(coder: NSCoder) { + super.init(coder: coder) + } - self?.title = showName - self?.titleContent = showName - let text = "\(chatLocalizable("send_to"))\(showName)" - let attribute = NSMutableAttributedString(string: text) - let style = NSMutableParagraphStyle() - style.lineBreakMode = .byTruncatingTail - style.alignment = .left - attribute.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: NSMakeRange(0, text.utf16.count)) - attribute.addAttribute(.foregroundColor, value: UIColor.gray, range: NSMakeRange(0, text.utf16.count)) - attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.utf16.count)) - self?.chatInputView.textView.attributedPlaceholder = attribute - self?.chatInputView.textView.setNeedsLayout() + override open var title: String? { + didSet { + super.title = title + let text = "\(chatLocalizable("send_to"))\(titleContent)" + let attribute = getPlaceHolder(text: text) + chatInputView.textView.attributedPlaceholder = attribute + chatInputView.textView.setNeedsLayout() } } - /// 创建个人聊天页构造方法 - /// - Parameter sessionId: 会话id - public init(sessionId: String) { - let session = NIMSession(sessionId, type: .P2P) - super.init(session: session) + private func getPlaceHolder(text: String) -> NSMutableAttributedString { + let attribute = NSMutableAttributedString(string: text) + let style = NSMutableParagraphStyle() + style.lineBreakMode = .byTruncatingTail + style.alignment = .left + attribute.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: NSMakeRange(0, text.utf16.count)) + attribute.addAttribute(.foregroundColor, value: UIColor.gray, range: NSMakeRange(0, text.utf16.count)) + attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.utf16.count)) + return attribute } - /// 重写父类的构造方法 - /// - Parameter session: sessionId - override public init(session: NIMSession) { - super.init(session: session) + override open func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { + chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("send_to")) + super.getSessionInfo(sessionId: sessionId) { [weak self] in + self?.viewModel.loadShowName([sessionId]) { + let name = self?.viewModel.getShowName(sessionId) ?? sessionId + self?.titleContent = name + self?.title = name + } + completion() + } } - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + /// 重写检查并发送正在输入状态 + /// - Parameter endEdit: 是否停止输入 + override open func checkAndSendTypingState(endEdit: Bool = false) { + guard let viewModel = viewModel as? P2PChatViewModel else { + return + } + + if endEdit { + viewModel.sendInputTypingEndState() + return + } + + if chatInputView.chatInpuMode == .normal { + if let content = chatInputView.textView.text, content.count > 0 { + viewModel.sendInputTypingState() + } else { + viewModel.sendInputTypingEndState() + } + } else { + var title = "" + var content = "" + + if let titleText = chatInputView.titleField.text { + title = titleText + } + + if let contentText = chatInputView.textView.text { + content = contentText + } + if title.count <= 0, content.count <= 0 { + viewModel.sendInputTypingEndState() + } else { + viewModel.sendInputTypingState() + } + } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/PinMessageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/PinMessageViewController.swift index eaa75e12..7b318ef6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/PinMessageViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/PinMessageViewController.swift @@ -18,6 +18,7 @@ open class PinMessageViewController: NEBasePinMessageViewController { ChatMessageHelper.getPinCellRegisterDic(isFun: false) } + /// 获取转发确认弹窗 - 协同版 override open func getForwardAlertController() -> NEBaseForwardAlertViewController { ForwardAlertViewController() } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift index 06781a12..0c692751 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/ReadViewController.swift @@ -4,32 +4,54 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @objcMembers open class ReadViewController: NEBaseReadViewController { - override init(message: NIMMessage) { - super.init(message: message) + override init(message: V2NIMMessage, teamId: String) { + super.init(message: message, teamId: teamId) navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func commonUI() { super.commonUI() navigationView.titleBarBottomLine.isHidden = false - readButton.setTitleColor(UIColor.ne_darkText, for: .normal) - line.backgroundColor = UIColor.ne_blueText + readButton.setTitleColor(UIColor.ne_normalTheme, for: .normal) + bottonBottomLine.backgroundColor = UIColor.ne_normalTheme readTableView.register( UserTableViewCell.self, forCellReuseIdentifier: "\(UserBaseTableViewCell.self)" ) readTableView.rowHeight = 62 + + unreadTableView.register( + UserTableViewCell.self, + forCellReuseIdentifier: "\(UserBaseTableViewCell.self)" + ) + unreadTableView.rowHeight = 62 + } + + /// 重写已读按钮点击事件 + /// - Parameter button: 按钮 + override open func readButtonEvent(button: UIButton) { + super.readButtonEvent(button: button) + readButton.setTitleColor(UIColor.ne_normalTheme, for: .normal) + unreadButton.setTitleColor(UIColor.ne_darkText, for: .normal) + } + + /// 重写未读按钮点击事件 + /// - Parameter button: 按钮 + override open func unreadButtonEvent(button: UIButton) { + super.unreadButtonEvent(button: button) + readButton.setTitleColor(UIColor.ne_darkText, for: .normal) + unreadButton.setTitleColor(UIColor.ne_normalTheme, for: .normal) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift index 9e9db17b..25a81491 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/SelectUserViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -15,7 +15,7 @@ open class SelectUserViewController: NEBaseSelectUserViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func commonUI() { diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/GroupChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/TeamChatViewController.swift similarity index 50% rename from NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/GroupChatViewController.swift rename to NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/TeamChatViewController.swift index e30091d5..b39f498c 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/GroupChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/TeamChatViewController.swift @@ -4,38 +4,38 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @objcMembers -open class GroupChatViewController: NormalChatViewController, TeamChatViewModelDelegate { +open class TeamChatViewController: NormalChatViewController, TeamChatViewModelDelegate { private var isLeaveTeamByOther = false // 是否被移出群聊 private var isLeaveTeamBySelf = false // 是否多端登录另一端退出群聊 private var isdismissTeam = false // 群聊是否已解散 private var isdismissDiscuss = false // 讨论组是否已解散 private var onCurrentPage = false // 是否位于聊天详情页 - public init(session: NIMSession, anchor: NIMMessage?) { - // self.viewmodel = ChatViewModel(session: session) - super.init(session: session) - viewmodel = TeamChatViewModel(session: session, anchor: anchor) - viewmodel.delegate = self + public init(conversationId: String, anchor: V2NIMMessage?) { + super.init(conversationId: conversationId) + viewModel = TeamChatViewModel(conversationId: conversationId, anchor: anchor) + viewModel.delegate = self } /// 创建群的构造方法 /// - Parameter sessionId: 会话id public init(sessionId: String) { - let session = NIMSession(sessionId, type: .team) - super.init(session: session) + let conversationId = V2NIMConversationIdUtil.teamConversationId(sessionId) ?? "" + super.init(conversationId: conversationId) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } deinit { NotificationCenter.default.removeObserver(self) + ChatTeamCache.shared.removeAllTeamInfo() } override open func viewWillAppear(_ animated: Bool) { @@ -51,16 +51,12 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD weak var weakSelf = self // 被移除群聊 if isLeaveTeamByOther { - showSingleAlert(message: chatLocalizable("team_has_been_quit")) { - weakSelf?.navigationController?.popViewController(animated: true) - } + showLeaveTeamAlert() } // 解散群聊 if isdismissTeam { - showSingleAlert(message: chatLocalizable("team_has_been_removed")) { - weakSelf?.navigationController?.popViewController(animated: true) - } + showDismissTeamAlert() } } @@ -74,11 +70,24 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD NotificationCenter.default.addObserver(self, selector: #selector(popGroupChatVC), name: NENotificationName.popGroupChatVC, object: nil) } - override open func getSessionInfo(session: NIMSession) { - if let vm = viewmodel as? TeamChatViewModel { - if let t = vm.getTeam(teamId: session.sessionId) { - updateTeamInfo(team: t) + override open func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { + chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("send_to")) + super.getSessionInfo(sessionId: sessionId) { [weak self] in + + if let vm = self?.viewModel as? TeamChatViewModel { + vm.getTeamInfo(teamId: sessionId) { error, team in + if let team = team { + if IMKitConfigCenter.shared.dismissTeamDeleteConversation == true, team.isValidTeam == false { + self?.showSingleAlert(message: coreLoader.localizable("team_not_exist")) { + NotificationCenter.default.post(name: NENotificationName.deleteConversationNotificationName, object: V2NIMConversationIdUtil.teamConversationId(team.teamId)) + self?.popGroupChatVC() + } + } + self?.updateTeamInfo(team: team) + } + } } + completion() } } @@ -113,10 +122,30 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD return attribute } - open func updateTeamInfo(team: NIMTeam) { - title = team.getShowName() + open func updateTeamTitle(_ noti: Notification) { + if let tid = noti.userInfo?["teamId"] as? String, + tid == viewModel.sessionId, + let team = ChatTeamCache.shared.getTeamInfo() { + updateTeamInfo(team: team) + } + } - if team.inAllMuteMode(), viewmodel.teamMember?.type != .manager, viewmodel.teamMember?.type != .owner { + /// 更新群聊信息(群聊名称、群禁言状态、缓存) + /// - Parameter team: 群聊信息 + open func updateTeamInfo(team: V2NIMTeam) { + title = team.name + ChatTeamCache.shared.updateTeamInfo(team) + setMute(team: team) + } + + /// 设置群禁言/取消群禁言状态 + /// - Parameter team: 群聊信息 + open func setMute(team: V2NIMTeam) { + guard let viewModel = viewModel as? TeamChatViewModel else { + return + } + + if team.chatBannedMode == .TEAM_CHAT_BANNED_MODE_BANNED_ALL || (team.chatBannedMode == .TEAM_CHAT_BANNED_MODE_BANNED_NORMAL && viewModel.teamMember?.memberRole == .TEAM_MEMBER_ROLE_NORMAL) { // 群禁言 isMute = true chatInputView.textView.isEditable = false @@ -134,7 +163,7 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD // 解除群禁言 isMute = false chatInputView.textView.isEditable = true - chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: "\(chatLocalizable("send_to"))\(team.getShowName())") + chatInputView.textView.attributedPlaceholder = getPlaceHolder(text: "\(chatLocalizable("send_to"))\(title ?? team.name)") chatInputView.textView.backgroundColor = .white chatInputView.stackView.isUserInteractionEnabled = true @@ -142,28 +171,39 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD } } - override open func onRecvMessages(_ messages: [NIMMessage]) { - super.onRecvMessages(messages) + override open func onRecvMessages(_ messages: [V2NIMMessage], _ index: [IndexPath]) { + super.onRecvMessages(messages, index) for message in messages { - if let object = message.messageObject as? NIMNotificationObject, - let content = object.content as? NIMTeamNotificationContent { - if content.operationType == .leave, - IMKitClient.instance.isMySelf(content.sourceID) { + if let content = message.attachment as? V2NIMMessageNotificationAttachment { + if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_UPDATE_TINFO, + let updatedTeamInfo = content.updatedTeamInfo { + if let name = updatedTeamInfo.name { + title = name + onTeamMemberUpdate([]) + } + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_INVITE, + let targetIDs = content.targetIds, + targetIDs.contains(IMKitClient.instance.account()) { + // 被重新拉进群聊 + isLeaveTeamByOther = false + if onCurrentPage { + dismissAlert() + } + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE, + message.senderId == IMKitClient.instance.account() { isLeaveTeamBySelf = true if onCurrentPage { popGroupChatVC() } - } else if content.operationType == .kick, - let targetIDs = content.targetIDs, - targetIDs.contains(IMKitClient.instance.imAccid()) { + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_KICK, + let targetIDs = content.targetIds, + targetIDs.contains(IMKitClient.instance.account()) { // 被移出群聊 isLeaveTeamByOther = true if onCurrentPage { - showSingleAlert(message: chatLocalizable("team_has_been_quit")) { [weak self] in - self?.navigationController?.popViewController(animated: true) - } + showLeaveTeamAlert() } - } else if content.operationType == .dismiss { + } else if content.type == .MESSAGE_NOTIFICATION_TYPE_TEAM_DISMISS { if isdismissDiscuss { return } @@ -171,44 +211,26 @@ open class GroupChatViewController: NormalChatViewController, TeamChatViewModelD // 解散群聊 isdismissTeam = true if onCurrentPage { - showSingleAlert(message: chatLocalizable("team_has_been_removed")) { [weak self] in - self?.navigationController?.popViewController(animated: true) - } + showDismissTeamAlert() } } } } } - // MARK: TeamChatViewModelDelegate + // MARK: - TeamChatViewModelDelegate - open func onTeamRemoved(team: NIMTeam) { - // 多端登录另一端解散、退出讨论组 - if team.isDisscuss() == true { - isdismissDiscuss = true - if onCurrentPage { - popGroupChatVC() - } - return - } - } - - open func onTeamUpdate(team: NIMTeam) { - if team.teamId != viewmodel.session.sessionId { - return - } + /// 群聊更新回调 + /// - Parameter team: 群聊 + public func onTeamUpdate(team: V2NIMTeam) { updateTeamInfo(team: team) } - open func onTeamMemberUpdate(team: NIMTeam) { - didRefreshTable() - } - - override public func onTeamMemberChange(team: NIMTeam) { - if viewmodel.session.sessionId != team.teamId { - return + /// 群成员更新回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberUpdate(_ teamMembers: [V2NIMTeamMember]) { + if let team = ChatTeamCache.shared.getTeamInfo() { + setMute(team: team) } - (viewmodel as? TeamChatViewModel)?.getTeamMember() - updateTeamInfo(team: team) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift index c2b1c882..77590cbd 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/Controller/UserSettingViewController.swift @@ -3,6 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit import NECommonKit import NIMSDK import UIKit @@ -20,15 +21,15 @@ open class UserSettingViewController: NEBaseUserSettingViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func setupUI() { super.setupUI() - userHeader.layer.cornerRadius = IMKitClient.instance.getConfigCenter().teamEnable ? 21.0 : 30.0 + userHeaderView.layer.cornerRadius = IMKitConfigCenter.shared.teamEnable ? 21.0 : 30.0 } - override func getPinMessageViewController(session: NIMSession) -> NEBasePinMessageViewController { - PinMessageViewController(session: session) + override func getPinMessageViewController(conversationId: String) -> NEBasePinMessageViewController { + PinMessageViewController(conversationId: conversationId) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift index 932a3f18..977d7fb3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/NormalUI/NormalChatRouter.swift @@ -7,40 +7,27 @@ import NIMSDK public extension ChatRouter { static func register() { + registerCommon() + // pin Router.shared.register(PushPinMessageVCRouter) { param in let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let pin = PinMessageViewController(session: session) + let pin = PinMessageViewController(conversationId: conversationId) nav?.pushViewController(pin, animated: true) } - // sendMessage - Router.shared.register(ChatAddFriendRouter) { param in - if let text = param["text"] as? String, - let sessionId = param["sessionId"] as? String, - let sessionType = param["sessionType"] as? NIMSessionType { - let msg = NIMMessage() - msg.text = text - let session = NIMSession(sessionId, type: sessionType) - NIMSDK.shared().chatManager.send(msg, to: session) { error in - if let err = error { - NELog.errorLog("ChatAddFriendRouter", desc: "send P2P message error:\(err.localizedDescription)") - } - } - } - } // p2p Router.shared.register(PushP2pChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let p2pChatVC = P2PChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let p2pChatVC = P2PChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { @@ -61,12 +48,12 @@ public extension ChatRouter { Router.shared.register(PushTeamChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let groupVC = GroupChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let groupVC = TeamChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { nav?.viewControllers[i] = groupVC diff --git a/NEContactUIKit/NEContactUIKit.podspec b/NEContactUIKit/NEContactUIKit.podspec index 39a52c66..af868b84 100644 --- a/NEContactUIKit/NEContactUIKit.podspec +++ b/NEContactUIKit/NEContactUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEContactUIKit' - s.version = '9.7.0' + s.version = '10.1.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. @@ -34,5 +34,5 @@ Pod::Spec.new do |s| s.resource = 'NEContactUIKit/Assets/**/*' s.dependency 'NEChatKit' s.dependency 'NECommonUIKit' - + s.dependency 'MJRefresh' end diff --git a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/Contents.json b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/Contents.json index 4a62afc6..9374a636 100644 --- a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/Contents.json +++ b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/Contents.json @@ -5,12 +5,12 @@ "scale" : "1x" }, { - "filename" : "search@2x.png", + "filename" : "funSearch@2x.png", "idiom" : "universal", "scale" : "2x" }, { - "filename" : "search@3x.png", + "filename" : "funSearch@3x.png", "idiom" : "universal", "scale" : "3x" } diff --git a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/funSearch@2x.png b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/funSearch@2x.png new file mode 100644 index 00000000..34c69262 Binary files /dev/null and b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/funSearch@2x.png differ diff --git a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/funSearch@3x.png b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/funSearch@3x.png new file mode 100644 index 00000000..a0c30820 Binary files /dev/null and b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/funSearch@3x.png differ diff --git a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/search@2x.png b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/search@2x.png deleted file mode 100644 index e2ce7a50..00000000 Binary files a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/search@2x.png and /dev/null differ diff --git a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/search@3x.png b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/search@3x.png deleted file mode 100644 index 36a33195..00000000 Binary files a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/funSearch.imageset/search@3x.png and /dev/null differ diff --git a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/fun_unselect.imageset/Contents.json b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/fun_unselect.imageset/Contents.json new file mode 100644 index 00000000..ccacda84 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/fun_unselect.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "fun_unselect@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "fun_unselect@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/fun_unselect.imageset/fun_unselect@2x.png b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/fun_unselect.imageset/fun_unselect@2x.png new file mode 100644 index 00000000..cc344d53 Binary files /dev/null and b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/fun_unselect.imageset/fun_unselect@2x.png differ diff --git a/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/fun_unselect.imageset/fun_unselect@3x.png b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/fun_unselect.imageset/fun_unselect@3x.png new file mode 100644 index 00000000..1296dd40 Binary files /dev/null and b/NEContactUIKit/NEContactUIKit/Assets/FunContactUIKit.xcassets/fun_unselect.imageset/fun_unselect@3x.png differ diff --git a/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/remove.imageset/Contents.json b/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/remove.imageset/Contents.json new file mode 100644 index 00000000..b3b739e4 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/remove.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "removeX@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "removeX@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/remove.imageset/removeX@2x.png b/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/remove.imageset/removeX@2x.png new file mode 100644 index 00000000..97c6ff75 Binary files /dev/null and b/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/remove.imageset/removeX@2x.png differ diff --git a/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/remove.imageset/removeX@3x.png b/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/remove.imageset/removeX@3x.png new file mode 100644 index 00000000..a260e1f6 Binary files /dev/null and b/NEContactUIKit/NEContactUIKit/Assets/NEBaseContactUIKit.xcassets/remove.imageset/removeX@3x.png differ diff --git a/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings b/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings index 52e6ae8a..c11b5a23 100644 --- a/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings +++ b/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings @@ -4,7 +4,6 @@ // found in the LICENSE file. "contact"="Contact"; -"search"="Search"; "alert_tip"="tip"; "alert_sure"="ok"; "alert_cancel"="cancel"; @@ -16,7 +15,7 @@ "remove_black" = "Remove"; "blacklist"="Blacklist"; -"mine_groupchat"="My Group"; +"my_teams"="My Group"; "save"="Save"; "input_noteName"="Please enter nick name"; "birthday"="birthday"; @@ -30,6 +29,9 @@ "send_friend_apply"="Contact request sent"; "validation_message"="Verify message"; "no_validation_message"="No Validation Message"; +"add_request"="Friend request"; +"agreed_request"="Approved your apply"; +"refused_request"="Reject your apply"; "clear"="Clear"; "agreed"="Added"; "refused"="Rejected"; @@ -41,6 +43,8 @@ "user_not_exist"="No contact"; "team_not_exist"="No team"; "no_friend"="No Friend"; +"session_empty"="No Session"; +"team_empty"="No Team"; "chat"="Chat"; "message_remind"="Message notification"; "clear_all_validate_message"="Wether to clear all verification"; @@ -51,6 +55,18 @@ "let_us_chat"="Nice to meet you, let's chat"; +// 转发多选 +"recent_forward"="Recent forward"; +"recent_session"="Recent"; +"my_friends"="Friends"; +"selected"="Selected"; +"multi_select"="Multi Select"; +"single_select"="Single Select"; +"complete"="Complete"; +"choose_one_chat"="Choose one chat"; +"exceeded_limit"="Exceed the %d person limit"; +"choose_max_limit"="Can only choose %d sessions"; + // error toast "validate_processed"="Already done on other devices"; "failed_operation"="Failed Operation"; diff --git a/NEContactUIKit/NEContactUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NEContactUIKit/NEContactUIKit/Assets/zh-Hans.lproj/Localizable.strings index 0c42b4d0..59a5ce02 100644 --- a/NEContactUIKit/NEContactUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEContactUIKit/NEContactUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -4,7 +4,6 @@ // found in the LICENSE file. "contact"="通讯录"; -"search"="搜索"; "alert_tip"="提示"; "alert_sure"="确定"; "alert_cancel"="取消"; @@ -16,7 +15,7 @@ "remove_black" = "解除"; "blacklist"="黑名单"; -"mine_groupchat"="我的群聊"; +"my_teams"="我的群聊"; "save"="保存"; "input_noteName"="请输入备注名"; "birthday"="生日"; @@ -30,6 +29,9 @@ "send_friend_apply"="好友申请已发送"; "validation_message"="验证消息"; "no_validation_message"="暂无验证消息"; +"add_request"="添加您为好友"; +"agreed_request"="同意了你的好友请求"; +"refused_request"="拒绝了你的好友请求"; "clear"="清空"; "agreed"="已同意"; "refused"="已拒绝"; @@ -41,6 +43,8 @@ "user_not_exist"="该用户不存在"; "team_not_exist"="该群不存在"; "no_friend"="暂无好友"; +"session_empty"="暂无会话"; +"team_empty"="暂无群组"; "chat"="聊天"; "message_remind"="消息提醒"; "clear_all_validate_message"="是否要清除所有验证消息?"; @@ -51,6 +55,18 @@ "let_us_chat"="我已经同意了你的申请,现在开始聊天吧~"; +// 转发多选 +"recent_forward"="最近转发"; +"recent_session"="最近会话"; +"my_friends"="我的好友"; +"selected"="已选"; +"multi_select"="多选"; +"single_select"="单选"; +"complete"="完成"; +"choose_one_chat"="选择一个聊天"; +"exceeded_limit"="超出%d人限制"; +"choose_max_limit"="最多只能选择%d个会话"; + // error toast "validate_processed"="该验证消息已在其他端处理"; "failed_operation"="操作失败"; diff --git a/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift index 88fc284c..0ee70357 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Base/NEBaseContactViewCell.swift @@ -7,21 +7,21 @@ import UIKit @objcMembers open class NEBaseContactViewCell: UITableViewCell { - public lazy var avatarImage: UIImageView = { - let avatar = UIImageView() - avatar.translatesAutoresizingMaskIntoConstraints = false - avatar.addSubview(nameLabel) - avatar.clipsToBounds = true - avatar.contentMode = .scaleAspectFill - avatar.backgroundColor = UIColor.colorWithNumber(number: 0) - avatar.accessibilityIdentifier = "id.avatar" + public lazy var avatarImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.addSubview(nameLabel) + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + imageView.backgroundColor = UIColor.colorWithNumber(number: 0) + imageView.accessibilityIdentifier = "id.avatar" NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: avatar.leftAnchor, constant: 1), - nameLabel.rightAnchor.constraint(equalTo: avatar.rightAnchor, constant: -1), - nameLabel.centerXAnchor.constraint(equalTo: avatar.centerXAnchor), - nameLabel.centerYAnchor.constraint(equalTo: avatar.centerYAnchor), + nameLabel.leftAnchor.constraint(equalTo: imageView.leftAnchor, constant: 1), + nameLabel.rightAnchor.constraint(equalTo: imageView.rightAnchor, constant: -1), + nameLabel.centerXAnchor.constraint(equalTo: imageView.centerXAnchor), + nameLabel.centerYAnchor.constraint(equalTo: imageView.centerYAnchor), ]) - return avatar + return imageView }() public lazy var redAngleView: RedAngleLabel = { @@ -40,14 +40,14 @@ open class NEBaseContactViewCell: UITableViewCell { }() public lazy var nameLabel: UILabel = { - let name = UILabel() - name.translatesAutoresizingMaskIntoConstraints = false - name.textColor = .white - name.textAlignment = .center - name.font = UIFont.systemFont(ofSize: 14.0) - name.adjustsFontSizeToFitWidth = true - name.accessibilityIdentifier = "id.noAvatar" - return name + let nameLabel = UILabel() + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.textColor = .white + nameLabel.textAlignment = .center + nameLabel.font = UIFont.systemFont(ofSize: 14.0) + nameLabel.adjustsFontSizeToFitWidth = true + nameLabel.accessibilityIdentifier = "id.noAvatar" + return nameLabel }() public lazy var titleLabel: UILabel = { @@ -82,8 +82,8 @@ open class NEBaseContactViewCell: UITableViewCell { } open func setupCommonCircleHeader() { - contentView.addSubview(avatarImage) - leftConstraint = avatarImage.leftAnchor.constraint( + contentView.addSubview(avatarImageView) + leftConstraint = avatarImageView.leftAnchor.constraint( equalTo: contentView.leftAnchor, constant: 20 ) diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift index 7b9b28ab..58f4a143 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/Cell/NEBaseBlackListCell.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objc @@ -14,14 +14,22 @@ protocol BlackListCellDelegate: AnyObject { open class NEBaseBlackListCell: NEBaseTeamTableViewCell { weak var delegate: BlackListCellDelegate? var index = 0 - private var model: NEKitUser? + private var model: NEUserWithFriend? var button = UIButton() + + private lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.ne_greyLine + return view + }() + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func commonUI() { @@ -55,11 +63,11 @@ open class NEBaseBlackListCell: NEBaseTeamTableViewCell { } func buttonEvent(sender: UIButton) { - delegate?.removeUser(account: model?.userId, index: index) + delegate?.removeUser(account: model?.user?.accountId, index: index) } override open func setModel(_ model: Any) { - guard let user = model as? NEKitUser else { + guard let user = model as? NEUserWithFriend else { return } self.model = user @@ -68,21 +76,14 @@ open class NEBaseBlackListCell: NEBaseTeamTableViewCell { titleLabel.text = user.showName() // avatar - if let imageUrl = user.userInfo?.avatarUrl, !imageUrl.isEmpty { + if let imageUrl = user.user?.avatar, !imageUrl.isEmpty { nameLabel.text = "" - avatarImage.sd_setImage(with: URL(string: imageUrl), completed: nil) - avatarImage.backgroundColor = .clear + avatarImageView.sd_setImage(with: URL(string: imageUrl), completed: nil) + avatarImageView.backgroundColor = .clear } else { - nameLabel.text = user.shortName(showAlias: false, count: 2) - avatarImage.image = nil - avatarImage.backgroundColor = UIColor.colorWithString(string: user.userId) + nameLabel.text = user.shortName(count: 2) + avatarImageView.image = nil + avatarImageView.backgroundColor = UIColor.colorWithString(string: user.user?.accountId) } } - - private lazy var bottomLine: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.ne_greyLine - return view - }() } diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift index ba573f1b..70c3307f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewController/NEBaseBlackListViewController.swift @@ -4,18 +4,32 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @objcMembers open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, - BlackListCellDelegate, UIGestureRecognizerDelegate { + BlackListCellDelegate, UIGestureRecognizerDelegate, BlackListViewModelDelegate { public let navigationView = NENavigationView() var tableView = UITableView(frame: .zero, style: .plain) var viewModel = BlackListViewModel() - public var blackList: [NEKitUser]? - var className = "BlackListBaseViewController" + + public lazy var headView: UIView = { + let headView = + UIView(frame: CGRect(x: 0, y: 0, width: Int(NEConstant.screenWidth), height: 40)) + return headView + }() + + public lazy var contentLabel: UILabel = { + let contentLabel = + UILabel(frame: CGRect(x: 20, y: 0, width: Int(NEConstant.screenWidth) - 20, height: 40)) + contentLabel.text = localizable("black_tip") + contentLabel.textColor = UIColor.ne_emptyTitleColor + contentLabel.font = UIFont.systemFont(ofSize: 14) + contentLabel.accessibilityIdentifier = "id.tips" + return contentLabel + }() override open func viewDidLoad() { super.viewDidLoad() @@ -32,6 +46,7 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, loadData() } + /// UI 初始化 func commonUI() { title = localizable("blacklist") navigationView.navTitle.text = title @@ -79,25 +94,17 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - let headView = - UIView(frame: CGRect(x: 0, y: 0, width: Int(NEConstant.screenWidth), height: 40)) - let contentLabel = - UILabel(frame: CGRect(x: 20, y: 0, width: Int(NEConstant.screenWidth) - 20, height: 40)) - contentLabel.text = localizable("black_tip") - contentLabel.textColor = UIColor.ne_emptyTitleColor - contentLabel.font = UIFont.systemFont(ofSize: 14) - contentLabel.accessibilityIdentifier = "id.tips" headView.addSubview(contentLabel) tableView.tableHeaderView = headView } func loadData() { - blackList = viewModel.getBlackList() + viewModel.getBlackList() tableView.reloadData() } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - blackList?.count ?? 0 + viewModel.blockList.count } open func tableView(_ tableView: UITableView, @@ -109,41 +116,28 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, navigationController?.popViewController(animated: true) } - open func getContactSelectVC() -> NEBaseContactsSelectedViewController { - NEBaseContactsSelectedViewController() + open func getContactSelectVC() -> NEBaseContactSelectedViewController { + NEBaseContactSelectedViewController() } func addBlack() { let contactSelectVC = getContactSelectVC() navigationController?.pushViewController(contactSelectVC, animated: true) contactSelectVC.callBack = { [weak self] selectMemberarray in - var users = [NEKitUser]() - selectMemberarray.forEach { memberInfo in + var users = [NEUserWithFriend]() + for memberInfo in selectMemberarray { if let u = memberInfo.user { users.append(u) } } - return self?.addBlackUsers(users: users) + self?.addBlackUsers(users: users) } } - func addBlackUsers(users: [NEKitUser]) { - var num = users.count - var suc = [NEKitUser]() - for user in users { - viewModel.addBlackList(account: user.userId ?? "") { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.className ?? "BlackListViewController"), - desc: "CALLBACK addBlackList " + (error?.localizedDescription ?? "no error") - ) - if error == nil { - suc.append(user) - } - num -= 1 - if num == 0 { - print("add black finished") - self?.loadData() - } + func addBlackUsers(users: [NEUserWithFriend]) { + viewModel.addBlackList(users: users) { [weak self] error in + if let err = error { + self?.showToast(err.localizedDescription) } } } @@ -160,30 +154,36 @@ open class NEBaseBlackListViewController: UIViewController, UITableViewDelegate, guard let acc = account else { return } - viewModel.removeFromBlackList(account: acc) { error in - NELog.infoLog( - ModuleName + " " + self.className, - desc: "CALLBACK removeFromBlackList " + (error?.localizedDescription ?? "no error") - ) - // 1.当前页面刷新 - if error == nil { - self.blackList?.remove(at: index) - self.tableView.reloadData() - } else { - print("removeFromBlackList error:\(error!)") + + viewModel.removeFromBlackList(account: acc) { [weak self] error in + if let err = error { + self?.showToast(err.localizedDescription) } } } -} -// MARK: FriendProviderDelegate + // MARK: BlackListViewModelDelegate -extension NEBaseBlackListViewController: FriendProviderDelegate { - public func onFriendChanged(user: NECoreIMKit.NEKitUser) {} + /// 重新加载表格 + public func tableViewReload() { + tableView.reloadData() + } - public func onUserInfoChanged(user: NECoreIMKit.NEKitUser) {} + /// 重新加载单元格 + /// - Parameter indexs: 单元格位置 + public func tableViewReload(_ indexs: [IndexPath]) { + tableView.reloadData(indexs) + } - public func onBlackListChanged() { - loadData() + /// 删除单元格 + /// - Parameter indexs: 单元格位置 + public func tableViewDelete(_ indexs: [IndexPath]) { + tableView.deleteData(indexs) + } + + /// 插入单元格 + /// - Parameter indexs: 单元格位置 + public func tableViewInsert(_ indexs: [IndexPath]) { + tableView.insertData(indexs) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift index 93c45992..7304d596 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/ViewModel/BlackListViewModel.swift @@ -4,60 +4,135 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit +public protocol BlackListViewModelDelegate: NSObjectProtocol { + func tableViewReload() + func tableViewReload(_ indexs: [IndexPath]) + func tableViewDelete(_ indexs: [IndexPath]) + func tableViewInsert(_ indexs: [IndexPath]) +} + @objcMembers -open class BlackListViewModel: NSObject, FriendProviderDelegate { +open class BlackListViewModel: NSObject, NEContactListener { var contactRepo = ContactRepo.shared - public weak var delegate: FriendProviderDelegate? - private let className = "BlackListViewModel" + public var blockList = [NEUserWithFriend]() + public weak var delegate: BlackListViewModelDelegate? override init() { - NELog.infoLog(ModuleName + " " + className, desc: #function) super.init() - contactRepo.addContactDelegate(delegate: self) + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + contactRepo.addContactListener(self) } - func getBlackList() -> [NEKitUser]? { - NELog.infoLog(ModuleName + " " + className, desc: #function) - return contactRepo.getBlackList() + /// 获取黑名单列表 + func getBlackList() { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + blockList = NEFriendUserCache.shared.getBlocklist().map(\.value) } + /// 移除黑名单 + /// - Parameters: + /// - account: 好友 Id + /// - index: 该用户在表格中的位置 + /// - completion: 回调 func removeFromBlackList(account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account:\(account)") - contactRepo.removeBlackList(account: account, completion) + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", account:\(account)") + contactRepo.removeBlockList(accountId: account) { error in + if let err = error as? NSError { + NEALog.errorLog(ModuleName + " " + BlackListViewModel.className(), desc: #function + ", error:\(err)") + completion(err) + } else { + NEFriendUserCache.shared.removeBlockAccount(account) + completion(nil) + } + } } - func addBlackList(account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account:\(account)") - contactRepo.addBlackList(account: account, completion) + /// 将好友添加到黑名单 + /// - Parameters: + /// - users: 好友列表 + /// - completion: 回调 + func addBlackList(users: [NEUserWithFriend], _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", users.count:\(users.count)") + + for (i, user) in users.enumerated() { + contactRepo.addBlockList(accountId: user.user?.accountId ?? "") { error in + if let err = error as? NSError { + NEALog.errorLog(ModuleName + " " + BlackListViewModel.className(), desc: #function + ", error:\(err)") + completion(err) + } else { + completion(nil) + } + } + } } - // MARK: callback + // MARK: - NEContactListener - public func onFriendChanged(user: NEKitUser) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:\(user.userId ?? "nil")") - delegate?.onFriendChanged(user: user) + /// 用户信息变更回调 + /// - Parameter users: 用户列表 + public func onUserProfileChanged(_ users: [V2NIMUser]) { + for user in users { + for (index, friendUser) in blockList.enumerated() { + if friendUser.user?.accountId == user.accountId { + friendUser.user = user + delegate?.tableViewReload([IndexPath(row: index, section: 0)]) + break + } + } + } } - public func onUserInfoChanged(user: NEKitUser) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:\(user.userId ?? "nil")") - delegate?.onUserInfoChanged(user: user) + /// 黑名单添加回调 + /// - Parameter user: 用户信息 + public func onBlockListAdded(_ user: V2NIMUser) { + guard let accountId = user.accountId else { return } + + // 黑名单中已存在 + if blockList.contains(where: { $0.user?.accountId == user.accountId }) { + return + } + + let blockUser = NEFriendUserCache.shared.getFriendInfo(accountId) ?? NEUserWithFriend(user: user) + let index = IndexPath(row: blockList.count, section: 0) + blockList.append(blockUser) + delegate?.tableViewInsert([index]) } - public func onBlackListChanged() { - NELog.infoLog(ModuleName + " " + className, desc: #function) - delegate?.onBlackListChanged() + /// 黑名单移除回调 + /// - Parameter accountId: 好友 Id + public func onBlockListRemoved(_ accountId: String) { + for (index, friendUser) in blockList.enumerated() { + if friendUser.user?.accountId == accountId { + blockList.remove(at: index) + delegate?.tableViewDelete([IndexPath(row: index, section: 0)]) + break + } + } } - public func onRecieveNotification(notification: NENotification) { - NELog.infoLog(ModuleName + " " + className, desc: #function) - print(#file + #function) + /// 删除好友通知 + /// 本端删除好友,多端同步 + /// - Parameters: + /// - accountId: 删除的好友账号ID + /// - deletionType: 好友删除的类型 + public func onFriendDeleted(_ accountId: String, deletionType: V2NIMFriendDeletionType) { + if NEFriendUserCache.shared.isBlockAccount(accountId) { + onBlockListRemoved(accountId) + } } - public func onNotificationUnreadCountChanged(count: Int) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", count:\(count)") - print(#file + #function) + /// 好友信息变更回调 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + for (index, friendUser) in blockList.enumerated() { + if friendUser.user?.accountId == friendInfo.accountId { + friendUser.friend = friendInfo + delegate?.tableViewReload([IndexPath(row: index, section: 0)]) + break + } + } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift b/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift index c43dccce..5b391b7a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift @@ -16,7 +16,7 @@ public enum ContactCellType: Int { public typealias ContactsSelectCompletion = ([ContactInfo]) -> Void? -let coreLoader = CoreLoader() +let coreLoader = CoreLoader() func localizable(_ key: String) -> String { coreLoader.localizable(key) } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift b/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift index addd0380..5a16230e 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Common/NEBaseContactRouter.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK diff --git a/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift b/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift index ef846f09..f5400a94 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ContactConfig/ContactUIConfig.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import UIKit + /// 头像枚举类型 @objc public enum NEContactAvatarType: Int { case rectangle = 1 // 矩形 @@ -55,7 +56,7 @@ public class ContactUIConfig: NSObject { public var contactProperties = ContactProperties() /// 通讯录列表的视图控制器回调,回调中会返回通讯录列表的视图控制器 - public var customController: ((NEBaseContactsViewController) -> Void)? + public var customController: ((NEBaseContactViewController) -> Void)? } /// 通讯录页面的 UI 个性化定制 diff --git a/NEContactUIKit/NEContactUIKit/Classes/Extension/ContactImageExtension.swift b/NEContactUIKit/NEContactUIKit/Classes/Extension/ContactImageExtension.swift index 90494e69..77085ecb 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Extension/ContactImageExtension.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Extension/ContactImageExtension.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import Foundation + public extension UIImage { class func ne_imageNamed(name: String?) -> UIImage? { guard let imageName = name, !imageName.isEmpty else { diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunBlackListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunBlackListCell.swift index afc57415..974d723d 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunBlackListCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunBlackListCell.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -19,15 +19,15 @@ class FunBlackListCell: NEBaseBlackListCell { button.layer.borderColor = UIColor(hexString: "#D9D9D9").cgColor button.setTitleColor(.ne_darkText, for: .normal) - avatarImage.layer.cornerRadius = 4 - avatarImage.updateLayoutConstraint(firstItem: avatarImage, seconedItem: nil, attribute: .width, constant: 40) - avatarImage.updateLayoutConstraint(firstItem: avatarImage, seconedItem: nil, attribute: .height, constant: 40) + avatarImageView.layer.cornerRadius = 4 + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .width, constant: 40) + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .height, constant: 40) titleLabel.textColor = .ne_darkText contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 1), diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift index c55db64e..0ecc0589 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactSelectedCell.swift @@ -10,18 +10,18 @@ open class FunContactSelectedCell: NEBaseContactSelectedCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 40), - avatarImage.heightAnchor.constraint(equalToConstant: 40), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } override open func commonUI() { super.commonUI() - sImage.highlightedImage = UIImage.ne_imageNamed(name: "fun_select") + sImageView.highlightedImage = UIImage.ne_imageNamed(name: "fun_select") NSLayoutConstraint.activate([ - sImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - sImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + sImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + sImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), ]) bottomLine.backgroundColor = .funContactLineBorderColor @@ -29,11 +29,11 @@ open class FunContactSelectedCell: NEBaseContactSelectedCell { override open func initSubviewsLayout() { if NEKitContactConfig.shared.ui.contactProperties.avatarType == .rectangle { - avatarImage.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius + avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius } else if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { - avatarImage.layer.cornerRadius = 20.0 + avatarImageView.layer.cornerRadius = 20.0 } else { - avatarImage.layer.cornerRadius = 4.0 // Fun UI + avatarImageView.layer.cornerRadius = 4.0 // Fun UI } } @@ -44,6 +44,6 @@ open class FunContactSelectedCell: NEBaseContactSelectedCell { override open func setModel(_ model: ContactInfo) { super.setModel(model) - arrow.isHidden = true + arrowImageView.isHidden = true } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift index bd6c2984..f4fc3abe 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactTableViewCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -13,26 +13,26 @@ open class FunContactTableViewCell: NEBaseContactTableViewCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 40), - avatarImage.heightAnchor.constraint(equalToConstant: 40), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } override open func commonUI() { super.commonUI() bottomLine.backgroundColor = .funContactLineBorderColor - contentView.removeLayoutConstraint(firstItem: redAngleView, seconedItem: arrow, attribute: .right) + contentView.removeLayoutConstraint(firstItem: redAngleView, seconedItem: arrowImageView, attribute: .right) redAngleView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -10).isActive = true } override open func initSubviewsLayout() { if NEKitContactConfig.shared.ui.contactProperties.avatarType == .rectangle { - avatarImage.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius + avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius } else if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { - avatarImage.layer.cornerRadius = 20.0 + avatarImageView.layer.cornerRadius = 20.0 } else { - avatarImage.layer.cornerRadius = 4.0 // Fun UI + avatarImageView.layer.cornerRadius = 4.0 // Fun UI } } @@ -43,6 +43,6 @@ open class FunContactTableViewCell: NEBaseContactTableViewCell { override open func setModel(_ model: ContactInfo) { super.setModel(model) - arrow.isHidden = true + arrowImageView.isHidden = true } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactUnCheckCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactUnCheckCell.swift index 2050b4eb..67f9757e 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactUnCheckCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunContactUnCheckCell.swift @@ -9,12 +9,24 @@ import UIKit open class FunContactUnCheckCell: NEBaseContactUnCheckCell { override func setupUI() { super.setupUI() - avatarImage.layer.cornerRadius = 4 + avatarImageView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - avatarImage.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - avatarImage.widthAnchor.constraint(equalToConstant: 40), - avatarImage.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), ]) } + + /// 重写控件赋值方法 + /// - Parameter model: 数据模型(ContactInfo) + override func configure(_ model: Any) { + guard let model = model as? ContactInfo else { return } + + avatarImageView.configHeadData( + headUrl: model.user?.user?.avatar, + name: model.user?.showName() ?? "", + uid: model.user?.user?.accountId ?? "" + ) + } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunRecentSelectCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunRecentSelectCell.swift new file mode 100644 index 00000000..4d85a12d --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunRecentSelectCell.swift @@ -0,0 +1,48 @@ +// 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 + +/// 转发-选择页面-最近转发 CollectionViewCell -通用版 +@objcMembers +open class FunRecentSelectCell: NEBaseRecentSelectCell { + /// 重写布局方法 + override func setupUI() { + avatarImageView.layer.cornerRadius = 4 + contentView.addSubview(avatarImageView) + NSLayoutConstraint.activate([ + avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: 56), + avatarImageView.heightAnchor.constraint(equalToConstant: 56), + ]) + + titleLabel.font = .systemFont(ofSize: 12) + titleLabel.textColor = .ne_greyText + contentView.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 8), + titleLabel.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor), + titleLabel.widthAnchor.constraint(equalToConstant: 64), + titleLabel.heightAnchor.constraint(equalToConstant: 16), + ]) + + contentView.addSubview(selectImageView) + NSLayoutConstraint.activate([ + selectImageView.topAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: 4), + selectImageView.rightAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: -4), + selectImageView.widthAnchor.constraint(equalToConstant: 18), + selectImageView.heightAnchor.constraint(equalToConstant: 18), + ]) + } + + /// 重写控件赋值方法 + /// - Parameter model: 数据模型(MultiSelectModel) + override func configure(_ model: Any) { + guard let model = model as? MultiSelectModel else { return } + + super.configure(model) + selectImageView.image = model.isSelected ? UIImage.ne_imageNamed(name: "fun_select") : UIImage.ne_imageNamed(name: "fun_unselect") + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSelectCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSelectCell.swift new file mode 100644 index 00000000..a9ebf468 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSelectCell.swift @@ -0,0 +1,47 @@ + +// 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 + +/// 转发-选择页面 TableViewCell -通用版 +@objcMembers +open class FunSelectCell: NEBaseSelectCell { + /// 重写初始化方法 + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + searchTextColor = .ne_funTheme + } + + /// 重写初始化方法 + public required init?(coder: NSCoder) { + super.init(coder: coder) + searchTextColor = .ne_funTheme + } + + /// 重写布局方法 + override open func commonUI() { + super.commonUI() + multiSelectImageView.highlightedImage = UIImage.ne_imageNamed(name: "fun_select") + } + + /// 重写设置文案字体方案 + override open func setConfig() { + super.setConfig() + titleLabel.font = .systemFont(ofSize: 16) + optionLabel.font = .systemFont(ofSize: 16) + memberLabelMaxWidth = 56 + } + + /// 重写设置头像方法 + override open func setupCommonCircleHeader() { + super.setupCommonCircleHeader() + avatarImageView.layer.cornerRadius = 4 + NSLayoutConstraint.activate([ + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + ]) + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSelectedCell.swift new file mode 100644 index 00000000..e2b8f194 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSelectedCell.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 NECommonUIKit +import UIKit + +/// 转发-选择页面-已选 CollectionViewCell -通用版 +@objcMembers +open class FunSelectedCell: NEBaseSelectedCell { + /// 重写布局方法 + override func setupUI() { + super.setupUI() + avatarImageView.layer.cornerRadius = 4 + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSelectedListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSelectedListCell.swift new file mode 100644 index 00000000..c485e9e1 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSelectedListCell.swift @@ -0,0 +1,37 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIM2Kit +import UIKit + +/// 转发-多选-已选详情页面 TableViewCell -通用版 +@objcMembers +open class FunSelectedListCell: NEBaseSelectedListCell { + /// 重写初始化方法 + 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 setupCommonCircleHeader() { + super.setupCommonCircleHeader() + avatarImageView.layer.cornerRadius = 4 + NSLayoutConstraint.activate([ + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + ]) + } + + /// 重写布局方法 + override open func commonUI() { + super.commonUI() + bottomLineLeftConstraint?.constant = 16 + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSystemNotificationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSystemNotificationCell.swift index 78a23e22..58d80a40 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSystemNotificationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunSystemNotificationCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -13,11 +13,11 @@ import UIKit open class FunSystemNotificationCell: NEBaseSystemNotificationCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() - avatarImage.layer.cornerRadius = 4 + avatarImageView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 40), - avatarImage.heightAnchor.constraint(equalToConstant: 40), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } @@ -26,8 +26,8 @@ open class FunSystemNotificationCell: NEBaseSystemNotificationCell { contentView.updateLayoutConstraint(firstItem: line, seconedItem: contentView, attribute: .right, constant: 0) line.backgroundColor = .funContactLineBorderColor - agreeBtn.backgroundColor = .funContactThemeColor - agreeBtn.setTitleColor(.white, for: .normal) - agreeBtn.layer.borderColor = UIColor.funContactThemeColor.cgColor + agreeButton.backgroundColor = .funContactThemeColor + agreeButton.setTitleColor(.white, for: .normal) + agreeButton.layer.borderColor = UIColor.funContactThemeColor.cgColor } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunTeamTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunTeamTableViewCell.swift index 5b940dfe..b09b6b17 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunTeamTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/Cell/FunTeamTableViewCell.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers @@ -16,15 +16,15 @@ open class FunTeamTableViewCell: NEBaseTeamTableViewCell { override func commonUI() { super.commonUI() - avatarImage.layer.cornerRadius = 4 - avatarImage.updateLayoutConstraint(firstItem: avatarImage, seconedItem: nil, attribute: .width, constant: 40) - avatarImage.updateLayoutConstraint(firstItem: avatarImage, seconedItem: nil, attribute: .height, constant: 40) + avatarImageView.layer.cornerRadius = 4 + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .width, constant: 40) + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .height, constant: 40) titleLabel.textColor = .ne_darkText contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 1), diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift index 508b2a8f..2efbb344 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactRouter.swift @@ -4,17 +4,16 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK public extension ContactRouter { static func registerFun() { Router.shared.register(ContactUserSelectRouter) { param in - print("param:\(param)") let nav = param["nav"] as? UINavigationController let filters = param["filters"] as? Set - let contactSelectVC = FunContactsSelectedViewController(filterUsers: filters) + let contactSelectVC = FunContactSelectedViewController(filterUsers: filters) if let limit = param["limit"] as? Int, limit > 0 { contactSelectVC.limit = limit } @@ -24,6 +23,21 @@ public extension ContactRouter { nav?.pushViewController(contactSelectVC, animated: true) } + // 转发选择页面 + Router.shared.register(ForwardMultiSelectRouter) { param in + let nav = param["nav"] as? UINavigationController + let filters = param["filters"] as? Set + let contactSelectVC = FunMultiSelectViewController(filterUsers: filters) + contactSelectVC.modalPresentationStyle = .formSheet + if let limit = param["limit"] as? Int, limit > 0 { + contactSelectVC.limit = limit + } + if let uid = param["uid"] as? String { + contactSelectVC.userId = uid + } + nav?.present(contactSelectVC, animated: true) + } + Router.shared.register(ContactAddFriendRouter) { param in let nav = param["nav"] as? UINavigationController let findFrined = FunFindFriendViewController() @@ -32,11 +46,11 @@ public extension ContactRouter { Router.shared.register(ContactUserInfoPageRouter) { param in if let nav = param["nav"] as? UINavigationController { - if let user = param["user"] as? NEKitUser { + if let user = param["user"] as? NEUserWithFriend { let userInfoVC = FunContactUserViewController(user: user) nav.pushViewController(userInfoVC, animated: true) - } else if let nimUser = param["nim_user"] as? NEKitUser { - let userInfoVC = FunContactUserViewController(user: nimUser) + } else if let nimUser = param["nim_user"] as? V2NIMUser { + let userInfoVC = FunContactUserViewController(nim_user: nimUser) nav.pushViewController(userInfoVC, animated: true) } else if let uid = param["uid"] as? String { let userInfoVC = FunContactUserViewController(uid: uid) @@ -47,7 +61,7 @@ public extension ContactRouter { Router.shared.register(ContactPageRouter) { param in if let nav = param["nav"] as? UINavigationController { - let contactVC = FunContactsViewController() + let contactVC = FunContactViewController() nav.pushViewController(contactVC, animated: true) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactUIColor.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactUIColor.swift index de33e8ef..bbe4af35 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactUIColor.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/FunContactUIColor.swift @@ -6,7 +6,8 @@ import Foundation import NECommonKit public extension UIColor { - static let funContactThemeColor = UIColor(hexString: "#58BE6B") + static let funContactThemeColor = UIColor.ne_funTheme + static let funContactThemeDisableColor = UIColor(hexString: "#58BE6B", 0.5) static let funContactBackgroundColor = UIColor(hexString: "#EDEDED") static let funContactLineBorderColor = UIColor(hexString: "#E5E5E5") static let funContactUserViewChatTitleTextColor = UIColor(hexString: "#525C8C") diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift index 3ec4e7df..d51bb5f7 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/View/FunUserInfoHeaderView.swift @@ -2,14 +2,14 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers open class FunUserInfoHeaderView: NEBaseUserInfoHeaderView { override open func commonUI() { super.commonUI() - avatarImage.layer.cornerRadius = 4 + avatarImageView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ lineView.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), @@ -18,25 +18,4 @@ open class FunUserInfoHeaderView: NEBaseUserInfoHeaderView { lineView.heightAnchor.constraint(equalToConstant: 1), ]) } - - override open func setData(user: NEKitUser?) { - super.setData(user: user) - guard let u = user else { - return - } - - // title - - if let alias = u.alias, !alias.isEmpty { - commonUI(showDetail: true) - titleLabel.text = alias - let uid = u.userId ?? "" - detailLabel.text = "\(localizable("nick")):\(u.userInfo?.nickName ?? uid)" - detailLabel2.text = "\(localizable("account")):\(uid)" - } else { - commonUI(showDetail: false) - titleLabel.text = u.showName() - detailLabel.text = "\(localizable("account")):\(u.userId ?? "")" - } - } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift index e33964a0..59e8c764 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunBlackListViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -12,11 +12,10 @@ import UIKit open class FunBlackListViewController: NEBaseBlackListViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - className = "FunBlackListViewController" } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func commonUI() { @@ -35,11 +34,15 @@ open class FunBlackListViewController: NEBaseBlackListViewController { ) as! FunBlackListCell cell.delegate = self cell.index = indexPath.row - cell.setModel(blackList?[indexPath.row] as Any) + cell.setModel(viewModel.blockList[indexPath.row] as Any) return cell } - override open func getContactSelectVC() -> NEBaseContactsSelectedViewController { - FunContactsSelectedViewController() + /// 黑名单选择页面 + /// - Returns: 人员选择控制器 + override open func getContactSelectVC() -> NEBaseContactSelectedViewController { + var filterUsers = Set() + filterUsers.insert(IMKitClient.instance.account()) + return FunContactSelectedViewController(filterUsers: filterUsers) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactRemakNameViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactRemakNameViewController.swift index 306847c9..b6f7f1eb 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactRemakNameViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactRemakNameViewController.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactSelectedViewController.swift similarity index 84% rename from NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsSelectedViewController.swift rename to NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactSelectedViewController.swift index bdc26ed0..08d909f2 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactSelectedViewController.swift @@ -6,8 +6,9 @@ import NECoreKit import NIMSDK import UIKit +/// 人员选择页面 - 通用版 @objcMembers -open class FunContactsSelectedViewController: NEBaseContactsSelectedViewController { +open class FunContactSelectedViewController: NEBaseContactSelectedViewController { override init(filterUsers: Set? = nil) { super.init(filterUsers: filterUsers) customCells = [ContactCellType.ContactPerson.rawValue: FunContactSelectedCell.self] @@ -15,7 +16,7 @@ open class FunContactsSelectedViewController: NEBaseContactsSelectedViewControll } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { @@ -23,7 +24,7 @@ open class FunContactsSelectedViewController: NEBaseContactsSelectedViewControll super.setupUI() emptyView.setEmptyImage(name: "fun_user_empty") collectionBackView.backgroundColor = .white - collection.register( + collectionView.register( FunContactUnCheckCell.self, forCellWithReuseIdentifier: "\(NSStringFromClass(FunContactUnCheckCell.self))" ) @@ -32,8 +33,10 @@ open class FunContactsSelectedViewController: NEBaseContactsSelectedViewControll override open func setupNavRightItem() { super.setupNavRightItem() + navigationView.setBackButtonTitle(localizable("close")) + navigationView.backButton.setTitleColor(.ne_darkText, for: .normal) navigationView.moreButton.backgroundColor = .funContactThemeColor - sureBtn.backgroundColor = .funContactThemeColor + sureButton.backgroundColor = .funContactThemeColor } override open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift index 13b2a39e..edecfe44 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactUserViewController.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -14,18 +14,23 @@ open class FunContactUserViewController: NEBaseContactUserViewController { headerView = FunUserInfoHeaderView() } - override public init(user: NEKitUser?) { - super.init(user: user) + override public init(uid: String) { + super.init(uid: uid) initFun() } - override public init(uid: String) { - super.init(uid: uid) + override public init(nim_user: V2NIMUser) { + super.init(nim_user: nim_user) + initFun() + } + + override public init(user: NEUserWithFriend?) { + super.init(user: user) initFun() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func commonUI() { @@ -60,8 +65,8 @@ open class FunContactUserViewController: NEBaseContactUserViewController { FunContactRemakNameViewController() } - override open func deleteFriend(user: NEKitUser?) { - let titleAction = NECustomAlertAction(title: String(format: localizable("delete_title"), user?.showName(true) ?? "")) {} + override open func deleteFriend(user: NEUserWithFriend?) { + let titleAction = NECustomAlertAction(title: String(format: localizable("delete_title"), user?.showName() ?? "")) {} titleAction.contentText.font = .systemFont(ofSize: 13) titleAction.contentText.textColor = UIColor(hexString: "#8F8F8F") diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactViewController.swift similarity index 83% rename from NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsViewController.swift rename to NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactViewController.swift index 7b212dcd..dabbf3bb 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactsViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunContactViewController.swift @@ -2,17 +2,18 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NEChatKit +import NECommonUIKit import NECoreKit import UIKit @objcMembers -open class FunContactsViewController: NEBaseContactsViewController { +open class FunContactViewController: NEBaseContactViewController { public lazy var searchView: FunSearchView = { let view = FunSearchView() view.translatesAutoresizingMaskIntoConstraints = false - view.searchBotton.setImage(UIImage.ne_imageNamed(name: "funSearch"), for: .normal) - view.searchBotton.setTitle(localizable("search"), for: .normal) + view.searchButton.setImage(UIImage.ne_imageNamed(name: "funSearch"), for: .normal) + view.searchButton.setTitle(commonLocalizable("search"), for: .normal) return view }() @@ -35,9 +36,9 @@ open class FunContactsViewController: NEBaseContactsViewController { ), ] - if IMKitClient.instance.getConfigCenter().teamEnable { + if IMKitConfigCenter.shared.teamEnable { contactHeaders.append(ContactHeadItem( - name: localizable("mine_groupchat"), + name: localizable("my_teams"), imageName: "funGroup", router: ContactTeamListRouter, color: UIColor(hexString: "#BE65D9") @@ -61,7 +62,7 @@ open class FunContactsViewController: NEBaseContactsViewController { deinit { if let searchViewGestures = searchView.gestureRecognizers { - searchViewGestures.forEach { gesture in + for gesture in searchViewGestures { searchView.removeGestureRecognizer(gesture) } } @@ -90,7 +91,7 @@ open class FunContactsViewController: NEBaseContactsViewController { forHeaderFooterViewReuseIdentifier: "\(NSStringFromClass(ContactSectionView.self))" ) - cellRegisterDic.forEach { (key: Int, value: NEBaseContactTableViewCell.Type) in + for (key, value) in cellRegisterDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } @@ -100,12 +101,27 @@ open class FunContactsViewController: NEBaseContactsViewController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let info = viewModel.contacts[indexPath.section].contacts[indexPath.row] - var reusedId = "\(info.contactCellType)" + let reusedId = "\(info.contactCellType)" let cell = tableView.dequeueReusableCell(withIdentifier: reusedId, for: indexPath) if let c = cell as? FunContactTableViewCell { + if IMKitConfigCenter.shared.onlineStatusEnable { + if indexPath.section != 0 { + c.avatarImageView.alpha = 0.5 + if let accountId = info.user?.user?.accountId { + if let event = viewModel.onlineStatusDic[accountId] { + if event.value == NIMSubscribeEventOnlineValue.login.rawValue { + c.avatarImageView.alpha = 1.0 + } + } + } + } else { + c.avatarImageView.alpha = 1.0 + } + } return configCell(info: info, c, indexPath) } + return cell } @@ -128,7 +144,7 @@ open class FunContactsViewController: NEBaseContactsViewController { } } -extension FunContactsViewController { +extension FunContactViewController { override open func initSystemNav() { edgesForExtendedLayout = [] let addItem = UIBarButtonItem( diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift index 384eaf2d..4e32dc84 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunFindFriendViewController.swift @@ -2,7 +2,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -17,37 +17,37 @@ open class FunFindFriendViewController: NEBaseFindFriendViewController { override open func setupUI() { view.backgroundColor = UIColor(hexString: "0xEDEDED") - let searchBack = UIView() - view.addSubview(searchBack) - searchBack.backgroundColor = UIColor.white - searchBack.translatesAutoresizingMaskIntoConstraints = false - searchBack.clipsToBounds = true - searchBack.layer.cornerRadius = 4.0 + let searchBackView = UIView() + view.addSubview(searchBackView) + searchBackView.backgroundColor = UIColor.white + searchBackView.translatesAutoresizingMaskIntoConstraints = false + searchBackView.clipsToBounds = true + searchBackView.layer.cornerRadius = 4.0 NSLayoutConstraint.activate([ - searchBack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - searchBack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - searchBack.topAnchor.constraint(equalTo: view.topAnchor, constant: 10 + topConstant), - searchBack.heightAnchor.constraint(equalToConstant: 36), + searchBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + searchBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + searchBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 10 + topConstant), + searchBackView.heightAnchor.constraint(equalToConstant: 36), ]) - let searchImage = UIImageView() - searchBack.addSubview(searchImage) - searchImage.image = UIImage.ne_imageNamed(name: "search") - searchImage.translatesAutoresizingMaskIntoConstraints = false + let searchImageView = UIImageView() + searchBackView.addSubview(searchImageView) + searchImageView.image = UIImage.ne_imageNamed(name: "search") + searchImageView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - searchImage.centerYAnchor.constraint(equalTo: searchBack.centerYAnchor), - searchImage.leftAnchor.constraint(equalTo: searchBack.leftAnchor, constant: 18), - searchImage.widthAnchor.constraint(equalToConstant: 13), - searchImage.heightAnchor.constraint(equalToConstant: 13), + searchImageView.centerYAnchor.constraint(equalTo: searchBackView.centerYAnchor), + searchImageView.leftAnchor.constraint(equalTo: searchBackView.leftAnchor, constant: 18), + searchImageView.widthAnchor.constraint(equalToConstant: 13), + searchImageView.heightAnchor.constraint(equalToConstant: 13), ]) - searchBack.addSubview(searchInput) + searchBackView.addSubview(searchInput) searchInput.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - searchInput.leftAnchor.constraint(equalTo: searchImage.rightAnchor, constant: 5), - searchInput.rightAnchor.constraint(equalTo: searchBack.rightAnchor, constant: -18), - searchInput.topAnchor.constraint(equalTo: searchBack.topAnchor), - searchInput.bottomAnchor.constraint(equalTo: searchBack.bottomAnchor), + searchInput.leftAnchor.constraint(equalTo: searchImageView.rightAnchor, constant: 5), + searchInput.rightAnchor.constraint(equalTo: searchBackView.rightAnchor, constant: -18), + searchInput.topAnchor.constraint(equalTo: searchBackView.topAnchor), + searchInput.bottomAnchor.constraint(equalTo: searchBackView.bottomAnchor), ]) searchInput.textColor = UIColor(hexString: "0x333333") searchInput.placeholder = localizable("input_userId") @@ -55,6 +55,10 @@ open class FunFindFriendViewController: NEBaseFindFriendViewController { searchInput.returnKeyType = .search searchInput.delegate = self searchInput.clearButtonMode = .always + searchInput.accessibilityIdentifier = "id.addFriendAccount" + if let clearButton = searchInput.value(forKey: "_clearButton") as? UIButton { + clearButton.accessibilityIdentifier = "id.clear" + } NotificationCenter.default.addObserver( self, @@ -67,6 +71,8 @@ open class FunFindFriendViewController: NEBaseFindFriendViewController { NSLayoutConstraint.activate([ emptyView.centerXAnchor.constraint(equalTo: view.centerXAnchor), emptyView.topAnchor.constraint(equalTo: searchInput.bottomAnchor, constant: 74), + emptyView.widthAnchor.constraint(equalToConstant: 200), + emptyView.heightAnchor.constraint(equalToConstant: 200), ]) emptyView.setEmptyImage(name: "fun_user_empty") diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunMultiSelectViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunMultiSelectViewController.swift new file mode 100644 index 00000000..6008153d --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunMultiSelectViewController.swift @@ -0,0 +1,132 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIM2Kit +import NECoreKit +import NIMSDK +import UIKit + +/// 转发 - 选择页面 - 通用版 +@objcMembers +open class FunMultiSelectViewController: NEBaseMultiSelectViewController { + override init(filterUsers: Set? = nil) { + super.init(filterUsers: filterUsers) + themeColor = .funContactThemeColor + titleText = localizable("select") + sureButtonText = localizable("complete") + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .ne_backcolor + + sureButton.setTitleColor(.white, for: .normal) + sureButton.backgroundColor = .funContactThemeDisableColor + sureButton.contentHorizontalAlignment = .center + + searchTextField.font = UIFont.systemFont(ofSize: 16) + searchTextField.backgroundColor = .white + searchTextField.addTarget(self, action: #selector(searchTextFieldBeginEdit), for: .editingDidBegin) + setSearchTextFieldLeftView() + + recentTableView.rowHeight = 64 + recentTableView.register(FunSelectCell.self, forCellReuseIdentifier: "\(NSStringFromClass(NEBaseSelectCell.self))") + + friendTableView.rowHeight = 64 + friendTableView.register(FunSelectCell.self, forCellReuseIdentifier: "\(NSStringFromClass(NEBaseSelectCell.self))") + + teamTableView.rowHeight = 64 + teamTableView.register(FunSelectCell.self, forCellReuseIdentifier: "\(NSStringFromClass(NEBaseSelectCell.self))") + + selectedCollectionView.register(FunSelectedCell.self, forCellWithReuseIdentifier: "\(NSStringFromClass(NEBaseSelectedCell.self))") + + recentLabel.textColor = .black + recentCollectionView.register(FunRecentSelectCell.self, forCellWithReuseIdentifier: "\(NSStringFromClass(NEBaseSelectedCell.self))") + + emptyView.setEmptyImage(name: "fun_user_empty") + } + + /// 设置搜索框 leftView + func setSearchTextFieldLeftView() { + if !searchTextField.isFirstResponder, searchTextField.text?.isEmpty == true { + let leftImageView = UIImageView(image: UIImage.ne_imageNamed(name: "funSearch")) + searchTextField.leftView = leftImageView + searchTextField.leftViewRectX = (NEConstant.screenWidth) / 2 - 50 + } else { + searchTextField.leftView = nil + searchTextField.leftViewRectX = nil + } + searchTextField.layoutIfNeeded() + } + + /// 监听搜索框开始编辑 + func searchTextFieldBeginEdit() { + setSearchTextFieldLeftView() + } + + /// 获取已选视图控制器 - 通用版 + /// - Parameter selectedArray: 已选列表 + /// - Returns: 已选页面的视图控制器 + override open func getMultiSelectedViewController(_ selectedArray: [MultiSelectModel]) -> NEBaseMultiSelectedViewController { + FunMultiSelectedViewController(selectedArray: selectedArray) + } + + // MARK: - UITableViewDataSource + + override open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if tableView == currentTableView { + let info = viewModel.sessions[indexPath.row] + let cell = tableView.dequeueReusableCell( + withIdentifier: "\(NSStringFromClass(NEBaseSelectCell.self))", + for: indexPath + ) as! FunSelectCell + cell.showSelect(isMultiSelect) + cell.setModel(info) + cell.searchText = searchText + return cell + } + + return UITableViewCell() + } + + /// 重写 tableView 点击事件,更新搜索框 leftView 位置 + override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + super.tableView(tableView, didSelectRowAt: indexPath) + setSearchTextFieldLeftView() + } + + // MARK: - UIScrollViewDelegate + + /// 重写 tableView 开始滚动事件,更新搜索框 leftView 位置 + override public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + super.scrollViewWillBeginDragging(scrollView) + setSearchTextFieldLeftView() + } + + // MARK: Collection View DataSource And Delegateoverride + + override open func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + if collectionView == recentCollectionView { + return CGSize(width: 72, height: 84) + } else if collectionView == selectedCollectionView { + return CGSize(width: 41, height: selectedContentViewHeight) + } else { + return CGSize.zero + } + } + + /// 重写【完成】按钮设置 + override func refreshSelectCount() { + super.refreshSelectCount() + sureButton.backgroundColor = selectedArray.count > 0 ? .funContactThemeColor : .funContactThemeDisableColor + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunMultiSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunMultiSelectedViewController.swift new file mode 100644 index 00000000..9eca399c --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunMultiSelectedViewController.swift @@ -0,0 +1,53 @@ + +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NECoreKit +import NIMSDK +import UIKit + +/// 转发多选-已选页面-通用版 +@objcMembers +open class FunMultiSelectedViewController: NEBaseMultiSelectedViewController { + override init(selectedArray: [MultiSelectModel] = [MultiSelectModel]()) { + super.init(selectedArray: selectedArray) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// 设置背景圆角、宽高 + override open func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + view.layer.cornerRadius = 8 + view.layer.borderWidth = 0.5 + view.layer.borderColor = UIColor.lightGray.cgColor + view.frame = CGRect(x: 0, y: NEConstant.screenHeight - 380, width: NEConstant.screenWidth, height: 380) + + // 父视图添加单击手势,点击背景区域 dismiss + let tap = UITapGestureRecognizer(target: self, action: #selector(tapAction)) + view.superview?.addGestureRecognizer(tap) + } + + override open func viewDidLoad() { + super.viewDidLoad() + commonUI() + + tableView.rowHeight = 64 + tableView.register(FunSelectedListCell.self, forCellReuseIdentifier: "\(NSStringFromClass(NEBaseSelectedListCell.self))") + tableViewTopAnchor?.constant = 50 + + emptyView.setEmptyImage(name: "fun_user_empty") + } + + /// 单击手势点击事件 + /// - Parameter tap: 单击手势 + func tapAction(_ tap: UITapGestureRecognizer) { + // 判断手势位置位于背景区域 + if tap.location(in: view).y < 0 { + cancelButtonClick() + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunTeamListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunTeamListViewController.swift index 3623240a..7a8c7f1d 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunTeamListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunTeamListViewController.swift @@ -16,6 +16,7 @@ open class FunTeamListViewController: NEBaseTeamListViewController { forCellReuseIdentifier: "\(NSStringFromClass(FunTeamTableViewCell.self))" ) tableView.rowHeight = 72 + emptyView.setEmptyImage(name: "fun_user_empty") } override open func tableView(_ tableView: UITableView, diff --git a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift index baa49432..088e8370 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/FunUI/ViewController/FunValidationMessageViewController.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -11,11 +11,10 @@ import UIKit open class FunValidationMessageViewController: NEBaseValidationMessageViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - tag = "FunValidationMessageViewController" } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift index 9ffbfeee..91333698 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -19,7 +19,7 @@ open class ContactInfo: NSObject { nil } - public var user: NEKitUser? + public var user: NEUserWithFriend? public var contactCellType = ContactCellType.ContactPerson.rawValue public var router = ContactPersonRouter public var isSelected = false diff --git a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift index d2eee358..46fe8c81 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactSection.swift @@ -5,7 +5,7 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit /* 通讯录 section 数据模型 diff --git a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseRecentSelectCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseRecentSelectCell.swift new file mode 100644 index 00000000..530af339 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseRecentSelectCell.swift @@ -0,0 +1,79 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +/// 转发-选择页面-最近转发 CollectionViewCell -基类 +@objcMembers +open class NEBaseRecentSelectCell: NEBaseSelectedCell { + /// 名称标签 + lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .ne_darkText + label.textAlignment = .center + label.font = .systemFont(ofSize: 12) + label.accessibilityIdentifier = "id.name" + return label + }() + + /// 选择状态 + lazy var selectImageView: UIImageView = { + let imageView = UIImageView(image: UIImage.ne_imageNamed(name: "unselect")) + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() + + /// 重写布局方法 + override func setupUI() { + contentView.addSubview(avatarImageView) + avatarImageView.layer.cornerRadius = 18 + avatarImageView.titleLabel.font = UIFont.systemFont(ofSize: 16.0) + NSLayoutConstraint.activate([ + avatarImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: 36), + avatarImageView.heightAnchor.constraint(equalToConstant: 36), + ]) + + contentView.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 8), + titleLabel.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor), + titleLabel.widthAnchor.constraint(equalToConstant: 72), + titleLabel.heightAnchor.constraint(equalToConstant: 16), + ]) + + contentView.addSubview(selectImageView) + NSLayoutConstraint.activate([ + selectImageView.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), + selectImageView.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor), + selectImageView.widthAnchor.constraint(equalToConstant: 18), + selectImageView.heightAnchor.constraint(equalToConstant: 18), + ]) + } + + /// 设置头像宽高 + /// - Parameter height: 宽高 + func setAvatarWH(_ height: CGFloat) { + avatarImageView.layer.cornerRadius = height / 2 + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .width, constant: height) + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .height, constant: height) + } + + /// 设置选中状态显隐 + /// - Parameter isShow: 是否显示 + func showSelect(_ isShow: Bool) { + selectImageView.isHidden = !isShow + } + + /// 重写控件赋值方法 + /// - Parameter model: 数据模型(MultiSelectModel) + override func configure(_ model: Any) { + guard let model = model as? MultiSelectModel else { return } + + super.configure(model) + titleLabel.text = model.name + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectCell.swift new file mode 100644 index 00000000..07d7bed4 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectCell.swift @@ -0,0 +1,145 @@ + +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import Foundation +import NEChatKit +import NECoreIM2Kit +import NECoreKit +import UIKit + +/// 转发-选择页面 TableViewCell - 基类 +@objcMembers +open class NEBaseSelectCell: NEBaseContactViewCell { + /// 名称最大宽度(不包含人数) + public var titleLabelMaxWidth = NEConstant.screenWidth - 170 { + didSet { + titleLabelWidthAnchor?.constant = titleLabelMaxWidth + } + } + + /// 人数最大宽度 + public var memberLabelMaxWidth: CGFloat = 52 { + didSet { + memberLabelWidthAnchor?.constant = titleLabelMaxWidth + } + } + + /// 名称宽度约束(受单选/多选状态影响) + public var titleLabelWidthAnchor: NSLayoutConstraint? + /// 名称宽度约束(受单选/多选状态影响) + public var memberLabelWidthAnchor: NSLayoutConstraint? + /// 搜索匹配字符颜色 + public var searchTextColor: UIColor = .ne_normalTheme + + /// 多选选中状态 + lazy var multiSelectImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = UIImage.ne_imageNamed(name: "unselect") + imageView.accessibilityIdentifier = "id.selector" + return imageView + }() + + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + commonUI() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// 初始化布局 + open func commonUI() { + setupCommonCircleHeader() + + // 选中的状态 + contentView.addSubview(multiSelectImageView) + NSLayoutConstraint.activate([ + multiSelectImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + multiSelectImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + ]) + + // 名称 + contentView.addSubview(titleLabel) + titleLabelWidthAnchor = titleLabel.widthAnchor.constraint(lessThanOrEqualToConstant: titleLabelMaxWidth) + titleLabelWidthAnchor?.isActive = true + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 12), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), + titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + + // 人数 + contentView.addSubview(optionLabel) + memberLabelWidthAnchor = optionLabel.widthAnchor.constraint(equalToConstant: memberLabelMaxWidth) + memberLabelWidthAnchor?.isActive = true + NSLayoutConstraint.activate([ + optionLabel.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: -0), + optionLabel.topAnchor.constraint(equalTo: contentView.topAnchor), + optionLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) + } + + /// 设置文案字体字号 + open func setConfig() { + titleLabel.textColor = NEKitContactConfig.shared.ui.contactProperties.itemTitleColor + nameLabel.font = UIFont.systemFont(ofSize: 14.0) + nameLabel.textColor = UIColor.white + } + + /// 设置选中状态显隐 + /// - Parameter isShow: 是否显示 + open func showSelect(_ isShow: Bool) { + multiSelectImageView.isHidden = !isShow + leftConstraint?.constant = isShow ? 50 : 20 + titleLabelMaxWidth = NEConstant.screenWidth - (isShow ? 118 + memberLabelMaxWidth : 88 + memberLabelMaxWidth) + } + + /// 设置控件内容 + /// - Parameter model: 数据模型 + open func setModel(_ model: MultiSelectModel) { + setConfig() + + multiSelectImageView.isHighlighted = model.isSelected + titleLabel.text = model.name + nameLabel.text = NEFriendUserCache.getShortName(model.name ?? "") + optionLabel.text = model.memberCount > 0 ? " (\(model.memberCount))" : nil + + // 单聊(人数为 0)不展示人数 + if model.memberCount == 0 { + titleLabelMaxWidth += memberLabelMaxWidth + } + + if let imageUrl = model.avatar, !imageUrl.isEmpty { + nameLabel.isHidden = true + avatarImageView.sd_setImage(with: URL(string: imageUrl), completed: nil) + avatarImageView.backgroundColor = .clear + } else { + nameLabel.isHidden = false + avatarImageView.sd_setImage(with: nil) + if let cid = model.conversationId, let sessionId = V2NIMConversationIdUtil.conversationTargetId(cid) { + avatarImageView.backgroundColor = UIColor.colorWithString(string: sessionId) + } + } + } + + /// 搜索字符 + public var searchText: String? { + didSet { + if let searchText = searchText, let titleText = titleLabel.text { + let attributedStr = NSMutableAttributedString(string: titleText) + // range 表示从索引几开始取几个字符 + let range = attributedStr.mutableString.range(of: searchText) + attributedStr.addAttribute( + .foregroundColor, + value: searchTextColor, + range: range + ) + titleLabel.attributedText = attributedStr + } + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectedCell.swift new file mode 100644 index 00000000..6d547d00 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectedCell.swift @@ -0,0 +1,35 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NECommonUIKit +import UIKit + +/// 转发-选择页面-已选 CollectionViewCell -基类 +@objcMembers +open class NEBaseSelectedCell: NEBaseContactUnCheckCell { + /// 重写布局方法 + override func setupUI() { + super.setupUI() + avatarImageView.layer.cornerRadius = 16 + avatarImageView.titleLabel.font = UIFont.systemFont(ofSize: 12.0) + NSLayoutConstraint.activate([ + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: 32), + avatarImageView.heightAnchor.constraint(equalToConstant: 32), + ]) + } + + /// 重写控件赋值方法 + /// - Parameter model: 数据模型(MultiSelectModel) + override func configure(_ model: Any) { + guard let model = model as? MultiSelectModel else { return } + + avatarImageView.configHeadData( + headUrl: model.avatar, + name: model.name ?? "", + uid: V2NIMConversationIdUtil.conversationTargetId(model.conversationId ?? "") ?? "" + ) + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectedListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectedListCell.swift new file mode 100644 index 00000000..e7504091 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Cell/NEBaseSelectedListCell.swift @@ -0,0 +1,101 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIM2Kit +import UIKit + +/// 转发 - 多选 - 已选详情页面 TableViewCell - 协议 +protocol NEBaseSelectedListCellDelegate: NSObjectProtocol { + /// 移除按钮点击事件 + /// - Parameter model: 数据模型 + func removeButtonAction(_ model: MultiSelectModel?) +} + +/// 转发 - 多选 - 已选详情页面 TableViewCell -通用版 +@objcMembers +open class NEBaseSelectedListCell: NEBaseSelectCell { + weak var delegate: NEBaseSelectedListCellDelegate? + private var contentModel: MultiSelectModel? + + /// 移除按钮 + lazy var removeButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(UIImage.ne_imageNamed(name: "remove"), for: .normal) + button.addTarget(self, action: #selector(removeButtonAction), for: .touchUpInside) + return button + }() + + /// 分割线 + public lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.ne_greyLine + return view + }() + + public var bottomLineLeftConstraint: NSLayoutConstraint? // 分割线左边约束 + + 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() { + super.commonUI() + + contentView.addSubview(removeButton) + NSLayoutConstraint.activate([ + removeButton.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), + removeButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + removeButton.heightAnchor.constraint(equalToConstant: 20), + removeButton.widthAnchor.constraint(equalToConstant: 20), + ]) + + contentView.addSubview(bottomLine) + bottomLineLeftConstraint = bottomLine.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 0) + bottomLineLeftConstraint?.isActive = true + NSLayoutConstraint.activate([ + bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + bottomLine.heightAnchor.constraint(equalToConstant: 1), + ]) + + contentView.updateLayoutConstraint(firstItem: titleLabel, seconedItem: contentView, attribute: .right, constant: -48) + } + + /// 重写设置文案字体方法 + override open func setConfig() { + super.setConfig() + titleLabel.textColor = .ne_darkText + titleLabel.font = .systemFont(ofSize: 16) + memberLabelMaxWidth = 56 + optionLabel.font = .systemFont(ofSize: 16) + } + + /// 重写设置model方法 + /// - Parameter model: 转发选择页面数据模型 + func removeButtonAction(_ sender: ExpandButton) { + delegate?.removeButtonAction(contentModel) + } + + /// 重写控件内容设置方法 + /// - Parameter model: 数据模型 + override open func setModel(_ model: MultiSelectModel) { + super.setModel(model) + contentModel = model + multiSelectImageView.isHidden = true + if model.memberCount == 0 { + // 单聊(人数为 0)不展示人数 + titleLabelMaxWidth = NEConstant.screenWidth - 70 - memberLabelMaxWidth + } else { + titleLabelMaxWidth = NEConstant.screenWidth - 122 - memberLabelMaxWidth + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Model/MultiSelectModel.swift b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Model/MultiSelectModel.swift new file mode 100644 index 00000000..6cac327c --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/Model/MultiSelectModel.swift @@ -0,0 +1,25 @@ + +// 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 MultiSelectModel: NSObject { + // 会话id + public var conversationId: String? + // 会话名称 + public var name: String? + // 会话头像 + public var avatar: String? + // 是否已选中 + public var isSelected = false + + // 会话人数,用于展示群人数(单聊默认为 0,群聊为群人数) + public var memberCount: Int = 0 + + // 扩展字段 + public var localExtension: [String: Any]? +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectViewController.swift new file mode 100644 index 00000000..62883e5a --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectViewController.swift @@ -0,0 +1,953 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIM2Kit +import NECoreKit +import NIMSDK +import UIKit + +/// 转发 - 选择页面 - 基类 +@objcMembers +open class NEBaseMultiSelectViewController: NEContactBaseViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UITableViewDelegate, UITableViewDataSource { + public let viewModel = MultiSelectViewModel() + public var filterUsers: Set? + public var limit = 9 // 最大可选数量(多选) + public var userId: String? // 单聊中对方的userId + public var selectedArray = [MultiSelectModel]() // 已选列表 + public var recentArray = [MultiSelectModel]() // 最近会话列表 + var tabIndex = 0 // 最近会话 - 0;我的好友 - 1;我的群聊 - 2 + var isMultiSelect = false // 是否处于多选 + var searchText: String? // 搜索文案 + var currentTableView: UITableView? // 当前展示的 tableView + + var selectedContentViewHeight: CGFloat = 59 // 已选区域高度 + var selectedContentViewHeightAnchor: NSLayoutConstraint? // 已选区域 高度约束 + var recentContentViewHeight: CGFloat = 145 // 最近转发区域高度 + var recentContentViewHeightAnchor: NSLayoutConstraint? // 最近会话 高度约束 + public var selectedLineLeftAnchor: NSLayoutConstraint? // 已选 tab 下划线左侧约束 + + public var themeColor: UIColor = .ne_normalTheme // 主题颜色 + public var titleText = localizable("select") // 标题文案 + public var sureButtonText = localizable("alert_sure") // 确定按钮文案 + + init(filterUsers: Set? = nil) { + super.init(nibName: nil, bundle: nil) + self.filterUsers = filterUsers + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// 设置背景圆角 + override open func viewWillLayoutSubviews() { + super.viewWillLayoutSubviews() + view.layer.cornerRadius = 8 + view.layer.borderWidth = 0.2 + view.layer.borderColor = UIColor.lightGray.cgColor + } + + override open func viewDidLoad() { + super.viewDidLoad() + + setupUI() + currentTableView = recentTableView + loadAllData() + } + + /// 加载所有数据 + func loadAllData() { + viewModel.loadAllData(filterUsers) { [weak self] error in + self?.setEmptyViewText(self?.searchText) + + if let sessions = self?.viewModel.sessions { + self?.emptyView.isHidden = sessions.count > 0 + self?.currentTableView?.reloadData() + } + + if let recentSessions = self?.viewModel.loadRecentForward(), !recentSessions.isEmpty { + self?.recentArray = recentSessions + self?.recentCollectionView.reloadData() + } else { + self?.recentContentView.isHidden = true + self?.recentContentViewHeightAnchor?.constant = 16 + } + } + } + + /// 加载数据(根据下标) + /// - Parameter index: tab 下标 + func loadData(_ index: Int = 0) { + viewModel.loadData(index, filterUsers) { [weak self] error in + self?.viewModel.searchText(self?.searchText ?? "") + self?.setEmptyViewText(self?.searchText) + self?.currentTableView?.reloadData() + self?.emptyView.isHidden = self?.viewModel.sessions.isEmpty == false + } + } + + /// 设置标题 + func setupTitle() { + navigationController?.isNavigationBarHidden = true + navigationView.isHidden = true + + // 取消按钮 + view.addSubview(cancelButton) + NSLayoutConstraint.activate([ + cancelButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + cancelButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 16), + cancelButton.widthAnchor.constraint(equalToConstant: 40), + cancelButton.heightAnchor.constraint(equalToConstant: 18), + ]) + + // 标题文案 + let titleLabel = UILabel() + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.text = titleText + titleLabel.font = UIFont.systemFont(ofSize: 16, weight: .semibold) + titleLabel.textAlignment = .center + titleLabel.textColor = .ne_darkText + view.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 16), + titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + titleLabel.heightAnchor.constraint(equalToConstant: 18), + ]) + + // 多选按钮 + view.addSubview(multiSelectButton) + NSLayoutConstraint.activate([ + multiSelectButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + multiSelectButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 16), + multiSelectButton.widthAnchor.constraint(equalToConstant: 40), + multiSelectButton.heightAnchor.constraint(equalToConstant: 18), + ]) + + // 确定按钮 + view.addSubview(sureButton) + NSLayoutConstraint.activate([ + sureButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + sureButton.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), + sureButton.widthAnchor.constraint(equalToConstant: 76), + sureButton.heightAnchor.constraint(equalToConstant: 32), + ]) + } + + /// 设置 UI 布局 + open func setupUI() { + // 设置标题 + setupTitle() + + // 搜索 + view.addSubview(searchTextField) + NSLayoutConstraint.activate([ + searchTextField.topAnchor.constraint(equalTo: view.topAnchor, constant: 58), + searchTextField.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + searchTextField.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + searchTextField.heightAnchor.constraint(equalToConstant: 32), + ]) + + // 已选 + view.addSubview(selectedContentView) + selectedContentViewHeightAnchor = selectedContentView.heightAnchor.constraint(equalToConstant: 0) + selectedContentViewHeightAnchor?.isActive = true + NSLayoutConstraint.activate([ + selectedContentView.topAnchor.constraint(equalTo: searchTextField.bottomAnchor), + selectedContentView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + selectedContentView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + ]) + + // 最近转发 + view.addSubview(recentContentView) + recentContentViewHeightAnchor = recentContentView.heightAnchor.constraint(equalToConstant: recentContentViewHeight) + recentContentViewHeightAnchor?.isActive = true + NSLayoutConstraint.activate([ + recentContentView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + recentContentView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + recentContentView.topAnchor.constraint(equalTo: selectedContentView.bottomAnchor), + ]) + + // 会话选择区域 + view.addSubview(sessionContentView) + NSLayoutConstraint.activate([ + sessionContentView.topAnchor.constraint(equalTo: recentContentView.bottomAnchor), + sessionContentView.leftAnchor.constraint(equalTo: view.leftAnchor), + sessionContentView.rightAnchor.constraint(equalTo: view.rightAnchor), + sessionContentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + // MARK: - lazy var + + /// 搜索区域 + public lazy var searchTextField1: FunSearchView = { + let view = FunSearchView(searchButtonLeftConstant: 16) + view.translatesAutoresizingMaskIntoConstraints = false + view.backView.backgroundColor = UIColor.ne_backcolor + view.searchButton.setImage(UIImage.ne_imageNamed(name: "funSearch"), for: .normal) + view.searchButton.setTitle(commonLocalizable("search"), for: .normal) + view.searchButton.contentHorizontalAlignment = .left + return view + }() + + public lazy var searchTextField: SearchTextField = { + let textField = SearchTextField() + let leftImageView = UIImageView(image: UIImage.ne_imageNamed(name: "search")) + textField.contentMode = .center + textField.leftView = leftImageView + textField.leftViewMode = .always + textField.placeholder = commonLocalizable("search") + textField.font = UIFont.systemFont(ofSize: 14) + textField.textColor = UIColor.ne_darkText + textField.translatesAutoresizingMaskIntoConstraints = false + textField.layer.cornerRadius = 8 + textField.backgroundColor = .ne_lightBackgroundColor + textField.clearButtonMode = .always + textField.returnKeyType = .search + textField.addTarget(self, action: #selector(searchTextFieldChange), for: .editingChanged) + + if let clearButton = textField.value(forKey: "_clearButton") as? UIButton { + clearButton.accessibilityIdentifier = "id.clear" + } + textField.accessibilityIdentifier = "id.search" + return textField + }() + + /// 已选区域 + public lazy var selectedContentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + view.isHidden = true + + view.addSubview(selectedCollectionView) + NSLayoutConstraint.activate([ + selectedCollectionView.centerYAnchor.constraint(equalTo: view.centerYAnchor), + selectedCollectionView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: -10), + selectedCollectionView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -30), + selectedCollectionView.heightAnchor.constraint(equalToConstant: 36), + ]) + + // 向右箭头(更多) + let arrowRight = UIImageView() + arrowRight.translatesAutoresizingMaskIntoConstraints = false + arrowRight.image = UIImage.ne_imageNamed(name: "arrowRight") + + view.addSubview(arrowRight) + NSLayoutConstraint.activate([ + arrowRight.centerYAnchor.constraint(equalTo: view.centerYAnchor), + arrowRight.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -5), + ]) + + // 分割线 + let dividerLine = UIView() + dividerLine.translatesAutoresizingMaskIntoConstraints = false + dividerLine.backgroundColor = UIColor.ne_backcolor + view.addSubview(dividerLine) + NSLayoutConstraint.activate([ + dividerLine.bottomAnchor.constraint(equalTo: view.bottomAnchor), + dividerLine.leftAnchor.constraint(equalTo: view.leftAnchor, constant: -20), + dividerLine.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 20), + dividerLine.heightAnchor.constraint(equalToConstant: 3), + ]) + + let tap = UITapGestureRecognizer(target: self, action: #selector(selectedCollectionViewAction)) + view.addGestureRecognizer(tap) + return view + }() + + /// 已选 + public lazy var selectedCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 0 + layout.minimumInteritemSpacing = 0 + let collectView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) + collectView.translatesAutoresizingMaskIntoConstraints = false + collectView.backgroundColor = .clear + collectView.delegate = self + collectView.dataSource = self + collectView.allowsMultipleSelection = false + collectView.contentInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) + collectView.accessibilityIdentifier = "id.selected" + return collectView + }() + + /// 最近转发区域 + public lazy var recentContentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + + // 最近转发文案 + view.addSubview(recentLabel) + NSLayoutConstraint.activate([ + recentLabel.leftAnchor.constraint(equalTo: view.leftAnchor), + recentLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 16), + recentLabel.widthAnchor.constraint(equalToConstant: 50), + recentLabel.heightAnchor.constraint(equalToConstant: 14), + ]) + + view.addSubview(recentCollectionView) + NSLayoutConstraint.activate([ + recentCollectionView.topAnchor.constraint(equalTo: recentLabel.bottomAnchor, constant: 12), + recentCollectionView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: -12), + recentCollectionView.rightAnchor.constraint(equalTo: view.rightAnchor), + recentCollectionView.heightAnchor.constraint(equalToConstant: 84), + ]) + + // 分割线 + let dividerLine = UIView() + dividerLine.translatesAutoresizingMaskIntoConstraints = false + dividerLine.backgroundColor = UIColor.ne_backcolor + view.addSubview(dividerLine) + NSLayoutConstraint.activate([ + dividerLine.bottomAnchor.constraint(equalTo: view.bottomAnchor), + dividerLine.leftAnchor.constraint(equalTo: view.leftAnchor, constant: -20), + dividerLine.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 20), + dividerLine.heightAnchor.constraint(equalToConstant: 3), + ]) + + return view + }() + + /// 最近转发文案 + lazy var recentLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = localizable("recent_forward") + label.font = .systemFont(ofSize: 12) + label.textColor = UIColor(hexString: "#B3B7BC") + return label + }() + + /// 最近转发 + public lazy var recentCollectionView: UICollectionView = { + let layout = UICollectionViewFlowLayout() + layout.scrollDirection = .horizontal + layout.minimumLineSpacing = 0 + layout.minimumInteritemSpacing = 0 + let collectView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) + collectView.translatesAutoresizingMaskIntoConstraints = false + collectView.backgroundColor = .clear + collectView.delegate = self + collectView.dataSource = self + collectView.allowsMultipleSelection = false + collectView.contentInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) + collectView.accessibilityIdentifier = "id.selected" + return collectView + }() + + /// 最近会话 + lazy var recentButton: UIButton = { + let button = UIButton() + button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .semibold) + button.setTitle(localizable("recent_session"), for: .normal) + button.setTitleColor(UIColor.ne_darkText, for: .normal) + button.setTitleColor(themeColor, for: .highlighted) + button.setTitleColor(themeColor, for: .selected) + button.backgroundColor = .white + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(recentButtonAction), for: .touchUpInside) + button.accessibilityIdentifier = "id.recentButton" + button.isSelected = true + return button + }() + + /// 我的好友 + lazy var friendButton: UIButton = { + let button = UIButton() + button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .semibold) + button.setTitle(localizable("my_friends"), for: .normal) + button.setTitleColor(UIColor.ne_darkText, for: .normal) + button.setTitleColor(themeColor, for: .highlighted) + button.setTitleColor(themeColor, for: .selected) + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = .white + button.addTarget(self, action: #selector(friendButtonAction), for: .touchUpInside) + button.accessibilityIdentifier = "id.friendButton" + return button + }() + + /// 我的群聊 + lazy var teamButton: UIButton = { + let button = UIButton() + button.titleLabel?.font = UIFont.systemFont(ofSize: 14, weight: .semibold) + button.setTitle(localizable("my_teams"), for: .normal) + button.setTitleColor(UIColor.ne_darkText, for: .normal) + button.setTitleColor(themeColor, for: .highlighted) + button.setTitleColor(themeColor, for: .selected) + button.translatesAutoresizingMaskIntoConstraints = false + button.backgroundColor = .white + button.addTarget(self, action: #selector(teamButtonAction), for: .touchUpInside) + button.accessibilityIdentifier = "id.teamButton" + return button + }() + + /// 取消按钮 + lazy var cancelButton: ExpandButton = { + let button = ExpandButton(type: .custom) + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityIdentifier = "id.cancel" + button.setTitle(localizable("alert_cancel"), for: .normal) + button.setTitleColor(.ne_greyText, for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 16) + button.addTarget(self, action: #selector(cancelButtonAction), for: .touchUpInside) + return button + }() + + /// 多选按钮 + lazy var multiSelectButton: ExpandButton = { + let button = ExpandButton(type: .custom) + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityIdentifier = "id.multiSelect" + button.setTitle(localizable("multi_select"), for: .normal) + button.setTitleColor(.ne_greyText, for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 16) + button.contentHorizontalAlignment = .right + button.addTarget(self, action: #selector(multiSelectButtonAction), for: .touchUpInside) + return button + }() + + /// 确定按钮 + lazy var sureButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityIdentifier = "id.sureButton" + button.setTitle(sureButtonText, for: .normal) + button.setTitleColor(UIColor.ne_normalTheme, for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 16) + button.layer.cornerRadius = 4 + button.addTarget(self, action: #selector(sureButtonAction), for: .touchUpInside) + button.isHidden = true + button.isEnabled = false + return button + }() + + /// Tab 选择区域(最近会话 | 我的好友 | 我的群聊) + public lazy var tabContentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + + let tabButtonWidth = IMKitConfigCenter.shared.teamEnable ? NEConstant.screenWidth / 3.0 : NEConstant.screenWidth / 2.0 + + view.addSubview(recentButton) + NSLayoutConstraint.activate([ + recentButton.topAnchor.constraint(equalTo: view.topAnchor), + recentButton.leftAnchor.constraint(equalTo: view.leftAnchor), + recentButton.heightAnchor.constraint(equalToConstant: 48), + recentButton.widthAnchor.constraint(equalToConstant: tabButtonWidth), + ]) + + view.addSubview(friendButton) + NSLayoutConstraint.activate([ + friendButton.topAnchor.constraint(equalTo: recentButton.topAnchor), + friendButton.leftAnchor.constraint(equalTo: recentButton.rightAnchor), + friendButton.widthAnchor.constraint(equalTo: recentButton.widthAnchor), + friendButton.heightAnchor.constraint(equalTo: recentButton.heightAnchor), + ]) + + if IMKitConfigCenter.shared.teamEnable { + view.addSubview(teamButton) + NSLayoutConstraint.activate([ + teamButton.topAnchor.constraint(equalTo: recentButton.topAnchor), + teamButton.leftAnchor.constraint(equalTo: friendButton.rightAnchor), + teamButton.widthAnchor.constraint(equalTo: recentButton.widthAnchor), + teamButton.heightAnchor.constraint(equalTo: recentButton.heightAnchor), + ]) + } + + // 未选中下划线 + var selectLine: UIView = .init() + selectLine.translatesAutoresizingMaskIntoConstraints = false + selectLine.backgroundColor = .ne_navLineColor + view.addSubview(selectLine) + NSLayoutConstraint.activate([ + selectLine.leftAnchor.constraint(equalTo: view.leftAnchor), + selectLine.rightAnchor.constraint(equalTo: view.rightAnchor), + selectLine.bottomAnchor.constraint(equalTo: recentButton.bottomAnchor, constant: 0), + selectLine.heightAnchor.constraint(equalToConstant: 1), + ]) + + // 选中下划线 + var selectedLine: UIView = .init() + selectedLine.translatesAutoresizingMaskIntoConstraints = false + selectedLine.backgroundColor = themeColor + view.addSubview(selectedLine) + selectedLineLeftAnchor = selectedLine.leftAnchor.constraint(equalTo: view.leftAnchor) + NSLayoutConstraint.activate([ + selectedLine.bottomAnchor.constraint(equalTo: recentButton.bottomAnchor, constant: 0), + selectedLine.heightAnchor.constraint(equalToConstant: 2), + selectedLine.widthAnchor.constraint(equalTo: recentButton.widthAnchor), + selectedLineLeftAnchor!, + ]) + + return view + }() + + // 【最近会话】的 tableView + public lazy var recentTableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.sectionIndexColor = .ne_greyText + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0 + } + return tableView + }() + + // 【我的好友】的 tableView + public lazy var friendTableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.sectionIndexColor = .ne_greyText + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0 + } + tableView.isHidden = true + return tableView + }() + + // 【我的群聊】的 tableView + public lazy var teamTableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.sectionIndexColor = .ne_greyText + tableView.delegate = self + tableView.dataSource = self + tableView.separatorStyle = .none + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0 + } + tableView.isHidden = true + return tableView + }() + + /// tableView 背景视图 + public lazy var tableViewContentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(recentTableView) + NSLayoutConstraint.activate([ + recentTableView.topAnchor.constraint(equalTo: view.topAnchor), + recentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + recentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + recentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + view.addSubview(friendTableView) + NSLayoutConstraint.activate([ + friendTableView.topAnchor.constraint(equalTo: view.topAnchor), + friendTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + friendTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + friendTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + view.addSubview(teamTableView) + NSLayoutConstraint.activate([ + teamTableView.topAnchor.constraint(equalTo: view.topAnchor), + teamTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + teamTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + teamTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + return view + }() + + /// 会话选择区域,包含 tab 和 tableView + public lazy var sessionContentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + + view.addSubview(emptyView) + NSLayoutConstraint.activate([ + emptyView.topAnchor.constraint(equalTo: view.topAnchor, constant: -40), + emptyView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + emptyView.leftAnchor.constraint(equalTo: view.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) + + view.addSubview(tabContentView) + NSLayoutConstraint.activate([ + tabContentView.leftAnchor.constraint(equalTo: view.leftAnchor), + tabContentView.rightAnchor.constraint(equalTo: view.rightAnchor), + tabContentView.topAnchor.constraint(equalTo: view.topAnchor), + tabContentView.heightAnchor.constraint(equalToConstant: 48), + ]) + + view.addSubview(tableViewContentView) + NSLayoutConstraint.activate([ + tableViewContentView.topAnchor.constraint(equalTo: view.topAnchor, constant: 48), + tableViewContentView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableViewContentView.rightAnchor.constraint(equalTo: view.rightAnchor), + tableViewContentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + return view + }() + + // MARK: - Action + + /// 搜索文案变动 + /// - Parameter textfield: 搜索框 + func searchTextFieldChange(textfield: SearchTextField) { + guard let searchText = textfield.text else { + return + } + + self.searchText = searchText + recentContentView.isHidden = recentArray.isEmpty || !searchText.isEmpty + recentContentViewHeightAnchor?.constant = recentContentView.isHidden ? 16 : recentContentViewHeight + + if searchText.isEmpty { + loadData(tabIndex) + return + } + + let textRange = textfield.markedTextRange + if textRange == nil || ((textRange?.isEmpty) == nil) { + loadData(tabIndex) + } + } + + /// 多选按钮点击事件 + /// - Parameter sender: 按钮 + open func multiSelectButtonAction(_ sender: UIButton) { + isMultiSelect = true + multiSelectButton.isHidden = true + sureButton.isHidden = false + recentCollectionView.reloadData() + currentTableView?.reloadData() + } + + /// 确认按钮点击事件 + /// - Parameter sender: 按钮 + open func sureButtonAction() { + // 校验网络 + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + // 如果是单选则从已选列表中移除 + if !isMultiSelect { + selectedArray.first?.isSelected = false + selectedArray.removeAll() + } + return + } + + var conversationJSONs = [[String: Any]]() + for model in selectedArray { + var conversationJSON = [String: Any]() + conversationJSON["conversationId"] = model.conversationId + conversationJSON["name"] = model.name + conversationJSON["avatar"] = model.avatar + conversationJSONs.append(conversationJSON) + } + + Router.shared.use(ForwardMultiSelectedRouter, parameters: ["conversations": conversationJSONs], closure: nil) + + cancelButtonAction() + } + + /// 取消按钮点击事件 + /// - Parameter button: 按钮 + func cancelButtonAction() { + dismiss(animated: true, completion: nil) + } + + /// 获取已选视图控制器 + /// - Parameter selectedArray: 已选列表 + /// - Returns: 已选页面的视图控制器 + open func getMultiSelectedViewController(_ selectedArray: [MultiSelectModel]) -> NEBaseMultiSelectedViewController { + NEBaseMultiSelectedViewController(selectedArray: selectedArray) + } + + /// 已选视图点击事件 + func selectedCollectionViewAction() { + let selectedVC = getMultiSelectedViewController(selectedArray) + selectedVC.delegate = self + present(selectedVC, animated: true) + } + + /// tab 点击事件 + /// - Parameter index: tab 下标 + func setTabButtonLine(_ index: Int) { + tabIndex = index + selectedLineLeftAnchor?.constant = recentButton.width * CGFloat(index) + + // 设置 tab 按钮文案颜色 + for (i, view) in tabContentView.subviews.enumerated() { + if let button = view as? UIButton { + button.isSelected = i == index + } + } + + // 设置 UITableView 显隐 + for (i, view) in tableViewContentView.subviews.enumerated() { + if let tableView = view as? UITableView { + if i == index { + currentTableView = tableView + } + tableView.isHidden = i != index + } + } + + UIView.animate(withDuration: 0.5) { + self.view.layoutIfNeeded() + } + + loadData(index) + } + + /// 设置空白占位图文案 + /// - Parameter text: 搜索文案 + func setEmptyViewText(_ text: String?) { + if let searchText = searchText, !searchText.isEmpty { + let att = NSMutableAttributedString(string: String(format: commonLocalizable("no_search_result"), searchText)) + let range = att.mutableString.range(of: searchText) + att.addAttribute(.foregroundColor, value: themeColor as Any, range: range) + emptyView.setAttributedText(att) + return + } + + switch tabIndex { + case 1: + emptyView.setText(localizable("no_friend")) + case 2: + emptyView.setText(localizable("team_empty")) + default: + emptyView.setText(localizable("session_empty")) + } + } + + /// 最近会话 tab 点击事件 + func recentButtonAction() { + if tabIndex == 0 { + return + } + + setTabButtonLine(0) + } + + /// 我的好友 tab 点击事件 + func friendButtonAction() { + if tabIndex == 1 { + return + } + + setTabButtonLine(1) + } + + /// 我的群聊 tab 点击事件 + func teamButtonAction() { + if tabIndex == 2 { + return + } + + setTabButtonLine(2) + } + + // MARK: - Table View DataSource And Delegate + + open func numberOfSections(in tableView: UITableView) -> Int { + if tableView == currentTableView { + return 1 + } + + return 0 + } + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if tableView == currentTableView { + return viewModel.sessions.count + } + + return 0 + } + + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if tableView == currentTableView { + let info = viewModel.sessions[indexPath.row] + let cell = tableView.dequeueReusableCell( + withIdentifier: "\(NSStringFromClass(NEBaseSelectCell.self))", + for: indexPath + ) as! SelectCell + cell.showSelect(isMultiSelect) + cell.setModel(info) + cell.searchText = searchText + return cell + } + + return UITableViewCell() + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let info = viewModel.sessions[indexPath.row] + if info.isSelected == true { + didUnselectContact(info) + } else { + if selectedArray.count >= limit { + view.makeToast(String(format: localizable("choose_max_limit"), limit)) + return + } + didSelectContact(info) + } + + // 选中后收起搜索键盘 + view.endEditing(true) + } + + // MARK: - UIScrollViewDelegate + + /// 监听滚动 + /// - Parameter scrollView: 滚动视图 + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + // 滚动时收起搜索键盘 + view.endEditing(true) + } + + // MARK: Collection View DataSource And Delegate + + open func collectionView(_ collectionView: UICollectionView, + numberOfItemsInSection section: Int) -> Int { + if collectionView == recentCollectionView { + return recentArray.count + } else if collectionView == selectedCollectionView { + return selectedArray.count + } else { + return 0 + } + } + + open func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + if collectionView == recentCollectionView { + return CGSize(width: 72, height: isMultiSelect ? 84 : 60) + } else if collectionView == selectedCollectionView { + return CGSize(width: 46, height: selectedContentViewHeight) + } else { + return CGSize.zero + } + } + + open func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + var contactInfo = MultiSelectModel() + + if collectionView == recentCollectionView { + contactInfo = recentArray[indexPath.row] + } else if collectionView == selectedCollectionView { + contactInfo = selectedArray[indexPath.row] + } + + let cell = collectionView.dequeueReusableCell( + withReuseIdentifier: "\(NSStringFromClass(NEBaseSelectedCell.self))", + for: indexPath + ) as? NEBaseSelectedCell + cell?.configure(contactInfo) + + // 最近转发中的 cell + if let c = cell as? NEBaseRecentSelectCell { + c.showSelect(isMultiSelect) + } + + return cell ?? UICollectionViewCell() + } + + open func collectionView(_ collectionView: UICollectionView, + didSelectItemAt indexPath: IndexPath) { + var contactInfo = MultiSelectModel() + + if collectionView == recentCollectionView { + contactInfo = recentArray[indexPath.row] + if contactInfo.isSelected { + didUnselectContact(contactInfo) + } else { + didSelectContact(contactInfo) + } + } else if collectionView == selectedCollectionView { + contactInfo = selectedArray[indexPath.row] + didUnselectContact(contactInfo) + } + } + + /// 选中事件 + /// - Parameter model: 数据模型 + func didSelectContact(_ model: MultiSelectModel) { + model.isSelected = true + if selectedArray.contains(where: { c in + model === c + }) == false { + selectedArray.append(model) + + // 单选点击直接弹窗 + if !isMultiSelect { + sureButtonAction() + return + } + + if (selectedContentViewHeightAnchor?.constant ?? 0) <= 0 { + selectedContentView.isHidden = false + selectedContentViewHeightAnchor?.constant = selectedContentViewHeight + } + } + + recentCollectionView.reloadData() + selectedCollectionView.reloadData() + currentTableView?.reloadData() + refreshSelectCount() + } + + /// 取消选中事件 + /// - Parameter model: 数据模型 + func didUnselectContact(_ model: MultiSelectModel) { + model.isSelected = false + selectedArray.removeAll { c in + model === c + } + if selectedArray.count <= 0 { + selectedContentView.isHidden = true + selectedContentViewHeightAnchor?.constant = 0 + } + + recentCollectionView.reloadData() + selectedCollectionView.reloadData() + currentTableView?.reloadData() + refreshSelectCount() + } + + /// 刷新(确定按钮)已选人数 + func refreshSelectCount() { + if selectedArray.count > 0 { + sureButton.isEnabled = true + sureButton.setTitle(sureButtonText + "(\(selectedArray.count))", for: .normal) + } else { + sureButton.isEnabled = false + sureButton.setTitle(sureButtonText, for: .normal) + } + } +} + +extension NEBaseMultiSelectViewController: NEBaseMultiSelectedViewControllerDelegate { + /// 移除按钮点击事件 + /// - Parameter model: 数据模型 + public func removeButtonAction(_ model: MultiSelectModel?) { + if let m = model { + didUnselectContact(m) + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectedViewController.swift new file mode 100644 index 00000000..29f88b9f --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewController/NEBaseMultiSelectedViewController.swift @@ -0,0 +1,156 @@ + +// 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 NECoreKit +import NIMSDK +import UIKit + +/// 转发多选-已选页面-协议 +public protocol NEBaseMultiSelectedViewControllerDelegate: NSObjectProtocol { + /// 移除按钮点击事件 + /// - Parameter model: 数据模型 + func removeButtonAction(_ model: MultiSelectModel?) +} + +/// 转发多选-已选页面-基类 +@objcMembers +open class NEBaseMultiSelectedViewController: NEContactBaseViewController, UITableViewDelegate, UITableViewDataSource { + public var selectedArray = [MultiSelectModel]() // 已选列表 + public var tableViewTopAnchor: NSLayoutConstraint? // tableView的top约束 + public weak var delegate: NEBaseMultiSelectedViewControllerDelegate? + + init(selectedArray: [MultiSelectModel] = [MultiSelectModel]()) { + self.selectedArray = selectedArray + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + commonUI() + } + + /// 设置标题 + func setupTitle() { + navigationController?.isNavigationBarHidden = true + navigationView.isHidden = true + + // 取消按钮 + view.addSubview(cancelButton) + NSLayoutConstraint.activate([ + cancelButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + cancelButton.topAnchor.constraint(equalTo: view.topAnchor, constant: 16), + cancelButton.widthAnchor.constraint(equalToConstant: 40), + cancelButton.heightAnchor.constraint(equalToConstant: 18), + ]) + + // 标题文案 + let titleLabel = UILabel() + titleLabel.translatesAutoresizingMaskIntoConstraints = false + titleLabel.text = localizable("selected") + titleLabel.font = UIFont.systemFont(ofSize: 16, weight: .semibold) + titleLabel.textAlignment = .center + titleLabel.textColor = .ne_darkText + view.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 16), + titleLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), + titleLabel.heightAnchor.constraint(equalToConstant: 18), + ]) + } + + func commonUI() { + view.backgroundColor = .white + setupTitle() + + view.addSubview(tableView) + tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: 58) + tableViewTopAnchor?.isActive = true + NSLayoutConstraint.activate([ + tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.rightAnchor.constraint(equalTo: view.rightAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -80), + ]) + + tableView.register(SelectedListCell.self, forCellReuseIdentifier: "\(NSStringFromClass(NEBaseSelectedListCell.self))") + + view.addSubview(emptyView) + emptyView.setText(commonLocalizable("no_content")) + NSLayoutConstraint.activate([ + emptyView.topAnchor.constraint(equalTo: view.topAnchor, constant: -60), + emptyView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + emptyView.leftAnchor.constraint(equalTo: view.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) + } + + /// 取消按钮 + lazy var cancelButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityIdentifier = "id.cancel" + button.setTitle(localizable("alert_cancel"), for: .normal) + button.setTitleColor(.ne_greyText, for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 16) + button.addTarget(self, action: #selector(cancelButtonClick), for: .touchUpInside) + return button + }() + + lazy var tableView: UITableView = { + var tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + return tableView + }() + + // MARK: - Action + + /// 取消按钮点击事件 + func cancelButtonClick() { + dismiss(animated: true, completion: nil) + } + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + selectedArray.count + } + + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let model = selectedArray[indexPath.row] + + let cell = tableView.dequeueReusableCell(withIdentifier: "\(NSStringFromClass(NEBaseSelectedListCell.self))", for: indexPath) as! NEBaseSelectedListCell + cell.setModel(model) + cell.delegate = self + return cell + } + + public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + 62 + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let model = selectedArray[indexPath.row] + print("didSelectRowAt: \(model)") + } +} + +// MARK: - NEBaseSelectedListCellDelegate + +extension NEBaseMultiSelectedViewController: NEBaseSelectedListCellDelegate { + /// 移除按钮点击事件 + /// - Parameter model: 数据模型 + func removeButtonAction(_ model: MultiSelectModel?) { + selectedArray.removeAll { $0 == model } + emptyView.isHidden = !selectedArray.isEmpty + tableView.reloadData() + + delegate?.removeButtonAction(model) + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewModel/MultiSelectViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewModel/MultiSelectViewModel.swift new file mode 100644 index 00000000..6b8b5acf --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Multiselect/ViewModel/MultiSelectViewModel.swift @@ -0,0 +1,256 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIM2Kit +import UIKit + +@objcMembers +open class MultiSelectViewModel: ContactViewModel { + public var conversationRepo = ConversationRepo.shared + public var teamRepo = TeamRepo.shared + public var settingRepo = SettingRepo.shared + + var conversationLimit = 100 // 最近会话最大数量 + var conversationList = [MultiSelectModel]() // 最近会话缓存 + var contactList = [MultiSelectModel]() // 好友缓存 + var teamList = [MultiSelectModel]() // 群组缓存 + public var sessions = [MultiSelectModel]() // 当前展示列表 + + /// 初始化 + init() { + super.init(contactHeaders: nil) + } + + /// 加载所有数据 + /// - Parameters: + /// - filters: 需要过滤的会话id列表 + /// - completion: 完成回调 + func loadAllData(_ filters: Set? = nil, _ completion: @escaping (NSError?) -> Void) { + // 加载群聊 + loadData(2, filters) { [weak self] error in + if let err = error { + self?.sessions.removeAll() + completion(err) + return + } + + // 加载好友 + self?.loadData(1, filters) { error in + if let err = error { + self?.sessions.removeAll() + completion(err) + return + } + + // 加载最近会话 + self?.loadData(0, filters, completion) + } + } + } + + /// 加载数据 + /// - Parameters: + /// - index: 0 - 最近会话;1 - 我的好友;2 - 我的群聊 + /// - filters: 需要过滤的会话id列表 + /// - completion: 完成回调 + func loadData(_ index: Int, _ filters: Set? = nil, _ completion: @escaping (NSError?) -> Void) { + sessions.removeAll() + if index == 0 { + if conversationList.isEmpty { + getConversationList(filters) { [weak self] error in + if let conversationList = self?.conversationList { + self?.sessions = conversationList + } + completion(error) + } + } else { + sessions = conversationList + completion(nil) + } + } else if index == 1 { + if contactList.isEmpty { + getContactList(filters) { [weak self] error in + if let contactList = self?.contactList { + self?.sessions = contactList + } + completion(error) + } + } else { + sessions = contactList + completion(nil) + } + } else if index == 2 { + if teamList.isEmpty { + getTeamList(filters) { [weak self] error in + if let teamList = self?.teamList { + self?.sessions = teamList + } + completion(error) + } + } else { + sessions = teamList + completion(nil) + } + } + } + + /// 加载最近转发 + /// - Returns: 最近转发列表 + func loadRecentForward() -> [MultiSelectModel] { + var recentSessions = [MultiSelectModel]() + + var recentList = settingRepo.getRecentForward() ?? [] + for recent in recentList { + // 从最近会话中查找对应的会话 + if let session = conversationList.first(where: { $0.conversationId == recent }) { + recentSessions.append(session) + continue + } + + // 从我的好友中查找对应的会话 + if let session = contactList.first(where: { $0.conversationId == recent }) { + recentSessions.append(session) + continue + } + + // 从我的群聊中查找对应的会话 + if let session = teamList.first(where: { $0.conversationId == recent }) { + recentSessions.append(session) + continue + } + + // 移除失效的会话 + recentList.removeAll(where: { $0 == recent }) + settingRepo.updateRecentForward(recentList) + } + + return recentSessions + } + + /// 获取会话列表 + /// - Parameters: + /// - filters: 需要过滤的会话id列表 + /// - completion: 完成回调 + func getConversationList(_ filters: Set? = nil, _ completion: @escaping (NSError?) -> Void) { + conversationRepo.getConversationList(0, conversationLimit) { [weak self] conversations, offset, finished, error in + if let error = error { + NEALog.errorLog(ModuleName + " " + MultiSelectViewModel.className(), desc: #function + ", error: " + error.localizedDescription) + } else if var conversations = conversations { + // 过滤 + if let filterConvs = filters { + conversations = conversations.filter { !filterConvs.contains($0.conversationId) } + } + + for conversation in conversations { + let model = MultiSelectModel() + + // 校验好友是否已存在 + if let model = self?.findFriend(conversation.conversationId) { + self?.conversationList.append(model) + continue + } + + // 校验群聊是否已存在 + if let model = self?.findTeam(conversation.conversationId) { + self?.conversationList.append(model) + continue + } + + model.conversationId = conversation.conversationId + model.name = conversation.name + model.avatar = conversation.avatar + self?.conversationList.append(model) + } + } + completion(error) + } + } + + /// 获取好友列表 + /// - Parameters: + /// - filters: 需要过滤的好友id列表 + /// - completion: 完成回调 + func getContactList(_ filters: Set? = nil, _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", filters.count: \(filters?.count ?? 0)") + + getContactList(filters) { [weak self] result, error in + for item in result ?? [] { + for contact in item.contacts { + let conversationId = V2NIMConversationIdUtil.p2pConversationId(contact.user?.user?.accountId ?? "") + let model = MultiSelectModel() + model.conversationId = conversationId + model.avatar = contact.user?.user?.avatar + model.name = contact.user?.showName() ?? "" + self?.contactList.append(model) + } + } + completion(error) + } + } + + /// 获取群聊列表 + /// - Parameters: + /// - filters: 需要过滤的群id列表 + /// - completion: 完成回调 + func getTeamList(_ filters: Set? = nil, _ completion: @escaping (NSError?) -> Void) { + teamRepo.getTeamList { [weak self] teams, error in + if let error = error { + NEALog.errorLog(ModuleName + " " + MultiSelectViewModel.className(), desc: #function + ", error: " + error.localizedDescription) + } else if var teams = teams { + // 过滤 + if let filterTeams = filters { + teams = teams.filter { !filterTeams.contains($0.teamId ?? "") } + } + + teams.sort(by: { team1, team2 in + (team1.createTime ?? 0) > (team2.createTime ?? 0) + }) + + for team in teams { + let conversationId = V2NIMConversationIdUtil.teamConversationId(team.teamId ?? "") + let model = MultiSelectModel() + model.conversationId = conversationId + model.name = team.teamName + model.avatar = team.avatarUrl + model.memberCount = team.memberNumber ?? 0 + self?.teamList.append(model) + } + + completion(nil) + } + } + } + + /// 在好友缓存中查找该会话 + /// - Parameter conversationId: 会话id + /// - Returns: 存在则返回该回话,不存在返回 nil + func findFriend(_ conversationId: String?) -> MultiSelectModel? { + for conv in contactList { + if conv.conversationId == conversationId { + return conv + } + } + return nil + } + + /// 在群聊缓存中查找该会话 + /// - Parameter conversationId: 会话id + /// - Returns: 存在则返回该回话,不存在返回 nil + func findTeam(_ conversationId: String?) -> MultiSelectModel? { + for conv in teamList { + if conv.conversationId == conversationId { + return conv + } + } + return nil + } + + func searchText(_ text: String) { + if text.isEmpty { + return + } + sessions = sessions.filter { $0.name?.contains(text) == true } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/BlackListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/BlackListCell.swift index 073bd43e..52fb15a8 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/BlackListCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/BlackListCell.swift @@ -2,14 +2,21 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers open class BlackListCell: NEBaseBlackListCell { + private lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.ne_greyLine + return view + }() + override func commonUI() { super.commonUI() - avatarImage.layer.cornerRadius = 21 + avatarImageView.layer.cornerRadius = 21 titleLabel.font = UIFont.systemFont(ofSize: 16) titleLabel.textColor = UIColor( @@ -31,11 +38,4 @@ open class BlackListCell: NEBaseBlackListCell { bottomLine.heightAnchor.constraint(equalToConstant: 1), ]) } - - private lazy var bottomLine: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.ne_greyLine - return view - }() } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactSelectedCell.swift index bfc10d15..4bfc8cb7 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactSelectedCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactSelectedCell.swift @@ -4,14 +4,15 @@ // found in the LICENSE file. import UIKit + @objcMembers open class ContactSelectedCell: NEBaseContactSelectedCell { override open func commonUI() { super.commonUI() - sImage.highlightedImage = UIImage.ne_imageNamed(name: "select") + sImageView.highlightedImage = UIImage.ne_imageNamed(name: "select") NSLayoutConstraint.activate([ - sImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - sImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + sImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + sImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), ]) } @@ -23,9 +24,9 @@ open class ContactSelectedCell: NEBaseContactSelectedCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 36), - avatarImage.heightAnchor.constraint(equalToConstant: 36), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 36), + avatarImageView.heightAnchor.constraint(equalToConstant: 36), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactTableViewCell.swift index ba12e5db..04dac9d2 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactTableViewCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -23,9 +23,9 @@ open class ContactTableViewCell: NEBaseContactTableViewCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 36), - avatarImage.heightAnchor.constraint(equalToConstant: 36), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 36), + avatarImageView.heightAnchor.constraint(equalToConstant: 36), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactUnCheckCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactUnCheckCell.swift index f7950ca4..516f2812 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactUnCheckCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/ContactUnCheckCell.swift @@ -9,12 +9,30 @@ import UIKit open class ContactUnCheckCell: NEBaseContactUnCheckCell { override func setupUI() { super.setupUI() - avatarImage.layer.cornerRadius = 18 + avatarImageView.layer.cornerRadius = 18 NSLayoutConstraint.activate([ - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - avatarImage.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - avatarImage.widthAnchor.constraint(equalToConstant: 36), - avatarImage.heightAnchor.constraint(equalToConstant: 36), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + avatarImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: 36), + avatarImageView.heightAnchor.constraint(equalToConstant: 36), ]) } + + func setAvatarWH(_ height: CGFloat) { + avatarImageView.layer.cornerRadius = height / 2 + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .width, constant: height) + avatarImageView.updateLayoutConstraint(firstItem: avatarImageView, seconedItem: nil, attribute: .height, constant: height) + } + + /// 重写控件赋值方法 + /// - Parameter model: 数据模型(ContactInfo) + override func configure(_ model: Any) { + guard let model = model as? ContactInfo else { return } + + avatarImageView.configHeadData( + headUrl: model.user?.user?.avatar, + name: model.user?.showName() ?? "", + uid: model.user?.user?.accountId ?? "" + ) + } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/RecentSelectCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/RecentSelectCell.swift new file mode 100644 index 00000000..738747eb --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/RecentSelectCell.swift @@ -0,0 +1,24 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +/// 转发-选择页面-最近转发 CollectionViewCell -协同版 +@objcMembers +open class RecentSelectCell: NEBaseRecentSelectCell { + /// 重写布局方法 + override func setupUI() { + super.setupUI() + setAvatarWH(36) + } + + /// 重写控件赋值方法 + /// - Parameter model: 数据模型(MultiSelectModel) + override func configure(_ model: Any) { + guard let model = model as? MultiSelectModel else { return } + + super.configure(model) + selectImageView.image = model.isSelected ? UIImage.ne_imageNamed(name: "select") : UIImage.ne_imageNamed(name: "unselect") + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SelectCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SelectCell.swift new file mode 100644 index 00000000..b0f685be --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SelectCell.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 + +/// 转发-选择页面 TableViewCell -协同版 +@objcMembers +open class SelectCell: NEBaseSelectCell { + /// 重写初始化方法 + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + searchTextColor = UIColor.ne_normalTheme + } + + /// 重写初始化方法 + public required init?(coder: NSCoder) { + super.init(coder: coder) + searchTextColor = UIColor.ne_normalTheme + } + + /// 重写布局方法 + override open func commonUI() { + super.commonUI() + multiSelectImageView.highlightedImage = UIImage.ne_imageNamed(name: "select") + } + + /// 重写设置文案字体方案 + override open func setConfig() { + super.setConfig() + titleLabel.font = .systemFont(ofSize: NEKitContactConfig.shared.ui.contactProperties.itemTitleSize > 0 ? NEKitContactConfig.shared.ui.contactProperties.itemTitleSize : 14) + } + + /// 重写设置头像方法 + override open func setupCommonCircleHeader() { + super.setupCommonCircleHeader() + avatarImageView.layer.cornerRadius = 18 + NSLayoutConstraint.activate([ + avatarImageView.widthAnchor.constraint(equalToConstant: 36), + avatarImageView.heightAnchor.constraint(equalToConstant: 36), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + ]) + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SelectedCell.swift new file mode 100644 index 00000000..85b4ea44 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SelectedCell.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 NECommonUIKit +import UIKit + +/// 转发-选择页面-已选 CollectionViewCell -协同版 +@objcMembers +open class SelectedCell: NEBaseSelectedCell { + /// 重写布局方法 + override func setupUI() { + super.setupUI() + avatarImageView.layer.cornerRadius = 16 + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SelectedListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SelectedListCell.swift new file mode 100644 index 00000000..7e765487 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SelectedListCell.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 NEChatKit +import NECoreIM2Kit +import UIKit + +/// 转发-多选-已选详情页面 TableViewCell -协同版 +@objcMembers +open class SelectedListCell: NEBaseSelectedListCell { + /// 重写设置头像方法 + override open func setupCommonCircleHeader() { + super.setupCommonCircleHeader() + avatarImageView.layer.cornerRadius = 21 + NSLayoutConstraint.activate([ + avatarImageView.widthAnchor.constraint(equalToConstant: 42), + avatarImageView.heightAnchor.constraint(equalToConstant: 42), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + ]) + } + + /// 重写布局方法 + override open func commonUI() { + super.commonUI() + bottomLineLeftConstraint?.constant = 74 + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SystemNotificationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SystemNotificationCell.swift index e01d5f87..245430af 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SystemNotificationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/SystemNotificationCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -13,11 +13,11 @@ import UIKit open class SystemNotificationCell: NEBaseSystemNotificationCell { override open func setupCommonCircleHeader() { super.setupCommonCircleHeader() - avatarImage.layer.cornerRadius = 18 + avatarImageView.layer.cornerRadius = 18 NSLayoutConstraint.activate([ - avatarImage.widthAnchor.constraint(equalToConstant: 36), - avatarImage.heightAnchor.constraint(equalToConstant: 36), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.widthAnchor.constraint(equalToConstant: 36), + avatarImageView.heightAnchor.constraint(equalToConstant: 36), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/TeamTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/TeamTableViewCell.swift index 1157c9d2..02a4e9c1 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/TeamTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/Cell/TeamTableViewCell.swift @@ -2,14 +2,14 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers open class TeamTableViewCell: NEBaseTeamTableViewCell { override func commonUI() { super.commonUI() - avatarImage.layer.cornerRadius = 21 + avatarImageView.layer.cornerRadius = 21 titleLabel.font = UIFont.systemFont(ofSize: 16) titleLabel.textColor = UIColor( diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift index 20486958..57de5c31 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactRouter.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK @@ -14,7 +14,7 @@ public extension ContactRouter { print("param:\(param)") let nav = param["nav"] as? UINavigationController let filters = param["filters"] as? Set - let contactSelectVC = ContactsSelectedViewController(filterUsers: filters) + let contactSelectVC = ContactSelectedViewController(filterUsers: filters) if let limit = param["limit"] as? Int, limit > 0 { contactSelectVC.limit = limit } @@ -24,6 +24,20 @@ public extension ContactRouter { nav?.pushViewController(contactSelectVC, animated: true) } + // 转发选择页面 + Router.shared.register(ForwardMultiSelectRouter) { param in + let nav = param["nav"] as? UINavigationController + let filters = param["filters"] as? Set + let contactSelectVC = MultiSelectViewController(filterUsers: filters) + if let limit = param["limit"] as? Int, limit > 0 { + contactSelectVC.limit = limit + } + if let uid = param["uid"] as? String { + contactSelectVC.userId = uid + } + nav?.present(contactSelectVC, animated: true) + } + Router.shared.register(ContactAddFriendRouter) { param in let nav = param["nav"] as? UINavigationController let findFrined = FindFriendViewController() @@ -32,11 +46,11 @@ public extension ContactRouter { Router.shared.register(ContactUserInfoPageRouter) { param in if let nav = param["nav"] as? UINavigationController { - if let user = param["user"] as? NEKitUser { + if let user = param["user"] as? NEUserWithFriend { let userInfoVC = ContactUserViewController(user: user) nav.pushViewController(userInfoVC, animated: true) - } else if let nimUser = param["nim_user"] as? NEKitUser { - let userInfoVC = ContactUserViewController(user: nimUser) + } else if let nimUser = param["nim_user"] as? V2NIMUser { + let userInfoVC = ContactUserViewController(nim_user: nimUser) nav.pushViewController(userInfoVC, animated: true) } else if let uid = param["uid"] as? String { let userInfoVC = ContactUserViewController(uid: uid) @@ -47,7 +61,7 @@ public extension ContactRouter { Router.shared.register(ContactPageRouter) { param in if let nav = param["nav"] as? UINavigationController { - let contactVC = ContactsViewController() + let contactVC = ContactViewController() nav.pushViewController(contactVC, animated: true) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactUIColor.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactUIColor.swift new file mode 100644 index 00000000..33bad259 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/NromalContactUIColor.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 +import NECommonKit + +public extension UIColor { + static let disableButtonTitleColor = UIColor(hexString: "#DDDDDD") + /// #F2F4F5, 搜索区域背景颜色 + static let searchTextFeildBackColor = UIColor(hexString: "#F2F4F5") +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift index c390bcb2..5abc4cf0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/View/UserInfoHeaderView.swift @@ -3,14 +3,14 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers open class UserInfoHeaderView: NEBaseUserInfoHeaderView { override open func commonUI() { super.commonUI() - avatarImage.layer.cornerRadius = 30 + avatarImageView.layer.cornerRadius = 30 NSLayoutConstraint.activate([ lineView.leftAnchor.constraint(equalTo: leftAnchor), @@ -19,24 +19,4 @@ open class UserInfoHeaderView: NEBaseUserInfoHeaderView { lineView.heightAnchor.constraint(equalToConstant: 6), ]) } - - override open func setData(user: NEKitUser?) { - super.setData(user: user) - guard let u = user else { - return - } - // title - - if let alias = u.alias, !alias.isEmpty { - commonUI(showDetail: true) - titleLabel.text = alias - let uid = u.userId ?? "" - detailLabel.text = "\(localizable("nick")):\(u.userInfo?.nickName ?? uid)" - detailLabel2.text = "\(localizable("account")):\(uid)" - } else { - commonUI(showDetail: false) - titleLabel.text = u.showName() - detailLabel.text = "\(localizable("account")):\(u.userId ?? "")" - } - } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift index 093dcd78..960a6991 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/BlackListViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -12,13 +12,12 @@ import UIKit open class BlackListViewController: NEBaseBlackListViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - className = "BlackListViewController" navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func commonUI() { @@ -30,8 +29,12 @@ open class BlackListViewController: NEBaseBlackListViewController { tableView.rowHeight = 62 } - override open func getContactSelectVC() -> NEBaseContactsSelectedViewController { - ContactsSelectedViewController() + /// 黑名单选择页面 + /// - Returns: 人员选择控制器 + override open func getContactSelectVC() -> NEBaseContactSelectedViewController { + var filterUsers = Set() + filterUsers.insert(IMKitClient.instance.account()) + return ContactSelectedViewController(filterUsers: filterUsers) } override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -41,7 +44,7 @@ open class BlackListViewController: NEBaseBlackListViewController { ) as! BlackListCell cell.delegate = self cell.index = indexPath.row - cell.setModel(blackList?[indexPath.row] as Any) + cell.setModel(viewModel.blockList[indexPath.row] as Any) return cell } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactRemakNameViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactRemakNameViewController.swift index b40957d5..f2cbc56a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactRemakNameViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactRemakNameViewController.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactSelectedViewController.swift similarity index 84% rename from NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsSelectedViewController.swift rename to NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactSelectedViewController.swift index db8c3f96..028bb3d3 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactSelectedViewController.swift @@ -6,9 +6,10 @@ import NECoreKit import NIMSDK import UIKit +/// 人员选择页面 - 协同版 @objcMembers -open class ContactsSelectedViewController: NEBaseContactsSelectedViewController { - override init(filterUsers: Set? = nil) { +open class ContactSelectedViewController: NEBaseContactSelectedViewController { + override public init(filterUsers: Set? = nil) { super.init(filterUsers: filterUsers) customCells = [ContactCellType.ContactPerson.rawValue: ContactSelectedCell.self] view.backgroundColor = .ne_backcolor @@ -17,13 +18,13 @@ open class ContactsSelectedViewController: NEBaseContactsSelectedViewController } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { super.setupUI() - collection.register( + collectionView.register( ContactUnCheckCell.self, forCellWithReuseIdentifier: "\(NSStringFromClass(ContactUnCheckCell.self))" ) @@ -34,8 +35,8 @@ open class ContactsSelectedViewController: NEBaseContactsSelectedViewController super.setupNavRightItem() navigationView.moreButton.backgroundColor = .white navigationView.moreButton.setTitleColor(UIColor(hexString: "337EFF"), for: .normal) - sureBtn.backgroundColor = .white - sureBtn.setTitleColor(UIColor(hexString: "337EFF"), for: .normal) + sureButton.backgroundColor = .white + sureButton.setTitleColor(UIColor(hexString: "337EFF"), for: .normal) } override open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift index 52cd8e37..b6c66265 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactUserViewController.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -15,18 +15,23 @@ open class ContactUserViewController: NEBaseContactUserViewController { headerView = UserInfoHeaderView() } - override public init(user: NEKitUser?) { - super.init(user: user) + override public init(uid: String) { + super.init(uid: uid) initNormal() } - override public init(uid: String) { - super.init(uid: uid) + override public init(nim_user: V2NIMUser) { + super.init(nim_user: nim_user) + initNormal() + } + + override public init(user: NEUserWithFriend?) { + super.init(user: user) initNormal() } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func commonUI() { diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactViewController.swift similarity index 84% rename from NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsViewController.swift rename to NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactViewController.swift index e93b878c..0f0af79d 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactsViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ContactViewController.swift @@ -2,12 +2,12 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NEChatKit import NECoreKit import UIKit @objcMembers -open class ContactsViewController: NEBaseContactsViewController { +open class ContactViewController: NEBaseContactViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nil, bundle: nil) var contactHeaders = [ContactHeadItem]() @@ -27,9 +27,9 @@ open class ContactsViewController: NEBaseContactsViewController { ), ] - if IMKitClient.instance.getConfigCenter().teamEnable { + if IMKitConfigCenter.shared.teamEnable { contactHeaders.append(ContactHeadItem( - name: localizable("mine_groupchat"), + name: localizable("my_teams"), imageName: "group", router: ContactTeamListRouter, color: UIColor(hexString: "#BE65D9") @@ -59,7 +59,7 @@ open class ContactsViewController: NEBaseContactsViewController { forHeaderFooterViewReuseIdentifier: "\(NSStringFromClass(ContactSectionView.self))" ) - cellRegisterDic.forEach { (key: Int, value: NEBaseContactTableViewCell.Type) in + for (key, value) in cellRegisterDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } @@ -71,17 +71,31 @@ open class ContactsViewController: NEBaseContactsViewController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let info = viewModel.contacts[indexPath.section].contacts[indexPath.row] - var reusedId = "\(info.contactCellType)" + let reusedId = "\(info.contactCellType)" let cell = tableView.dequeueReusableCell(withIdentifier: reusedId, for: indexPath) if let c = cell as? ContactTableViewCell { + if IMKitConfigCenter.shared.onlineStatusEnable { + if indexPath.section != 0 { + c.avatarImageView.alpha = 0.5 + if let accountId = info.user?.user?.accountId { + if let event = viewModel.onlineStatusDic[accountId] { + if event.value == NIMSubscribeEventOnlineValue.login.rawValue { + c.avatarImageView.alpha = 1.0 + } + } + } + } else { + c.avatarImageView.alpha = 1.0 + } + } return configCell(info: info, c, indexPath) } return cell } } -extension ContactsViewController { +extension ContactViewController { override open func initSystemNav() { super.initSystemNav() let addItem = UIBarButtonItem( diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift index a5a6f29e..1b27b893 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/FindFriendViewController.swift @@ -13,6 +13,6 @@ open class FindFriendViewController: NEBaseFindFriendViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/MultiSelectViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/MultiSelectViewController.swift new file mode 100644 index 00000000..e9ef4137 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/MultiSelectViewController.swift @@ -0,0 +1,63 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIM2Kit +import NECoreKit +import NIMSDK +import UIKit + +/// 转发 - 选择页面 - 协同版 +@objcMembers +open class MultiSelectViewController: NEBaseMultiSelectViewController { + override init(filterUsers: Set? = nil) { + super.init(filterUsers: filterUsers) + + searchTextField.backgroundColor = .searchTextFeildBackColor + + sureButton.contentHorizontalAlignment = .right + sureButton.setTitleColor(.disableButtonTitleColor, for: .disabled) + + recentTableView.rowHeight = 56 + recentTableView.register(SelectCell.self, forCellReuseIdentifier: "\(NSStringFromClass(NEBaseSelectCell.self))") + + friendTableView.rowHeight = 56 + friendTableView.register(SelectCell.self, forCellReuseIdentifier: "\(NSStringFromClass(NEBaseSelectCell.self))") + + teamTableView.rowHeight = 56 + teamTableView.register(SelectCell.self, forCellReuseIdentifier: "\(NSStringFromClass(NEBaseSelectCell.self))") + + selectedCollectionView.register(SelectedCell.self, forCellWithReuseIdentifier: "\(NSStringFromClass(NEBaseSelectedCell.self))") + + recentCollectionView.register(RecentSelectCell.self, forCellWithReuseIdentifier: "\(NSStringFromClass(NEBaseSelectedCell.self))") + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + /// 获取已选视图控制器-协同版 + /// - Parameter selectedArray: 已选列表 + /// - Returns: 已选页面的视图控制器 + override open func getMultiSelectedViewController(_ selectedArray: [MultiSelectModel]) -> NEBaseMultiSelectedViewController { + MultiSelectedViewController(selectedArray: selectedArray) + } + + override open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if tableView == currentTableView { + let info = viewModel.sessions[indexPath.row] + let cell = tableView.dequeueReusableCell( + withIdentifier: "\(NSStringFromClass(NEBaseSelectCell.self))", + for: indexPath + ) as! SelectCell + cell.showSelect(isMultiSelect) + cell.setModel(info) + cell.searchText = searchText + return cell + } + + return UITableViewCell() + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/MultiSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/MultiSelectedViewController.swift new file mode 100644 index 00000000..90ba3f9f --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/MultiSelectedViewController.swift @@ -0,0 +1,31 @@ + +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NECoreKit +import NIMSDK +import UIKit + +/// 转发多选-已选页面-协同版 +@objcMembers +open class MultiSelectedViewController: NEBaseMultiSelectedViewController { + override init(selectedArray: [MultiSelectModel] = [MultiSelectModel]()) { + super.init(selectedArray: selectedArray) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + commonUI() + + cancelButton.setTitle(nil, for: .normal) + cancelButton.setImage(UIImage.ne_imageNamed(name: "backArrow"), for: .normal) + + tableView.rowHeight = 62 + tableView.register(SelectedListCell.self, forCellReuseIdentifier: "\(NSStringFromClass(NEBaseSelectedListCell.self))") + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift index 22b9487d..a0942e91 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/NormalUI/ViewController/ValidationMessageViewController.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @@ -11,13 +11,12 @@ import UIKit open class ValidationMessageViewController: NEBaseValidationMessageViewController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - tag = "ValidationMessageViewController" navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift index 7798afdf..ea71d49c 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/Cell/NEBaseTeamTableViewCell.swift @@ -2,15 +2,47 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import UIKit @objcMembers open class NEBaseTeamTableViewCell: UITableViewCell { - public var avatarImage = UIImageView() - public var nameLabel = UILabel() - public var titleLabel = UILabel() -// public var arrow = UIImageView(image:UIImage.ne_imageNamed(name: "arrowRight")) + // 头像图片 + lazy var avatarImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + imageView.accessibilityIdentifier = "id.avatar" + return imageView + }() + + // 头像文本 + lazy var nameLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.font = UIFont.systemFont(ofSize: 16) + label.textColor = .white + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + // 名称 + lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.accessibilityIdentifier = "id.name" + return label + }() + + // 右侧图标 + lazy var arrowImageView: UIImageView = { + let arrow = UIImageView(image: UIImage.ne_imageNamed(name: "arrowRight")) + arrow.translatesAutoresizingMaskIntoConstraints = false + arrow.contentMode = .center + arrow.isHidden = true + return arrow + }() override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) @@ -18,55 +50,43 @@ open class NEBaseTeamTableViewCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { selectionStyle = .none - avatarImage.translatesAutoresizingMaskIntoConstraints = false - avatarImage.clipsToBounds = true - avatarImage.contentMode = .scaleAspectFill - avatarImage.accessibilityIdentifier = "id.avatar" - contentView.addSubview(avatarImage) + + contentView.addSubview(avatarImageView) NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), - avatarImage.widthAnchor.constraint(equalToConstant: 42), - avatarImage.heightAnchor.constraint(equalToConstant: 42), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + avatarImageView.widthAnchor.constraint(equalToConstant: 42), + avatarImageView.heightAnchor.constraint(equalToConstant: 42), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: 0), ]) - nameLabel.textAlignment = .center - nameLabel.font = UIFont.systemFont(ofSize: 16) - nameLabel.textColor = .white - nameLabel.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(nameLabel) NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), - nameLabel.rightAnchor.constraint(equalTo: avatarImage.rightAnchor), - nameLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor), - nameLabel.bottomAnchor.constraint(equalTo: avatarImage.bottomAnchor), + nameLabel.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), + nameLabel.rightAnchor.constraint(equalTo: avatarImageView.rightAnchor), + nameLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor), + nameLabel.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), ]) - titleLabel.translatesAutoresizingMaskIntoConstraints = false - titleLabel.accessibilityIdentifier = "id.name" contentView.addSubview(titleLabel) NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 12), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 12), titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35), titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) -// self.arrow.translatesAutoresizingMaskIntoConstraints = false -// self.arrow.isHidden = true -// self.arrow.contentMode = .center -// self.contentView.addSubview(self.arrow) -// NSLayoutConstraint.activate([ -// self.arrow.rightAnchor.constraint(equalTo: self.contentView.rightAnchor, constant: -20), -// self.arrow.widthAnchor.constraint(equalToConstant: 15), -// self.arrow.topAnchor.constraint(equalTo: self.contentView.topAnchor), -// self.arrow.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor) -// ]) + contentView.addSubview(arrowImageView) + NSLayoutConstraint.activate([ + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowImageView.widthAnchor.constraint(equalToConstant: 15), + arrowImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + arrowImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + ]) } open func setModel(_ model: Any) { @@ -78,13 +98,13 @@ open class NEBaseTeamTableViewCell: UITableViewCell { } titleLabel.text = name // self.nameLabel.text = name.count > 2 ? String(name[name.index(name.endIndex, offsetBy: -2)...]) : name - if let url = team.thumbAvatarUrl, !url.isEmpty { - avatarImage.sd_setImage(with: URL(string: url), completed: nil) - avatarImage.backgroundColor = .clear + if let url = team.avatarUrl, !url.isEmpty { + avatarImageView.sd_setImage(with: URL(string: url), completed: nil) + avatarImageView.backgroundColor = .clear } else { // random avatar // avatarImage.image = randomAvatar(teamId: team.teamId) - avatarImage.backgroundColor = UIColor.colorWithString(string: team.teamId) + avatarImageView.backgroundColor = UIColor.colorWithString(string: team.teamId) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift index 672b98eb..193fa676 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/NEBaseTeamListViewController.swift @@ -8,8 +8,7 @@ import NIMSDK import UIKit @objcMembers -open class NEBaseTeamListViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate { - public let navigationView = NENavigationView() +open class NEBaseTeamListViewController: NEContactBaseViewController, UITableViewDelegate, UITableViewDataSource { var tableView = UITableView(frame: .zero, style: .plain) var viewModel = TeamListViewModel() var isClickCallBack = false @@ -28,12 +27,13 @@ open class NEBaseTeamListViewController: UIViewController, UITableViewDelegate, loadData() weak var weakSelf = self viewModel.refresh = { + weakSelf?.emptyView.isHidden = (weakSelf?.viewModel.teamList.count ?? 0) > 0 weakSelf?.tableView.reloadData() } } func commonUI() { - title = localizable("mine_groupchat") + title = localizable("my_teams") navigationView.navTitle.text = title let image = UIImage.ne_imageNamed(name: "backArrow")?.withRenderingMode(.alwaysOriginal) let backItem = UIBarButtonItem( @@ -69,11 +69,26 @@ open class NEBaseTeamListViewController: UIViewController, UITableViewDelegate, tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + + emptyView.setText(localizable("team_empty")) + view.addSubview(emptyView) + NSLayoutConstraint.activate([ + emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), + emptyView.topAnchor.constraint(equalTo: tableView.topAnchor), + emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), + ]) } func loadData() { - viewModel.getTeamList() - tableView.reloadData() + viewModel.getTeamList { [weak self] teams, error in + if let err = error { + print("getTeamList error: \(err)") + } else { + self?.tableView.reloadData() + self?.emptyView.isHidden = (teams?.count ?? 0) > 0 + } + } } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -90,17 +105,17 @@ open class NEBaseTeamListViewController: UIViewController, UITableViewDelegate, if isClickCallBack == true { Router.shared.use( ContactTeamDataRouter, - parameters: ["team": model.nimTeam as Any], + parameters: ["team": model.v2Team as Any], closure: nil ) navigationController?.popViewController(animated: true) return } if let teamid = model.teamId { - let session = NIMSession(teamid, type: .team) + let conversationId = V2NIMConversationIdUtil.teamConversationId(teamid) Router.shared.use( PushTeamChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any], + parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any], closure: nil ) } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift index dc08705b..231fcce9 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift @@ -4,52 +4,57 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit @objcMembers -open class TeamListViewModel: NSObject, NIMTeamManagerDelegate { - var contactRepo = ContactRepo.shared +open class TeamListViewModel: NSObject, NETeamListener { + var teamRepo = TeamRepo.shared var refresh: () -> Void = {} public var teamList = [NETeam]() - private let className = "TeamListViewModel" override public init() { super.init() - contactRepo.addTeamDelegate(delegate: self) + teamRepo.addTeamListener(self) } deinit { - contactRepo.removeTeamDelegate(delegate: self) + teamRepo.removeTeamListener(self) } - func getTeamList() -> [NETeam]? { - NELog.infoLog(ModuleName + " " + className, desc: #function) - teamList = contactRepo.getTeamList() - teamList.sort(by: { team1, team2 in - (team1.createTime ?? 0) > (team2.createTime ?? 0) - }) - return teamList + func getTeamList(_ completion: @escaping ([NETeam]?, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + teamRepo.getTeamList { [weak self] teams, error in + if let error = error { + NEALog.errorLog(ModuleName + " " + (self?.className() ?? ""), desc: #function + ", error: " + error.localizedDescription) + } else if let teams = teams { + self?.teamList = teams + self?.teamList.sort(by: { team1, team2 in + (team1.createTime ?? 0) > (team2.createTime ?? 0) + }) + completion(teams, nil) + } + } } // MARK: NIMTeamManagerDelegate - public func onTeamAdded(_ team: NIMTeam) { - teamList.insert(NETeam(teamInfo: team), at: 0) + public func onTeamAdded(_ team: V2NIMTeam) { + teamList.insert(NETeam(v2teamInfo: team), at: 0) refresh() } - public func onTeamUpdated(_ team: NIMTeam) { + public func onTeamUpdated(_ team: V2NIMTeam) { for (i, t) in teamList.enumerated() { if t.teamId == team.teamId { - teamList[i] = NETeam(teamInfo: team) + teamList[i] = NETeam(v2teamInfo: team) refresh() break } } } - public func onTeamRemoved(_ team: NIMTeam) { + public func onTeamRemoved(_ team: V2NIMTeam) { for (i, t) in teamList.enumerated() { if t.teamId == team.teamId { teamList.remove(at: i) @@ -58,4 +63,26 @@ open class TeamListViewModel: NSObject, NIMTeamManagerDelegate { } } } + + // MARK: - V2NIMTeamListener + + public func onTeamCreated(_ team: V2NIMTeam) { + onTeamAdded(team) + } + + public func onTeamJoined(_ team: V2NIMTeam) { + onTeamAdded(team) + } + + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + onTeamUpdated(team) + } + + public func onTeamLeft(_ team: V2NIMTeam, isKicked: Bool) { + onTeamRemoved(team) + } + + public func onTeamDismissed(_ team: V2NIMTeam) { + onTeamRemoved(team) + } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/CenterTextCell.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/CenterTextCell.swift index 06edabf2..be8f59a0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/CenterTextCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/CenterTextCell.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import UIKit + @objcMembers open class CenterTextCell: UITableViewCell { public var titleLabel: UILabel = .init() @@ -36,6 +37,6 @@ open class CenterTextCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/ContactBaseTextCell.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/ContactBaseTextCell.swift index 0362b53e..5587825a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/ContactBaseTextCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/ContactBaseTextCell.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import UIKit + @objcMembers open class ContactBaseTextCell: UITableViewCell { public var titleLabel: UILabel = .init() @@ -37,7 +38,7 @@ open class ContactBaseTextCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setModel(model: UserItem) { diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithDetailTextCell.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithDetailTextCell.swift index 1dbd77e4..2843416e 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithDetailTextCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithDetailTextCell.swift @@ -4,6 +4,7 @@ // found in the LICENSE file. import UIKit + @objcMembers open class TextWithDetailTextCell: ContactBaseTextCell { public var detailTitleLabel = UILabel() @@ -28,6 +29,6 @@ open class TextWithDetailTextCell: ContactBaseTextCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithRightArrowCell.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithRightArrowCell.swift index 09aadb5c..c05fb79b 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithRightArrowCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithRightArrowCell.swift @@ -4,25 +4,26 @@ // found in the LICENSE file. import UIKit + @objcMembers open class TextWithRightArrowCell: ContactBaseTextCell { - public var arrowImage = UIImageView(image: UIImage.ne_imageNamed(name: "arrowRight")) + public var arrowImageView = UIImageView(image: UIImage.ne_imageNamed(name: "arrowRight")) override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) - arrowImage.translatesAutoresizingMaskIntoConstraints = false - arrowImage.contentMode = .center - contentView.addSubview(arrowImage) + arrowImageView.translatesAutoresizingMaskIntoConstraints = false + arrowImageView.contentMode = .center + contentView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - arrowImage.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - arrowImage.widthAnchor.constraint(equalToConstant: 20), - arrowImage.heightAnchor.constraint(equalToConstant: 20), - arrowImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowImageView.widthAnchor.constraint(equalToConstant: 20), + arrowImageView.heightAnchor.constraint(equalToConstant: 20), + arrowImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override func setModel(model: UserItem) { diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithSwitchCell.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithSwitchCell.swift index 65b51add..e82df8fe 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithSwitchCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/Views/TextWithSwitchCell.swift @@ -27,7 +27,7 @@ open class TextWithSwitchCell: ContactBaseTextCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func valueChanged(switchBtn: UISwitch) { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift index 02b3820c..5cef0e7b 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Controller/NEBaseValidationMessageViewController.swift @@ -2,15 +2,14 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import MJRefresh +import NECoreIM2Kit import NECoreKit import UIKit @objcMembers -open class NEBaseValidationMessageViewController: NEBaseContactViewController { +open class NEBaseValidationMessageViewController: NEContactBaseViewController { public let viewModel = ValidationMessageViewModel() - public let tableView = UITableView() - var tag = "ValidationMessageViewController" override open func viewDidLoad() { super.viewDidLoad() @@ -26,26 +25,97 @@ open class NEBaseValidationMessageViewController: NEBaseContactViewController { NotificationCenter.default.addObserver(self, selector: #selector(appEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) } - // 返回上一级页面 + override open func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewModel.setAddApplicationRead(nil) + } + + /// 返回上一级页面 override open func backToPrevious() { super.backToPrevious() - viewModel.clearNotiUnreadCount() + viewModel.setAddApplicationRead(nil) } + /// 进入后台,清空未读 func appEnterBackground() { - viewModel.clearNotiUnreadCount() - loadData() + viewModel.setAddApplicationRead { [weak self] success, error in + if success { + self?.loadData() + } + } + } + + public lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.showsVerticalScrollIndicator = false + tableView.delegate = self + tableView.dataSource = self + tableView.backgroundColor = .clear + tableView.keyboardDismissMode = .onDrag + + tableView.mj_header = MJRefreshNormalHeader( + refreshingTarget: self, + refreshingAction: #selector(loadData) + ) + return tableView + }() + + /// 表格添加底部 loading + func addBottomLoadMore() { + let footer = MJRefreshAutoFooter( + refreshingTarget: self, + refreshingAction: #selector(loadMoreData) + ) + footer.triggerAutomaticallyRefreshPercent = -10 + tableView.mj_footer = footer + } + + /// 表格移除底部 loading + func removeBottomLoadMore() { + tableView.mj_footer?.endRefreshingWithNoMoreData() + tableView.mj_footer = nil } + /// 加载数据 func loadData() { - weak var weakSelf = self - viewModel.getValidationMessage { - NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "✅ getValidationMessage SUCCESS") - weakSelf?.emptyView.isHidden = (weakSelf?.viewModel.datas.count ?? 0) > 0 - weakSelf?.tableView.reloadData() + viewModel.loadApplicationList(true) { [weak self] finished, error in + if let err = error { + NEALog.errorLog(ModuleName + " " + NEBaseValidationMessageViewController.className(), desc: "loadApplicationList CALLBACK error: \(err.localizedDescription)") + } else { + if finished { + self?.removeBottomLoadMore() + } else { + self?.addBottomLoadMore() + } + + self?.tableView.mj_header?.endRefreshing() + self?.emptyView.isHidden = (self?.viewModel.datas.count ?? 0) > 0 + self?.tableView.reloadData() + } } } + /// 加载更多 + func loadMoreData() { + viewModel.loadApplicationList(false) { [weak self] finished, error in + if let err = error { + NEALog.errorLog(ModuleName + " " + NEBaseValidationMessageViewController.className(), desc: "loadMoreApplicationList CALLBACK error: \(err.localizedDescription)") + } else { + if finished { + self?.removeBottomLoadMore() + } else { + self?.addBottomLoadMore() + } + + self?.tableView.mj_footer?.endRefreshing() + self?.tableView.reloadData() + } + } + } + + /// 控件初始化 open func setupUI() { let clearItem = UIBarButtonItem( title: localizable("clear"), @@ -66,11 +136,7 @@ open class NEBaseValidationMessageViewController: NEBaseContactViewController { navigationView.moreButton.setTitleColor(.ne_darkText, for: .normal) navigationView.addMoreButtonTarget(target: self, selector: #selector(clearMessage)) - tableView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(tableView) - tableView.dataSource = self - tableView.delegate = self - tableView.separatorStyle = .none NSLayoutConstraint.activate([ tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), @@ -79,7 +145,7 @@ open class NEBaseValidationMessageViewController: NEBaseContactViewController { tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - emptyView.settingContent(content: localizable("no_validation_message")) + emptyView.setText(localizable("no_validation_message")) view.addSubview(emptyView) NSLayoutConstraint.activate([ emptyView.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 100), @@ -89,17 +155,14 @@ open class NEBaseValidationMessageViewController: NEBaseContactViewController { ]) } + /// 清空好友申请 func clearMessage() { - viewModel.clearAllNoti { - NELog.infoLog(ModuleName + " " + self.tag, desc: "✅ clearAllNoti SUCCESS") - tableView.reloadData() - emptyView.isHidden = false - } + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + viewModel.clearNotification() } open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { - viewModel.clearNotiUnreadCount() - return true + true } } @@ -120,114 +183,74 @@ extension NEBaseValidationMessageViewController: UITableViewDelegate, UITableVie } extension NEBaseValidationMessageViewController: SystemNotificationCellDelegate { - open func changeValidationStatus(notifiModel: NENotification, notiStatus: NEHandleStatus) { - var notifiModels = [NENotification]() - if let msgList = notifiModel.msgList, - msgList.count > 0 { - for msg in msgList { - notifiModels.append(msg) - } + /// 处理好友申请 + /// - Parameters: + /// - notifiModel: 申请模型 + /// - notiStatus: 处理状态 + public func changeValidationStatus(notifiModel: NENotification, notiStatus: NEHandleStatus) { + notifiModel.handleStatus = notiStatus + notifiModel.unReadCount = 0 + for msg in notifiModel.msgList ?? [] { + msg.handleStatus = notiStatus } - notifiModel.handleStatus = notiStatus - notifiModel.imNotification?.handleStatus = notiStatus.rawValue - viewModel.clearSingleNotifyUnreadCount(notification: notifiModel.imNotification!) - for noti in notifiModels { - noti.handleStatus = notiStatus - noti.imNotification?.handleStatus = notiStatus.rawValue - viewModel.clearSingleNotifyUnreadCount(notification: noti.imNotification!) + DispatchQueue.main.async { + self.tableView.reloadData() } - notifiModel.unReadCount = 0 - loadData() } + /// 同意好友申请 + /// - Parameter notifiModel: 申请模型 open func onAccept(_ notifiModel: NENotification) { weak var weakSelf = self - guard let teamId = notifiModel.targetID, let invitorId = notifiModel.sourceID else { + guard let info = notifiModel.v2Notification else { return } - if notifiModel.type == .teamInvite { - viewModel.acceptInviteWithTeam(teamId, invitorId) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), - desc: "CALLBACK acceptInviteWithTeam " + (error?.localizedDescription ?? "no error") - ) - if let err = error as? NSError { - NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "❌CALLBACK acceptInviteWithTeam failed,error = \(error!.localizedDescription)") - if err.code == 807 || err.code == 809 { - weakSelf?.showToast(localizable("validate_processed")) - } else if err.code == teamNotExistCode { - weakSelf?.showToast(localizable("team_not_exist")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) - } - } - } else if notifiModel.type == .addFriendRequest { - viewModel.agreeRequest(invitorId) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), - desc: "CALLBACK agreeRequest " + (error?.localizedDescription ?? "no error") - ) - if let err = error { - NELog.infoLog(ModuleName + " " + self.tag, desc: "❌CALLBACK agreeRequest failed,error = \(error!)") + viewModel.agreeRequest(application: info) { error in + if let err = error as? NSError, err.code != friendAlreadyExist { + NEALog.errorLog(ModuleName + " " + NEBaseValidationMessageViewController.className(), desc: "CALLBACK agreeRequest failed,error = \(err.localizedDescription)") + switch err.code { + case protocolSendFailed: + weakSelf?.showToast(commonLocalizable("network_error")) + default: weakSelf?.showToast(localizable("failed_operation")) - } else { - weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) + } + } else { + weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) + weakSelf?.viewModel.setAddApplicationRead(nil) + if let accid = info.applicantAccountId, let conversationId = V2NIMConversationIdUtil.p2pConversationId(accid) { Router.shared.use(ChatAddFriendRouter, parameters: ["text": localizable("let_us_chat"), - "sessionId": invitorId, - "sessionType": NIMSessionType.P2P]) + "conversationId": conversationId as Any]) } } } } + /// 拒绝好友申请 + /// - Parameter notifiModel: 申请模型 open func onRefuse(_ notifiModel: NENotification) { weak var weakSelf = self - guard let teamId = notifiModel.targetID, let invitorId = notifiModel.sourceID else { + guard let info = notifiModel.v2Notification else { return } - if notifiModel.type == .teamInvite { - weakSelf?.viewModel.rejectInviteWithTeam(teamId, invitorId) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), - desc: "CALLBACK rejectInviteWithTeam " + (error?.localizedDescription ?? "no error") - ) - if let err = error as? NSError { - NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "❌CALLBACK rejectInviteWithTeam failed,error = \(error!.localizedDescription)") - if err.code == 807 || err.code == 809 { - weakSelf?.showToast(localizable("validate_processed")) - } else if err.code == teamNotExistCode { - weakSelf?.showToast(localizable("team_not_exist")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeNo) - } - } - } else if notifiModel.type == .addFriendRequest { - viewModel.refuseRequest(invitorId) { error in - NELog.infoLog( - ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), - desc: "CALLBACK refuseRequest " + (error?.localizedDescription ?? "no error") - ) - if let err = error { - NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "❌CALLBACK agreeRequest failed,error = \(err.localizedDescription)") - if err.code == 509 { - weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) - weakSelf?.showToast(localizable("validate_processed")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeNo) + viewModel.refuseRequest(application: info) { error in + if let err = error as? NSError { + NEALog.errorLog(ModuleName + " " + NEBaseValidationMessageViewController.className(), desc: "CALLBACK agreeRequest failed,error = \(err.localizedDescription)") + switch err.code { + case protocolSendFailed: + weakSelf?.showToast(commonLocalizable("network_error")) + case friendAlreadyExist: + weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) + weakSelf?.showToast(localizable("validate_processed")) + default: + weakSelf?.showToast(localizable("failed_operation")) } + } else { + weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeNo) + weakSelf?.viewModel.setAddApplicationRead(nil) } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift index 4c6265dd..7e2c43a6 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift @@ -4,58 +4,110 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit @objcMembers -open class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate { - typealias DataRefresh = () -> Void - - var dataRefresh: DataRefresh? - private let className = "ValidationMessageViewModel" +open class ValidationMessageViewModel: NSObject, NEContactListener { let contactRepo = ContactRepo.shared var datas = [NENotification]() + var dataRefresh: (() -> Void)? + var offset: UInt = 0 // 查询的偏移量 + var pageMaxLimit: UInt = 100 // 查询的每页数量 override init() { - NELog.infoLog(ModuleName + " " + className, desc: #function) super.init() - contactRepo.notiDelegate = self + contactRepo.addContactListener(self) + } + + deinit { + contactRepo.removeContactListener(self) + } + + /// 加载(更多)好友申请消息 + /// - Parameter firstLoad: 是否是首次加载 + /// - Parameter completin: 完成回调,(是否还有数据,错误信息) + func loadApplicationList(_ firstLoad: Bool, _ completin: @escaping (Bool, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + + let offset = firstLoad ? 0 : offset + if firstLoad { + datas.removeAll() + } + getValidationMessage(offset) { [weak self] offset, finished, error in + if let err = error { + completin(finished, err) + } else { + self?.offset = offset + completin(finished, nil) + } + } } - open func onNotificationUnreadCountChanged(_ count: Int) {} - - // 内容待完善 -// open func onRecieveNotification(_ notification: XNotification) { -// NELog.infoLog(className, desc: #function) -// var isInsert = true -// for notify in datas { -// if notify.sourceID == notification.sourceID, notify.type == notification.type { -// isInsert = false -// break -// } -// } -// if isInsert { -// datas.insert(notification, at: 0) -// } -// contactRepo.clearNotificationUnreadCount() -// if let block = dataRefresh { -// block() -// } -// } - // 内容待完善 -// func getValidationMessage(_ completin: @escaping () -> Void) { -// NELog.infoLog(className, desc: #function) -// weak var weakSelf = self -// contactRepo.getNotificationList(limit: 500) { notifications in -// weakSelf?.datas = notifications -// if let count = weakSelf?.datas.count, count > 0 { -// completin() -// } else { -// NELog.warn(weakSelf?.className ?? "ValidationMessageViewModel", desc: "⚠️NotificationList is empty") -// } -// } -// } + /// 分页查询好友验证消息 + /// - Parameters: + /// - offset: 偏移量 + /// - completin: 完成回调(验证消息列表,下一次偏移量,是否还有数据,错误信息) + func getValidationMessage(_ offset: UInt, _ completin: @escaping (UInt, Bool, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + + let option = V2NIMFriendAddApplicationQueryOption() + option.offset = offset + option.limit = pageMaxLimit + + contactRepo.getAddApplicationList(option: option) { [weak self] result, error in + if let err = error { + completin(0, false, err) + } else if let result = result, let infos = result.infos { + let dateNow = Date().timeIntervalSince1970 + let group = DispatchGroup() + + for info in infos { + var noti = NENotification(info: info) + + // 过期事件:7天(604800s) + if noti.handleStatus == .HandleTypePending, + dateNow - (noti.timestamp ?? 0) > 604_800 { + noti.handleStatus = .HandleTypeOutOfDate + } + + // 查询用户信息 + var uid: String? + // 自己申请添加别人,则存储操作者的信息 + if noti.applicantAccid == IMKitClient.instance.account() { + uid = noti.operatorAccid + } else { + // 别人申请添加自己,则存储申请者的信息 + uid = noti.applicantAccid + } + + if let uid = uid { + group.enter() + self?.contactRepo.getUserWithFriend(accountIds: [uid]) { users, error in + noti.userInfo = users?.first + if var datas = self?.datas, self?.isExist(xNoti: ¬i, list: &datas) == false { + self?.datas.append(noti) + } + group.leave() + } + } + } + + group.notify(queue: .main) { [weak self] in + self?.datas.sort { xNoti1, xNoti2 in + (xNoti1.timestamp ?? 0) > (xNoti2.timestamp ?? 0) + } + completin(result.offset, result.finished, nil) + } + } + } + } + /// 判断该条申请是否已存在(是否可以聚合) + /// - Parameters: + /// - xNoti: 申请 + /// - list: 聚合列表 + /// - Returns: 是否已存在 func isExist(xNoti: inout NENotification, list: inout [NENotification]) -> Bool { for loopList in list { if xNoti.isEqualTo(noti: loopList) { @@ -83,89 +135,94 @@ open class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate { return false } - open func onRecieveNotification(_ notification: NENotification) { - NELog.infoLog(ModuleName + " " + className, desc: #function) - var noti = notification - if !isExist(xNoti: ¬i, list: &datas) { - datas.insert(notification, at: 0) - } - datas.sort { xNoti1, xNoti2 in - (xNoti1.timestamp ?? 0) > (xNoti2.timestamp ?? 0) - } - if let block = dataRefresh { - block() - } - } - - func getValidationMessage(_ completin: @escaping () -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) - contactRepo.getNotificationList(limit: 500) { [weak self] xNotiList in - var data = [NENotification]() - let dateNow = Date().timeIntervalSince1970 - for xNoti in xNotiList { - var noti = xNoti - - // 过期事件:7天(604800s) - if noti.handleStatus == .HandleTypePending, - dateNow - (noti.timestamp ?? 0) > 604_800 { - noti.handleStatus = .HandleTypeOutOfDate - } - - if !self!.isExist(xNoti: ¬i, list: &data) { - data.append(xNoti) - } - } - self!.datas = data.sorted(by: { xNoti1, xNoti2 in - (xNoti1.timestamp ?? 0) > (xNoti2.timestamp ?? 0) - }) - if self!.datas.count <= 0 { - NELog.warn(ModuleName + " " + self!.className, desc: "⚠️NotificationList is empty") + /// 设置所有好友申请已读 + /// - Parameter completion: 完成回调 + func setAddApplicationRead(_ completion: ((Bool, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + contactRepo.setAddApplicationRead { success, error in + completion?(success, error as? NSError) + DispatchQueue.main.async { + NotificationCenter.default.post(name: NENotificationName.clearValidationUnreadCount, object: nil) } - completin() } } - func clearNotiUnreadCount() { - contactRepo.clearNotificationUnreadCount() + /// 同意好友申请 + /// - Parameters: + /// - application: 好友申请 + /// - completion: 完成回调 + func agreeRequest(application: V2NIMFriendAddApplication, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", operatorAccountId:\(String(describing: application.operatorAccountId))") + contactRepo.acceptAddApplication(application: application, completion) } - func clearSingleNotifyUnreadCount(notification: NIMSystemNotification) { - contactRepo.clearSingleNotifyUnreadCount(notification: notification) + /// 拒绝好友申请 + /// - Parameters: + /// - application: 好友申请 + /// - completion: 完成回调 + func refuseRequest(application: V2NIMFriendAddApplication, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", operatorAccountId:\(String(describing: application.operatorAccountId))") + contactRepo.rejectAddApplication(application: application, completion) } - func clearAllNoti(_ completion: () -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) + /// 清空好友申请通知 + func clearNotification() { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) contactRepo.clearNotification() datas.removeAll() - completion() + dataRefresh?() } - open func acceptInviteWithTeam(_ teamId: String, _ invitorId: String, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") - contactRepo.acceptTeamInvite(teamId, invitorId, completion) - } + // MARK: - NEContactListener + + /// 好友添加申请变更 + /// - Parameter application: 申请添加好友信息 + func applicationChanged(_ application: V2NIMFriendAddApplication) { + var noti = NENotification(info: application) + let group = DispatchGroup() + + // 查询用户信息 + var uid: String? + // 自己申请添加别人,则存储操作者的信息 + if noti.applicantAccid == IMKitClient.instance.account() { + uid = noti.operatorAccid + } else { + // 别人申请添加自己,则存储申请者的信息 + uid = noti.applicantAccid + } + + if let uid = uid { + group.enter() + contactRepo.getUserWithFriend(accountIds: [uid]) { [self] users, error in + noti.userInfo = users?.first + if !isExist(xNoti: ¬i, list: &datas) { + datas.insert(noti, at: 0) + } + datas.sort { xNoti1, xNoti2 in + (xNoti1.timestamp ?? 0) > (xNoti2.timestamp ?? 0) + } + group.leave() + } + } - open func rejectInviteWithTeam(_ teamId: String, _ invitorId: String, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") - contactRepo.rejectTeamInvite(teamId, invitorId, completion) + group.notify(queue: .main) { [weak self] in + self?.dataRefresh?() + } } - func agreeRequest(_ account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account:\(account)") - let request = NEAddFriendRequest() - request.account = account - request.operationType = .verify - contactRepo.addFriend(request: request, completion) + /// 收到好友添加申请回调 + /// - Parameter application: 申请添加好友信息 + public func onFriendAddApplication(_ application: V2NIMFriendAddApplication) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + applicationChanged(application) } - func refuseRequest(_ account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account:\(account)") - print("account : ", account) - let request = NEAddFriendRequest() - request.account = account - request.operationType = .reject - contactRepo.addFriend(request: request, completion) + /// 好友添加申请被拒绝回调 + /// - Parameter rejectionInfo: 申请添加好友拒绝信息 + public func onFriendAddRejected(_ rejectionInfo: V2NIMFriendAddApplication) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + applicationChanged(rejectionInfo) } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift index 09ef5d3d..1973160f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseSystemNotificationCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -14,6 +14,52 @@ open class NEBaseSystemNotificationCell: NEBaseValidationCell { private var notifModel: NENotification? public weak var delegate: SystemNotificationCellDelegate? + public var rejectButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(localizable("refuse"), for: .normal) + button.setTitleColor(UIColor(hexString: "333333"), for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 14.0) + button.clipsToBounds = false + button.layer.cornerRadius = 4 + button.layer.borderColor = UIColor(hexString: "D9D9D9").cgColor + button.layer.borderWidth = 1 + button.accessibilityIdentifier = "id.reject" + return button + }() + + public var agreeButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(localizable("agree"), for: .normal) + let blue = UIColor(hexString: "337EFF") + button.setTitleColor(blue, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 14) + button.clipsToBounds = true + button.layer.cornerRadius = 4 + button.layer.borderWidth = 1 + button.layer.borderColor = blue.cgColor + button.accessibilityIdentifier = "id.accept" + return button + }() + + public lazy var resultImage: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = UIImage.ne_imageNamed(name: "finishFlag") + return imageView + }() + + public lazy var resultLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor(hexString: "B3B7BC") + label.font = UIFont.systemFont(ofSize: 14.0) + label.textAlignment = .right + label.accessibilityIdentifier = "id.verifyResult" + return label + }() + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) } @@ -24,26 +70,26 @@ open class NEBaseSystemNotificationCell: NEBaseValidationCell { override open func setupUI() { super.setupUI() - contentView.addSubview(agreeBtn) - contentView.addSubview(rejectBtn) + contentView.addSubview(agreeButton) + contentView.addSubview(rejectButton) contentView.addSubview(resultImage) contentView.addSubview(resultLabel) resultLabel.text = "" NSLayoutConstraint.activate([ - agreeBtn.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - agreeBtn.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - agreeBtn.widthAnchor.constraint(equalToConstant: 60), - agreeBtn.heightAnchor.constraint(equalToConstant: 32), + agreeButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + agreeButton.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + agreeButton.widthAnchor.constraint(equalToConstant: 60), + agreeButton.heightAnchor.constraint(equalToConstant: 32), ]) - agreeBtn.addTarget(self, action: #selector(agreeClick(_:)), for: .touchUpInside) + agreeButton.addTarget(self, action: #selector(agreeClick(_:)), for: .touchUpInside) NSLayoutConstraint.activate([ - rejectBtn.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - rejectBtn.rightAnchor.constraint(equalTo: agreeBtn.leftAnchor, constant: -16), - rejectBtn.widthAnchor.constraint(equalToConstant: 60), - rejectBtn.heightAnchor.constraint(equalToConstant: 32), + rejectButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + rejectButton.rightAnchor.constraint(equalTo: agreeButton.leftAnchor, constant: -16), + rejectButton.widthAnchor.constraint(equalToConstant: 60), + rejectButton.heightAnchor.constraint(equalToConstant: 32), ]) - rejectBtn.addTarget(self, action: #selector(rejectClick(_:)), for: .touchUpInside) + rejectButton.addTarget(self, action: #selector(rejectClick(_:)), for: .touchUpInside) NSLayoutConstraint.activate([ resultLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), @@ -61,116 +107,43 @@ open class NEBaseSystemNotificationCell: NEBaseValidationCell { override open func confige(_ model: NENotification) { super.confige(model) notifModel = model - let hideActionButton = shouldHideActionButton() - agreeBtn.isHidden = hideActionButton - rejectBtn.isHidden = hideActionButton - - if hideActionButton { - let hidden = shouldHideResultStatus() - - resultLabel.isHidden = hidden - resultImage.isHidden = hidden - - switch notifModel?.handleStatus { - case .HandleTypeOk: - resultLabel.text = localizable("agreed") - resultImage.image = UIImage.ne_imageNamed(name: "finishFlag") - case .HandleTypeNo: - resultLabel.text = localizable("refused") - resultImage.image = UIImage.ne_imageNamed(name: "refused") - case .HandleTypeOutOfDate: - resultLabel.text = localizable("expired") - resultImage.image = UIImage.ne_imageNamed(name: "refused") - default: - resultLabel.text = "" + + if model.handleStatus != .HandleTypePending { + agreeButton.isHidden = true + rejectButton.isHidden = true + titleLabelRightMargin?.constant = -90 + + if model.applicantAccid == IMKitClient.instance.account() { + // 自己申请的,不展示结果 + resultLabel.isHidden = true + resultImage.isHidden = true + } else { + resultLabel.isHidden = false + resultImage.isHidden = false + + switch model.handleStatus { + case .HandleTypeOk: + resultLabel.text = localizable("agreed") + resultImage.image = UIImage.ne_imageNamed(name: "finishFlag") + case .HandleTypeNo: + resultLabel.text = localizable("refused") + resultImage.image = UIImage.ne_imageNamed(name: "refused") + case .HandleTypeOutOfDate: + resultLabel.text = localizable("expired") + resultImage.image = UIImage.ne_imageNamed(name: "refused") + default: + resultLabel.text = "" + } } - titleLabelRightMargin?.constant = hidden ? -20 : -90 } else { + agreeButton.isHidden = false + rejectButton.isHidden = false resultLabel.isHidden = true resultImage.isHidden = true titleLabelRightMargin?.constant = -180 } } - func shouldHideActionButton() -> Bool { - let type = notifModel?.type - let handled = notifModel?.handleStatus != .HandleTypePending - var needHandel = false - if type == .teamApply || - type == .teamInvite || - type == .superTeamInvite || - type == .superTeamApply { - needHandel = true - } - - if type == .addFriendRequest { - if let obj = notifModel?.attachment { - if obj.isKind(of: NIMUserAddAttachment.self) { - let operation = (obj as NIMUserAddAttachment).operationType - needHandel = operation == .request - } - } - } - return !(!handled && needHandel) - } - - func shouldHideResultStatus() -> Bool { - let type = notifModel?.type - if type == .addFriendVerify || - type == .addFriendReject || - type == .teamInviteReject { - return true - } else { - return false - } - } - - public var rejectBtn: ExpandButton = { - let button = ExpandButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(localizable("refuse"), for: .normal) - button.setTitleColor(UIColor(hexString: "333333"), for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 14.0) - button.clipsToBounds = false - button.layer.cornerRadius = 4 - button.layer.borderColor = UIColor(hexString: "D9D9D9").cgColor - button.layer.borderWidth = 1 - button.accessibilityIdentifier = "id.reject" - return button - }() - - public var agreeBtn: ExpandButton = { - let button = ExpandButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(localizable("agree"), for: .normal) - let blue = UIColor(hexString: "337EFF") - button.setTitleColor(blue, for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 14) - button.clipsToBounds = true - button.layer.cornerRadius = 4 - button.layer.borderWidth = 1 - button.layer.borderColor = blue.cgColor - button.accessibilityIdentifier = "id.accept" - return button - }() - - public lazy var resultImage: UIImageView = { - let rightImage = UIImageView() - rightImage.translatesAutoresizingMaskIntoConstraints = false - rightImage.image = UIImage.ne_imageNamed(name: "finishFlag") - return rightImage - }() - - public lazy var resultLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = UIColor(hexString: "B3B7BC") - label.font = UIFont.systemFont(ofSize: 14.0) - label.textAlignment = .right - label.accessibilityIdentifier = "id.verifyResult" - return label - }() - open func rejectClick(_ sender: UIButton) { if let model = notifModel { delegate?.onRefuse(model) diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift index d8dab8b7..b6dff29d 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/NEBaseValidationCell.swift @@ -3,7 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -38,15 +38,15 @@ open class NEBaseValidationCell: NEBaseContactViewCell { contentView.addSubview(redAngleView) NSLayoutConstraint.activate([ - redAngleView.centerXAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: -8), - redAngleView.centerYAnchor.constraint(equalTo: avatarImage.topAnchor, constant: 8), + redAngleView.centerXAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: -8), + redAngleView.centerYAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: 8), redAngleView.heightAnchor.constraint(equalToConstant: 18), ]) addSubview(titleLabel) NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 10), - titleLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 10), + titleLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor), ]) titleLabelRightMargin = titleLabel.rightAnchor.constraint( equalTo: contentView.rightAnchor, @@ -56,7 +56,7 @@ open class NEBaseValidationCell: NEBaseContactViewCell { addSubview(optionLabel) NSLayoutConstraint.activate([ - optionLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 10), + optionLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 10), optionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor), optionLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -180), ]) @@ -74,46 +74,21 @@ open class NEBaseValidationCell: NEBaseContactViewCell { open func confige(_ model: NENotification) { var optionLabelContent = "" - var nickName = "" - var teamName = "" - // 设置操作者名称 - - if let alias = model.userInfo?.alias { - nickName = alias - } else if let nick = model.userInfo?.userInfo?.nickName { - nickName = nick - } else if let source = model.sourceName { - nickName = source - } - if model.userInfo == nil, let uid = model.sourceID { - let user = NIMSDK.shared().userManager.userInfo(uid) - if let alias = user?.alias, !alias.isEmpty { - nickName = alias - } else if let nick = user?.userInfo?.nickName, !nick.isEmpty { - nickName = nick - } - } // 设置头像 - if let headerUrl = model.userInfo?.userInfo?.avatarUrl, !headerUrl.isEmpty { - avatarImage.sd_setImage(with: URL(string: headerUrl), completed: nil) + if let headerUrl = model.userInfo?.user?.avatar, !headerUrl.isEmpty { + avatarImageView.sd_setImage(with: URL(string: headerUrl), completed: nil) nameLabel.text = "" - avatarImage.backgroundColor = .clear + avatarImageView.backgroundColor = .clear } else if let teamUrl = model.teamInfo?.avatarUrl, !teamUrl.isEmpty { - avatarImage.sd_setImage(with: URL(string: teamUrl), completed: nil) + avatarImageView.sd_setImage(with: URL(string: teamUrl), completed: nil) nameLabel.text = "" - avatarImage.backgroundColor = .clear + avatarImageView.backgroundColor = .clear } else { // 无头像设置其name - if !nickName.isEmpty { - showNameOnCircleHeader(nickName) - } else { - if let id = model.sourceID { - showNameOnCircleHeader(id) - } - } - avatarImage.sd_setImage(with: URL(string: ""), completed: nil) - avatarImage.backgroundColor = UIColor.colorWithString(string: model.userInfo?.userId) + showNameOnCircleHeader(model.userInfo?.showName() ?? "") + avatarImageView.sd_setImage(with: URL(string: ""), completed: nil) + avatarImageView.backgroundColor = UIColor.colorWithString(string: model.userInfo?.user?.accountId) } // 设置未读状态(未读数角标+底色) @@ -127,43 +102,17 @@ open class NEBaseValidationCell: NEBaseContactViewCell { } } - if let t = model.targetName { - teamName = t - } - if let type = model.type { - switch type { - case .teamApply: - optionLabelContent = "申请加入群聊 \"\(teamName)\"" - case .teamApplyReject: - optionLabelContent = "拒绝入群邀请 \"\(teamName)\"" - case .teamInvite: - optionLabelContent = "邀请您加入群聊 \"\(teamName)\"" - case .teamInviteReject: - optionLabelContent = "拒绝入群邀请 \"\(teamName)\"" - - case .superTeamApply: - optionLabelContent = "申请加入超大群" - case .superTeamApplyReject: - optionLabelContent = "拒绝加入超大群" - - case .superTeamInvite: - optionLabelContent = "邀请您加入群聊 \"\(teamName)\"" - case .superTeamInviteReject: - optionLabelContent = "拒绝入群邀请 \"\(teamName)\"" - case .addFriendDirectly: - optionLabelContent = "添加您为好友" - case .addFriendRequest: - optionLabelContent = "好友申请" - case .addFriendVerify: - optionLabelContent = "同意了你的好友请求" - case .addFriendReject: - optionLabelContent = "拒绝了你的好友请求" - @unknown default: - optionLabelContent = "未知操作" + if model.applicantAccid != IMKitClient.instance.account() { + optionLabelContent = localizable("add_request") + } else { + if model.handleStatus == .HandleTypeNo { + optionLabelContent = localizable("refused_request") + } else if model.handleStatus == .HandleTypeOk { + optionLabelContent = localizable("agreed_request") } } - titleLabel.text = nickName + titleLabel.text = model.userInfo?.showName() optionLabel.text = optionLabelContent } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift index ad7bd690..9503e16e 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactUserViewModel.swift @@ -5,7 +5,7 @@ import CoreMedia import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit @objcMembers @@ -13,51 +13,46 @@ open class ContactUserViewModel: NSObject { let contactRepo = ContactRepo.shared private let className = "ContactUserViewModel" - func addFriend(_ account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) - let request = NEAddFriendRequest() - request.account = account - request.operationType = .addRequest - contactRepo.addFriend(request: request, completion) + func addFriend(_ account: String, _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) + let params = V2NIMFriendAddParams() + params.addMode = .FRIEND_MODE_TYPE_APPLAY + contactRepo.addFriend(accountId: account, params: params, completion) } - open func deleteFriend(account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) - contactRepo.deleteFriend(account: account, completion) - } + open func deleteFriend(account: String, _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) - open func isFriend(account: String) -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) - return contactRepo.isFriend(account: account) + let params = V2NIMFriendDeleteParams() + params.deleteAlias = true + contactRepo.deleteFriend(account: account, params: params, completion) } - open func isBlack(account: String) -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) - return contactRepo.isBlackList(account: account) + open func removeBlackList(account: String, _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) + contactRepo.removeBlockList(accountId: account, completion) } - open func removeBlackList(account: String, _ completion: @escaping (NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", account: " + account) - return contactRepo.removeBlackList(account: account, completion) - } + /// 更新好友备注 + /// - Parameters: + /// - accountId: 用户Id + /// - alias: 备注 + /// - completion: 请求回调 + open func updateAlias(accountId: String, + alias: String, + _ completion: @escaping (Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", userId: \(accountId)") - open func update(_ user: NEKitUser, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + (user.userId ?? "nil")) - contactRepo.updateUser(user, completion) - } + let params = V2NIMFriendSetParams() + params.alias = alias - 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) - } + contactRepo.setFriendInfo(accountId: accountId, params: params, completion) } - open func fetchUserInfo(accountList: [String], - _ completion: @escaping ([NEKitUser]?, NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", uid: \(accountList)") - contactRepo.fetchUserInfo(accountList: accountList) { users, error in - completion(users, error) + open func getUserInfo(_ uid: String, _ completion: @escaping (NEUserWithFriend?, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", uid: " + uid) + contactRepo.getUserWithFriend(accountIds: [uid]) { userFriends, error in + completion(userFriends?.first, error) } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift index 579710d7..676ba3a0 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift @@ -4,189 +4,214 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit +public protocol ContactViewModelDelegate: NSObjectProtocol { + func reloadTableView() + func reloadTableView(_ index: IndexPath) +} + @objcMembers -open class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { +open class ContactViewModel: NSObject, NEContactListener, NEEventListener { typealias RefreshBlock = () -> Void public var contacts: [ContactSection] = [] public var indexs: [String]? - private var contactHeaders: [ContactHeadItem]? + private var contactHeaders: ContactSection? public var contactRepo = ContactRepo.shared private var initalDict = [String: [ContactInfo]]() - private let className = "ContactViewModel" + public weak var delegate: ContactViewModelDelegate? - var unreadCount = 0 + /// 在线状态记录 + public var onlineStatusDic = [String: NIMSubscribeEvent]() + + var unreadCount = 0 { + didSet { + refresh?() + } + } var refresh: RefreshBlock? init(contactHeaders: [ContactHeadItem]?) { super.init() - NELog.infoLog( - ModuleName + " " + className, + NEALog.infoLog( + ModuleName + " " + className(), desc: #function + ", contactHeaders.count: \(contactHeaders?.count ?? 0)" ) - contactRepo.notiDelegate = self - unreadCount = contactRepo.getNotificationUnreadCount() - self.contactHeaders = contactHeaders - } - open func onNotificationUnreadCountChanged(_ count: Int) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", count: \(count)") - print("onNotificationUnreadCountChanged : ", count) - unreadCount = count - if let block = refresh { - block() + contactRepo.addContactListener(self) + + if let headSection = headerSection(headerItem: contactHeaders) { + self.contactHeaders = headSection + contacts.append(headSection) + } + + if IMKitConfigCenter.shared.onlineStatusEnable { + EventSubscribeRepo.shared.addListener(self) } } - open func onRecieveNotification(_ notification: NENotification) {} + deinit { + if IMKitConfigCenter.shared.onlineStatusEnable { + EventSubscribeRepo.shared.removeListener(self) + } + } - func loadData(fetch: Bool = false, _ filters: Set? = nil, completion: @escaping (NSError?, Int) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) + func loadData(_ filters: Set? = nil, completion: @escaping (NSError?, Int) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) weak var weakSelf = self - getContactList(fetch, filters) { contacts, error in + getContactList(filters) { contacts, error in if let users = contacts { - NELog.infoLog("contact loadData", desc: "contact data:\(users)") - weakSelf?.contacts = users - weakSelf?.indexs = self.getIndexs(contactSections: users) - if let headSection = weakSelf?.headerSection(headerItem: weakSelf?.contactHeaders) { - weakSelf?.contacts.insert(headSection, at: 0) + NEALog.infoLog("contact loadData", desc: "contact data:\(users)") + weakSelf?.contacts.removeAll() + if let contactHeaders = weakSelf?.contactHeaders { + weakSelf?.contacts.append(contactHeaders) } + weakSelf?.contacts.append(contentsOf: users) + weakSelf?.indexs = self.getIndexs(contactSections: users) completion(nil, users.count) + if IMKitConfigCenter.shared.onlineStatusEnable { + weakSelf?.subscribeOnlineStatus() + } + } else { + completion(nil, 0) } } } - func getContactList(_ fetch: Bool = false, _ filters: Set? = nil, _ completion: @escaping ([ContactSection]?, NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", filters.count: \(filters?.count ?? 0)") + func getContactList(_ filters: Set? = nil, _ completion: @escaping ([ContactSection]?, NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", filters.count: \(filters?.count ?? 0)") + + // 优选从缓存中取 + if !NEFriendUserCache.shared.isEmpty() { + let friends = NEFriendUserCache.shared.getFriendListNotInBlocklist().map(\.value) + let contactList = formatData(friends, filters) + completion(contactList, nil) + return + } + + // 缓存中没有则远端查询 + contactRepo.getContactList { [weak self] friends, error in + NEALog.infoLog("contact bar getFriendList", desc: "friend count:\(String(describing: friends?.count))") + let contactList = self?.formatData(friends, filters) + completion(contactList, error as? NSError) + } + } + + /// 数据格式化 + /// - Parameters: + /// - friends: 好友列表 + /// - filters: 过滤列表 + /// - Returns: 格式化后的好友列表 + func formatData(_ friends: [NEUserWithFriend]?, _ filters: Set? = nil) -> [ContactSection] { var contactList: [ContactSection] = [] - weak var weakSelf = self - var local = false - if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { - local = true - } - contactRepo.getFriendList(fetch, local: local) { friends, error in - if var users = friends { - NELog.infoLog("contact bar getFriendList", desc: "friend count:\(users.count)") - weakSelf?.initalDict = [String: [ContactInfo]]() - if let filterUsers = filters { - users = users.filter { user in - if let uid = user.userId, filterUsers.contains(uid) { - return false - } - return true + if var users = friends { + initalDict = [String: [ContactInfo]]() + if let filterUsers = filters { + users = users.filter { userFriend in + if let uid = userFriend.user?.accountId, filterUsers.contains(uid) { + return false } + return true } + } - if users.isEmpty { - completion(contactList, nil) - return - } + if users.isEmpty { + return contactList + } - let digitRegular = NSPredicate(format: "SELF MATCHES %@", "[0-9]") - let azRegular = NSPredicate(format: "SELF MATCHES %@", "[A-Z]") - var digitList = [ContactInfo]() - var specialCharList = [ContactInfo]() - for contact: NEKitUser in users { - // get inital of name - var name = contact.alias?.isEmpty == false ? contact.alias : contact.userInfo?.nickName - if name == nil { - name = contact.userId - } - let inital = name?.initalLetter() ?? "#" - let contactInfo = ContactInfo() - contactInfo.user = contact - contactInfo.headerBackColor = UIColor.colorWithString(string: contact.userId ?? "") - - if digitRegular.evaluate(with: inital) { // [0-9] - digitList.append(contactInfo) - } else if !azRegular.evaluate(with: inital) { // [#] - specialCharList.append(contactInfo) - } else { // [A-Z] - if weakSelf?.initalDict[inital] != nil { - weakSelf?.initalDict[inital]?.append(contactInfo) - } else { - weakSelf?.initalDict[inital] = [contactInfo] - } - } + let digitRegular = NSPredicate(format: "SELF MATCHES %@", "[0-9]") + let azRegular = NSPredicate(format: "SELF MATCHES %@", "[A-Z]") + var digitList = [ContactInfo]() + var specialCharList = [ContactInfo]() + for userFriend: NEUserWithFriend in users { + // get inital of name + var name = userFriend.user?.name ?? userFriend.user?.accountId + if let alias = userFriend.friend?.alias, !alias.isEmpty { + name = alias } - digitList.sort { s1, s2 in - s1.user!.showName()! < s2.user!.showName()! - } - specialCharList.sort { s1, s2 in - s1.user!.showName()! < s2.user!.showName()! - } + let inital = name?.initalLetter() ?? "#" + let contactInfo = ContactInfo() + contactInfo.user = userFriend + contactInfo.headerBackColor = UIColor.colorWithString(string: userFriend.user?.accountId ?? "") - guard let initalDict = weakSelf?.initalDict else { - return + if digitRegular.evaluate(with: inital) { // [0-9] + digitList.append(contactInfo) + } else if !azRegular.evaluate(with: inital) { // [#] + specialCharList.append(contactInfo) + } else { // [A-Z] + if initalDict[inital] != nil { + initalDict[inital]?.append(contactInfo) + } else { + initalDict[inital] = [contactInfo] + } } + } - for key in initalDict.keys { - if var value = weakSelf?.initalDict[key] { - value.sort { s1, s2 in - s1.user!.showName()! < s2.user!.showName()! - } - contactList.append(ContactSection(initial: key, contacts: value)) + digitList.sort { s1, s2 in + s1.user!.showName()! < s2.user!.showName()! + } + specialCharList.sort { s1, s2 in + s1.user!.showName()! < s2.user!.showName()! + } + + for key in initalDict.keys { + if var value = initalDict[key] { + value.sort { s1, s2 in + s1.user!.showName()! < s2.user!.showName()! } + contactList.append(ContactSection(initial: key, contacts: value)) } + } - var result = contactList.sorted { s1, s2 in - s1.initial < s2.initial - } + var result = contactList.sorted { s1, s2 in + s1.initial < s2.initial + } - let specialList = digitList + specialCharList - if specialList.count > 0 { - result.append(ContactSection(initial: "#", contacts: specialList)) - } - completion(result, nil) + let specialList = digitList + specialCharList + if specialList.count > 0 { + result.append(ContactSection(initial: "#", contacts: specialList)) } + return result + } + return contactList + } + + /// 返回好友列表 + /// - Returns: 不包含顶部预设数据(验证消息、黑名单、我的群聊)的好友列表 + func getFriendSections() -> [ContactSection] { + let friendSections = contacts.filter { $0.initial != "" } + return friendSections + } + + func getAddApplicationUnreadCount(_ completion: ((Int, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + contactRepo.getUnreadApplicationCount { [weak self] count, error in + self?.unreadCount = count + completion?(count, error as? NSError) } } func headerSection(headerItem: [ContactHeadItem]?) -> ContactSection? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", headerItem.count: \(headerItem?.count ?? 0)") + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", headerItem.count: \(headerItem?.count ?? 0)") guard let header = headerItem else { return nil } var infos: [ContactInfo] = [] for item in header { - let user = NEKitUser() - user.alias = item.name - let userInfo = NEKitUserInfo(nickName: "", avatar: item.imageName) - user.userInfo = userInfo - let info = ContactInfo() - info.user = user + info.user = NEUserWithFriend(alias: item.name, avatar: item.imageName) info.contactCellType = ContactCellType.ContactOthers.rawValue info.router = item.router info.headerBackColor = item.color - if let _ = user.userId { - info.headerBackColor = UIColor.colorWithString(string: user.userId) - } infos.append(info) } return ContactSection(initial: "", contacts: infos) } func getIndexs(contactSections: [ContactSection]?) -> [String]? { - // 根据用户列表获取导航标签 -// NELog.infoLog( -// ModuleName + " " + className, -// desc: #function + ", contactSections.count: \(contactSections?.count ?? 0)" -// ) -// guard let sections = contactSections else { -// return nil -// } -// var indexs: [String] = [] -// for section in sections { -// if section.initial.count > 0 { -// indexs.append(section.initial) -// } -// } - // ["A"..."Z", "#"] let idx = UnicodeScalar("A").value ... UnicodeScalar("Z").value var indexs = (idx.map { String(UnicodeScalar($0)!) }) @@ -194,4 +219,137 @@ open class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { return indexs } + + // MARK: - NEContactListener + + /// 从通讯录中移除 + /// - Parameter accountId: 好友 Id + func removeFromContacts(_ accountId: String) { + for (title, section) in contacts.enumerated() { + for (i, contact) in section.contacts.enumerated() { + if contact.user?.user?.accountId == accountId { + section.contacts.remove(at: i) + + // 该分组无好友后要删除该分组 + if section.contacts.isEmpty { + contacts.remove(at: title) + } + delegate?.reloadTableView() + return + } + } + } + } + + /// 好友添加回调 + /// - Parameter friendInfo: 好友信息 + public func onFriendAdded(_ friendInfo: V2NIMFriend) { + loadData { [weak self] _, _ in + self?.delegate?.reloadTableView() + } + } + + /// 删除好友通知 + /// 本端删除好友,多端同步 + /// - Parameters: + /// - accountId: 删除的好友账号ID + /// - deletionType: 好友删除的类型 + public func onFriendDeleted(_ accountId: String, deletionType: V2NIMFriendDeletionType) { + if NEFriendUserCache.shared.isBlockAccount(accountId) { + return + } + + removeFromContacts(accountId) + } + + /// 收到好友添加申请回调 + /// - Parameter application: 申请添加好友信息 + public func onFriendAddApplication(_ application: V2NIMFriendAddApplication) { + getAddApplicationUnreadCount(nil) + } + + /// 好友添加申请被拒绝回调 + /// - Parameter rejectionInfo: 申请添加好友拒绝信息 + public func onFriendAddRejected(_ rejectionInfo: V2NIMFriendAddApplication) { + getAddApplicationUnreadCount(nil) + } + + /// 好友信息变更回调 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + guard let accountId = friendInfo.accountId else { return } + + if NEFriendUserCache.shared.isBlockAccount(accountId) { + return + } + + loadData { [weak self] _, _ in + self?.delegate?.reloadTableView() + } + } + + /// 黑名单添加回调 + /// - Parameter user: 用户信息 + public func onBlockListAdded(_ user: V2NIMUser) { + guard let accountId = user.accountId else { return } + removeFromContacts(accountId) + } + + /// 黑名单移除回调 + /// - Parameter accountId: 用户 Id + public func onBlockListRemoved(_ accountId: String) { + NEFriendUserCache.shared.removeBlockAccount(accountId) + if NEFriendUserCache.shared.isFriend(accountId) { + loadData { [weak self] _, _ in + self?.delegate?.reloadTableView() + } + } + } + + // MARK: - NEEventListener + + /// 订阅在线状态 + public func subscribeOnlineStatus() { + var subscribeList: [String] = [] + for section in contacts { + for contact in section.contacts { + if let accountId = contact.user?.user?.accountId { + subscribeList.append(accountId) + } + } + } + weak var weakSelf = self + if subscribeList.count > 0 { + NEEventSubscribeManager.shared.subscribeUsersOnlineState(subscribeList) { error in + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + " contact subscribeUsersOnlineState : \(error?.localizedDescription ?? "")") + } + } + } + + /// 取消订阅 + public func unsubscribeOnlineStatus() { + var subscribeList: [String] = [] + for section in contacts { + for contact in section.contacts { + if let accountId = contact.user?.user?.accountId { + subscribeList.append(accountId) + } + } + } + weak var weakSelf = self + NEEventSubscribeManager.shared.unSubscribeUsersOnlineState(subscribeList) { error in + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + " contact unSubscribeUsersOnlineState : \(error?.localizedDescription ?? "")") + } + } + + public func onRecvSubscribeEvents(_ event: [NIMSubscribeEvent]) { + NEALog.infoLog(className(), desc: #function + " event count : \(event.count)") + for e in event { + print("event from : \(e.from ?? "") event value : \(e.value) event type : \(e.type)") + if e.type == NIMSubscribeSystemEventType.online.rawValue, let acountId = e.from { + onlineStatusDic[acountId] = e + } + } + delegate?.reloadTableView() + } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift index 3bc5bcf8..a841220e 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/FindFriendViewModel.swift @@ -4,7 +4,7 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit @objcMembers @@ -12,8 +12,11 @@ open class FindFriendViewModel: NSObject { let contactRepo = ContactRepo.shared private let className = "FindFriendViewModel" - func searchFriend(_ text: String, _ completion: @escaping ([NEKitUser]?, NSError?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", text: \(text.count)") - contactRepo.fetchUserInfo(accountList: [text], completion) + func searchFriend(_ text: String, _ completion: @escaping (NEUserWithFriend?, Error?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", text: \(text.count)") + + contactRepo.getUserWithFriend(accountIds: [text]) { userFriends, error in + completion(userFriends?.first, error) + } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift index c61d8f08..00316843 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactSelectedCell.swift @@ -4,37 +4,26 @@ // found in the LICENSE file. import UIKit + @objcMembers open class NEBaseContactSelectedCell: NEBaseContactTableViewCell { - let sImage = UIImageView() + let sImageView = UIImageView() var sModel: ContactInfo? override open func commonUI() { super.commonUI() leftConstraint?.constant = 50 - contentView.addSubview(sImage) - sImage.image = UIImage.ne_imageNamed(name: "unselect") - sImage.translatesAutoresizingMaskIntoConstraints = false - sImage.accessibilityIdentifier = "id.selector" + contentView.addSubview(sImageView) + sImageView.image = UIImage.ne_imageNamed(name: "unselect") + sImageView.translatesAutoresizingMaskIntoConstraints = false + sImageView.accessibilityIdentifier = "id.selector" } + /// 重写 setModel 方法,设置 sImageView + /// - Parameter model: ContactInfo override open func setModel(_ model: ContactInfo) { super.setModel(model) - if model.isSelected == false { - sImage.isHighlighted = false - } else { - sImage.isHighlighted = true - } - } - - func setSelect() { - sModel?.isSelected = true - sImage.isHighlighted = true - } - - func setUnselect() { - sModel?.isSelected = false - sImage.isHighlighted = false + sImageView.isHighlighted = model.isSelected } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift index 7830549b..c9f59061 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactTableViewCell.swift @@ -5,13 +5,13 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @objcMembers open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataProtrol { - public lazy var arrow: UIImageView = { + public lazy var arrowImageView: UIImageView = { let imageView = UIImageView(image: UIImage.ne_imageNamed(name: "arrowRight")) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.contentMode = .center @@ -32,7 +32,7 @@ open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataPro } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { @@ -40,23 +40,23 @@ open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataPro contentView.addSubview(titleLabel) NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 12), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 12), titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -35), titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), titleLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) - contentView.addSubview(arrow) + contentView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - arrow.widthAnchor.constraint(equalToConstant: 15), - arrow.topAnchor.constraint(equalTo: contentView.topAnchor), - arrow.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowImageView.widthAnchor.constraint(equalToConstant: 15), + arrowImageView.topAnchor.constraint(equalTo: contentView.topAnchor), + arrowImageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 1), @@ -65,18 +65,18 @@ open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataPro contentView.addSubview(redAngleView) NSLayoutConstraint.activate([ redAngleView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - redAngleView.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -10), + redAngleView.rightAnchor.constraint(equalTo: arrowImageView.leftAnchor, constant: -10), redAngleView.heightAnchor.constraint(equalToConstant: 18), ]) } open func initSubviewsLayout() { if NEKitContactConfig.shared.ui.contactProperties.avatarType == .rectangle { - avatarImage.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius + avatarImageView.layer.cornerRadius = NEKitContactConfig.shared.ui.contactProperties.avatarCornerRadius } else if NEKitContactConfig.shared.ui.contactProperties.avatarType == .cycle { - avatarImage.layer.cornerRadius = 18.0 + avatarImageView.layer.cornerRadius = 18.0 } else { - avatarImage.layer.cornerRadius = 18.0 // Normal UI + avatarImageView.layer.cornerRadius = 18.0 // Normal UI } } @@ -92,33 +92,34 @@ open class NEBaseContactTableViewCell: NEBaseContactViewCell, ContactCellDataPro } setConfig() - if let userId = user.userId, let u = ChatUserCache.getUserInfo(userId) { + // 更新用户信息 + if let userId = user.user?.accountId, let u = NEFriendUserCache.shared.getFriendInfo(userId) { user = u } if model.contactCellType == 1 { - NELog.infoLog("contact other cell configData", desc: "\(user.alias), image name:\(user.userInfo?.avatarUrl)") + NEALog.infoLog("contact other cell configData", desc: "\(user.friend?.alias), image name:\(user.user?.avatar)") nameLabel.text = "" - titleLabel.text = user.alias - avatarImage.image = UIImage.ne_imageNamed(name: user.userInfo?.avatarUrl) - avatarImage.backgroundColor = model.headerBackColor - arrow.isHidden = false + titleLabel.text = user.friend?.alias + avatarImageView.image = UIImage.ne_imageNamed(name: user.user?.avatar) + avatarImageView.backgroundColor = model.headerBackColor + arrowImageView.isHidden = false } else { // person、custom titleLabel.text = user.showName() - nameLabel.text = user.shortName(showAlias: false, count: 2) + nameLabel.text = user.shortName(count: 2) - if let imageUrl = user.userInfo?.avatarUrl, !imageUrl.isEmpty { - NELog.infoLog("contact p2p cell configData", desc: "imageName:\(imageUrl)") + if let imageUrl = user.user?.avatar, !imageUrl.isEmpty { + NEALog.infoLog("contact p2p cell configData", desc: "imageName:\(imageUrl)") nameLabel.isHidden = true - avatarImage.sd_setImage(with: URL(string: imageUrl), completed: nil) + avatarImageView.sd_setImage(with: URL(string: imageUrl), completed: nil) } else { - NELog.infoLog("contact p2p cell configData", desc: "imageName is nil") + NEALog.infoLog("contact p2p cell configData", desc: "imageName is nil") nameLabel.isHidden = false - avatarImage.sd_setImage(with: nil) - avatarImage.backgroundColor = model.headerBackColor + avatarImageView.sd_setImage(with: nil) + avatarImageView.backgroundColor = model.headerBackColor } - arrow.isHidden = true + arrowImageView.isHidden = true } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactUnCheckCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactUnCheckCell.swift index 8f60c979..4b45552c 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactUnCheckCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/Cell/NEBaseContactUnCheckCell.swift @@ -17,10 +17,10 @@ open class NEBaseContactUnCheckCell: UICollectionViewCell { } func setupUI() { - contentView.addSubview(avatarImage) + contentView.addSubview(avatarImageView) } - lazy var avatarImage: NEUserHeaderView = { + lazy var avatarImageView: NEUserHeaderView = { let view = NEUserHeaderView(frame: .zero) view.titleLabel.font = UIFont.systemFont(ofSize: 16.0) view.clipsToBounds = true @@ -28,11 +28,7 @@ open class NEBaseContactUnCheckCell: UICollectionViewCell { return view }() - func configure(_ model: ContactInfo) { - avatarImage.configHeadData( - headUrl: model.user?.userInfo?.avatarUrl, - name: model.user?.showName() ?? "", - uid: model.user?.userId ?? "" - ) - } + /// 控件赋值方法,具体实现见子类 + /// - Parameter model: 数据模型(Any) + func configure(_ model: Any) {} } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/ContactSectionView.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/ContactSectionView.swift index 0a5d6376..fceebfdb 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/ContactSectionView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/ContactSectionView.swift @@ -16,7 +16,7 @@ open class ContactSectionView: UITableViewHeaderFooterView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func commonUI() { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift index bb6bd6e7..4c5bec97 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactRemakNameViewController.swift @@ -4,16 +4,16 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import UIKit @objcMembers -open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UITextFieldDelegate { - typealias ModifyBlock = (_ user: NEKitUser) -> Void +open class NEBaseContactRemakNameViewController: NEContactBaseViewController, UITextFieldDelegate { + typealias ModifyBlock = (_ user: NEUserWithFriend) -> Void var completion: ModifyBlock? - var user: NEKitUser? + var user: NEUserWithFriend? let viewmodel = ContactUserViewModel() let textLimit = 15 lazy var aliasInput: UITextField = { @@ -59,7 +59,7 @@ open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UI view.addSubview(aliasInput) aliasInput.placeholder = localizable("input_noteName") - if let alias = user?.alias, !alias.isEmpty { + if let alias = user?.friend?.alias, !alias.isEmpty { aliasInput.text = alias } } @@ -83,15 +83,12 @@ open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UI return } - if user?.alias != aliasInput.text { - user?.alias = aliasInput.text - NotificationCenter.default.post(name: NENotificationName.updateFriendInfo, object: user) - } + user?.friend?.alias = aliasInput.text - if let u = user { + if let uid = user?.user?.accountId, let alias = aliasInput.text { view.makeToastActivity(.center) - viewmodel.update(u) { error in - NELog.infoLog( + viewmodel.updateAlias(accountId: uid, alias: alias) { error in + NEALog.infoLog( "ContactRemakNameViewController", desc: "CALLBACK update " + (error?.localizedDescription ?? "no error") ) @@ -103,7 +100,7 @@ open class NEBaseContactRemakNameViewController: NEBaseContactViewController, UI position: .center ) } else { - if let block = weakSelf?.completion { + if let block = weakSelf?.completion, let u = weakSelf?.user { block(u) } weakSelf?.navigationController?.popViewController(animated: true) diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactSelectedViewController.swift similarity index 65% rename from NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsSelectedViewController.swift rename to NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactSelectedViewController.swift index c29e1b79..15744080 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactSelectedViewController.swift @@ -2,12 +2,15 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit +/// 人员选择页面 - 基类 @objcMembers -open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UITableViewDelegate, UITableViewDataSource { +open class NEBaseContactSelectedViewController: NEContactBaseViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UITableViewDelegate, UITableViewDataSource { public var callBack: ContactsSelectCompletion? public var filterUsers: Set? @@ -19,6 +22,7 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI public var selectArray = [ContactInfo]() public let selectDic = [String: ContactInfo]() + public var isCreating = false // 是否正在创建群组 public lazy var collectionBackView: UIView = { let view = UIView() @@ -29,18 +33,18 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI return view }() - public lazy var collection: UICollectionView = { + public lazy var collectionView: UICollectionView = { let layout = UICollectionViewFlowLayout() layout.scrollDirection = .horizontal layout.minimumLineSpacing = 0 layout.minimumInteritemSpacing = 0 - let collect = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) - collect.contentInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) - collect.accessibilityIdentifier = "id.selected" - return collect + let collectView = UICollectionView(frame: CGRect.zero, collectionViewLayout: layout) + collectView.contentInset = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 5) + collectView.accessibilityIdentifier = "id.selected" + return collectView }() - public var sureBtn = UIButton(frame: CGRect(x: 0, y: 0, width: 76, height: 32)) + public var sureButton = UIButton(frame: CGRect(x: 0, y: 0, width: 76, height: 32)) var collectionBackViewTopMargin: CGFloat = 0 var collectionBackViewHeight: CGFloat = 52 @@ -50,19 +54,19 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI public let viewModel = ContactViewModel(contactHeaders: nil) public lazy var tableView: UITableView = { - let table = UITableView(frame: .zero, style: .plain) - table.backgroundColor = .clear - table.sectionIndexColor = .ne_greyText - table.delegate = self - table.dataSource = self - table.translatesAutoresizingMaskIntoConstraints = false - table.separatorStyle = .none - table.contentInset = .init(top: -10, left: 0, bottom: 0, right: 0) + let tableView = UITableView(frame: .zero, style: .plain) + tableView.backgroundColor = .clear + tableView.sectionIndexColor = .ne_greyText + tableView.delegate = self + tableView.dataSource = self + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.contentInset = .init(top: -10, left: 0, bottom: 0, right: 0) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0 + tableView.sectionHeaderTopPadding = 0 } - return table + return tableView }() var tableViewTopAnchor: NSLayoutConstraint? @@ -73,14 +77,14 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() title = localizable("select") navigationView.navTitle.text = title - emptyView.settingContent(content: localizable("no_friend")) + emptyView.setText(localizable("no_friend")) setupUI() setupNavRightItem() @@ -100,27 +104,27 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI collectionBackView.heightAnchor.constraint(equalToConstant: collectionBackViewHeight), ]) - collection.backgroundColor = .clear - collection.delegate = self - collection.dataSource = self - collection.allowsMultipleSelection = false - collection.translatesAutoresizingMaskIntoConstraints = false - collectionBackView.addSubview(collection) + collectionView.backgroundColor = .clear + collectionView.delegate = self + collectionView.dataSource = self + collectionView.allowsMultipleSelection = false + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionBackView.addSubview(collectionView) NSLayoutConstraint.activate([ - collection.centerYAnchor.constraint(equalTo: collectionBackView.centerYAnchor), - collection.leftAnchor.constraint(equalTo: collectionBackView.leftAnchor), - collection.rightAnchor.constraint(equalTo: collectionBackView.rightAnchor), - collection.heightAnchor.constraint(equalToConstant: collectionBackViewHeight), + collectionView.centerYAnchor.constraint(equalTo: collectionBackView.centerYAnchor), + collectionView.leftAnchor.constraint(equalTo: collectionBackView.leftAnchor), + collectionView.rightAnchor.constraint(equalTo: collectionBackView.rightAnchor), + collectionView.heightAnchor.constraint(equalToConstant: collectionBackViewHeight), ]) view.addSubview(tableView) tableViewTopAnchor = tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant) + tableViewTopAnchor?.isActive = true NSLayoutConstraint.activate([ tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - tableViewTopAnchor!, ]) tableView.register( @@ -128,9 +132,9 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI forHeaderFooterViewReuseIdentifier: "\(NSStringFromClass(ContactSectionView.self))" ) - customCells.forEach { (key: Int, value: AnyClass) in + for (key, value) in customCells { if value is ContactCellDataProtrol.Type { - self.tableView.register( + tableView.register( value, forCellReuseIdentifier: "\(NSStringFromClass(value))" ) @@ -148,33 +152,36 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI open func setupNavRightItem() { if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { - let rightItem = UIBarButtonItem(customView: sureBtn) + let rightItem = UIBarButtonItem(customView: sureButton) navigationItem.rightBarButtonItem = rightItem - sureBtn.addTarget(self, action: #selector(sureClick(_:)), for: .touchUpInside) - sureBtn.setTitle(localizable("alert_sure"), for: .normal) - sureBtn.setTitleColor(.white, for: .normal) - sureBtn.layer.cornerRadius = 4 - sureBtn.contentHorizontalAlignment = .center - sureBtn.titleLabel?.font = UIFont.systemFont(ofSize: 16.0) + sureButton.addTarget(self, action: #selector(sureClick(_:)), for: .touchUpInside) + sureButton.setTitle(localizable("alert_sure"), for: .normal) + sureButton.setTitleColor(.white, for: .normal) + sureButton.layer.cornerRadius = 4 + sureButton.contentHorizontalAlignment = .center + sureButton.titleLabel?.font = UIFont.systemFont(ofSize: 16.0) } else { navigationView.setMoreButtonTitle(localizable("alert_sure")) navigationView.moreButton.setTitleColor(.white, for: .normal) navigationView.moreButton.layer.cornerRadius = 4 navigationView.moreButton.contentHorizontalAlignment = .center navigationView.addMoreButtonTarget(target: self, selector: #selector(sureClick(_:))) - navigationView.setBackButtonTitle(localizable("close")) - navigationView.backButton.setTitleColor(.ne_darkText, for: .normal) - sureBtn = navigationView.moreButton + sureButton = navigationView.moreButton } } open func sureClick(_ sender: UIButton) { + // 防止多次点击确定按钮会多次创建群聊 + if isCreating { + return + } + if selectArray.count <= 0 { showToast(localizable("select_contact")) return } - if !NEChatDetectNetworkTool.shareInstance.isNetworkRecahability() { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { showToast(localizable("network_error")) return } @@ -185,35 +192,51 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI return } + isCreating = true var accids = [String]() var names = [String]() + let group = DispatchGroup() + var mine: NEUserWithFriend? - names.append(viewModel.contactRepo.getUserName()) - - var users = [NIMUser]() - for c in selectArray { - accids.append(c.user?.userId ?? "") - if let name = c.user?.userInfo?.nickName { - names.append(name) - } else if let accid = c.user?.userId { - names.append(accid) - } - if let user = c.user?.imUser { - users.append(user) + if let mineInfo = NEFriendUserCache.shared.getFriendInfo(IMKitClient.instance.account()) { + mine = mineInfo + } else { + group.enter() + ContactRepo.shared.getUserList(accountIds: [IMKitClient.instance.account()]) { users, error in + mine = users?.first + group.leave() } } - if let uid = userId { - accids.append(uid) + group.notify(queue: .main) { [weak self] in + let myName = mine?.showName() ?? IMKitClient.instance.account() + names.append(myName) + var users = [V2NIMUser]() + for c in self?.selectArray ?? [] { + accids.append(c.user?.user?.accountId ?? "") + if let name = c.user?.user?.name { + names.append(name) + } else if let accid = c.user?.user?.accountId { + names.append(accid) + } + if let user = c.user?.user { + users.append(user) + } + } + + if let uid = self?.userId { + accids.append(uid) + } + let nameString = names.joined(separator: "、") + print("name string : ", nameString) + Router.shared.use( + ContactSelectedUsersRouter, + parameters: ["accids": accids, "names": nameString, "im_user": users], + closure: nil + ) + self?.navigationController?.popViewController(animated: true) + self?.isCreating = false } - let nameString = names.joined(separator: "、") - print("name string : ", nameString) - Router.shared.use( - ContactSelectedUsersRouter, - parameters: ["accids": accids, "names": nameString, "im_user": users], - closure: nil - ) - navigationController?.popViewController(animated: true) } // MARK: - Table View DataSource And Delegate @@ -257,17 +280,14 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let info = viewModel.contacts[indexPath.section].contacts[indexPath.row] - let cell = tableView.cellForRow(at: indexPath) as? NEBaseContactSelectedCell if info.isSelected == true { didUnselectContact(info) - cell?.setSelect() } else { if selectArray.count >= limit { - view.makeToast("超出\(limit)人限制") + view.makeToast(String(format: localizable("exceeded_limit"), limit)) return } didSelectContact(info) - cell?.setUnselect() } } @@ -320,7 +340,7 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI tableViewTopAnchor?.constant += collectionBackViewHeight + collectionBackViewTopMargin * 2 } } - collection.reloadData() + collectionView.reloadData() tableView.reloadData() refreshSelectCount() } @@ -334,16 +354,16 @@ open class NEBaseContactsSelectedViewController: NEBaseContactViewController, UI collectionBackView.isHidden = true tableViewTopAnchor?.constant -= collectionBackViewHeight + collectionBackViewTopMargin * 2 } - collection.reloadData() + collectionView.reloadData() tableView.reloadData() refreshSelectCount() } func refreshSelectCount() { if selectArray.count > 0 { - sureBtn.setTitle("确定(\(selectArray.count))", for: .normal) + sureButton.setTitle("确定(\(selectArray.count))", for: .normal) } else { - sureBtn.setTitle(localizable("alert_sure"), for: .normal) + sureButton.setTitle(localizable("alert_sure"), for: .normal) } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift index 3c3afa2d..11158a63 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactUserViewController.swift @@ -4,17 +4,17 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit +/// 用户信息界面 - 基类 @objcMembers -open class NEBaseContactUserViewController: NEBaseContactViewController, UITableViewDelegate, +open class NEBaseContactUserViewController: NEContactBaseViewController, UITableViewDelegate, UITableViewDataSource { - var user: NEKitUser? + var user: NEUserWithFriend? var uid: String? - public var isBlack: Bool = false var className = "ContactUserViewController" public let viewModel = ContactUserViewModel() @@ -22,19 +22,31 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable var data = [[UserItem]]() public var headerView = NEBaseUserInfoHeaderView() - public init(user: NEKitUser?) { + /// 使用 accountId 初始化 + /// - Parameter accountId: 用户 id + public init(uid: String) { super.init(nibName: nil, bundle: nil) - self.user = user - uid = user?.userId + self.uid = uid } - public init(uid: String) { + /// 使用 V2NIMUser 初始化 + /// - Parameter nim_user: V2NIMUser 对象 + public init(nim_user: V2NIMUser) { super.init(nibName: nil, bundle: nil) - self.uid = uid + user = NEUserWithFriend(user: nim_user) + uid = user?.user?.accountId + } + + /// 使用 NEUserWithFriend 初始化 + /// - Parameter user: NEUserWithFriend 对象 + public init(user: NEUserWithFriend?) { + super.init(nibName: nil, bundle: nil) + self.user = user + uid = user?.user?.accountId } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -47,23 +59,22 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable weakSelf?.showToast(commonLocalizable("network_error"), .bottom) } view.makeToastActivity(.center) - viewModel.fetchUserInfo(accountList: [userId]) { users, error in + viewModel.getUserInfo(userId) { user, error in weakSelf?.view.hideToastActivity() - NELog.infoLog( + NEALog.infoLog( weakSelf?.className ?? "ContactUserViewController", desc: "CALLBACK getUserInfo " + (error?.localizedDescription ?? "no error") ) if let err = error { weakSelf?.showToast(err.localizedDescription) - } else if let u = users?.first { + } else if let u = user { weakSelf?.user = u - ChatUserCache.updateUserInfo(u) weakSelf?.loadData() } } } - NIMSDK.shared().systemNotificationManager.add(self) + ContactRepo.shared.addContactListener(self) } open func commonUI() { @@ -108,36 +119,35 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable } func loadData() { - let isFriend = viewModel.contactRepo.isFriend(account: user?.userId ?? "") - isBlack = viewModel.contactRepo.isBlackList(account: user?.userId ?? "") + guard let uid = user?.user?.accountId else { return } - if isFriend { + if NEFriendUserCache.shared.isFriend(uid) { data = [ [ UserItem(title: localizable("noteName"), - detailTitle: user?.alias, + detailTitle: user?.friend?.alias, value: false, textColor: UIColor.darkText, cellClass: TextWithRightArrowCell.self), ], [ UserItem(title: localizable("birthday"), - detailTitle: user?.userInfo?.birth, + detailTitle: user?.user?.birthday, value: false, textColor: UIColor.darkText, cellClass: TextWithDetailTextCell.self), UserItem(title: localizable("phone"), - detailTitle: user?.userInfo?.mobile, + detailTitle: user?.user?.mobile, value: false, textColor: UIColor.darkText, cellClass: TextWithDetailTextCell.self), UserItem(title: localizable("email"), - detailTitle: user?.userInfo?.email, + detailTitle: user?.user?.email, value: false, textColor: UIColor.darkText, cellClass: TextWithDetailTextCell.self), UserItem(title: localizable("sign"), - detailTitle: user?.userInfo?.sign, + detailTitle: user?.user?.sign, value: false, textColor: UIColor.darkText, cellClass: TextWithDetailTextCell.self), @@ -146,7 +156,7 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable [ UserItem(title: localizable("add_blackList"), detailTitle: "", - value: isBlack, + value: NEFriendUserCache.shared.isBlockAccount(uid), textColor: UIColor.darkText, cellClass: TextWithSwitchCell.self), ], @@ -167,7 +177,7 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable data = [ [ UserItem(title: localizable("add_friend"), - detailTitle: user?.alias, + detailTitle: "", value: false, textColor: UIColor(hexString: "#337EFF"), cellClass: CenterTextCell.self), @@ -215,7 +225,6 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable c.titleLabel.text = item.title c.switchButton.isOn = item.value c.block = { [weak self] title, value in - print("title:\(title) value\(value)") if title == localizable("add_blackList") { self?.blackList(isBlack: value) { c.switchButton.isOn = !c.switchButton.isOn @@ -242,9 +251,9 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable } open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = UIColor.clear - return header + let headerView = UIView() + headerView.backgroundColor = UIColor.clear + return headerView } open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { @@ -273,11 +282,12 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable deleteFriend(user: user) } if item.title == localizable("add_friend") { - if let uId = user?.userId, - viewModel.isFriend(account: uId) { - loadData() - } else { - addFriend() + if let uid = user?.user?.accountId { + if NEFriendUserCache.shared.isFriend(uid) { + loadData() + } else { + addFriend() + } } } } @@ -292,7 +302,6 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable remark.completion = { [weak self] u in self?.user = u self?.headerView.setData(user: u) - ChatUserCache.updateUserInfo(u) } navigationController?.pushViewController(remark, animated: true) @@ -303,80 +312,74 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable print("edit remarks") } - func blackList(isBlack: Bool, completion: () -> Void) { - weak var weakSelf = self + func blackList(isBlack: Bool, completion: @escaping () -> Void) { if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { - weakSelf?.showToast(commonLocalizable("network_error")) + showToast(commonLocalizable("network_error")) completion() return } - guard let userId = user?.userId else { + guard let userId = user?.user?.accountId else { return } + if isBlack { // add - viewModel.contactRepo.addBlackList(account: userId) { [weak self] error in - if error != nil { - self?.view.makeToast(error?.localizedDescription) - } else { - // success - self?.isBlack = true - self?.loadData() + viewModel.contactRepo.addBlockList(accountId: userId) { [weak self] error in + if let err = error { + self?.showToast(err.localizedDescription) + completion() } } } else { // remove - viewModel.contactRepo.removeBlackList(account: userId) { [weak self] error in - if error != nil { - self?.view.makeToast(error?.localizedDescription) - } else { - // success - self?.isBlack = false - self?.loadData() + viewModel.contactRepo.removeBlockList(accountId: userId) { [weak self] error in + if let err = error { + self?.showToast(err.localizedDescription) + completion() } } } } - func chat(user: NEKitUser?) { - guard let accid = self.user?.userId else { + func chat(user: NEUserWithFriend?) { + guard let accid = self.user?.user?.accountId else { return } - let session = NIMSession(accid, type: .P2P) + let conversationId = V2NIMConversationIdUtil.p2pConversationId(accid) Router.shared.use( PushP2pChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session, "removeUserVC": true], + parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any, "removeUserVC": true], closure: nil ) } - func deleteFriendAction(user: NEKitUser?) { + func deleteFriendAction(user: NEUserWithFriend?) { weak var weakSelf = self if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { weakSelf?.showToast(commonLocalizable("network_error")) return } - if let userId = user?.userId { + if let userId = user?.user?.accountId { viewModel.deleteFriend(account: userId) { error in - NELog.infoLog( + NEALog.infoLog( self.className, desc: "CALLBACK deleteFriend " + (error?.localizedDescription ?? "no error") ) if error != nil { self.showToast(error?.localizedDescription ?? "") } else { - ChatUserCache.removeUserInfo(userId) + NEFriendUserCache.shared.removeFriendInfo(userId) self.navigationController?.popViewController(animated: true) } } } } - open func deleteFriend(user: NEKitUser?) { - let alertTitle = String(format: localizable("delete_title"), user?.showName(true) ?? "") + open func deleteFriend(user: NEUserWithFriend?) { + let alertTitle = String(format: localizable("delete_title"), user?.showName() ?? "") let alertController = UIAlertController( title: alertTitle, message: nil, @@ -411,23 +414,24 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable weakSelf?.showToast(commonLocalizable("network_error")) return } - if let account = user?.userId { + if let account = user?.user?.accountId { viewModel.addFriend(account) { error in - NELog.infoLog( + NEALog.infoLog( self.className, desc: "CALLBACK addFriend " + (error?.localizedDescription ?? "no error") ) if let err = error { - NELog.errorLog("ContactUserViewController", desc: "❌add friend failed :\(err)") + NEALog.errorLog("ContactUserViewController", desc: "❌add friend failed :\(err)") } else { weakSelf?.showToast(localizable("send_friend_apply")) - if let model = weakSelf?.viewModel, - model.isBlack(account: account) { - weakSelf?.viewModel.removeBlackList(account: account) { err in - NELog.infoLog( - self.className, - desc: #function + "CALLBACK " + (err?.localizedDescription ?? "no error") - ) + if let model = weakSelf?.viewModel { + if NEFriendUserCache.shared.isBlockAccount(account) { + weakSelf?.viewModel.removeBlackList(account: account) { err in + NEALog.infoLog( + self.className, + desc: #function + "CALLBACK " + (err?.localizedDescription ?? "no error") + ) + } } } } @@ -436,11 +440,49 @@ open class NEBaseContactUserViewController: NEBaseContactViewController, UITable } } -extension NEBaseContactUserViewController: NIMSystemNotificationManagerDelegate { - open func onReceive(_ notification: NIMSystemNotification) { - if notification.type == .friendAdd, - let obj = notification.attachment as? NIMUserAddAttachment, - obj.operationType == .verify { +// MARK: - NEContactListener + +extension NEBaseContactUserViewController: NEContactListener { + /// 黑名单添加回调 + /// - Parameter user: 加入黑名单的好友 + public func onBlockListAdded(_ user: V2NIMUser) { + guard let accountId = user.accountId else { return } + NEFriendUserCache.shared.addBlockAccount(accountId) + if accountId == uid { + loadData() + } + } + + /// 黑名单移除回调 + /// - Parameter accountId: 移除黑名的用户账号ID + public func onBlockListRemoved(_ accountId: String) { + NEFriendUserCache.shared.removeBlockAccount(accountId) + if accountId == uid { + loadData() + } + } + + /// 添加好友通知 + /// - Parameter friendInfo: 好友信息 + public func onFriendAdded(_ friendInfo: V2NIMFriend) { + if friendInfo.accountId == uid { + user?.friend = friendInfo + loadData() + } + } + + public func onFriendDeleted(_ accountId: String, deletionType: V2NIMFriendDeletionType) { + NEFriendUserCache.shared.removeFriendInfo(accountId) + if accountId == uid { + loadData() + } + } + + /// 好友信息变更回调 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + if friendInfo.accountId == uid { + user?.friend = friendInfo loadData() } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactViewController.swift index 0cf505a0..234b383a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactViewController.swift @@ -1,78 +1,433 @@ - // Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit +import NECoreIM2Kit +import NECoreKit import UIKit +@objc +public protocol NEBaseContactViewControllerDelegate { + func onDataLoaded() +} + +/// 通讯录页面 - 基类 @objcMembers -open class NEBaseContactViewController: UIViewController, UIGestureRecognizerDelegate { - var topConstant: CGFloat = 0 - public let navigationView = NENavigationView() - - public lazy var emptyView: NEEmptyDataView = { - let view = NEEmptyDataView( - imageName: "user_empty", - content: "", - frame: CGRect.zero - ) +open class NEBaseContactViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate, TabNavigationViewDelegate, ContactViewModelDelegate { + public var delegate: NEBaseContactViewControllerDelegate? + + // custom ui cell + public var cellRegisterDic = [Int: NEBaseContactTableViewCell.Type]() + + public var viewModel = ContactViewModel(contactHeaders: nil) + private var lastTitleIndex = 0 + + public var bodyTopViewHeight: CGFloat = 0 { + didSet { + bodyTopViewHeightAnchor?.constant = bodyTopViewHeight + bodyTopView.isHidden = bodyTopViewHeight <= 0 + } + } + + public var bodyBottomViewHeight: CGFloat = 0 { + didSet { + bodyBottomViewHeightAnchor?.constant = bodyBottomViewHeight + bodyBottomView.isHidden = bodyBottomViewHeight <= 0 + } + } + + public var topConstant: CGFloat = 0 + private var bodyTopViewHeightAnchor: NSLayoutConstraint? + private var bodyBottomViewHeightAnchor: NSLayoutConstraint? + + public lazy var navigationView: TabNavigationView = { + let nav = TabNavigationView(frame: CGRect.zero) + nav.translatesAutoresizingMaskIntoConstraints = false + nav.delegate = self + + if let addImg = NEKitContactConfig.shared.ui.titleBarRightRes { + nav.addBtn.setImage(addImg, for: .normal) + } + if let searchImg = NEKitContactConfig.shared.ui.titleBarRight2Res { + nav.searchBtn.setImage(searchImg, for: .normal) + } + return nav + }() + + public lazy var bodyTopView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + return view + }() + + public lazy var bodyView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + view.addSubview(contentView) + + NSLayoutConstraint.activate([ + contentView.topAnchor.constraint(equalTo: view.topAnchor), + contentView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + return view + }() + + public lazy var contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + view.addSubview(tableView) + view.addSubview(emptyView) + + NSLayoutConstraint.activate([ + tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.rightAnchor.constraint(equalTo: view.rightAnchor), + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + NSLayoutConstraint.activate([ + emptyView.leftAnchor.constraint(equalTo: view.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: view.rightAnchor), + emptyView.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.screenWidth / 2), + emptyView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + return view + }() + + public lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .grouped) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + tableView.backgroundColor = UIColor.ne_backgroundColor + tableView.sectionFooterHeight = 0 + tableView.sectionIndexColor = .ne_greyText + + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + return tableView + }() + + lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView(imageName: "user_empty", content: localizable("no_friend"), frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false + view.backgroundColor = .clear view.isHidden = true return view + }() + public lazy var bodyBottomView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + return view }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if navigationController?.viewControllers.count ?? 0 > 0 { + if let root = navigationController?.viewControllers[0] as? UIViewController { + if root.isKind(of: NEBaseContactViewController.self) { + navigationController?.interactivePopGestureRecognizer?.delegate = self + } + } + } + loadData() + viewModel.getAddApplicationUnreadCount(nil) + } + override open func viewDidLoad() { super.viewDidLoad() + showTitleBar() + commonUI() - // Do any additional setup after loading the view. - view.backgroundColor = .white - edgesForExtendedLayout = [] - navigationController?.interactivePopGestureRecognizer?.delegate = self + weak var weakSelf = self + viewModel.delegate = self + viewModel.refresh = { + weakSelf?.didRefreshTable() + } + + NotificationCenter.default.addObserver(self, selector: #selector(clearValidationUnreadCount), name: NENotificationName.clearValidationUnreadCount, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(clearValidationUnreadCount), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(loadData), name: NENotificationName.friendCacheInit, object: nil) + } + + /// 清除未读数 + func clearValidationUnreadCount() { + viewModel.unreadCount = 0 + } + open func showTitleBar() { if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { - navigationController?.isNavigationBarHidden = false - setupBackUI() + navigationView.isHidden = true topConstant = 0 + if NEKitContactConfig.shared.ui.showTitleBar { + navigationController?.isNavigationBarHidden = false + } else { + navigationController?.isNavigationBarHidden = true + if #available(iOS 10, *) { + topConstant += NEConstant.statusBarHeight + } + } } else { navigationController?.isNavigationBarHidden = true - topConstant = NEConstant.navigationAndStatusHeight - navigationView.translatesAutoresizingMaskIntoConstraints = false - navigationView.addBackButtonTarget(target: self, selector: #selector(backToPrevious)) - navigationView.moreButton.isHidden = true - view.addSubview(navigationView) - NSLayoutConstraint.activate([ - navigationView.leftAnchor.constraint(equalTo: view.leftAnchor), - navigationView.rightAnchor.constraint(equalTo: view.rightAnchor), - navigationView.topAnchor.constraint(equalTo: view.topAnchor), - navigationView.heightAnchor.constraint(equalToConstant: topConstant), - ]) - } - } - - open func setupBackUI() { - navigationController?.navigationBar.tintColor = .white - let backItem = UIBarButtonItem( - image: UIImage.ne_imageNamed(name: "backArrow"), - style: .plain, - target: self, - action: #selector(backToPrevious) + if NEKitContactConfig.shared.ui.showTitleBar { + navigationView.isHidden = false + topConstant = NEConstant.navigationHeight + } else { + navigationView.isHidden = true + topConstant = 0 + } + if #available(iOS 10, *) { + topConstant += NEConstant.statusBarHeight + } + } + } + + /// UI初始化 + open func commonUI() { + initSystemNav() + view.addSubview(navigationView) + view.addSubview(bodyTopView) + view.addSubview(bodyView) + view.addSubview(bodyBottomView) + + NSLayoutConstraint.activate([ + navigationView.topAnchor.constraint(equalTo: view.topAnchor), + navigationView.leftAnchor.constraint(equalTo: view.leftAnchor), + navigationView.rightAnchor.constraint(equalTo: view.rightAnchor), + navigationView.heightAnchor + .constraint(equalToConstant: NEConstant.navigationAndStatusHeight), + ]) + + NSLayoutConstraint.activate([ + bodyTopView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + bodyTopView.leftAnchor.constraint(equalTo: view.leftAnchor), + bodyTopView.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) + bodyTopViewHeightAnchor = bodyTopView.heightAnchor.constraint(equalToConstant: bodyTopViewHeight) + bodyTopViewHeightAnchor?.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), + ]) + + NSLayoutConstraint.activate([ + bodyBottomView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + bodyBottomView.leftAnchor.constraint(equalTo: view.leftAnchor), + bodyBottomView.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) + bodyBottomViewHeightAnchor = bodyBottomView.heightAnchor.constraint(equalToConstant: bodyBottomViewHeight) + bodyBottomViewHeightAnchor?.isActive = true + + if let customController = NEKitContactConfig.shared.ui.customController { + customController(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 loadData() { + viewModel.loadData { [weak self] error, userSectionCount in + if error == nil { + self?.delegate?.onDataLoaded() + self?.didRefreshTable() + } + } + } + + // UITableViewDataSource + open func numberOfSections(in tableView: UITableView) -> Int { + viewModel.contacts.count + } + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + NEALog.infoLog(ModuleName + " " + className(), desc: "contact section: \(section), count:\(viewModel.contacts[section].contacts.count)") + + return viewModel.contacts[section].contacts.count + } + + open func configCell(info: ContactInfo, _ cell: NEBaseContactTableViewCell, _ indexPath: IndexPath) -> UITableViewCell { + cell.setModel(info) + if indexPath.section == 0, indexPath.row == 0, viewModel.unreadCount > 0 { + cell.redAngleView.isHidden = false + cell.redAngleView.text = viewModel.unreadCount > 99 ? "99+" : "\(viewModel.unreadCount)" + } else { + cell.redAngleView.isHidden = true + } + return cell + } + + // 具体逻辑在子类实现 + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + UITableViewCell() + } + + open func tableView(_ tableView: UITableView, + viewForHeaderInSection section: Int) -> UIView? { + let sectionView: ContactSectionView = tableView + .dequeueReusableHeaderFooterView( + withIdentifier: "\(NSStringFromClass(ContactSectionView.self))" + ) as! ContactSectionView + sectionView.titleLabel.text = viewModel.contacts[section].initial + return sectionView + } + + open func tableView(_ tableView: UITableView, + heightForHeaderInSection section: Int) -> CGFloat { + if viewModel.contacts[section].initial.count > 0 { + return 40 + } + return 0 + } + + open func sectionIndexTitles(for tableView: UITableView) -> [String]? { + viewModel.indexs + } + + open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, + at index: Int) -> Int { + for (i, t) in viewModel.contacts.enumerated() { + if t.initial == title { + lastTitleIndex = i + return i + } + } + return lastTitleIndex + } + + open func tableView(_ tableView: UITableView, + heightForRowAt indexPath: IndexPath) -> CGFloat { + 52 + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let info = viewModel.contacts[indexPath.section].contacts[indexPath.row] + + if info.contactCellType == ContactCellType.ContactOthers.rawValue { + if let headerItemClick = NEKitContactConfig.shared.ui.headerItemClick { + headerItemClick(info, indexPath) + return + } + + switch info.router { + case ValidationMessageRouter: + Router.shared.use(ValidationMessageRouter, + parameters: ["nav": navigationController as Any], + closure: nil) + case ContactBlackListRouter: + Router.shared.use(ContactBlackListRouter, + parameters: ["nav": navigationController as Any], + closure: nil) + + case ContactTeamListRouter: + // My Team + Router.shared.use(ContactTeamListRouter, + parameters: ["nav": navigationController as Any], + closure: nil) + + case ContactPersonRouter: + break + + case ContactComputerRouter: + break + + default: + break + } + } else { + if let friendItemClick = NEKitContactConfig.shared.ui.friendItemClick { + friendItemClick(info, indexPath) + return + } + + Router.shared.use( + ContactUserInfoPageRouter, + parameters: ["nav": navigationController as Any, "user": info.user as Any], + closure: nil + ) + } + } + + func didRefreshTable() { + tableView.reloadData() + emptyView.isHidden = viewModel.getFriendSections().count > 0 + } + + public func reloadTableView() { + didRefreshTable() + } + + public func reloadTableView(_ index: IndexPath) { + tableView.reloadData([index]) + } +} + +extension NEBaseContactViewController { + open func initSystemNav() { + edgesForExtendedLayout = [] + } + + open func getFindFriendViewController() -> NEBaseFindFriendViewController { + NEBaseFindFriendViewController() + } + + @objc open func goToFindFriend() { + let findFriendController = getFindFriendViewController() + navigationController?.pushViewController(findFriendController, animated: true) + } + + @objc open func searchContact() { + Router.shared.use( + SearchContactPageRouter, + parameters: ["nav": navigationController as Any], + closure: nil ) - backItem.accessibilityIdentifier = "id.backArrow" - backItem.tintColor = UIColor(hexString: "333333") - navigationItem.leftBarButtonItem = backItem } - open func backToPrevious() { - navigationController?.popViewController(animated: true) + // MARK: TabNavigationViewDelegate + + open func searchAction() { + if let searchBlock = NEKitContactConfig.shared.ui.titleBarRight2Click { + searchBlock() + return + } + searchContact() } - /* - // MARK: - Navigation - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ + open func didClickAddBtn() { + if let addBlock = NEKitContactConfig.shared.ui.titleBarRightClick { + addBlock() + return + } + goToFindFriend() + } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsViewController.swift deleted file mode 100644 index 4f418728..00000000 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseContactsViewController.swift +++ /dev/null @@ -1,463 +0,0 @@ -// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NEChatKit -import NECoreIMKit -import NECoreKit -import UIKit - -@objc -public protocol NEBaseContactsViewControllerDelegate { - func onDataLoaded() -} - -@objcMembers -open class NEBaseContactsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, - SystemMessageProviderDelegate, FriendProviderDelegate, TabNavigationViewDelegate, UIGestureRecognizerDelegate { - public var delegate: NEBaseContactsViewControllerDelegate? - - // custom ui cell - public var cellRegisterDic = [Int: NEBaseContactTableViewCell.Type]() - - public var viewModel = ContactViewModel(contactHeaders: nil) - private var lastTitleIndex = 0 - - public var bodyTopViewHeight: CGFloat = 0 { - didSet { - bodyTopViewHeightAnchor?.constant = bodyTopViewHeight - bodyTopView.isHidden = bodyTopViewHeight <= 0 - } - } - - public var bodyBottomViewHeight: CGFloat = 0 { - didSet { - bodyBottomViewHeightAnchor?.constant = bodyBottomViewHeight - bodyBottomView.isHidden = bodyBottomViewHeight <= 0 - } - } - - public var topConstant: CGFloat = 0 - private var bodyTopViewHeightAnchor: NSLayoutConstraint? - private var bodyBottomViewHeightAnchor: NSLayoutConstraint? - - override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nil, bundle: nil) - viewModel.contactRepo.addNotificationDelegate(delegate: self) - viewModel.contactRepo.addContactDelegate(delegate: self) - } - - public required init?(coder: NSCoder) { - super.init(coder: coder) - } - - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - - // 通讯录异步进行远端加载 - if IMKitConfigCenter.shared.contactAsyncLoadEnable { - DispatchQueue.main.async { - self.loadData(fetch: true) - } - } - if navigationController?.viewControllers.count ?? 0 > 0 { - if let root = navigationController?.viewControllers[0] as? UIViewController { - if root.isKind(of: NEBaseContactsViewController.self) { - navigationController?.interactivePopGestureRecognizer?.delegate = self - } - } - } - } - - override open func viewDidLoad() { - super.viewDidLoad() - showTitleBar() - commonUI() - viewModel.refresh = { [weak self] in - self?.didRefreshTable() - } - - loadData(fetch: true) - - NotificationCenter.default.addObserver(self, selector: #selector(didRefreshTable), name: NENotificationName.updateFriendInfo, object: nil) - } - - deinit { - viewModel.contactRepo.removeNotificationDelegate(delegate: self) - viewModel.contactRepo.removeContactDelegate(delegate: self) - } - - open func showTitleBar() { - if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { - navigationView.isHidden = true - topConstant = 0 - if NEKitContactConfig.shared.ui.showTitleBar { - navigationController?.isNavigationBarHidden = false - } else { - navigationController?.isNavigationBarHidden = true - if #available(iOS 10, *) { - topConstant += NEConstant.statusBarHeight - } - } - } else { - navigationController?.isNavigationBarHidden = true - if NEKitContactConfig.shared.ui.showTitleBar { - navigationView.isHidden = false - topConstant = NEConstant.navigationHeight - } else { - navigationView.isHidden = true - topConstant = 0 - } - if #available(iOS 10, *) { - topConstant += NEConstant.statusBarHeight - } - } - } - - open func commonUI() { - initSystemNav() - view.addSubview(navigationView) - view.addSubview(bodyTopView) - view.addSubview(bodyView) - view.addSubview(bodyBottomView) - - NSLayoutConstraint.activate([ - navigationView.topAnchor.constraint(equalTo: view.topAnchor), - navigationView.leftAnchor.constraint(equalTo: view.leftAnchor), - navigationView.rightAnchor.constraint(equalTo: view.rightAnchor), - navigationView.heightAnchor - .constraint(equalToConstant: NEConstant.navigationAndStatusHeight), - ]) - - NSLayoutConstraint.activate([ - bodyTopView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - bodyTopView.leftAnchor.constraint(equalTo: view.leftAnchor), - bodyTopView.rightAnchor.constraint(equalTo: view.rightAnchor), - ]) - bodyTopViewHeightAnchor = bodyTopView.heightAnchor.constraint(equalToConstant: bodyTopViewHeight) - bodyTopViewHeightAnchor?.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), - ]) - - NSLayoutConstraint.activate([ - bodyBottomView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - bodyBottomView.leftAnchor.constraint(equalTo: view.leftAnchor), - bodyBottomView.rightAnchor.constraint(equalTo: view.rightAnchor), - ]) - bodyBottomViewHeightAnchor = bodyBottomView.heightAnchor.constraint(equalToConstant: bodyBottomViewHeight) - bodyBottomViewHeightAnchor?.isActive = true - - if let customController = NEKitContactConfig.shared.ui.customController { - customController(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 - } - - // MARK: lazy load - - public lazy var navigationView: TabNavigationView = { - let nav = TabNavigationView(frame: CGRect.zero) - nav.translatesAutoresizingMaskIntoConstraints = false - nav.delegate = self - - if let addImg = NEKitContactConfig.shared.ui.titleBarRightRes { - nav.addBtn.setImage(addImg, for: .normal) - } - if let searchImg = NEKitContactConfig.shared.ui.titleBarRight2Res { - nav.searchBtn.setImage(searchImg, for: .normal) - } - return nav - }() - - public lazy var bodyTopView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - return view - }() - - public lazy var bodyView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - view.addSubview(contentView) - - NSLayoutConstraint.activate([ - contentView.topAnchor.constraint(equalTo: view.topAnchor), - contentView.leftAnchor.constraint(equalTo: view.leftAnchor), - contentView.rightAnchor.constraint(equalTo: view.rightAnchor), - contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - return view - }() - - public lazy var contentView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - view.addSubview(tableView) - view.addSubview(emptyView) - - NSLayoutConstraint.activate([ - tableView.leftAnchor.constraint(equalTo: view.leftAnchor), - tableView.rightAnchor.constraint(equalTo: view.rightAnchor), - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - NSLayoutConstraint.activate([ - emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), - emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), - emptyView.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 100), - emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), - ]) - - return view - }() - - public lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .grouped) - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.backgroundColor = UIColor.ne_backgroundColor - tableView.sectionFooterHeight = 0 - tableView.sectionIndexColor = .ne_greyText - - tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) - return tableView - }() - - lazy var emptyView: NEEmptyDataView = { - let view = NEEmptyDataView(imageName: "user_empty", content: localizable("no_friend"), frame: .zero) - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - view.isHidden = true - return view - }() - - public lazy var bodyBottomView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - return view - }() - - open func loadData(fetch: Bool = false) { - viewModel.loadData(fetch: fetch) { [weak self] error, userSectionCount in - self?.emptyView.isHidden = userSectionCount > 0 - if error == nil { - self?.delegate?.onDataLoaded() - self?.didRefreshTable() - } - } - } - - // UITableViewDataSource - open func numberOfSections(in tableView: UITableView) -> Int { - viewModel.contacts.count - } - - open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - NELog.infoLog(ModuleName + " " + className(), desc: "contact section: \(section), count:\(viewModel.contacts[section].contacts.count)") - - return viewModel.contacts[section].contacts.count - } - - open func configCell(info: ContactInfo, _ cell: NEBaseContactTableViewCell, _ indexPath: IndexPath) -> UITableViewCell { - cell.setModel(info) - if indexPath.section == 0, indexPath.row == 0, viewModel.unreadCount > 0 { - cell.redAngleView.isHidden = false - cell.redAngleView.text = viewModel.unreadCount > 99 ? "99+" : "\(viewModel.unreadCount)" - } else { - cell.redAngleView.isHidden = true - } - return cell - } - - // 具体逻辑在子类实现 - open func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { - UITableViewCell() - } - - open func tableView(_ tableView: UITableView, - viewForHeaderInSection section: Int) -> UIView? { - let sectionView: ContactSectionView = tableView - .dequeueReusableHeaderFooterView( - withIdentifier: "\(NSStringFromClass(ContactSectionView.self))" - ) as! ContactSectionView - sectionView.titleLabel.text = viewModel.contacts[section].initial - return sectionView - } - - open func tableView(_ tableView: UITableView, - heightForHeaderInSection section: Int) -> CGFloat { - if viewModel.contacts[section].initial.count > 0 { - return 40 - } - return 0 - } - - open func sectionIndexTitles(for tableView: UITableView) -> [String]? { - viewModel.indexs - } - - open func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, - at index: Int) -> Int { - for (i, t) in viewModel.contacts.enumerated() { - if t.initial == title { - lastTitleIndex = i - return i - } - } - return lastTitleIndex - } - - open func tableView(_ tableView: UITableView, - heightForRowAt indexPath: IndexPath) -> CGFloat { - 52 - } - - open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let info = viewModel.contacts[indexPath.section].contacts[indexPath.row] - - if info.contactCellType == ContactCellType.ContactOthers.rawValue { - if let headerItemClick = NEKitContactConfig.shared.ui.headerItemClick { - headerItemClick(info, indexPath) - return - } - - switch info.router { - case ValidationMessageRouter: - Router.shared.use(ValidationMessageRouter, - parameters: ["nav": navigationController as Any], - closure: nil) - case ContactBlackListRouter: - Router.shared.use(ContactBlackListRouter, - parameters: ["nav": navigationController as Any], - closure: nil) - - case ContactTeamListRouter: - // My Team - Router.shared.use(ContactTeamListRouter, - parameters: ["nav": navigationController as Any], - closure: nil) - - case ContactPersonRouter: - break - - case ContactComputerRouter: - break - - default: - break - } - } else { - if let friendItemClick = NEKitContactConfig.shared.ui.friendItemClick { - friendItemClick(info, indexPath) - return - } - - Router.shared.use( - ContactUserInfoPageRouter, - parameters: ["nav": navigationController as Any, "user": info.user as Any], - closure: nil - ) - } - } - - func didRefreshTable() { - tableView.reloadData() - } - -// MARK: SystemMessageProviderDelegate - - open func onRecieveNotification(notification: NENotification) { - print("onRecieveNotification type:\(notification.type)") - if notification.type == .addFriendDirectly { - loadData() - } - } - - open func onNotificationUnreadCountChanged(count: Int) { - print("unread count:\(count)") - viewModel.unreadCount = count - didRefreshTable() - } - -// MARK: FriendProviderDelegate - - open func onFriendChanged(user: NEKitUser) { - print("onFriendChanged:\(user.userId)") - loadData() - } - - open func onBlackListChanged() { - print("onBlackListChanged") - loadData() - } - - open func onUserInfoChanged(user: NEKitUser) { - print("onUserInfoChanged:\(user.userId)") - loadData() - } - - open func onReceive(_ notification: NIMCustomSystemNotification) {} -} - -extension NEBaseContactsViewController { - open func initSystemNav() { - edgesForExtendedLayout = [] - } - - open func getFindFriendViewController() -> NEBaseFindFriendViewController { - NEBaseFindFriendViewController() - } - - @objc open func goToFindFriend() { - let findFriendController = getFindFriendViewController() - navigationController?.pushViewController(findFriendController, animated: true) - } - - @objc open func searchContact() { - Router.shared.use( - SearchContactPageRouter, - parameters: ["nav": navigationController as Any], - closure: nil - ) - } - - // MARK: TabNavigationViewDelegate - - open func searchAction() { - if let searchBlock = NEKitContactConfig.shared.ui.titleBarRight2Click { - searchBlock() - return - } - searchContact() - } - - open func didClickAddBtn() { - if let addBlock = NEKitContactConfig.shared.ui.titleBarRightClick { - addBlock() - return - } - goToFindFriend() - } -} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift index 9ce375e1..e9c8a718 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseFindFriendViewController.swift @@ -3,21 +3,54 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECommonKit +import NECoreIM2Kit import NECoreKit import UIKit @objcMembers -open class NEBaseFindFriendViewController: NEBaseContactViewController, UITextFieldDelegate { +open class NEBaseFindFriendViewController: NEContactBaseViewController, UITextFieldDelegate { public let viewModel = FindFriendViewModel() public let hasRequest = false - public let searchInput = UITextField() + + /// 搜索输入框 + public let searchInput: UITextField = { + let searchInput = UITextField() + searchInput.translatesAutoresizingMaskIntoConstraints = false + searchInput.textColor = UIColor(hexString: "333333") + searchInput.placeholder = localizable("input_userId") + searchInput.font = UIFont.systemFont(ofSize: 14.0) + searchInput.returnKeyType = .search + searchInput.clearButtonMode = .always + searchInput.accessibilityIdentifier = "id.addFriendAccount" + return searchInput + }() + + public var isRequesting = false + + /// 搜索背景 + public lazy var searchBackView: UIView = { + let searchBackView = UIView() + searchBackView.backgroundColor = UIColor(hexString: "F2F4F5") + searchBackView.translatesAutoresizingMaskIntoConstraints = false + searchBackView.clipsToBounds = true + searchBackView.layer.cornerRadius = 4.0 + return searchBackView + }() + + /// 搜索图片 + public lazy var searchImageView: UIImageView = { + let searchImageView = UIImageView() + searchImageView.image = UIImage.ne_imageNamed(name: "search") + searchImageView.translatesAutoresizingMaskIntoConstraints = false + return searchImageView + }() override open func viewDidLoad() { super.viewDidLoad() title = localizable("add_friend") navigationView.navTitle.text = title - emptyView.settingContent(content: localizable("user_not_exist")) + emptyView.setText(localizable("user_not_exist")) setupUI() DispatchQueue.main.asyncAfter(deadline: .now() + 0.52, execute: DispatchWorkItem(block: { [weak self] in @@ -25,49 +58,37 @@ open class NEBaseFindFriendViewController: NEBaseContactViewController, UITextFi })) } + /// UI 初始化 open func setupUI() { - let searchBack = UIView() - view.addSubview(searchBack) - searchBack.backgroundColor = UIColor(hexString: "F2F4F5") - searchBack.translatesAutoresizingMaskIntoConstraints = false - searchBack.clipsToBounds = true - searchBack.layer.cornerRadius = 4.0 + view.addSubview(searchBackView) NSLayoutConstraint.activate([ - searchBack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - searchBack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - searchBack.topAnchor.constraint(equalTo: view.topAnchor, constant: 20 + topConstant), - searchBack.heightAnchor.constraint(equalToConstant: 32), + searchBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + searchBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + searchBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 20 + topConstant), + searchBackView.heightAnchor.constraint(equalToConstant: 32), ]) - let searchImage = UIImageView() - searchBack.addSubview(searchImage) - searchImage.image = UIImage.ne_imageNamed(name: "search") - searchImage.translatesAutoresizingMaskIntoConstraints = false + searchBackView.addSubview(searchImageView) NSLayoutConstraint.activate([ - searchImage.centerYAnchor.constraint(equalTo: searchBack.centerYAnchor), - searchImage.leftAnchor.constraint(equalTo: searchBack.leftAnchor, constant: 18), - searchImage.widthAnchor.constraint(equalToConstant: 13), - searchImage.heightAnchor.constraint(equalToConstant: 13), + searchImageView.centerYAnchor.constraint(equalTo: searchBackView.centerYAnchor), + searchImageView.leftAnchor.constraint(equalTo: searchBackView.leftAnchor, constant: 18), + searchImageView.widthAnchor.constraint(equalToConstant: 13), + searchImageView.heightAnchor.constraint(equalToConstant: 13), ]) - searchBack.addSubview(searchInput) - searchInput.translatesAutoresizingMaskIntoConstraints = false + searchBackView.addSubview(searchInput) + searchInput.delegate = self + NSLayoutConstraint.activate([ - searchInput.leftAnchor.constraint(equalTo: searchImage.rightAnchor, constant: 5), - searchInput.rightAnchor.constraint(equalTo: searchBack.rightAnchor, constant: -18), - searchInput.topAnchor.constraint(equalTo: searchBack.topAnchor), - searchInput.bottomAnchor.constraint(equalTo: searchBack.bottomAnchor), + searchInput.leftAnchor.constraint(equalTo: searchImageView.rightAnchor, constant: 5), + searchInput.rightAnchor.constraint(equalTo: searchBackView.rightAnchor, constant: -18), + searchInput.topAnchor.constraint(equalTo: searchBackView.topAnchor), + searchInput.bottomAnchor.constraint(equalTo: searchBackView.bottomAnchor), ]) - searchInput.textColor = UIColor(hexString: "333333") - searchInput.placeholder = localizable("input_userId") - searchInput.font = UIFont.systemFont(ofSize: 14.0) - searchInput.returnKeyType = .search - searchInput.delegate = self - searchInput.clearButtonMode = .always + if let clearButton = searchInput.value(forKey: "_clearButton") as? UIButton { clearButton.accessibilityIdentifier = "id.clear" } - searchInput.accessibilityIdentifier = "id.addFriendAccount" NotificationCenter.default.addObserver( self, @@ -105,7 +126,12 @@ open class NEBaseFindFriendViewController: NEBaseContactViewController, UITextFi } open func startSearch(_ text: String) { - if IMKitClient.instance.isMySelf(text) { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + if IMKitClient.instance.isMe(text) { Router.shared.use( MeSettingRouter, parameters: ["nav": navigationController as Any], @@ -114,18 +140,23 @@ open class NEBaseFindFriendViewController: NEBaseContactViewController, UITextFi return } + if isRequesting == true { + return + } + isRequesting = true weak var weakSelf = self - viewModel.searchFriend(text) { users, error in - NELog.infoLog( + viewModel.searchFriend(text) { user, error in + weakSelf?.isRequesting = false + NEALog.infoLog( "NEBaseFindFriendViewController", desc: "CALLBACK searchFriend " + (error?.localizedDescription ?? "no error") ) if error == nil { - if let user = users?.first { + if let user = user, user.user != nil || user.friend != nil { // go to detail Router.shared.use( ContactUserInfoPageRouter, - parameters: ["nav": weakSelf?.navigationController as Any, "nim_user": user], + parameters: ["nav": weakSelf?.navigationController as Any, "user": user], closure: nil ) weakSelf?.emptyView.isHidden = true diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift index 6878dfae..8c58a125 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEBaseUserInfoHeaderView.swift @@ -3,20 +3,21 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECoreIMKit +import NECommonUIKit +import NECoreIM2Kit import UIKit @objcMembers open class NEBaseUserInfoHeaderView: UIView { public var labelConstraints = [NSLayoutConstraint]() - public lazy var avatarImage: UIImageView = { - let avatarImage = UIImageView() - avatarImage.backgroundColor = UIColor(hexString: "#537FF4") - avatarImage.translatesAutoresizingMaskIntoConstraints = false - avatarImage.contentMode = .scaleAspectFill - avatarImage.clipsToBounds = true - avatarImage.accessibilityIdentifier = "id.avatar" - return avatarImage + public lazy var avatarImageView: UIImageView = { + let imageView = UIImageView() + imageView.backgroundColor = UIColor(hexString: "#537FF4") + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + imageView.clipsToBounds = true + imageView.accessibilityIdentifier = "id.avatar" + return imageView }() public lazy var nameLabel: UILabel = { @@ -28,8 +29,8 @@ open class NEBaseUserInfoHeaderView: UIView { return nameLabel }() - public lazy var titleLabel: UILabel = { - let titleLabel = UILabel() + public lazy var titleLabel: CopyableLabel = { + let titleLabel = CopyableLabel() titleLabel.translatesAutoresizingMaskIntoConstraints = false titleLabel.font = UIFont.boldSystemFont(ofSize: 22) titleLabel.textColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 1.0) @@ -37,8 +38,8 @@ open class NEBaseUserInfoHeaderView: UIView { return titleLabel }() - public lazy var detailLabel: UILabel = { - let detailLabel = UILabel() + public lazy var detailLabel: CopyableLabel = { + let detailLabel = CopyableLabel() detailLabel.translatesAutoresizingMaskIntoConstraints = false detailLabel.font = UIFont.systemFont(ofSize: 16) detailLabel.textColor = .ne_greyText @@ -46,8 +47,8 @@ open class NEBaseUserInfoHeaderView: UIView { return detailLabel }() - public lazy var detailLabel2: UILabel = { - let detailLabel = UILabel() + public lazy var detailLabel2: CopyableLabel = { + let detailLabel = CopyableLabel() detailLabel.translatesAutoresizingMaskIntoConstraints = false detailLabel.font = UIFont.systemFont(ofSize: 16) detailLabel.textColor = .ne_greyText @@ -68,29 +69,29 @@ open class NEBaseUserInfoHeaderView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func commonUI() { backgroundColor = .white - addSubview(avatarImage) + addSubview(avatarImageView) addSubview(nameLabel) addSubview(titleLabel) addSubview(detailLabel) addSubview(lineView) NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: leftAnchor, constant: 20), - avatarImage.widthAnchor.constraint(equalToConstant: 60), - avatarImage.heightAnchor.constraint(equalToConstant: 60), - avatarImage.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 0), + avatarImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: 20), + avatarImageView.widthAnchor.constraint(equalToConstant: 60), + avatarImageView.heightAnchor.constraint(equalToConstant: 60), + avatarImageView.centerYAnchor.constraint(equalTo: centerYAnchor, constant: 0), ]) NSLayoutConstraint.activate([ - nameLabel.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), - nameLabel.rightAnchor.constraint(equalTo: avatarImage.rightAnchor), - nameLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor), - nameLabel.bottomAnchor.constraint(equalTo: avatarImage.bottomAnchor), + nameLabel.leftAnchor.constraint(equalTo: avatarImageView.leftAnchor), + nameLabel.rightAnchor.constraint(equalTo: avatarImageView.rightAnchor), + nameLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor), + nameLabel.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), ]) commonUI(showDetail: false) @@ -103,9 +104,9 @@ open class NEBaseUserInfoHeaderView: UIView { var detail2Constraint = [NSLayoutConstraint]() if showDetail { titleConstraint = [ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 20), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 20), titleLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -35), - titleLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor, constant: -2), + titleLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: -2), titleLabel.heightAnchor.constraint(equalToConstant: 22), ] @@ -125,9 +126,9 @@ open class NEBaseUserInfoHeaderView: UIView { ] } else { titleConstraint = [ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 16), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 16), titleLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -20), - titleLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor, constant: 7), + titleLabel.topAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: 7), titleLabel.heightAnchor.constraint(equalToConstant: 22), ] @@ -146,21 +147,37 @@ open class NEBaseUserInfoHeaderView: UIView { updateConstraintsIfNeeded() } - open func setData(user: NEKitUser?) { - guard let user = user else { + open func setData(user: NEUserWithFriend?) { + guard let userFriend = user else { return } // avatar - if let imageUrl = user.userInfo?.avatarUrl, !imageUrl.isEmpty { - avatarImage.sd_setImage(with: URL(string: imageUrl), completed: nil) - avatarImage.backgroundColor = .clear + if let imageUrl = userFriend.user?.avatar, !imageUrl.isEmpty { + avatarImageView.sd_setImage(with: URL(string: imageUrl), completed: nil) + avatarImageView.backgroundColor = .clear nameLabel.isHidden = true } else { - avatarImage.sd_setImage(with: nil) - avatarImage.backgroundColor = UIColor.colorWithString(string: user.userId) - nameLabel.text = user.shortName(showAlias: false, count: 2) + avatarImageView.sd_setImage(with: nil) + avatarImageView.backgroundColor = UIColor.colorWithString(string: userFriend.user?.accountId) + nameLabel.text = userFriend.shortName(count: 2) nameLabel.isHidden = false } + + // title + let uid = userFriend.user?.accountId ?? "" + if let alias = userFriend.friend?.alias, !alias.isEmpty { + commonUI(showDetail: true) + titleLabel.text = alias + detailLabel.text = "\(localizable("nick")):\(userFriend.user?.name ?? uid)" + detailLabel.copyString = userFriend.user?.name ?? uid + detailLabel2.text = "\(localizable("account")):\(uid)" + detailLabel2.copyString = uid + } else { + commonUI(showDetail: false) + titleLabel.text = userFriend.showName() + detailLabel.text = "\(localizable("account")):\(uid)" + detailLabel.copyString = uid + } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/NEContactBaseViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/NEContactBaseViewController.swift new file mode 100644 index 00000000..9e402be0 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/NEContactBaseViewController.swift @@ -0,0 +1,79 @@ + +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +/// 通讯录模块 ViewController 基类 +@objcMembers +open class NEContactBaseViewController: UIViewController, UIGestureRecognizerDelegate { + var topConstant: CGFloat = 0 + public let navigationView = NENavigationView() + + public lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView( + imageName: "user_empty", + content: "", + frame: CGRect.zero + ) + view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false + view.isHidden = true + return view + }() + + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + view.backgroundColor = .white + edgesForExtendedLayout = [] + navigationController?.interactivePopGestureRecognizer?.delegate = self + + if let useSystemNav = NEConfigManager.instance.getParameter(key: useSystemNav) as? Bool, useSystemNav { + navigationController?.isNavigationBarHidden = false + setupBackUI() + topConstant = 0 + } else { + navigationController?.isNavigationBarHidden = true + topConstant = NEConstant.navigationAndStatusHeight + navigationView.translatesAutoresizingMaskIntoConstraints = false + navigationView.addBackButtonTarget(target: self, selector: #selector(backToPrevious)) + navigationView.moreButton.isHidden = true + view.addSubview(navigationView) + NSLayoutConstraint.activate([ + navigationView.leftAnchor.constraint(equalTo: view.leftAnchor), + navigationView.rightAnchor.constraint(equalTo: view.rightAnchor), + navigationView.topAnchor.constraint(equalTo: view.topAnchor), + navigationView.heightAnchor.constraint(equalToConstant: topConstant), + ]) + } + } + + open func setupBackUI() { + navigationController?.navigationBar.tintColor = .white + let backItem = UIBarButtonItem( + image: UIImage.ne_imageNamed(name: "backArrow"), + style: .plain, + target: self, + action: #selector(backToPrevious) + ) + backItem.accessibilityIdentifier = "id.backArrow" + backItem.tintColor = UIColor(hexString: "333333") + navigationItem.leftBarButtonItem = backItem + } + + open func backToPrevious() { + navigationController?.popViewController(animated: true) + } + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ +} diff --git a/NEConversationUIKit/NEConversationUIKit.podspec b/NEConversationUIKit/NEConversationUIKit.podspec index ee0ea46a..63b76dcb 100644 --- a/NEConversationUIKit/NEConversationUIKit.podspec +++ b/NEConversationUIKit/NEConversationUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEConversationUIKit' - s.version = '9.7.0' + s.version = '10.1.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. @@ -40,4 +40,5 @@ TODO: Add long description of the pod here. s.dependency 'NECommonUIKit' s.dependency 'NEChatKit' + s.dependency 'MJRefresh' end diff --git a/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings b/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings index 0fbc65a4..846b1d14 100644 --- a/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings +++ b/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings @@ -15,7 +15,6 @@ "group"="Group"; "discussion_group"="Temp Group"; "senior_group"="Group"; -"search"="Search"; "cancel"="Cancel"; "search_keyword"="Enter the key words"; "user_not_exist"="Not Exist"; diff --git a/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings index 6369c851..687ba504 100644 --- a/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -15,7 +15,6 @@ "group"="群聊"; "discussion_group"="讨论组"; "senior_group"="高级群"; -"search"="搜索"; "cancel"="取消"; "search_keyword"="请输入你要搜索的关键字"; "user_not_exist"="该用户不存在"; diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationConstant.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationConstant.swift index 53a2f356..512c7683 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationConstant.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationConstant.swift @@ -12,6 +12,3 @@ func localizable(_ key: String) -> String { } public let ModuleName = "NEConversationUIKit" - -// 创建群聊 选择人数限制 -public var inviteNumberLimit: Int = 200 diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift index 3350b588..12a37b1a 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationDeduplicationHelper.swift @@ -3,9 +3,11 @@ // found in the LICENSE file. import Foundation +import NECoreIM2Kit import NIMSDK + @objcMembers -public class ConversationDeduplicationHelper: NSObject, NIMLoginManagerDelegate { +public class ConversationDeduplicationHelper: NSObject, NEIMKitClientListener { // 单例变量 static let instance = ConversationDeduplicationHelper() // 最多缓存数量,可外部修改 @@ -15,20 +17,20 @@ public class ConversationDeduplicationHelper: NSObject, NIMLoginManagerDelegate override private init() { super.init() - NIMSDK.shared().loginManager.add(self) + IMKitClient.instance.addLoginListener(self) } deinit { - NIMSDK.shared().loginManager.remove(self) + IMKitClient.instance.removeLoginListener(self) } - public func onLogin(_ step: NIMLoginStep) { - if step == .logout { + public func onLoginStatus(_ status: V2NIMLoginStatus) { + if status == .LOGIN_STATUS_LOGOUT { clearCache() } } - public func onKickout(_ result: NIMLoginKickoutResult) { + public func onKickedOffline(_ detail: V2NIMKickedOfflineDetail) { clearCache() } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationUI.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationUI.swift index 2b66118b..e16e831f 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationUI.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Common/ConversationUI.swift @@ -8,6 +8,6 @@ import Foundation @_exported import NEChatKit @_exported import NECommonKit @_exported import NECommonUIKit -@_exported import NECoreIMKit +@_exported import NECoreIM2Kit @_exported import NECoreKit @_exported import NIMSDK diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift index 0cd25e6b..0e754d59 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationListCell.swift @@ -8,9 +8,8 @@ import UIKit @objcMembers open class NEBaseConversationListCell: UITableViewCell { -// private var viewModel = ConversationViewModel() public var topStickInfos = [NIMSession: NIMStickTopSessionInfo]() - private let repo = ConversationRepo.shared + private var timeWidth: NSLayoutConstraint? override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -20,7 +19,7 @@ open class NEBaseConversationListCell: UITableViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupSubviews() { @@ -29,16 +28,16 @@ open class NEBaseConversationListCell: UITableViewCell { backgroundColor = bgColor } - contentView.addSubview(headImge) + contentView.addSubview(headImageView) contentView.addSubview(redAngleView) - contentView.addSubview(title) - contentView.addSubview(subTitle) + contentView.addSubview(titleLabel) + contentView.addSubview(subTitleLabel) contentView.addSubview(timeLabel) - contentView.addSubview(notifyMsg) + contentView.addSubview(notifyMsgView) NSLayoutConstraint.activate([ - redAngleView.centerXAnchor.constraint(equalTo: headImge.rightAnchor, constant: -8), - redAngleView.centerYAnchor.constraint(equalTo: headImge.topAnchor, constant: 8), + redAngleView.centerXAnchor.constraint(equalTo: headImageView.rightAnchor, constant: -8), + redAngleView.centerYAnchor.constraint(equalTo: headImageView.topAnchor, constant: 8), redAngleView.heightAnchor.constraint(equalToConstant: 18), ]) timeWidth = timeLabel.widthAnchor.constraint(equalToConstant: 0) @@ -52,69 +51,89 @@ open class NEBaseConversationListCell: UITableViewCell { ]) NSLayoutConstraint.activate([ - subTitle.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - subTitle.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), - subTitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), + subTitleLabel.leftAnchor.constraint(equalTo: headImageView.rightAnchor, constant: 12), + subTitleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), + subTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), ]) } func initSubviewsLayout() {} - open func configData(sessionModel: ConversationListModel?) { + /// 数据绑定UI + /// - Parameter sessionModel: 会话数据 + open func configureData(_ sessionModel: NEConversationListModel?) { guard let conversationModel = sessionModel else { return } - - if let userId = conversationModel.userInfo?.userId, - let user = ChatUserCache.getUserInfo(userId) { - conversationModel.userInfo = user - } - - if conversationModel.recentSession?.session?.sessionType == .P2P { + if conversationModel.conversation?.type == .CONVERSATION_TYPE_P2P { // p2p head image - if let imageName = conversationModel.userInfo?.userInfo?.avatarUrl, !imageName.isEmpty { - headImge.setTitle("") - headImge.sd_setImage(with: URL(string: imageName), completed: nil) - headImge.backgroundColor = .clear + if let imageName = conversationModel.conversation?.avatar, !imageName.isEmpty { + headImageView.setTitle("") + headImageView.sd_setImage(with: URL(string: imageName), completed: nil) + headImageView.backgroundColor = .clear } else { - headImge.setTitle(conversationModel.userInfo?.shortName(showAlias: false, count: 2) ?? "") - headImge.sd_setImage(with: nil, completed: nil) - headImge.backgroundColor = UIColor - .colorWithString(string: conversationModel.userInfo?.userId) + if let name = conversationModel.conversation?.shortName(count: 2) { + headImageView.setTitle(name) + } else if let conversationId = conversationModel.conversation?.conversationId { + // 截断长度 + let count = 2 + let showId = conversationId + .count > count ? String(conversationId[conversationId.index(conversationId.endIndex, offsetBy: -count)...]) : conversationId + headImageView.setTitle(showId) + } + headImageView.sd_setImage(with: nil, completed: nil) + if let cid = conversationModel.conversation?.conversationId, let uid = V2NIMConversationIdUtil.conversationTargetId(cid) { + headImageView.backgroundColor = UIColor + .colorWithString(string: uid) + } } // p2p nickName - title.text = conversationModel.userInfo?.showName() - - // notifyForNewMsg -// notifyMsg.isHidden = viewModel -// .notifyForNewMsg(userId: conversationModel.userInfo?.userId) - notifyMsg.isHidden = repo.isNeedNotify(userId: conversationModel.userInfo?.userId) + if let name = conversationModel.conversation?.name, name.count > 0 { + titleLabel.text = conversationModel.conversation?.name + } else if let conversationId = conversationModel.conversation?.conversationId, let accountId = V2NIMConversationIdUtil.conversationTargetId(conversationId) { + titleLabel.text = accountId + } - } else if conversationModel.recentSession?.session?.sessionType == .team { + } else if conversationModel.conversation?.type == .CONVERSATION_TYPE_TEAM { // team head image - if let imageName = conversationModel.teamInfo?.avatarUrl, !imageName.isEmpty { - headImge.setTitle("") - headImge.sd_setImage(with: URL(string: imageName), completed: nil) - headImge.backgroundColor = .clear + if let imageName = conversationModel.conversation?.avatar, !imageName.isEmpty { + headImageView.setTitle("") + headImageView.sd_setImage(with: URL(string: imageName), completed: nil) + headImageView.backgroundColor = .clear } else { - headImge.setTitle(conversationModel.teamInfo?.getShowName() ?? "") - headImge.sd_setImage(with: nil, completed: nil) - headImge.backgroundColor = UIColor - .colorWithString(string: conversationModel.teamInfo?.teamId) + headImageView.setTitle(conversationModel.conversation?.name ?? "") + headImageView.sd_setImage(with: nil, completed: nil) + if let name = conversationModel.conversation?.shortName(count: 2) { + headImageView.setTitle(name) + } else if let conversationId = conversationModel.conversation?.conversationId { + // 截断长度 + let count = 2 + let showId = conversationId + .count > count ? String(conversationId[conversationId.index(conversationId.endIndex, offsetBy: -count)...]) : conversationId + headImageView.setTitle(showId) + } + if let cid = conversationModel.conversation?.conversationId, let uid = V2NIMConversationIdUtil.conversationTargetId(cid) { + headImageView.backgroundColor = UIColor + .colorWithString(string: uid) + } } - title.text = conversationModel.teamInfo?.getShowName() + titleLabel.text = conversationModel.conversation?.name + if let name = conversationModel.conversation?.name { + titleLabel.text = name + } else if let conversationId = conversationModel.conversation?.conversationId, let teamId = V2NIMConversationIdUtil.conversationTargetId(conversationId) { + titleLabel.text = teamId + } + } - // notifyForNewMsg -// let teamNotifyState = viewModel -// .notifyStateForNewMsg(teamId: conversationModel.teamInfo?.teamId) - let teamNotifyState = repo.isNeedNotifyForTeam(teamId: conversationModel.teamInfo?.teamId) - notifyMsg.isHidden = teamNotifyState == .none ? false : true + // notifyForNewMsg + if let mute = conversationModel.conversation?.mute { + notifyMsgView.isHidden = !mute } // last message - if let lastMessage = conversationModel.recentSession?.lastMessage { - let text = contentForRecentSession(message: lastMessage) + if let lastMessage = conversationModel.conversation?.lastMessage { + let text = contentForConversation(lastMessage: lastMessage) let mutaAttri = NSMutableAttributedString(string: text) - if let sessionId = sessionModel?.recentSession?.session?.sessionId { + if let sessionId = conversationModel.conversation?.conversationId { let isAtMessage = NEAtMessageManager.instance?.isAtCurrentUser(sessionId: sessionId) if isAtMessage == true { let atStr = localizable("you_were_mentioned") @@ -123,17 +142,17 @@ open class NEBaseConversationListCell: UITableViewCell { mutaAttri.addAttribute(NSAttributedString.Key.font, value: UIFont.systemFont(ofSize: NEKitConversationConfig.shared.ui.conversationProperties.itemContentSize > 0 ? NEKitConversationConfig.shared.ui.conversationProperties.itemContentSize : 13), range: NSMakeRange(0, mutaAttri.length)) } } - subTitle.attributedText = mutaAttri // contentForRecentSession(message: lastMessage) + subTitleLabel.attributedText = mutaAttri } else { - subTitle.attributedText = nil + subTitleLabel.attributedText = nil } // unRead message count - if let unReadCount = conversationModel.recentSession?.unreadCount { + if let unReadCount = conversationModel.conversation?.unreadCount { if unReadCount <= 0 { redAngleView.isHidden = true } else { - redAngleView.isHidden = notifyMsg.isHidden ? false : true + redAngleView.isHidden = notifyMsgView.isHidden ? false : true if unReadCount <= 99 { redAngleView.text = "\(unReadCount)" } else { @@ -143,16 +162,26 @@ open class NEBaseConversationListCell: UITableViewCell { } // time - if let rencentSession = conversationModel.recentSession { + var useTime: TimeInterval? + + if let createTime = conversationModel.conversation?.lastMessage?.messageRefer.createTime { + useTime = createTime + + } else if let updateTime = conversationModel.conversation?.updateTime { + useTime = updateTime + } + if let time = useTime { timeLabel .text = - dealTime(time: timestampDescriptionForRecentSession(recentSession: rencentSession)) + dealTime(time: time) if let text = timeLabel.text { let maxSize = CGSize(width: UIScreen.main.bounds.width, height: 0) let attibutes = [NSAttributedString.Key.font: timeLabel.font] - let labelSize = NSString(string: text).boundingRect(with: maxSize, attributes: attibutes, context: nil) + let labelSize = NSString(string: text).boundingRect(with: maxSize, attributes: attibutes as [NSAttributedString.Key: Any], context: nil) timeWidth?.constant = labelSize.width + 1 // ceil() } + } else { + timeLabel.text = "" } } @@ -188,14 +217,14 @@ open class NEBaseConversationListCell: UITableViewCell { } } - open func contentForRecentSession(message: NIMMessage) -> String { - let text = NEMessageUtil.messageContent(message: message) + open func contentForConversation(lastMessage: V2NIMLastMessage) -> String { + let text = NEMessageUtil.messageContent(lastMessage.messageType, lastMessage.text, lastMessage.attachment) return text } // MARK: lazy Method - public lazy var headImge: NEUserHeaderView = { + public lazy var headImageView: NEUserHeaderView = { let headView = NEUserHeaderView(frame: .zero) headView.titleLabel.textColor = .white headView.titleLabel.font = NEConstant.defaultTextFont(14) @@ -221,7 +250,7 @@ open class NEBaseConversationListCell: UITableViewCell { }() // 会话列表会话名称 - public lazy var title: UILabel = { + public lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = NEKitConversationConfig.shared.ui.conversationProperties.itemTitleColor @@ -232,7 +261,7 @@ open class NEBaseConversationListCell: UITableViewCell { }() // 会话列表外露消息 - public lazy var subTitle: UILabel = { + public lazy var subTitleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = NEKitConversationConfig.shared.ui.conversationProperties.itemContentColor @@ -253,12 +282,12 @@ open class NEBaseConversationListCell: UITableViewCell { }() // 免打扰icon - public lazy var notifyMsg: UIImageView = { - let notify = UIImageView() - notify.translatesAutoresizingMaskIntoConstraints = false - notify.image = UIImage.ne_imageNamed(name: "noNeed_notify") - notify.isHidden = true - notify.accessibilityIdentifier = "id.mute" - return notify + public lazy var notifyMsgView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = UIImage.ne_imageNamed(name: "noNeed_notify") + imageView.isHidden = true + imageView.accessibilityIdentifier = "id.mute" + return imageView }() } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift index 10be5e15..7e584792 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Cell/NEBaseConversationSearchCell.swift @@ -12,37 +12,37 @@ open class NEBaseConversationSearchCell: TextBaseCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } public var searchModel: ConversationSearchListModel? { didSet { if let _ = searchModel { - if let userInfo = searchModel?.userInfo { - titleLabel.text = userInfo.showName() - subTitleLabel.text = userInfo.userId + if let userFriend = searchModel?.userInfo { + titleLabel.text = userFriend.showName() + subTitleLabel.text = userFriend.user?.accountId - if let imageName = userInfo.userInfo?.avatarUrl, !imageName.isEmpty { - headImge.setTitle("") - headImge.sd_setImage(with: URL(string: imageName), completed: nil) - headImge.backgroundColor = .clear + if let imageName = userFriend.user?.avatar, !imageName.isEmpty { + headImageView.setTitle("") + headImageView.sd_setImage(with: URL(string: imageName), completed: nil) + headImageView.backgroundColor = .clear } else { - headImge.setTitle(userInfo.showName() ?? "") - headImge.sd_setImage(with: nil, completed: nil) - headImge.backgroundColor = UIColor.colorWithString(string: userInfo.userId) + headImageView.setTitle(userFriend.showName() ?? "") + headImageView.sd_setImage(with: nil, completed: nil) + headImageView.backgroundColor = UIColor.colorWithString(string: userFriend.user?.accountId) } } - if let teamInfo = searchModel?.teamInfo { + if let teamInfo = searchModel?.team { titleLabel.text = teamInfo.getShowName() subTitleLabel.text = nil - if let imageName = teamInfo.avatarUrl, !imageName.isEmpty { - headImge.setTitle("") - headImge.sd_setImage(with: URL(string: imageName), completed: nil) - headImge.backgroundColor = .clear + if let imageName = teamInfo.avatar, !imageName.isEmpty { + headImageView.setTitle("") + headImageView.sd_setImage(with: URL(string: imageName), completed: nil) + headImageView.backgroundColor = .clear } else { - headImge.setTitle(teamInfo.getShowName()) - headImge.sd_setImage(with: nil, completed: nil) - headImge.backgroundColor = UIColor.colorWithString(string: teamInfo.teamId) + headImageView.setTitle(teamInfo.getShowName()) + headImageView.sd_setImage(with: nil, completed: nil) + headImageView.backgroundColor = UIColor.colorWithString(string: teamInfo.teamId) } } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift index 708b4de6..536321d2 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationController.swift @@ -3,20 +3,21 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import NECommonUIKit -import NECoreKit +import MJRefresh +import NEChatKit +import NECommonKit import NIMSDK -import UIKit @objc public protocol NEBaseConversationControllerDelegate { func onDataLoaded() } +/// 会话列表页面 - 基类 @objcMembers -open class NEBaseConversationController: UIViewController, NIMChatManagerDelegate, UIGestureRecognizerDelegate { +open class NEBaseConversationController: UIViewController, UIGestureRecognizerDelegate { var className = "NEBaseConversationController" - public var deleteBottonBackgroundColor: UIColor = NEConstant.hexRGB(0xA8ABB6) + public var deleteButtonBackgroundColor: UIColor = NEConstant.hexRGB(0xA8ABB6) public var brokenNetworkViewHeight = 36.0 private var bodyTopViewHeightAnchor: NSLayoutConstraint? @@ -27,6 +28,9 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat public var delegate: NEBaseConversationControllerDelegate? + /// 是否取过数据 + public var isRequestedData = false + public var bodyTopViewHeight: CGFloat = 0 { didSet { bodyTopViewHeightAnchor?.constant = bodyTopViewHeight @@ -44,6 +48,138 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat public var cellRegisterDic = [0: NEBaseConversationListCell.self] public let viewModel = ConversationViewModel() + public lazy var navigationView: TabNavigationView = { + let nav = TabNavigationView(frame: CGRect.zero) + nav.translatesAutoresizingMaskIntoConstraints = false + nav.delegate = self + + nav.brandBtn.addTarget(self, action: #selector(brandBtnClick), for: .touchUpInside) + + if let brandTitle = NEKitConversationConfig.shared.ui.titleBarTitle { + nav.brandBtn.setTitle(brandTitle, for: .normal) + } + if let brandTitleColor = NEKitConversationConfig.shared.ui.titleBarTitleColor { + nav.brandBtn.setTitleColor(brandTitleColor, for: .normal) + } + if !NEKitConversationConfig.shared.ui.showTitleBarLeftIcon { + nav.brandBtn.setImage(nil, for: .normal) + // 如果左侧图标为空,则左侧文案左对齐 + nav.brandBtn.layoutButtonImage(style: .left, space: 0) + } + if let brandImg = NEKitConversationConfig.shared.ui.titleBarLeftRes { + nav.brandBtn.setImage(brandImg, for: .normal) + if brandImg.size.width == 0, brandImg.size.height == 0 { + // 如果左侧图标为空,则左侧文案左对齐 + nav.brandBtn.layoutButtonImage(style: .left, space: 0) + } + } + if let rightImg = NEKitConversationConfig.shared.ui.titleBarRightRes { + nav.addBtn.setImage(rightImg, for: .normal) + } + if let right2Img = NEKitConversationConfig.shared.ui.titleBarRight2Res { + nav.searchBtn.setImage(right2Img, for: .normal) + } + return nav + }() + + public lazy var bodyTopView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + return view + }() + + public lazy var bodyView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + + view.addSubview(brokenNetworkView) + view.addSubview(contentView) + + NSLayoutConstraint.activate([ + brokenNetworkView.topAnchor.constraint(equalTo: view.topAnchor), + brokenNetworkView.leftAnchor.constraint(equalTo: view.leftAnchor), + brokenNetworkView.rightAnchor.constraint(equalTo: view.rightAnchor), + brokenNetworkView.heightAnchor.constraint(equalToConstant: brokenNetworkViewHeight), + ]) + + contentViewTopAnchor = contentView.topAnchor.constraint(equalTo: view.topAnchor) + contentViewTopAnchor?.isActive = true + NSLayoutConstraint.activate([ + contentView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + + return view + }() + + public lazy var brokenNetworkView: NEBrokenNetworkView = { + let view = NEBrokenNetworkView() + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + return view + }() + + public lazy var contentView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + view.addSubview(tableView) + view.addSubview(emptyView) + + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) + + NSLayoutConstraint.activate([ + emptyView.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 100), + emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), + emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), + ]) + + return view + }() + + public lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView( + imageName: "user_empty", + content: localizable("session_empty"), + frame: CGRect.zero + ) + view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false + view.isHidden = true + view.backgroundColor = .clear + return view + }() + + public lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + tableView.mj_footer = MJRefreshBackNormalFooter( + refreshingTarget: self, + refreshingAction: #selector(loadMoreData) + ) + return tableView + }() + + public lazy var bodyBottomView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + return view + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nil, bundle: nil) } @@ -55,16 +191,10 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) showTitleBar() - viewModel.loadStickTopSessionInfos { [weak self] error, sessionInfos in - NELog.infoLog( - ModuleName + " " + (self?.className ?? "NEBaseConversationController"), - desc: "CALLBACK loadStickTopSessionInfos " + (error?.localizedDescription ?? "no error") - ) - if let infos = sessionInfos { - self?.viewModel.stickTopInfos = infos - self?.reloadTableView() - self?.delegate?.onDataLoaded() - } + + // 是否取过数据,如果取过数据再刷新页面 + if isRequestedData == true { + reloadTableView() } NEChatDetectNetworkTool.shareInstance.netWorkReachability { [weak self] status in @@ -92,18 +222,18 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat setupSubviews() requestData() initialConfig() - NIMSDK.shared().chatManager.add(self) - NotificationCenter.default.addObserver(self, selector: #selector(didRefreshTable), name: NENotificationName.updateFriendInfo, object: nil) + + // 拉取好友信息 + DispatchQueue.global().async { + ContactRepo.shared.getUserList(accountIds: [IMKitClient.instance.account()]) { _, _ in } + ContactRepo.shared.getContactList { _, _ in } + } } override open func viewWillDisappear(_ animated: Bool) { popListView.removeSelf() } - deinit { - NIMSDK.shared().chatManager.remove(self) - } - open func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let navigationController = navigationController, navigationController.responds(to: #selector(getter: UINavigationController.interactivePopGestureRecognizer)), @@ -193,7 +323,7 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat bodyBottomViewHeightAnchor = bodyBottomView.heightAnchor.constraint(equalToConstant: bodyBottomViewHeight) bodyBottomViewHeightAnchor?.isActive = true - cellRegisterDic.forEach { (key: Int, value: NEBaseConversationListCell.Type) in + for (key, value) in cellRegisterDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } @@ -206,164 +336,52 @@ open class NEBaseConversationController: UIViewController, NIMChatManagerDelegat viewModel.delegate = self } - func requestData() { - let params = NIMFetchServerSessionOption() - params.minTimestamp = 0 - params.maxTimestamp = Date().timeIntervalSince1970 * 1000 - params.limit = 50 - weak var weakSelf = self - viewModel.fetchServerSessions(option: params) { error, recentSessions in - if error == nil { - NELog.infoLog(ModuleName + " " + self.className, desc: "✅CALLBACK fetchServerSessions SUCCESS") - if let recentList = recentSessions { - NELog.infoLog(ModuleName + " " + self.className, desc: "✅CALLBACK fetchServerSessions SUCCESS count : \(recentList.count)") - if recentList.count > 0 { - weakSelf?.emptyView.isHidden = true - weakSelf?.reloadTableView() - weakSelf?.delegate?.onDataLoaded() - } else { - weakSelf?.emptyView.isHidden = false - } + func loadMoreData() { + viewModel.getConversationListByPage { [weak self] error, finishied in + self?.isRequestedData = true + if let end = finishied, end == true { + self?.tableView.mj_footer?.endRefreshingWithNoMoreData() + DispatchQueue.main.async { + self?.tableView.mj_footer = nil } - } else { - NELog.errorLog( - ModuleName + " " + self.className, - desc: "❌CALLBACK fetchServerSessions failed,error = \(error!)" - ) - weakSelf?.emptyView.isHidden = false + self?.tableView.mj_footer?.endRefreshing() } + self?.delegate?.onDataLoaded() + self?.reloadTableView() } } - // MARK: lazyMethod - - public lazy var navigationView: TabNavigationView = { - let nav = TabNavigationView(frame: CGRect.zero) - nav.translatesAutoresizingMaskIntoConstraints = false - nav.delegate = self - - nav.brandBtn.addTarget(self, action: #selector(brandBtnClick), for: .touchUpInside) - - if let brandTitle = NEKitConversationConfig.shared.ui.titleBarTitle { - nav.brandBtn.setTitle(brandTitle, for: .normal) - } - if let brandTitleColor = NEKitConversationConfig.shared.ui.titleBarTitleColor { - nav.brandBtn.setTitleColor(brandTitleColor, for: .normal) - } - if !NEKitConversationConfig.shared.ui.showTitleBarLeftIcon { - nav.brandBtn.setImage(nil, for: .normal) - // 如果左侧图标为空,则左侧文案左对齐 - nav.brandBtn.layoutButtonImage(style: .left, space: 0) - } - if let brandImg = NEKitConversationConfig.shared.ui.titleBarLeftRes { - nav.brandBtn.setImage(brandImg, for: .normal) - if brandImg.size.width == 0, brandImg.size.height == 0 { - // 如果左侧图标为空,则左侧文案左对齐 - nav.brandBtn.layoutButtonImage(style: .left, space: 0) + func requestData() { + viewModel.getConversationListByPage { [weak self] error, finished in + + if let err = error { + self?.view.ne_makeToast(err.localizedDescription) + self?.emptyView.isHidden = false + NEALog.errorLog( + ModuleName + " " + (self?.className ?? ""), + desc: "❌CALLBACK requestData failed,error = \(error!)" + ) + } else { + if let end = finished, end == true { + DispatchQueue.main.async { + self?.tableView.mj_footer = nil + } + } + if let topDats = self?.viewModel.stickTopConversations, let normalDatas = self?.viewModel.conversationListData { + if topDats.count <= 0, normalDatas.count <= 0 { + self?.emptyView.isHidden = false + } else { + self?.emptyView.isHidden = true + self?.reloadTableView() + self?.delegate?.onDataLoaded() + } + } } } - if let rightImg = NEKitConversationConfig.shared.ui.titleBarRightRes { - nav.addBtn.setImage(rightImg, for: .normal) - } - if let right2Img = NEKitConversationConfig.shared.ui.titleBarRight2Res { - nav.searchBtn.setImage(right2Img, for: .normal) - } - return nav - }() - - public lazy var bodyTopView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - return view - }() - - public lazy var bodyView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - - view.addSubview(brokenNetworkView) - view.addSubview(contentView) - - NSLayoutConstraint.activate([ - brokenNetworkView.topAnchor.constraint(equalTo: view.topAnchor), - brokenNetworkView.leftAnchor.constraint(equalTo: view.leftAnchor), - brokenNetworkView.rightAnchor.constraint(equalTo: view.rightAnchor), - brokenNetworkView.heightAnchor.constraint(equalToConstant: brokenNetworkViewHeight), - ]) - - contentViewTopAnchor = contentView.topAnchor.constraint(equalTo: view.topAnchor) - contentViewTopAnchor?.isActive = true - NSLayoutConstraint.activate([ - contentView.leftAnchor.constraint(equalTo: view.leftAnchor), - contentView.rightAnchor.constraint(equalTo: view.rightAnchor), - contentView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - - return view - }() - - public lazy var brokenNetworkView: NEBrokenNetworkView = { - let view = NEBrokenNetworkView() - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - return view - }() - - public lazy var contentView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - view.addSubview(tableView) - view.addSubview(emptyView) - - NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor), - tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), - tableView.leftAnchor.constraint(equalTo: view.leftAnchor), - tableView.rightAnchor.constraint(equalTo: view.rightAnchor), - ]) - - NSLayoutConstraint.activate([ - emptyView.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 100), - emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), - emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), - emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), - ]) - - return view - }() - - public lazy var emptyView: NEEmptyDataView = { - let view = NEEmptyDataView( - imageName: "user_empty", - content: localizable("session_empty"), - frame: CGRect.zero - ) - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - view.backgroundColor = .clear - return view - }() - - public lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .plain) - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) - return tableView - }() + } - public lazy var bodyBottomView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = .clear - return view - }() + // MARK: lazyMethod } extension NEBaseConversationController: TabNavigationViewDelegate { @@ -372,6 +390,7 @@ extension NEBaseConversationController: TabNavigationViewDelegate { NEKitConversationConfig.shared.ui.titleBarLeftClick?() } + /// 点击搜索会话 open func searchAction() { if let searchBlock = NEKitConversationConfig.shared.ui.titleBarRight2Click { searchBlock() @@ -429,7 +448,7 @@ extension NEBaseConversationController: TabNavigationViewDelegate { return } - if IMKitClient.instance.getConfigCenter().teamEnable { + if IMKitConfigCenter.shared.teamEnable { popListView.itemDatas = getPopListItems() popListView.frame = CGRect(origin: .zero, size: view.frame.size) popListView.removeSelf() @@ -443,14 +462,22 @@ extension NEBaseConversationController: TabNavigationViewDelegate { } } + /// 创建讨论组 open func createDiscussGroup() { Router.shared.register(ContactSelectedUsersRouter) { param in print("user setting accids : ", param) Router.shared.use(TeamCreateDisuss, parameters: param, closure: nil) } + + // 创建讨论组-人员选择页面不包含自己 + var filters = Set() + filters.insert(IMKitClient.instance.account()) + Router.shared.use( ContactUserSelectRouter, - parameters: ["nav": navigationController as Any, "limit": inviteNumberLimit], + parameters: ["nav": navigationController as Any, + "limit": inviteNumberLimit, + "filters": filters], closure: nil ) weak var weakSelf = self @@ -458,26 +485,34 @@ extension NEBaseConversationController: TabNavigationViewDelegate { print("create discuss ", param) if let code = param["code"] as? Int, let teamid = param["teamId"] as? String, code == 0 { - let session = weakSelf?.viewModel.repo.createTeamSession(teamid) - Router.shared.use( - PushTeamChatVCRouter, - parameters: ["nav": weakSelf?.navigationController as Any, - "session": session as Any], - closure: nil - ) + if let conversationId = V2NIMConversationIdUtil.teamConversationId(teamid) { + var params = [String: Any]() + params["nav"] = weakSelf?.navigationController as Any + params["conversationId"] = conversationId as Any + + Router.shared.use(PushTeamChatVCRouter, parameters: params, closure: nil) + } } else if let msg = param["msg"] as? String { weakSelf?.showToast(msg) } } } + /// 创建高级群 open func createSeniorGroup() { Router.shared.register(ContactSelectedUsersRouter) { param in Router.shared.use(TeamCreateSenior, parameters: param, closure: nil) } + + // 创建高级群-人员选择页面不包含自己 + var filters = Set() + filters.insert(IMKitClient.instance.account()) + Router.shared.use( ContactUserSelectRouter, - parameters: ["nav": navigationController as Any, "limit": 200], + parameters: ["nav": navigationController as Any, + "limit": inviteNumberLimit, + "filters": filters], closure: nil ) weak var weakSelf = self @@ -485,199 +520,166 @@ extension NEBaseConversationController: TabNavigationViewDelegate { print("create senior : ", param) if let code = param["code"] as? Int, let teamid = param["teamId"] as? String, code == 0 { - let session = weakSelf?.viewModel.repo.createTeamSession(teamid) - Router.shared.use( - PushTeamChatVCRouter, - parameters: ["nav": weakSelf?.navigationController as Any, - "session": session as Any], - closure: nil - ) + if let conversationId = V2NIMConversationIdUtil.teamConversationId(teamid) { + var params = [String: Any]() + params["nav"] = weakSelf?.navigationController as Any + params["conversationId"] = conversationId as Any + + Router.shared.use(PushTeamChatVCRouter, parameters: params, closure: nil) + } } else if let msg = param["msg"] as? String { weakSelf?.showToast(msg) } } } +} - // MARK: =========================NIMChatManagerDelegate======================== - - open func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { - guard let msg = notification.message else { - return - } - - if ConversationDeduplicationHelper.instance.isRevokeMessageSaved(messageId: msg.messageId) { - return - } - saveRevokeMessage(msg) { [weak self] error in - } +extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSource { + public func numberOfSections(in tableView: UITableView) -> Int { + 2 } - open func saveRevokeMessage(_ message: NIMMessage, _ completion: @escaping (Error?) -> Void) { - let messageNew = NIMMessage() - messageNew.text = localizable("message_recalled") - var muta = [String: Any]() - muta[revokeLocalMessage] = true -// if message.messageType == .text { -// muta[revokeLocalMessageContent] = message.text -// } - messageNew.timestamp = message.timestamp - messageNew.from = message.from - messageNew.localExt = muta - let setting = NIMMessageSetting() - setting.shouldBeCounted = false - setting.isSessionUpdate = false - messageNew.setting = setting - if let session = message.session { - viewModel.repo.saveMessageToDB(messageNew, session, completion) + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if section == 0 { + return viewModel.stickTopConversations.count } - } -} -// MARK: - UITableViewDelegate, UITableViewDataSource + if section == 1 { + let conversationCount = viewModel.conversationListData.count + return conversationCount + } -extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSource { - open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - let count = viewModel.conversationListArray?.count ?? 0 - NELog.infoLog(ModuleName + " " + "ConversationController", - desc: "numberOfRowsInSection count : \(count)") - return count + return 0 } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewModel.conversationListArray?[indexPath.row] + var model: NEConversationListModel? + + if indexPath.section == 0 { + model = viewModel.stickTopConversations[indexPath.row] + } else if indexPath.section == 1 { + model = viewModel.conversationListData[indexPath.row] + } + let reusedId = "\(model?.customType ?? 0)" let cell = tableView.dequeueReusableCell(withIdentifier: reusedId, for: indexPath) - if let c = cell as? NEBaseConversationListCell { - c.topStickInfos = viewModel.stickTopInfos - c.configData(sessionModel: model) + if let c = cell as? NEBaseConversationListCell, let m = model { + c.configureData(m) } return cell } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let conversationModel = viewModel.conversationListArray?[indexPath.row] + var conversationModel: NEConversationListModel? + if indexPath.section == 0 { + conversationModel = viewModel.stickTopConversations[indexPath.row] + } else if indexPath.section == 1 { + conversationModel = viewModel.conversationListData[indexPath.row] + } - if let didClick = NEKitConversationConfig.shared.ui.itemClick { - didClick(conversationModel, indexPath) + if let didClick = NEKitConversationConfig.shared.ui.itemClick, let model = conversationModel { + didClick(model, indexPath) return } - let sid = conversationModel?.recentSession?.session?.sessionId ?? "" - let sessionType = conversationModel?.recentSession?.session?.sessionType ?? .P2P - onselectedTableRow(sessionType: sessionType, sessionId: sid, indexPath: indexPath) + if let conversation = conversationModel?.conversation { + onselectedTableRow(conversation: conversation) + } } open func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { weak var weakSelf = self - var rowActions = [UITableViewRowAction]() - - let conversationModel = weakSelf?.viewModel.conversationListArray?[indexPath.row] - guard let recentSession = conversationModel?.recentSession, - let session = recentSession.session else { - return rowActions - } + var rowActions = [UITableViewRowAction]() let deleteAction = UITableViewRowAction(style: .destructive, - title: NEKitConversationConfig.shared.ui.deleteBottonTitle) { action, indexPath in + title: NEKitConversationConfig.shared.ui.deleteButtonTitle) { action, indexPath in weakSelf?.deleteActionHandler(action: action, indexPath: indexPath) } // 置顶和取消置顶 - let isTop = viewModel.stickTopInfos[session] != nil + let isTop = indexPath.section == 0 ? true : false // viewModel.stickTopInfos[session] != nil let topAction = UITableViewRowAction(style: .destructive, - title: isTop ? NEKitConversationConfig.shared.ui.stickTopBottonCancelTitle : - NEKitConversationConfig.shared.ui.stickTopBottonTitle) { action, indexPath in + title: isTop ? NEKitConversationConfig.shared.ui.stickTopButtonCancelTitle : + NEKitConversationConfig.shared.ui.stickTopButtonTitle) { action, indexPath in weakSelf?.topActionHandler(action: action, indexPath: indexPath, isTop: isTop) } - deleteAction.backgroundColor = NEKitConversationConfig.shared.ui.deleteBottonBackgroundColor ?? deleteBottonBackgroundColor - topAction.backgroundColor = NEKitConversationConfig.shared.ui.stickTopBottonBackgroundColor ?? NEConstant.hexRGB(0x337EFF) + deleteAction.backgroundColor = NEKitConversationConfig.shared.ui.deleteButtonBackgroundColor ?? deleteButtonBackgroundColor + topAction.backgroundColor = NEKitConversationConfig.shared.ui.stickTopButtonBackgroundColor ?? NEConstant.hexRGB(0x337EFF) rowActions.append(deleteAction) rowActions.append(topAction) return rowActions } - /* - @available(iOS 11.0, *) - open func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - - var rowActions = [UIContextualAction]() - - let deleteAction = UIContextualAction(style: .normal, title: "删除") { (action, sourceView, completionHandler) in - - // self.dataSource.remove(at: indexPath.row) - // tableView.deleteRows(at: [indexPath], with: .automatic) - // 需要返回true,否则没有反应 - completionHandler(true) - } - deleteAction.backgroundColor = NEConstant.hexRGB(0xA8ABB6) - rowActions.append(deleteAction) - - let topAction = UIContextualAction(style: .normal, title: "置顶") { (action, sourceView, completionHandler) in - - // self.dataSource.remove(at: indexPath.row) - // tableView.deleteRows(at: [indexPath], with: .automatic) - // 需要返回true,否则没有反应 - completionHandler(true) - } - topAction.backgroundColor = NEConstant.hexRGB(0x337EFF) - rowActions.append(topAction) + /// 删除会话 + open func deleteActionHandler(action: UITableViewRowAction?, indexPath: IndexPath) { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } - let actionConfig = UISwipeActionsConfiguration.init(actions: rowActions) - actionConfig.performsFirstActionWithFullSwipe = false + var conversationModel: NEConversationListModel? - return actionConfig - } - */ + if indexPath.section == 0 { + conversationModel = viewModel.stickTopConversations[indexPath.row] - open func deleteActionHandler(action: UITableViewRowAction?, indexPath: IndexPath) { - let conversationModel = viewModel.conversationListArray?[indexPath.row] + } else if indexPath.section == 1 { + conversationModel = viewModel.conversationListData[indexPath.row] + } - if let deleteBottonClick = NEKitConversationConfig.shared.ui.deleteBottonClick { - deleteBottonClick(conversationModel, indexPath) + if let deleteButtonClick = NEKitConversationConfig.shared.ui.deleteButtonClick { + deleteButtonClick(conversationModel, indexPath) return } - if let recentSession = conversationModel?.recentSession { - viewModel.deleteRecentSession(recentSession: recentSession) - didDeleteConversationCell( - model: conversationModel ?? ConversationListModel(), - indexPath: indexPath - ) + if let conversation = conversationModel?.conversation { + viewModel.deleteConversation(conversation) { [weak self] error in + if let err = error { + self?.view.ne_makeToast(err.localizedDescription) + } + self?.reloadTableView() + } } } + /// 点击会话 open func topActionHandler(action: UITableViewRowAction?, indexPath: IndexPath, isTop: Bool) { - if !NEChatDetectNetworkTool.shareInstance.isNetworkRecahability() { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { showToast(localizable("network_error")) return } - let conversationModel = viewModel.conversationListArray?[indexPath.row] + var conversationModel: NEConversationListModel? + if indexPath.section == 0 { + conversationModel = viewModel.stickTopConversations[indexPath.row] + } else { + conversationModel = viewModel.conversationListData[indexPath.row] + } - if let stickTopBottonClick = NEKitConversationConfig.shared.ui.stickTopBottonClick { - stickTopBottonClick(conversationModel, indexPath) + if let stickTopButtonClick = NEKitConversationConfig.shared.ui.stickTopButtonClick { + stickTopButtonClick(conversationModel, indexPath) return } - if let recentSession = conversationModel?.recentSession { - onTopRecentAtIndexPath( - rencent: recentSession, - indexPath: indexPath, - isTop: isTop - ) { [weak self] error, sessionInfo in - if error == nil { + if let conversation = conversationModel?.conversation { + onTopRecentAtIndexPath(conversation: conversation, + indexPath: indexPath, + isTop: isTop) { [weak self] error in + + if let err = error { + self?.view.ne_makeToast(err.localizedDescription) + } else { if isTop { self?.didRemoveStickTopSession( - model: conversationModel ?? ConversationListModel(), + model: conversationModel ?? NEConversationListModel(), indexPath: indexPath ) } else { self?.didAddStickTopSession( - model: conversationModel ?? ConversationListModel(), + model: conversationModel ?? NEConversationListModel(), indexPath: indexPath ) } @@ -686,55 +688,81 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour } } - private func onTopRecentAtIndexPath(rencent: NIMRecentSession, indexPath: IndexPath, - isTop: Bool, - _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) - -> Void) { - guard let session = rencent.session else { - NELog.errorLog(ModuleName + " " + className, desc: "❌session is nil") - return + /// 非置顶变为置顶 + /// - Parameter conversation: 会话 + private func moveNormalConversationToTop(conversation: V2NIMConversation) { + var addModel: NEConversationListModel? + viewModel.conversationListData.removeAll(where: { model in + if model.conversation?.conversationId == conversation.conversationId { + addModel = model + return true + } + return false + }) + if let model = addModel { + viewModel.stickTopConversations.append(model) } - weak var weakSelf = self - if isTop { - guard let params = viewModel.stickTopInfos[session] else { - return + } + + /// 置顶变为非置顶 + /// - Parameter conversation: 会话 + private func moveTopToNormalConversation(conversation: V2NIMConversation) { + var addModel: NEConversationListModel? + viewModel.stickTopConversations.removeAll(where: { model in + if model.conversation?.conversationId == conversation.conversationId { + addModel = model + return true } + return false + }) + if let model = addModel { + viewModel.conversationListData.append(model) + } + } - viewModel.removeStickTopSession(params: params) { error, topSessionInfo in + /// 点击回调 + /// - Parameter conversation: 会话 + /// - Parameter indexPath: 索引 + /// - Parameter isTop: 置顶 + /// - Parameter completion: 完成回调 + func onTopRecentAtIndexPath(conversation: V2NIMConversation, indexPath: IndexPath, + isTop: Bool, + _ completion: @escaping (NSError?) + -> Void) { + weak var weakSelf = self + if indexPath.section == 0 { + viewModel.removeStickTop(conversation: conversation) { error in if let err = error { - NELog.errorLog( - ModuleName + " " + (weakSelf?.className ?? "ConversationController"), - desc: "❌CALLBACK removeStickTopSession failed,error = \(err)" - ) - completion(error as NSError?, nil) + NEALog.errorLog(ModuleName + " " + (weakSelf?.className ?? "ConversationController"), desc: "❌CALLBACK removeStickTopSession failed,error = \(err)") + completion(error) return } else { - NELog.infoLog( - ModuleName + " " + (weakSelf?.className ?? "ConversationController"), - desc: "✅CALLBACK removeStickTopSession SUCCESS" + NEALog.infoLog( + ModuleName + " " + (weakSelf?.className ?? "ConversationController"), desc: "✅CALLBACK removeStickTopSession SUCCESS" ) - weakSelf?.viewModel.stickTopInfos[session] = nil + weakSelf?.moveTopToNormalConversation(conversation: conversation) + weakSelf?.reloadTableView() - completion(nil, topSessionInfo) + completion(nil) } } } else { - viewModel.addStickTopSession(session: session) { error, newInfo in + viewModel.addStickTop(conversation: conversation) { error in if let err = error { - NELog.errorLog( + NEALog.errorLog( ModuleName + " " + (weakSelf?.className ?? "ConversationController"), desc: "❌CALLBACK addStickTopSession failed,error = \(err)" ) - completion(error as NSError?, nil) + completion(error) return } else { - NELog.infoLog(ModuleName + " " + (weakSelf?.className ?? "ConversationController"), - desc: "✅CALLBACK addStickTopSession callback SUCCESS") - weakSelf?.viewModel.stickTopInfos[session] = newInfo + NEALog.infoLog(ModuleName + " " + (weakSelf?.className ?? "ConversationController"), + desc: "✅CALLBACK addStickTopSession callback SUCCESS") + weakSelf?.moveNormalConversationToTop(conversation: conversation) weakSelf?.reloadTableView() - completion(nil, newInfo) + completion(nil) } } } @@ -745,72 +773,73 @@ extension NEBaseConversationController: UITableViewDelegate, UITableViewDataSour extension NEBaseConversationController { /// cell点击事件,可重写该事件处理自己的逻辑业务,例如跳转到指定的会话页面 - /// - Parameters: - /// - sessionType: 会话类型 - /// - sessionId: 会话id - /// - indexPath: indexpath - open func onselectedTableRow(sessionType: NIMSessionType, sessionId: String, - indexPath: IndexPath) { - if sessionType == .P2P { - let session = NIMSession(sessionId, type: .P2P) + /// - Parameter conversation: 会话 + open func onselectedTableRow(conversation: V2NIMConversation) { + let conversationId = conversation.conversationId + + let param = ["sessionId": conversationId] + Router.shared.use("ClearAtMessageRemind", parameters: param, closure: nil) + + // 路由跳转到聊天页面 + if conversation.type == .CONVERSATION_TYPE_P2P { Router.shared.use( PushP2pChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any], + parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any], closure: nil ) - } else if sessionType == .team { - let session = NIMSession(sessionId, type: .team) + } else if conversation.type == .CONVERSATION_TYPE_TEAM { Router.shared.use( PushTeamChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any], + parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any], closure: nil ) } } /// 删除会话 - /// - Parameters: - /// - model: 会话模型 - /// - indexPath: indexpath - open func didDeleteConversationCell(model: ConversationListModel, indexPath: IndexPath) {} + /// - parameter model: 会话模型 + /// - parameter indexpath: 索引 + open func didDeleteConversationCell(model: NEConversationListModel, indexPath: IndexPath) {} /// 删除一条置顶记录 - /// - Parameters: - /// - model: 会话模型 - /// - indexPath: indexpath - open func didRemoveStickTopSession(model: ConversationListModel, indexPath: IndexPath) {} + /// - parameter model: 会话模型 + /// - parameter indexpath + open func didRemoveStickTopSession(model: NEConversationListModel, indexPath: IndexPath) {} /// 添加一条置顶记录 - /// - Parameters: - /// - model: 会话模型 - /// - indexPath: indexpath - open func didAddStickTopSession(model: ConversationListModel, indexPath: IndexPath) {} + /// - Parameter model: 会话模型 + /// - Parameter indexpath: 索引 + open func didAddStickTopSession(model: NEConversationListModel, indexPath: IndexPath) {} } // MARK: ================= ConversationViewModelDelegate=================== extension NEBaseConversationController: ConversationViewModelDelegate { - open func didAddRecentSession() { - NELog.infoLog("ConversationController", desc: "didAddRecentSession") - reloadTableView() - } - - open func didUpdateRecentSession(index: Int) { - let indexPath = IndexPath(row: index, section: 0) - tableView.reloadRows(at: [indexPath], with: .none) - } - open func reloadData() { delegate?.onDataLoaded() } - open func didRefreshTable() { + /// 带排序的刷新 + open func reloadTableView() { + if viewModel.stickTopConversations.count <= 0, viewModel.conversationListData.count <= 0 { + emptyView.isHidden = false + } else { + emptyView.isHidden = true + } + viewModel.conversationListData.sort() + viewModel.stickTopConversations.sort() tableView.reloadData() } - open func reloadTableView() { - emptyView.isHidden = (viewModel.conversationListArray?.count ?? 0) > 0 - viewModel.sortRecentSession() - didRefreshTable() + /// 由于数据变更可能导致底部有更多数据,此方法重新使列表加载更多能力开启 + public func loadMoreStateChange(_ finish: Bool) { + if finish { + tableView.mj_footer = nil + } else { + tableView.mj_footer = MJRefreshBackNormalFooter( + refreshingTarget: self, + refreshingAction: #selector(loadMoreData) + ) + } } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift index 66b23264..ba45c942 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationSearchController.swift @@ -3,17 +3,70 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NECommonUIKit import NIMSDK import UIKit @objcMembers -open class NEBaseConversationSearchController: NEBaseConversationNavigationController, UITableViewDelegate, +open class NEBaseConversationSearchController: NEConversationBaseViewController, UITableViewDelegate, UITableViewDataSource { var viewModel = ConversationSearchViewModel() var tag = "ConversationSearchBaseController" var searchStr = "" var headTitleArr = [localizable("friend")] + public lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.keyboardDismissMode = .onDrag + tableView.delegate = self + tableView.dataSource = self + tableView.rowHeight = 60 + tableView.backgroundColor = .white + tableView.sectionHeaderHeight = 30 + tableView.sectionFooterHeight = 0 + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + return tableView + }() + + public lazy var searchTextField: SearchTextField = { + let textField = SearchTextField() + let leftImageView = UIImageView(image: UIImage + .ne_imageNamed(name: "conversation_search_icon")) + textField.contentMode = .center + textField.leftView = leftImageView + textField.leftViewMode = .always + textField.placeholder = commonLocalizable("search") + textField.font = UIFont.systemFont(ofSize: 14) + textField.textColor = UIColor.ne_greyText + textField.translatesAutoresizingMaskIntoConstraints = false + textField.layer.cornerRadius = 8 + textField.backgroundColor = .ne_lightBackgroundColor + textField.clearButtonMode = .always + textField.returnKeyType = .search + textField.addTarget(self, action: #selector(searchTextFieldChange), for: .editingChanged) + + if let clearButton = textField.value(forKey: "_clearButton") as? UIButton { + clearButton.accessibilityIdentifier = "id.clear" + } + textField.accessibilityIdentifier = "id.search" + return textField + }() + + public lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView( + imageName: "user_empty", + content: localizable("user_not_exist"), + frame: CGRect.zero + ) + view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false + view.isHidden = true + view.backgroundColor = .clear + return view + }() + override open func viewDidLoad() { super.viewDidLoad() initialConfig() @@ -25,7 +78,7 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr } open func initialConfig() { - title = localizable("search") + title = commonLocalizable("search") // 可在此处选择是否展示群聊结果 headTitleArr.append(contentsOf: [localizable("discussion_group"), @@ -53,9 +106,9 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr } if searchText.count <= 0 { emptyView.isHidden = true - viewModel.searchResult?.friend = [ConversationSearchListModel]() - viewModel.searchResult?.contactGroup = [ConversationSearchListModel]() - viewModel.searchResult?.seniorGroup = [ConversationSearchListModel]() + viewModel.friendDatas.removeAll() + viewModel.discussionDatas.removeAll() + viewModel.seniorDatas.removeAll() tableView.reloadData() return } @@ -64,76 +117,17 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr if textRange == nil || ((textRange?.isEmpty) == nil) { weak var weakSelf = self searchStr = searchText - viewModel.doSearch(searchStr: searchText) { error, tupleInfo in - if let err = error { - NELog.errorLog(ModuleName + " " + self.tag, desc: "❌CALLBACK doSearch failed,error = \(err)") + viewModel.doSearch(searchText) { + if weakSelf?.viewModel.friendDatas.count == 0, weakSelf?.viewModel.discussionDatas.count == 0, weakSelf?.viewModel.seniorDatas.count == 0 { + weakSelf?.emptyView.isHidden = false } else { - NELog.infoLog(ModuleName + " " + self.tag, desc: "✅CALLBACK doSearch SUCCESS") - if tupleInfo?.friend.count == 0, tupleInfo?.contactGroup.count == 0, - tupleInfo?.seniorGroup.count == 0 { - weakSelf?.emptyView.isHidden = false - } else { - weakSelf?.emptyView.isHidden = true - } - weakSelf?.tableView.reloadData() + weakSelf?.emptyView.isHidden = true } + weakSelf?.tableView.reloadData() } } } - // MARK: lazy method - - public lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .plain) - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.separatorStyle = .none - tableView.keyboardDismissMode = .onDrag - tableView.delegate = self - tableView.dataSource = self - tableView.rowHeight = 60 - tableView.backgroundColor = .white - tableView.sectionHeaderHeight = 30 - tableView.sectionFooterHeight = 0 - tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) - return tableView - }() - - public lazy var searchTextField: SearchTextField = { - let textField = SearchTextField() - let leftImageView = UIImageView(image: UIImage - .ne_imageNamed(name: "conversation_search_icon")) - textField.contentMode = .center - textField.leftView = leftImageView - textField.leftViewMode = .always - textField.placeholder = localizable("search") - textField.font = UIFont.systemFont(ofSize: 14) - textField.textColor = UIColor.ne_greyText - textField.translatesAutoresizingMaskIntoConstraints = false - textField.layer.cornerRadius = 8 - textField.backgroundColor = .ne_lightBackgroundColor - textField.clearButtonMode = .always - textField.returnKeyType = .search - textField.addTarget(self, action: #selector(searchTextFieldChange), for: .editingChanged) - - if let clearButton = textField.value(forKey: "_clearButton") as? UIButton { - clearButton.accessibilityIdentifier = "id.clear" - } - textField.accessibilityIdentifier = "id.search" - return textField - }() - - public lazy var emptyView: NEEmptyDataView = { - let view = NEEmptyDataView( - imageName: "user_empty", - content: localizable("user_not_exist"), - frame: CGRect.zero - ) - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - view.backgroundColor = .clear - return view - }() - // MARK: UITableViewDelegate, UITableViewDataSource open func numberOfSections(in tableView: UITableView) -> Int { @@ -141,15 +135,14 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr } open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if let friend = viewModel.searchResult?.friend, section == 0 { - return friend.count - } else if let contactGroup = viewModel.searchResult?.contactGroup, section == 1 { - return contactGroup.count - } else if let seniorGroup = viewModel.searchResult?.seniorGroup, section == 2 { - return seniorGroup.count - } else { - return 0 + if section == 0 { + return viewModel.friendDatas.count + } else if section == 1 { + return viewModel.discussionDatas.count + } else if section == 2 { + return viewModel.seniorDatas.count } + return 0 } open func tableView(_ tableView: UITableView, @@ -159,11 +152,11 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr for: indexPath ) as? NEBaseConversationSearchCell { if indexPath.section == 0 { - cell.searchModel = viewModel.searchResult?.friend[indexPath.row] + cell.searchModel = viewModel.friendDatas[indexPath.row] } else if indexPath.section == 1 { - cell.searchModel = viewModel.searchResult?.contactGroup[indexPath.row] + cell.searchModel = viewModel.discussionDatas[indexPath.row] } else { - cell.searchModel = viewModel.searchResult?.seniorGroup[indexPath.row] + cell.searchModel = viewModel.seniorDatas[indexPath.row] } cell.searchText = searchStr return cell @@ -174,53 +167,61 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { weak var weakSelf = self if indexPath.section == 0 { - let searchModel = viewModel.searchResult?.friend[indexPath.row] - if let userId = searchModel?.userInfo?.userId { - let session = NIMSession(userId, type: .P2P) + let searchModel = viewModel.friendDatas[indexPath.row] + if let userId = searchModel.userInfo?.user?.accountId { + let conversationId = V2NIMConversationIdUtil.p2pConversationId(userId) Router.shared.use( PushP2pChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any], + parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any], closure: nil ) } } else if indexPath.section == 1 { - let searchModel = viewModel.searchResult?.contactGroup[indexPath.row] - if let teamId = searchModel?.teamInfo?.teamId { - TeamRepo.shared.fetchTeamInfo(teamId) { error, teamInfo in - if let err = error as? NSError { - if err.code == noNetworkCode { + let searchModel = viewModel.discussionDatas[indexPath.row] + if let teamId = searchModel.team?.teamId { + TeamRepo.shared.getTeamInfo(teamId) { team, error in + if let err = error { + if err.code == protocolSendFailed { weakSelf?.showToast(commonLocalizable("network_error")) } else { weakSelf?.showSingleAlert(title: localizable("leave_team"), message: localizable("leave_team_desc")) {} } } else { - let session = NIMSession(teamId, type: .team) + if team?.isValidTeam == false { + weakSelf?.showSingleAlert(title: localizable("leave_team"), message: localizable("leave_team_desc")) {} + return + } + let conversationId = V2NIMConversationIdUtil.teamConversationId(teamId) Router.shared.use( PushTeamChatVCRouter, parameters: ["nav": weakSelf?.navigationController as Any, - "session": session as Any], + "conversationId": conversationId as Any], closure: nil ) } } } } else { - let searchModel = viewModel.searchResult?.seniorGroup[indexPath.row] - if let teamId = searchModel?.teamInfo?.teamId { - TeamRepo.shared.fetchTeamInfo(teamId) { error, teamInfo in - if let err = error as? NSError { - if err.code == noNetworkCode { + let searchModel = viewModel.seniorDatas[indexPath.row] + if let teamId = searchModel.team?.teamId { + TeamRepo.shared.getTeamInfo(teamId) { team, error in + if let err = error { + if err.code == protocolSendFailed { weakSelf?.showToast(commonLocalizable("network_error")) } else { weakSelf?.showSingleAlert(title: localizable("leave_team"), message: localizable("leave_team_desc")) {} } } else { - let session = NIMSession(teamId, type: .team) + if team?.isValidTeam == false { + weakSelf?.showSingleAlert(title: localizable("leave_team"), message: localizable("leave_team_desc")) {} + return + } + let conversationId = V2NIMConversationIdUtil.teamConversationId(teamId) Router.shared.use( PushTeamChatVCRouter, parameters: ["nav": weakSelf?.navigationController as Any, - "session": session as Any], + "conversationId": conversationId as Any], closure: nil ) } @@ -243,14 +244,11 @@ open class NEBaseConversationSearchController: NEBaseConversationNavigationContr open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - if let friend = viewModel.searchResult?.friend, friend.count > 0, section == 0 { + if section == 0, viewModel.friendDatas.count > 0 { return 30 - - } else if let contactGroup = viewModel.searchResult?.contactGroup, contactGroup.count > 0, - section == 1 { + } else if section == 1, viewModel.discussionDatas.count > 0 { return 30 - } else if let seniorGroup = viewModel.searchResult?.seniorGroup, seniorGroup.count > 0, - section == 2 { + } else if section == 2, viewModel.seniorDatas.count > 0 { return 30 } else { return 0 diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListView.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListView.swift index a956c346..d2bcafee 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListView.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBasePopListView.swift @@ -21,10 +21,11 @@ open class PopListItem: NSObject { @objcMembers open class NEBasePopListView: UIView { public let shadowView = UIView() + public let popView = UIView() + public var buttonHeight: CGFloat = 32.0 - let popView = UIView() public var popViewWidth: CGFloat = 122.0 - var popViewHeight: CGFloat = 0 + public var popViewHeight: CGFloat = 0 public var popViewRadius: CGFloat = 8.0 public var topConstant: CGFloat = 0 @@ -45,7 +46,7 @@ open class NEBasePopListView: UIView { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setupUI() { diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationNavigationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEConversationBaseViewController.swift similarity index 93% rename from NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationNavigationController.swift rename to NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEConversationBaseViewController.swift index b642412c..e953378a 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEBaseConversationNavigationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/NEConversationBaseViewController.swift @@ -5,8 +5,9 @@ import NECoreKit import UIKit +/// 会话模块 ViewController 基类 @objcMembers -open class NEBaseConversationNavigationController: UIViewController, UIGestureRecognizerDelegate { +open class NEConversationBaseViewController: UIViewController, UIGestureRecognizerDelegate { var topConstant: CGFloat = 0 public let navigationView = NENavigationView() diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ConversationRouter/NEBaseConversationRouter.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ConversationRouter/NEBaseConversationRouter.swift index ae111007..f09b7573 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ConversationRouter/NEBaseConversationRouter.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ConversationRouter/NEBaseConversationRouter.swift @@ -6,4 +6,13 @@ import Foundation @objcMembers -open class ConversationRouter: NSObject {} +open class ConversationRouter: NSObject { + /// 注册通用路由 + static func registerCommon() { + Router.shared.register("ClearAtMessageRemind") { param in + if let sessionId = param["sessionId"] as? String { + NEAtMessageManager.instance?.clearAtRecord(sessionId) + } + } + } +} diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift index 15469be7..000dba35 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationSearchViewModel.swift @@ -2,41 +2,205 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit +import NECoreIM2Kit import NIMSDK import UIKit @objcMembers -open class ConversationSearchViewModel: NSObject { - let repo = ConversationRepo.shared - public var searchResult: ( - friend: [ConversationSearchListModel], - contactGroup: [ConversationSearchListModel], - seniorGroup: [ConversationSearchListModel] - )? +open class ConversationSearchViewModel: NSObject, NETeamListener, NEContactListener, NEIMKitClientListener { + let conversationRepo = ConversationRepo.shared + + /// 群数据缓存 + var teamDic = [String: ConversationSearchListModel]() + /// 好友数据缓存 + var friendDic = [String: ConversationSearchListModel]() + /// 好友搜索结果 + var friendDatas = [ConversationSearchListModel]() + /// 讨论组搜索结果 + var discussionDatas = [ConversationSearchListModel]() + /// 高级群搜索结果 + var seniorDatas = [ConversationSearchListModel]() + private let className = "ConversationSearchViewModel" override public init() { super.init() + ContactRepo.shared.addContactListener(self) + TeamRepo.shared.addTeamListener(self) + IMKitClient.instance.addLoginListener(self) + + weak var weakSelf = self + getSearchData { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "get data finish") + } } - /// 请求接口 - /// - Parameters: - /// - searchStr: 搜索的内容 - /// - completion: 回调结果 - open func doSearch(searchStr: String, - _ completion: @escaping (NSError?, ( - friend: [ConversationSearchListModel], - contactGroup: [ConversationSearchListModel], - seniorGroup: [ConversationSearchListModel] - )?) -> Void) { - NELog.infoLog( + deinit { + ContactRepo.shared.removeContactListener(self) + TeamRepo.shared.removeTeamListener(self) + IMKitClient.instance.removeLoginListener(self) + } + + /// 搜索 + /// - Parameter searchText: 搜索文案 + /// - Parameter completion: 完成回调 + open func doSearch(_ searchText: String?, _ completion: @escaping () -> Void) { + NEALog.infoLog( ModuleName + " " + className, - desc: #function + ", searchStr.count:\(searchStr.count)" + desc: #function + ", searchTexty: \(searchText ?? "")" ) + + friendDatas.removeAll() + discussionDatas.removeAll() + seniorDatas.removeAll() + + guard let search = searchText else { + completion() + return + } + for (_, value) in friendDic { + if let user = value.userInfo { + if user.showName()?.contains(search) == true { + friendDatas.append(value) + } else if user.user?.accountId?.contains(search) == true { + friendDatas.append(value) + } + } + } + for (_, value) in teamDic { + if let showName = value.team?.getShowName() { + if showName.contains(search) == true { + if let serverExtension = value.team?.serverExtension, serverExtension.contains(discussTeamKey) == true { + discussionDatas.append(value) + } else { + seniorDatas.append(value) + } + } + } + } + friendDatas.sort() + discussionDatas.sort() + seniorDatas.sort() + completion() + } + + /// 获取所有数据 + /// - Parameter completion: 完成回调 + func getSearchData(_ removeTeamData: Bool = false, _ completion: @escaping () -> Void) { + let workingGroup = DispatchGroup() + let workingQueue = DispatchQueue(label: "get_search_data_queue") + weak var weakSelf = self + workingGroup.enter() + workingQueue.async { + let userFriends = NEFriendUserCache.shared.getFriendListNotInBlocklist() + for (uid, userFriend) in userFriends { + let model = ConversationSearchListModel() + model.userInfo = userFriend + weakSelf?.friendDic[uid] = model + } + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + "conversation search get friend list ") + workingGroup.leave() + } + + workingGroup.enter() + workingQueue.async { + TeamRepo.shared.getTeamList { teams, error in + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + " conversation search get team list \(error?.localizedDescription ?? "")") + if removeTeamData == true { + if error == nil { + weakSelf?.teamDic.removeAll() + } + } + teams?.forEach { team in + if let tid = team.v2Team?.teamId { + let model = ConversationSearchListModel() + model.team = team.v2Team + weakSelf?.teamDic[tid] = model + } + } + workingGroup.leave() + } + } + + workingGroup.notify(queue: workingQueue) { + DispatchQueue.main.async { + completion() + } + } + } + + // MARK: - NEContactListener + + /// 好友信息更新 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + if let uid = friendInfo.accountId { + let model = ConversationSearchListModel() + model.userInfo = NEFriendUserCache.shared.getFriendInfo(uid) + friendDic[uid] = model + } + } + + // MARK: - V2NIMTeamListener + + /// 群信息更新回调 + /// - Parameter team: 群 + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + if let model = teamDic[team.teamId] { + model.team = team + } else { + addTeam(team) + } + } + + /// 加入群回调 + /// - Parameters: + /// - team: 群 + public func onTeamJoined(_ team: V2NIMTeam) { + addTeam(team) + } + + /// 创建群回调 + /// - Parameter team: 群 + public func onTeamCreated(_ team: V2NIMTeam) { + addTeam(team) + } + + /// 群解散回调 + /// - Parameter team: 群 + public func onTeamDismissed(_ team: V2NIMTeam) { + removeTeam(team) + } + + /// 退出群回调 + /// - Parameters: + /// - team: 群 + /// - isKicked: 是否被踢 + public func onTeamLeft(_ team: V2NIMTeam, isKicked: Bool) { + removeTeam(team) + } + + /// 移除群 + /// - Parameter team: 群 + private func removeTeam(_ team: V2NIMTeam) { + teamDic.removeValue(forKey: team.teamId) + } + + /// 添加群 + /// - Parameter team: 群 + private func addTeam(_ team: V2NIMTeam) { + let model = ConversationSearchListModel() + model.team = team + teamDic[team.teamId] = model + } + + /// 群数据同步完成回调 + public func onTeamSyncFinished() { + NEALog.infoLog(className(), desc: #function + ", onTeamSyncFinished get search data") weak var weakSelf = self - repo.searchContact(searchStr: searchStr) { error, searchResult in - weakSelf?.searchResult = searchResult - completion(error, searchResult) + getSearchData(true) { + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + ", get data finish") } } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift index 9225ca5a..ef36ad3a 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift @@ -4,628 +4,511 @@ import Foundation import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK -let revokeLocalMessage = "revoke_message_local" -let revokeLocalMessageContent = "revoke_message_local_content" - @objc public protocol ConversationViewModelDelegate: NSObjectProtocol { - func didAddRecentSession() - func didUpdateRecentSession(index: Int) func reloadData() func reloadTableView() + /// 底部加载更多状态变更 + func loadMoreStateChange(_ finish: Bool) } +public typealias ConversationCallBack = (NSError?, Bool?) -> Void + @objcMembers -open class ConversationViewModel: NSObject, ConversationRepoDelegate, - NIMConversationManagerDelegate, NIMTeamManagerDelegate, NIMUserManagerDelegate, NIMChatManagerDelegate { - public var conversationListArray: [ConversationListModel]? - public var stickTopInfos = [NIMSession: NIMStickTopSessionInfo]() +open class ConversationViewModel: NSObject, NEConversationListener, NETeamListener, NEChatListener, NEContactListener, NEIMKitClientListener { public weak var delegate: ConversationViewModelDelegate? private let className = "ConversationViewModel" - public let repo = ConversationRepo.shared - var cacheUpdateSessionDic = [String: NIMRecentSession]() - var cacheAddSessionDic = [String: ConversationListModel]() + /// 会话API单例 + public let conversationRepo = ConversationRepo.shared + + /// 会话列表起始索引 + public var offset: Int64 = 0 + + /// 会话列表分页大小 + public var page = 200 + + /// 非置顶会话数据 + public var conversationListData = [NEConversationListModel]() + + /// 置顶会话数据 + public var stickTopConversations = [NEConversationListModel]() + + /// 所有会话数据记录 + public var conversationDic = [String: NEConversationListModel]() + + /// 当前是否在请求会话列表 + private var isRequesting = false + + /// 是否同步完成 + public var syncFinished = false { + didSet { + print("syncFinished ", syncFinished) + } + } + + /// 回调 + public var callBack: ConversationCallBack? override public init() { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) super.init() - repo.delegate = self - repo.addSessionDelegate(delegate: self) - repo.addChatDelegate(delegate: self) - repo.addTeamDelegate(delegate: self) - stickTopInfos = repo.getStickTopInfos() - NIMSDK.shared().userManager.add(self) NotificationCenter.default.addObserver(self, selector: #selector(atMessageChange), name: Notification.Name(AtMessageChangeNoti), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(deleteConversationNoti), name: NENotificationName.deleteConversationNotificationName, object: nil) + conversationRepo.addListener(self) + ChatRepo.shared.addChatListener(self) + TeamRepo.shared.addTeamListener(self) + ContactRepo.shared.addContactListener(self) + IMKitClient.instance.addLoginListener(self) } deinit { - NELog.infoLog(ModuleName + className(), desc: #function) - repo.removeSessionDelegate(delegate: self) - repo.removeChatDelegate(delegate: self) - repo.removeTeamDelegate(delegate: self) - NIMSDK.shared().userManager.remove(self) + NEALog.infoLog(ModuleName + className(), desc: #function) NotificationCenter.default.removeObserver(self) + conversationRepo.removeListener(self) + ChatRepo.shared.removeChatListener(self) + TeamRepo.shared.removeTeamListener(self) + ContactRepo.shared.removeContactListener(self) + IMKitClient.instance.removeLoginListener(self) } func atMessageChange() { - NELog.infoLog(className(), desc: "atMessageChange") + NEALog.infoLog(className(), desc: "atMessageChange") delegate?.reloadTableView() } - open func fetchServerSessions(option: NIMFetchServerSessionOption, - _ completion: @escaping (NSError?, [ConversationListModel]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) - weak var weakSelf = self - repo.getSessionList { error, conversaitonList in - DispatchQueue.main.async { - weakSelf?.conversationListArray = conversaitonList - NELog.infoLog(ModuleName, desc: "get session list : \(conversaitonList?.count ?? 0)") - var set = Set() - conversaitonList?.forEach { model in - NELog.infoLog(ModuleName, desc: "get session sid : \(model.recentSession?.session?.sessionId ?? "nil")") - if let recentSession = model.recentSession, let sid = recentSession.session?.sessionId { - set.insert(sid) - if let recent = weakSelf?.cacheUpdateSessionDic[sid] { - NELog.infoLog(ModuleName, desc: "cacheUpdateSessionDic fitler sid: \(recent.session?.sessionId ?? "nil")") - if let time1 = recentSession.lastMessage?.timestamp, let time2 = recent.lastMessage?.timestamp, time1 < time2 { - model.recentSession = recent - } - } - - if let recent = weakSelf?.cacheAddSessionDic[sid]?.recentSession { - NELog.infoLog(ModuleName, desc: "cacheAddSessionDic fitler sid: \(recent.session?.sessionId ?? "nil")") - if let time1 = recentSession.lastMessage?.timestamp, let time2 = recent.lastMessage?.timestamp, time1 < time2 { - model.recentSession = recent - } - } - } - } - NELog.infoLog(ModuleName, desc: "cacheAddSessionDic count: \(weakSelf?.cacheAddSessionDic.count ?? 0)") - weakSelf?.cacheAddSessionDic.forEach { (key: String, value: ConversationListModel) in - NELog.infoLog(ModuleName, desc: "cacheAddSessionDic key: \(key)") - if set.contains(key) == false { - if let recent = weakSelf?.cacheUpdateSessionDic[key] { - if let time1 = value.recentSession?.lastMessage?.timestamp, let time2 = recent.lastMessage?.timestamp, time1 < time2 { - value.recentSession = recent - } - } - NELog.infoLog(ModuleName, desc: "cacheAddSessionDic : \(key)") - weakSelf?.conversationListArray?.append(value) - } - } - weakSelf?.cacheAddSessionDic.removeAll() - - // 可在此处对会话列表进行过滤 + func deleteConversationNoti(_ noti: NSNotification) { + if let conversationId = noti.object as? String { + weak var weakSelf = self + conversationRepo.deleteConversation(conversationId) { error in + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + " deleteConversationNoti \(error?.localizedDescription ?? "") ") + } + } + } - NELog.infoLog(ModuleName, desc: "conversationListArray count : \(weakSelf?.conversationListArray?.count ?? 0)") - completion(error, weakSelf?.conversationListArray) + /// 分页获取会话列表 + open func getConversationListByPage(_ completion: @escaping (NSError?, Bool?) -> Void) { + if syncFinished == false { + callBack = completion + return + } - // 拉取好友信息 - ChatUserCache.removeAllUserInfo() - ContactRepo.shared.getFriendList(true, local: false) { friends, error in - if let friends = friends { - friends.forEach { user in - ChatUserCache.updateUserInfo(user) - } - } + if isRequesting == true { + // 防止多次请求造成数据混乱,等上次请求成功后进行下一次 + completion(nil, false) + return + } + isRequesting = true + print("did getConversationList") + conversationRepo.getConversationList(offset, page) { [weak self] conversations, offset, finished, error in + if error == nil { + if let set = offset { + // 更新索引 + self?.offset = set + } + self?.isRequesting = false + // 区分置顶消息和非置顶消息 + conversations?.forEach { conversation in + self?.addOrUpdateConversationData(conversation) } } + completion(error, finished) } } - open func deleteRecentSession(recentSession: NIMRecentSession) { - NELog.infoLog( - ModuleName + " " + className, - desc: #function + ", sessionId:" + (recentSession.session?.sessionId ?? "nil") - ) - weak var weakSelf = self - let option = NIMDeleteRecentSessionOption() - option.isDeleteRoamMessage = true - option.shouldMarkAllMessagesReadInSessions = true - repo.deleteRecentConversation(recentSession, option) { error in - weakSelf?.repo.deleteLocalSession(recentSession: recentSession) + /// 添加或者更新会话 + /// - Parameter conversation 会话对象 + open func addOrUpdateConversationData(_ conversation: V2NIMConversation) { + if let cacheModel = conversationDic[conversation.conversationId] { + cacheModel.conversation = conversation + } else { + let model = NEConversationListModel() + model.conversation = conversation + conversationDic[conversation.conversationId] = model + if conversation.stickTop == true { + stickTopConversations.insert(model, at: 0) + } else { + conversationListData.insert(model, at: 0) + } } } - open func stickTopInfoForSession(session: NIMSession) -> NIMStickTopSessionInfo? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + session.sessionId) - return repo.getStickTopSessionInfo(session: session) + /// 删除会话 + /// - Parameter conversation 会话对象 + /// - Parameter completion 完成回调 + open func deleteConversation(_ conversation: V2NIMConversation, _ completion: @escaping (NSError?) -> Void) { + conversationRepo.deleteConversation(conversation.conversationId) { error in + if let err = error { + completion(err) + } else { + // 通知界面刷新 + completion(nil) + } + } } - open func addStickTopSession(session: NIMSession, - _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + session.sessionId) - let params = NIMAddStickTopSessionParams(session: session) - repo.addStickTop(params: params) { error, stickTopSessionInfo in - completion(error as NSError?, stickTopSessionInfo) + /// 添加置顶 + /// - Parameter conversation 会话对象 + /// - Parameter completion 完成回调 + open func addStickTop(conversation: V2NIMConversation, + _ completion: @escaping (NSError?) + -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + conversation.conversationId) + conversationRepo.setStickTop(conversation.conversationId, true) { error in + completion(error) } } - 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) + /// 取消置顶 + /// - Parameter conversation 会话对象 + /// - Parameter completion 完成回调 + open func removeStickTop(conversation: V2NIMConversation, + _ completion: @escaping (NSError?) + -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", sessionId:" + conversation.conversationId) + conversationRepo.setStickTop(conversation.conversationId, false) { error in + completion(error) } } - open func loadStickTopSessionInfos(_ completion: - @escaping (NSError?, [NIMSession: NIMStickTopSessionInfo]?) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function) - repo.getStickTopSessionList(completion) + open func onMuteListChanged() { + delegate?.reloadTableView() } - open func notifyForNewMsg(userId: String?) -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:" + (userId ?? "nil")) - return repo.isNeedNotify(userId: userId) + public func updateUserInfo(_ model: NEConversationListModel, _ user: NEUserWithFriend, _ conversation: V2NIMConversation) { + model.conversation = conversation } - open func notifyStateForNewMsg(teamId: String?) -> NIMTeamNotifyState { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (teamId ?? "nil")) - return repo.isNeedNotifyForTeam(teamId: teamId) + public func updateTeamInfo(_ model: NEConversationListModel, _ team: V2NIMTeam, _ conversation: V2NIMConversation) { + model.conversation = conversation } - // MARK: ======================== private method ============================== - - open func sortRecentSession() { - NELog.infoLog(ModuleName + " " + className, desc: #function) - var tempArr = [NIMRecentSession]() - var dic = [String: ConversationListModel]() - conversationListArray?.forEach { listModel in - if let session = listModel.recentSession { - tempArr.append(session) - if let sessionId = session.session?.sessionId { - dic[sessionId] = listModel + /// 处理置顶变更逻辑 + public func filterStickTopData(_ conversations: [V2NIMConversation]) { + var changeTostickTopSet = Set() + var changeToUnStickTopDic = Set() + for conversation in conversations { + if let model = conversationDic[conversation.conversationId] { + if model.conversation?.stickTop != conversation.stickTop { + if conversation.stickTop == true { + changeTostickTopSet.insert(conversation.conversationId) + } else { + changeToUnStickTopDic.insert(conversation.conversationId) + } } + model.conversation = conversation } } - let resultArr = repo.sortSessionList(recentSessions: tempArr, stickTopInfo: stickTopInfos) - var sortResultArr = [ConversationListModel]() - resultArr.forEach { recentSession in - let listModel = ConversationListModel() - listModel.recentSession = recentSession - if recentSession.session?.sessionType == .P2P { - if let sessionId = recentSession.session?.sessionId, - let userInfo = dic[sessionId]?.userInfo { - listModel.userInfo = userInfo - } - - } else if recentSession.session?.sessionType == .team { - if let sessionId = recentSession.session?.sessionId, - let teamInfo = dic[sessionId]?.teamInfo { - listModel.teamInfo = teamInfo + conversationListData.removeAll { model in + if let cid = model.conversation?.conversationId { + if changeTostickTopSet.contains(cid) { + stickTopConversations.insert(model, at: 0) + return true } } - sortResultArr.append(listModel) + return false } - conversationListArray = sortResultArr - } - - // 本地排序 在didUpdate的时候如有需要在打开 - func findInsertPlace(recentSession: NIMRecentSession) -> NSInteger { - NELog.infoLog( - ModuleName + " " + className, - desc: #function + ", sessionId:" + (recentSession.session?.sessionId ?? "nil") - ) - var matchIndex = 0 - var find = false - if let conversationArr = conversationListArray { - for (i, listModel) in conversationArr.enumerated() { - if let enumTime = listModel.recentSession?.lastMessage?.timestamp, - let targetTime = recentSession.lastMessage?.timestamp { - if enumTime <= targetTime { - find = true - matchIndex = i - break - } + + stickTopConversations.removeAll { model in + if let cid = model.conversation?.conversationId { + if changeToUnStickTopDic.contains(cid) { + conversationListData.append(model) + return true } } + return false } + } - if find { - return matchIndex - } else { - return conversationListArray?.count ?? 0 + // 创建会话回调 + public func onConversationCreated(_ conversation: V2NIMConversation) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", did add session targetId:" + conversation.conversationId) + if checkDismissTeamNoti(conversation) { + return } + + addOrUpdateConversationData(conversation) + delegate?.reloadTableView() } - // MARK: ==================== NIMChatManagerDelegate ========================== + /// 会话变更 + /// - Parameter conversations 会话列表 + public func onConversationChanged(_ conversations: [V2NIMConversation]) { + // 置顶逻辑处理 + filterStickTopData(conversations) - open func onRecvMessageReceipts(_ receipts: [NIMMessageReceipt]) { - receipts.forEach { receipt in - if receipt.session?.sessionType == .P2P { - if let listArr = conversationListArray { - for (i, listModel) in listArr.enumerated() { - if listModel.recentSession?.session?.sessionType == .P2P, - receipt.session?.sessionId == listModel.recentSession?.session?.sessionId { - delegate?.didUpdateRecentSession(index: i) - } - } - } + for conversation in conversations { + if checkDismissTeamNoti(conversation) { + continue } + addOrUpdateConversationData(conversation) } - } - - // MARK: ==================== ConversationRepoDelegate ========================== - 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() } - open func onNotifyRemoveStickTopSession(_ removedInfo: NIMStickTopSessionInfo) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ",onNotifyRemoveStickTopSession sessionId:" + removedInfo.session.sessionId) - stickTopInfos[removedInfo.session] = nil + /// 会话删除 + /// - Parameter conversationIds: 会话id列表 + public func onConversationDeleted(_ conversationIds: [String]) { + var removeFlagSet = Set() + for id in conversationIds { + removeFlagSet.insert(id) + conversationDic.removeValue(forKey: id) + } + stickTopConversations.removeAll(where: { + if let sid = $0.conversation?.conversationId, removeFlagSet.contains(sid) { + return true + } + return false + }) + conversationListData.removeAll(where: { + if let sid = $0.conversation?.conversationId, removeFlagSet.contains(sid) { + return true + } + return false + }) delegate?.reloadTableView() } - open func onNotifySyncStickTopSessions(_ response: NIMSyncStickTopSessionResponse) { - loadStickTopSessionInfos { [weak self] error, sessionInfos in - NELog.infoLog( - ModuleName + " " + (self?.className ?? "ConversationViewModel"), - desc: "CALLBACK loadStickTopSessionInfos " + (error?.localizedDescription ?? "no error") - ) - if error != nil { - if let infos = self?.repo.getStickTopInfos() { - self?.stickTopInfos = infos - } - } else if let infos = sessionInfos { - self?.stickTopInfos = infos - } - self?.delegate?.reloadTableView() - self?.delegate?.reloadData() + /// 检查会话是否包含解散通知的变更 + /// - Parameter conversation: 会话 + public func checkDismissTeamNoti(_ conversation: V2NIMConversation) -> Bool { + if IMKitConfigCenter.shared.dismissTeamDeleteConversation == false { + return false } - } - - open func didServerSessionUpdated(_ recentSession: NIMRecentSession?) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ",didServerSessionUpdated:" + (recentSession?.session?.sessionId ?? "")) - } - - // MARK: ====================NIMConversationManagerDelegate===================== - open func didAdd(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { - guard let targetId = recentSession.session?.sessionId else { - NELog.errorLog(ModuleName + " " + className, desc: "❌sessionId is nil") - return + if conversation.type != V2NIMConversationType.CONVERSATION_TYPE_TEAM { + return false } - NELog.infoLog(ModuleName + " " + className, desc: #function + ", did add session targetId:" + targetId) - // 解散、退出群聊 - if let object = recentSession.lastMessage?.messageObject as? NIMNotificationObject, object.notificationType == .team { - if let content = object.content as? NIMTeamNotificationContent { - if content.operationType == .dismiss || - (content.operationType == .kick && - content.targetIDs?.contains(IMKitClient.instance.imAccid()) == true) || - (content.operationType == .leave && - IMKitClient.instance.isMySelf(content.sourceID)) { + let targetId = conversation.conversationId + + if conversation.lastMessage?.messageType == V2NIMMessageType.MESSAGE_TYPE_NOTIFICATION { + if let content = conversation.lastMessage?.attachment as? V2NIMMessageNotificationAttachment { + if content.type == V2NIMMessageNotificationType.MESSAGE_NOTIFICATION_TYPE_TEAM_DISMISS || + (content.type == V2NIMMessageNotificationType.MESSAGE_NOTIFICATION_TYPE_TEAM_KICK && + content.targetIds?.contains(IMKitClient.instance.account()) == true) || + (content.type == V2NIMMessageNotificationType.MESSAGE_NOTIFICATION_TYPE_TEAM_LEAVE && + IMKitClient.instance.isMe(conversation.lastMessage?.messageRefer.senderId)) { // 群聊被解散 // 被踢出群聊 // 主动退出群聊 - NELog.infoLog( + NEALog.infoLog( ModuleName + " " + className, - desc: #function + "didAdd team dismiss or leave noti" + (recentSession.session?.sessionId ?? "nil") + desc: #function + "didAdd team dismiss or leave noti " + targetId ) - repo.deleteLocalSession(recentSession: recentSession) + conversationRepo.deleteConversation(targetId) { error in + } // 移除置顶 - if let session = recentSession.session { - if let param = stickTopInfos[session] { - removeStickTopSession(params: param) { error, _ in - if let err = error { - NELog.errorLog( - ModuleName + " ConversationViewModel", - desc: "CALLBACK removeStickTopSession failed,error = \(err)" - ) - } - } + conversationDic.removeValue(forKey: targetId) + stickTopConversations.removeAll { model in + if model.conversation?.conversationId == targetId { + return true } - stickTopInfos.removeValue(forKey: session) - } - return - } - } - } - - weak var weakSelf = self - var listModel = ConversationListModel() - if let sid = recentSession.session?.sessionId { - print("session session id : ", sid) - if let model = cacheAddSessionDic[sid] { - listModel = model - NELog.infoLog( - ModuleName + " " + className, - desc: #function + "didAdd team has added" + (recentSession.session?.sessionId ?? "nil") - ) - } - cacheAddSessionDic[sid] = listModel - } - listModel.recentSession = recentSession - - if recentSession.session?.sessionType == .P2P { - repo.fetchUserInfo(accountList: [targetId]) { users, error in - if error == nil { - listModel.userInfo = users?.first - if let model = weakSelf?.sessionIsExist(listModel) { - model.userInfo = users?.first - } else { - weakSelf?.conversationListArray?.append(listModel) - } - weakSelf?.delegate?.didAddRecentSession() - } - } - - } 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 { - if listModel.teamInfo != nil { - // 会话列表新增一项 - weakSelf?.conversationListArray?.append(listModel) - weakSelf?.delegate?.didAddRecentSession() + return false } + delegate?.reloadTableView() + return true } } } + return false } - open func didUpdate(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { - guard let targetId = recentSession.session?.sessionId else { - NELog.errorLog(ModuleName + " " + className, desc: "❌sessionId is nil") - return + /// 保存撤回消息 + /// - Parameter conversationId: 会话id + /// - Parameter createTime: 撤回时间 + /// - Parameter revokeAccountId: 撤回人id + /// - Parameter extention: 扩展信息 + /// - Parameter completion: 完成回调 + open func saveRevokeMessage(_ messageRevoke: V2NIMMessageRevokeNotification, + _ completion: @escaping (NSError?) -> Void) { + let messageNew = V2NIMMessageCreator.createTextMessage(localizable("message_recalled")) + messageNew.messageConfig?.unreadEnabled = true + + if let ext = messageRevoke.serverExtension { + messageNew.localExtension = ext + } else { + var muta = [String: Any]() + muta[revokeLocalMessage] = true + messageNew.localExtension = NECommonUtil.getJSONStringFromDictionary(muta) } - NELog.infoLog( - ModuleName + " " + className, - desc: #function + "recentSession, didUpdate sessionId: \(targetId), unread count : \(totalUnreadCount)" - ) - - if recentSession.unreadCount <= 0 { - if NEAtMessageManager.instance?.isAtCurrentUser(sessionId: targetId) == true { - NEAtMessageManager.instance?.clearAtRecord(targetId) - } + ChatRepo.shared.insertMessageToLocal(message: messageNew, + conversationId: messageRevoke.messageRefer?.conversationId ?? "", + senderId: messageRevoke.revokeAccountId, + createTime: messageRevoke.messageRefer?.createTime) { _, error in + completion(error) } + } - if let object = recentSession.lastMessage?.messageObject as? NIMNotificationObject, object.notificationType == .team { - if let content = object.content as? NIMTeamNotificationContent { - if content.operationType == .dismiss || - (content.operationType == .kick && - content.targetIDs?.contains(IMKitClient.instance.imAccid()) == true) || - (content.operationType == .leave && - IMKitClient.instance.isMySelf(content.sourceID)) { - // 群聊被解散 - // 被踢出群聊 - // 主动退出群聊 - NELog.infoLog( - ModuleName + " " + className, - desc: #function + "didUpdate team dismiss or leave noti: \(targetId)" - ) - repo.deleteLocalSession(recentSession: recentSession) + /// 撤回通知监听 + /// - Parameter revokeNotifications: 撤回通知列表 + public func onMessageRevokeNotifications(_ revokeNotifications: [V2NIMMessageRevokeNotification]) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + "onMessageRevokeNotifications ids: \(revokeNotifications.map { $0.messageRefer?.messageServerId })") - // 移除置顶 - 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 - } + for messageRevoke in revokeNotifications { + guard let msgServerId = messageRevoke.messageRefer?.messageServerId else { + return } - } - cacheUpdateSessionDic[targetId] = recentSession - if let model = cacheAddSessionDic[targetId], let recent = model.recentSession { - if let time1 = recentSession.lastMessage?.timestamp, let time2 = recent.lastMessage?.timestamp, time1 > time2 { - model.recentSession = recentSession + // 防止重复插入本地撤回消息 + if ConversationDeduplicationHelper.instance.isRevokeMessageSaved(messageId: msgServerId) { + return } - } - weak var weakSelf = self - if let _ = conversationListArray { - for i in 0 ..< conversationListArray!.count { - let listModel = conversationListArray![i] - NELog.infoLog( - ModuleName + " " + className, - desc: #function + "update session id : " + (listModel.recentSession?.session?.sessionId ?? "nil") - ) - if targetId == listModel.recentSession?.session?.sessionId { - listModel.recentSession = recentSession - if recentSession.session?.sessionType == .P2P { - repo.fetchUserInfo(accountList: [targetId]) { users, error in - if error == nil { - listModel.userInfo = users?.first - } - } - } else if recentSession.session?.sessionType == .team { - repo.getTeamInfo(teamId: targetId) { error, teamInfo in - listModel.teamInfo = teamInfo - } - } - weakSelf?.delegate?.reloadTableView() - return + saveRevokeMessage(messageRevoke) { error in + if let err = error { + NEALog.infoLog(ModuleName + " " + ConversationViewModel.className(), desc: "saveRevokeMessage error \(err)") } } - - // 会话列表中没有该回话则添加 - didAdd(recentSession, totalUnreadCount: totalUnreadCount) } } - open func didRemove(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { - guard let targetId = recentSession.session?.sessionId else { - NELog.errorLog(ModuleName + " " + className, desc: "❌sessionId is nil") - return - } - - NELog.infoLog( - ModuleName + " " + className, - desc: #function + ",didRemove recentSession sessionId: \(targetId)" - ) - - cacheUpdateSessionDic.removeValue(forKey: targetId) - - if let conversationArr = conversationListArray { - for i in 0 ..< conversationArr.count { - if conversationArr[i].recentSession?.session?.sessionId.count ?? 0 <= 0 { - NELog.infoLog( - ModuleName + " " + className, - desc: #function + ",didRemove recentSession sessionId is empty user: \(conversationArr[i].userInfo?.userId ?? "") team: \(conversationArr[i].teamInfo?.teamId ?? "")" - ) - } - if conversationArr[i].recentSession?.session?.sessionId == recentSession.session? - .sessionId { - NELog.infoLog( - ModuleName + " " + className, - desc: #function + ",remove session list at index : \(i) sessionid : \(targetId)" - ) - - conversationListArray?.remove(at: i) + /// 收到点对点已读回执 + /// - Parameter readReceipts: 已读回执 + public func onReceiveP2PMessageReadReceipts(_ readReceipts: [V2NIMP2PMessageReadReceipt]) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + "onReceive p2p readReceipts count: \(readReceipts.count)") + for receipt in readReceipts { + if let cid = receipt.conversationId { + if conversationDic[cid] != nil { + delegate?.reloadTableView() break } } } - delegate?.reloadTableView() } - // 收到多端登录单向删除通知,手动更新会话最后一条消息 - open func onRecvMessagesDeleted(_ messages: [NIMMessage], exts: [String: String]?) { - for message in messages { - guard let session = message.session else { return } + /// 加入群回调 + /// - Parameter team: 群信息 + public func onTeamJoined(_ team: V2NIMTeam) {} - 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 ?? "")") + /// 建群回调 + /// - Parameter team: 群信息 + public func onTeamCreated(_ team: V2NIMTeam) {} - // 手动查询最后一条消息 - 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 - } - } + public func onTeamLeft(_ team: V2NIMTeam, isKicked: Bool) { + NEALog.infoLog(className(), desc: "conversation onTeamLeft team id: \(team.teamId) team name : \(team.name) isKicked : \(isKicked)") + if let cid = V2NIMConversationIdUtil.teamConversationId(team.teamId) { + didDeleteConversation(cid) } } - // MARK: ========================NIMUserManagerDelegate========================= - - open func onFriendChanged(_ user: NIMUser) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:" + (user.userId ?? "nil")) - if let listArr = conversationListArray { - for (i, listModel) in listArr.enumerated() { - if listModel.recentSession?.session?.sessionType == .P2P { - if listModel.userInfo?.userId == user.userId { - listModel.userInfo = NEKitUser(user: user) - delegate?.didUpdateRecentSession(index: i) - break - } - } + /// 群解散回调 + /// - Parameter team: 群信息 + public func onTeamDismissed(_ team: V2NIMTeam) { + NEALog.infoLog(className(), desc: "onTeamDismissed team id : \(team.teamId) team name: \(team.name)") + if IMKitConfigCenter.shared.dismissTeamDeleteConversation { + if let cid = V2NIMConversationIdUtil.teamConversationId(team.teamId) { + didDeleteConversation(cid) } } } - // MARK: =========================NIMTeamManagerDelegate======================== - - open func onTeamUpdated(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (team.teamId ?? "nil")) - guard let conversationArr = conversationListArray else { + private func didDeleteConversation(_ cid: String) { + if IMKitConfigCenter.shared.dismissTeamDeleteConversation == false { return } - for (i, listModel) in conversationArr.enumerated() { - if listModel.recentSession?.session?.sessionId == team.teamId { - listModel.teamInfo = team - delegate?.didUpdateRecentSession(index: i) - break + conversationRepo.deleteConversation(cid) { [weak self] error in + if let err = error { + NEALog.infoLog(self?.className() ?? " ", desc: "onTeamDismissed delete conversation error : \(err.localizedDescription)") + } else { + self?.conversationDic.removeValue(forKey: cid) + self?.stickTopConversations.removeAll { model in + if model.conversation?.conversationId == cid { + return true + } + return false + } + self?.conversationListData.removeAll { model in + if model.conversation?.conversationId == cid { + return true + } + return false + } + self?.delegate?.reloadTableView() } } } - open func onTeamAdded(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + "onTeamAdded, teamId:" + (team.teamId ?? "nil")) - guard let tid = team.teamId else { - return - } - let _ = repo.createTeamSession(tid) - delegate?.didAddRecentSession() + public func onConversationSyncFinished() { + NEALog.infoLog(className(), desc: "onConversationSyncFinished") + delegate?.reloadTableView() } - open func onTeamRemoved(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (team.teamId ?? "nil")) - // 做删除会话操作(自己退出群聊会触发) - guard let conversationArr = conversationListArray else { - return - } - - DispatchQueue.main.async { - for listModel in conversationArr { - if let teamInfo = listModel.teamInfo, teamInfo.teamId == team.teamId { - if let recentSession = listModel.recentSession { - self.deleteRecentSession(recentSession: recentSession) - break - } - } + public func onDataSync(_ type: V2NIMDataSyncType, state: V2NIMDataSyncState, error: V2NIMError?) { + if type == .DATA_SYNC_TYPE_MAIN, state == .DATA_SYNC_STATE_COMPLETED { + /// 设置同步完成标识 + syncFinished = true + + if let completion = callBack { + NEALog.infoLog(className(), desc: "onConversationSyncFinished getConversationListByPage") + /// 取数据 + getConversationListByPage(completion) + /// 回调置空 + callBack = nil + } else { + NEALog.infoLog(className(), desc: #function + " retrieveConversationDatas") + retrieveConversationDatas() } } } - open func onTeamMemberChanged(_ team: NIMTeam) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:" + (team.teamId ?? "nil")) - guard let conversationArr = conversationListArray else { - return + /// 发生重连的情况重新获取数据 + public func retrieveConversationDatas() { + var limit = 0 + if conversationDic.count > page { + limit = conversationDic.count + } else { + limit = page } - for (i, listModel) in conversationArr.enumerated() { - if listModel.recentSession?.session?.sessionId == team.teamId { - listModel.teamInfo = team - delegate?.didUpdateRecentSession(index: i) - break + conversationRepo.getConversationList(0, limit) { [weak self] conversations, offset, finished, error in + if error == nil { + if let set = offset { + // 更新索引 + self?.offset = set + } + // 清理之前数据 + self?.stickTopConversations.removeAll() + self?.conversationListData.removeAll() + self?.conversationDic.removeAll() + // 区分置顶消息和非置顶消息 + conversations?.forEach { conversation in + self?.addOrUpdateConversationData(conversation) + } + self?.delegate?.reloadTableView() + if let complete = finished { + self?.delegate?.loadMoreStateChange(complete) + } } } } - private func sessionIsExist(_ model: ConversationListModel) -> ConversationListModel? { - if let array = conversationListArray { - for index in 0 ..< array.count { - let m = array[index] - if m.recentSession?.session?.sessionId == model.recentSession?.session?.sessionId { - return m - } - } - } - return nil + public func onFriendDeleted(_ accountId: String, deletionType: V2NIMFriendDeletionType) { + delegate?.reloadTableView() } - open func onMuteListChanged() { + public func onTeamSyncFinished() { + delegate?.reloadTableView() + } + + public func onConversationSyncFailed(_ error: V2NIMError) { + NEALog.infoLog(className(), desc: "onConversationSyncFailed : \(error.desc)") + } + + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + NEALog.infoLog(className(), desc: "onFriendInfoChanged : \(friendInfo.accountId ?? "")") + delegate?.reloadTableView() + } + + /// 用户信息变更回调 + /// - Parameter users: 用户信息列表 + public func onUserProfileChanged(_ users: [V2NIMUser]) { delegate?.reloadTableView() } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift b/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift index 67d155a8..fa0a422f 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift @@ -45,20 +45,20 @@ public class ConversationUIConfig: NSObject { public var conversationProperties = ConversationProperties() /// 会话列表 cell 左划置顶按钮文案内容 - public var stickTopBottonTitle = localizable("stickTop") + public var stickTopButtonTitle = localizable("stickTop") /// 会话列表 cell 左划取消置顶按钮文案内容(会话置顶后生效) - public var stickTopBottonCancelTitle = localizable("cancel_stickTop") + public var stickTopButtonCancelTitle = localizable("cancel_stickTop") /// 会话列表 cell 左划置顶按钮背景颜色 - public var stickTopBottonBackgroundColor: UIColor? + public var stickTopButtonBackgroundColor: UIColor? /// 会话列表 cell 左划置顶按钮点击事件 - public var stickTopBottonClick: ((ConversationListModel?, IndexPath) -> Void)? + public var stickTopButtonClick: ((NEConversationListModel?, IndexPath) -> Void)? /// 会话列表 cell 左划删除按钮文案内容 - public var deleteBottonTitle = localizable("delete") + public var deleteButtonTitle = localizable("delete") /// 会话列表 cell 左划删除按钮背景颜色 - public var deleteBottonBackgroundColor: UIColor? + public var deleteButtonBackgroundColor: UIColor? /// 会话列表 cell 左划删除按钮点击事件 - public var deleteBottonClick: ((ConversationListModel?, IndexPath) -> Void)? + public var deleteButtonClick: ((NEConversationListModel?, IndexPath) -> Void)? /// 标题栏左侧按钮点击事件 public var titleBarLeftClick: (() -> Void)? @@ -70,7 +70,7 @@ public class ConversationUIConfig: NSObject { public var titleBarRight2Click: (() -> Void)? /// 会话列表点击事件 - public var itemClick: ((ConversationListModel?, IndexPath) -> Void)? + public var itemClick: ((NEConversationListModel?, IndexPath) -> Void)? /// 会话列表的视图控制器回调,回调中会返回会话列表的视图控制器 public var customController: ((NEBaseConversationController) -> Void)? diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift index 2079eea4..9dda6639 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationListCell.swift @@ -7,68 +7,69 @@ import UIKit @objcMembers open class FunConversationListCell: NEBaseConversationListCell { - var contentModel: ConversationListModel? + var contentModel: NEConversationListModel? + /// 分割线视图 + public lazy var bottomLine: UIView = { + let bottomLine = UIView() + bottomLine.translatesAutoresizingMaskIntoConstraints = false + bottomLine.backgroundColor = .funConversationListLineBorderColor + return bottomLine + }() + + /// UI 初始化 override open func setupSubviews() { super.setupSubviews() NSLayoutConstraint.activate([ - headImge.leftAnchor.constraint( + headImageView.leftAnchor.constraint( equalTo: contentView.leftAnchor, constant: NEConstant.screenInterval ), - headImge.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - headImge.widthAnchor.constraint(equalToConstant: 48), - headImge.heightAnchor.constraint(equalToConstant: 48), + headImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + headImageView.widthAnchor.constraint(equalToConstant: 48), + headImageView.heightAnchor.constraint(equalToConstant: 48), ]) - title.font = .systemFont(ofSize: NEKitConversationConfig.shared.ui.conversationProperties.itemTitleSize > 0 ? NEKitConversationConfig.shared.ui.conversationProperties.itemTitleSize : 17) + titleLabel.font = .systemFont(ofSize: NEKitConversationConfig.shared.ui.conversationProperties.itemTitleSize > 0 ? NEKitConversationConfig.shared.ui.conversationProperties.itemTitleSize : 17) NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - title.rightAnchor.constraint(equalTo: timeLabel.leftAnchor, constant: -5), - title.topAnchor.constraint(equalTo: headImge.topAnchor, constant: 4), + titleLabel.leftAnchor.constraint(equalTo: headImageView.rightAnchor, constant: 12), + titleLabel.rightAnchor.constraint(equalTo: timeLabel.leftAnchor, constant: -5), + titleLabel.topAnchor.constraint(equalTo: headImageView.topAnchor, constant: 4), ]) - let bottomLine = UIView() - bottomLine.translatesAutoresizingMaskIntoConstraints = false - bottomLine.backgroundColor = .funConversationListLineBorderColor contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: title.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 0.5), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), ]) NSLayoutConstraint.activate([ - notifyMsg.rightAnchor.constraint(equalTo: timeLabel.rightAnchor, constant: -2), - notifyMsg.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 10), - notifyMsg.widthAnchor.constraint(equalToConstant: 14), - notifyMsg.heightAnchor.constraint(equalToConstant: 14), + notifyMsgView.rightAnchor.constraint(equalTo: timeLabel.rightAnchor, constant: -2), + notifyMsgView.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 10), + notifyMsgView.widthAnchor.constraint(equalToConstant: 14), + notifyMsgView.heightAnchor.constraint(equalToConstant: 14), ]) } override func initSubviewsLayout() { if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .rectangle { - headImge.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius + headImageView.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius } else if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .cycle { - headImge.layer.cornerRadius = 24.0 + headImageView.layer.cornerRadius = 24.0 } else { - headImge.layer.cornerRadius = 4.0 + headImageView.layer.cornerRadius = 4.0 } } - override open func configData(sessionModel: ConversationListModel?) { - super.configData(sessionModel: sessionModel) + override open func configureData(_ sessionModel: NEConversationListModel?) { + super.configureData(sessionModel) contentModel = sessionModel - - // backgroundColor - if let session = sessionModel?.recentSession?.session { - let isTop = topStickInfos[session] != nil - if isTop { - contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemStickTopBackground ?? .funConversationBackgroundColor - } else { - contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemBackground ?? .white - } + if sessionModel?.conversation?.stickTop == true { + contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemStickTopBackground ?? .funConversationBackgroundColor + } else { + contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemBackground ?? .white } } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift index a1618580..d8b33c86 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Cell/FunConversationSearchCell.swift @@ -7,19 +7,25 @@ import UIKit @objcMembers open class FunConversationSearchCell: NEBaseConversationSearchCell { + /// 分割线视图 + lazy var bottomLine: UIView = { + let bottomLine = UIView() + bottomLine.translatesAutoresizingMaskIntoConstraints = false + bottomLine.backgroundColor = .funConversationLineBorderColor + return bottomLine + }() + + /// UI 初始化 override open func setupSubviews() { super.setupSubviews() - headImge.layer.cornerRadius = 4.0 + headImageView.layer.cornerRadius = 4.0 - headImge.updateLayoutConstraint(firstItem: headImge, seconedItem: nil, attribute: .width, constant: 40) - headImge.updateLayoutConstraint(firstItem: headImge, seconedItem: nil, attribute: .height, constant: 40) + headImageView.updateLayoutConstraint(firstItem: headImageView, seconedItem: nil, attribute: .width, constant: 40) + headImageView.updateLayoutConstraint(firstItem: headImageView, seconedItem: nil, attribute: .height, constant: 40) - let bottomLine = UIView() - bottomLine.translatesAutoresizingMaskIntoConstraints = false - bottomLine.backgroundColor = .funConversationLineBorderColor contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ - bottomLine.leftAnchor.constraint(equalTo: headImge.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: headImageView.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 1), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift index 968b50f9..9732c3fc 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationController.swift @@ -9,30 +9,32 @@ import UIKit @objcMembers open class FunConversationController: NEBaseConversationController { + /// 搜索视图 + public lazy var searchView: FunSearchView = { + let view = FunSearchView() + view.translatesAutoresizingMaskIntoConstraints = false + view.searchButton.setImage(UIImage.ne_imageNamed(name: "funSearch"), for: .normal) + view.searchButton.setTitle(commonLocalizable("search"), for: .normal) + view.searchButton.accessibilityIdentifier = "id.titleBarSearchImg" + return view + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) className = "FunConversationController" - deleteBottonBackgroundColor = .funConversationdeleteActionColor + deleteButtonBackgroundColor = .funConversationdeleteActionColor cellRegisterDic = [0: FunConversationListCell.self] brokenNetworkViewHeight = 48 - brokenNetworkView.errorIcon.isHidden = false + brokenNetworkView.errorIconView.isHidden = false brokenNetworkView.backgroundColor = .funConversationNetworkBrokenBackgroundColor - brokenNetworkView.content.textColor = .funConversationNetworkBrokenTitleColor + brokenNetworkView.contentLabel.textColor = .funConversationNetworkBrokenTitleColor emptyView.setEmptyImage(name: "fun_user_empty") } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } - public lazy var searchView: FunSearchView = { - let view = FunSearchView() - view.translatesAutoresizingMaskIntoConstraints = false - view.searchBotton.setImage(UIImage.ne_imageNamed(name: "funSearch"), for: .normal) - view.searchBotton.setTitle(localizable("search"), for: .normal) - return view - }() - override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funConversationBackgroundColor @@ -42,7 +44,7 @@ open class FunConversationController: NEBaseConversationController { deinit { if let searchViewGestures = searchView.gestureRecognizers { - searchViewGestures.forEach { gesture in + for gesture in searchViewGestures { searchView.removeGestureRecognizer(gesture) } } @@ -51,6 +53,7 @@ open class FunConversationController: NEBaseConversationController { override func initSystemNav() { super.initSystemNav() let addBarButton = UIButton() + addBarButton.accessibilityIdentifier = "id.titleBarMoreImg" addBarButton.setImage(UIImage.ne_imageNamed(name: "chat_add"), for: .normal) addBarButton.addTarget(self, action: #selector(didClickAddBtn), for: .touchUpInside) let addBarItem = UIBarButtonItem(customView: addBarButton) diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift index 55aa8002..f5305c33 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/Controller/FunConversationSearchController.swift @@ -7,13 +7,26 @@ import UIKit @objcMembers open class FunConversationSearchController: NEBaseConversationSearchController { + /// 取消按钮 + lazy var cancelButton: UIButton = { + let cancelButton = UIButton() + cancelButton.translatesAutoresizingMaskIntoConstraints = false + cancelButton.setTitle(localizable("cancel"), for: .normal) + cancelButton.setTitleColor(.ne_greyText, for: .normal) + cancelButton.addTarget(self, action: #selector(backEvent), for: .touchUpInside) + cancelButton.titleLabel?.adjustsFontSizeToFitWidth = true + cancelButton.contentHorizontalAlignment = .center + cancelButton.accessibilityIdentifier = "id.cancelBtn" + return cancelButton + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) tag = "FunConversationSearchController" } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -24,6 +37,7 @@ open class FunConversationSearchController: NEBaseConversationSearchController { emptyView.setEmptyImage(name: "fun_user_empty") } + /// 初始化子视图 override open func setupSubviews() { super.setupSubviews() let leftImageView = UIImageView(image: UIImage @@ -40,13 +54,6 @@ open class FunConversationSearchController: NEBaseConversationSearchController { searchTextField.heightAnchor.constraint(equalToConstant: 36), ]) - let cancelButton = UIButton() - cancelButton.translatesAutoresizingMaskIntoConstraints = false - cancelButton.setTitle(localizable("cancel"), for: .normal) - cancelButton.setTitleColor(.ne_greyText, for: .normal) - cancelButton.addTarget(self, action: #selector(backEvent), for: .touchUpInside) - cancelButton.titleLabel?.adjustsFontSizeToFitWidth = true - cancelButton.contentHorizontalAlignment = .center view.addSubview(cancelButton) NSLayoutConstraint.activate([ cancelButton.centerYAnchor.constraint(equalTo: searchTextField.centerYAnchor), @@ -87,7 +94,7 @@ open class FunConversationSearchController: NEBaseConversationSearchController { .dequeueReusableHeaderFooterView( withIdentifier: "\(NSStringFromClass(SearchSessionBaseView.self))" ) as! FunSearchSessionHeaderView - sectionView.title.textColor = .funConversationSearchHeaderViewTitleColor + sectionView.titleLabel.textColor = .funConversationSearchHeaderViewTitleColor sectionView.bottomLine.backgroundColor = .funConversationLineBorderColor sectionView.setUpTitle(title: headTitleArr[section]) return sectionView diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/FunConversationRouter.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/FunConversationRouter.swift index c8323663..78506eca 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/FunConversationRouter.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/FunConversationRouter.swift @@ -7,6 +7,8 @@ import Foundation public extension ConversationRouter { static func registerFun() { + registerCommon() + Router.shared.register(SearchContactPageRouter) { param in let nav = param["nav"] as? UINavigationController let searchCtrl = FunConversationSearchController() @@ -18,11 +20,5 @@ public extension ConversationRouter { let conversation = FunConversationController() nav?.pushViewController(conversation, animated: true) } - - Router.shared.register("ClearAtMessageRemind") { param in - if let sessionId = param["sessionId"] as? String { - NEAtMessageManager.instance?.clearAtRecord(sessionId) - } - } } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/FunConversationUIColor.swift b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/FunConversationUIColor.swift index db74c392..ba7e9b40 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/FunConversationUIColor.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/FunUI/FunConversationUIColor.swift @@ -7,7 +7,7 @@ import NECommonKit extension UIColor { static let funConversationPopViewBg = UIColor(hexString: "#4C4C4C") - static let funConversationThemeColor = UIColor(hexString: "#58BE6B") + static let funConversationThemeColor = UIColor.ne_funTheme static let funConversationBackgroundColor = UIColor(hexString: "#EDEDED") static let funConversationLineBorderColor = UIColor(hexString: "#E5E5E5") static let funConversationListLineBorderColor = UIColor(hexString: "#D8D8D8") diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift index a592f4dd..09fac0bf 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift @@ -3,6 +3,8 @@ // found in the LICENSE file. import Foundation +import NEChatKit +import NECoreIM2Kit import NIMSDK public let atAllKey = "ait_all" @@ -23,7 +25,7 @@ open class AtMEMessageRecord: NSObject { } @objcMembers -open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManagerDelegate { +open class NEAtMessageManager: NSObject, NEIMKitClientListener, NEChatListener { public static var instance: NEAtMessageManager? private let workQueue = DispatchQueue(label: "AtMessageWorkQueue") private let lock = NSLock() @@ -32,47 +34,52 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager override private init() { super.init() - NIMSDK.shared().chatManager.add(self) - NIMSDK.shared().loginManager.add(self) + ChatRepo.shared.addChatListener(self) + IMKitClient.instance.addLoginListener(self) } deinit { - NIMSDK.shared().chatManager.remove(self) - NIMSDK.shared().loginManager.remove(self) + ChatRepo.shared.removeChatListener(self) + IMKitClient.instance.removeLoginListener(self) } + /// 初始化 public static func setupInstance() { NEAtMessageManager.instance = NEAtMessageManager() } - open func onLogin(_ step: NIMLoginStep) { - if step == .loginOK { - NELog.infoLog(className(), desc: "login ok") - currentAccid = NIMSDK.shared().loginManager.currentAccount() + /// 登录状态变更 + /// - Parameter status: 登录状态 + public func onLoginStatus(_ status: V2NIMLoginStatus) { + if status == .LOGIN_STATUS_LOGINED { + NEALog.infoLog(className(), desc: "login ok") + currentAccid = IMKitClient.instance.account() weak var weakSelf = self let newDic = [String: AtMEMessageRecord]() setMessageDic(newDic) workQueue.async { weakSelf?.loadCacheFromDocument() } - } else if step == .syncing { - NELog.infoLog(className(), desc: "roaming messages start") - } else if step == .syncOK { - NELog.infoLog(className(), desc: "roaming messages finish") - if currentAccid.count <= 0 { - currentAccid = NIMSDK.shared().loginManager.currentAccount() - } - startFilterRoamingMessagesTask() } } - open func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { - guard let msg = notification.message else { - return + /// 数据同步回调 + /// - Parameter type: 同步类型 + /// - Parameter state: 同步状态 + /// - Parameter error: 错误信息 + public func onDataSync(_ type: V2NIMDataSyncType, state: V2NIMDataSyncState, error: V2NIMError?) { + if state == .DATA_SYNC_STATE_COMPLETED { + if currentAccid.count <= 0 { + currentAccid = IMKitClient.instance.account() + } + } else if state == .DATA_SYNC_STATE_SYNCING { + NEALog.infoLog(className(), desc: "roaming messages start") + } else if state == .DATA_SYNC_STATE_WAITING { + NEALog.infoLog(className(), desc: "roaming messages waitting") } - removeRevokeAtMessage(messages: [msg]) } + /// 获取当前at消息内存缓存 private func getMessageDic() -> [String: AtMEMessageRecord] { lock.lock() let result = atMessageDic @@ -80,29 +87,39 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager return result } + /// 设置at消息缓存 + /// - Parameter dic: at消息缓存 private func setMessageDic(_ dic: [String: AtMEMessageRecord]) { lock.lock() atMessageDic = dic lock.unlock() } + /// 判断是否是当前用户 + /// - Parameter sessionId: 会话id + /// - Returns: 是否是当前用户 open func isAtCurrentUser(sessionId: String) -> Bool { let dic = getMessageDic() - NELog.infoLog(className(), desc: "session id : \(sessionId)") - NELog.infoLog(className(), desc: "dic : \(dic)") + NEALog.infoLog(className(), desc: "session id : \(sessionId)") + NEALog.infoLog(className(), desc: "dic : \(dic)") + if let model = dic[sessionId], model.isRead == false { + NEALog.infoLog(className(), desc: "read == false") return true } return false } - open func clearAtRecord(_ sessionId: String) { + /// 清理at消息记录 + /// - Parameter conversationId: 会话id + open func clearAtRecord(_ conversationId: String) { + NEALog.infoLog(className(), desc: "clearAtRecord session id : \(conversationId)") weak var weakSelf = self workQueue.async { guard let dic = weakSelf?.getMessageDic() else { return } - if let model = dic[sessionId] { + if let model = dic[conversationId] { model.isRead = true model.atMessages.removeAll() weakSelf?.setMessageDic(dic) @@ -111,8 +128,10 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } } - open func filterAtMessage(messages: [NIMMessage]) { - NELog.infoLog(className(), desc: "at manager filterAtMessage : \(messages.count)") + /// 过滤 at 消息 + /// - Parameter messages: 消息列表 + open func filterAtMessage(messages: [V2NIMMessage]) { + NEALog.infoLog(className(), desc: "at manager filterAtMessage : \(messages.count)") weak var weakSelf = self workQueue.async { if let result = weakSelf?.filterAtMessageInWorkqueue(messages: messages), result == true { @@ -124,13 +143,7 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } } - open func removeRevokeAtMessage(messages: [NIMMessage]) { - weak var weakSelf = self - workQueue.async { - weakSelf?.removeRevokeAtMessageInWorkqueue(messages: messages) - } - } - + /// 开启遍历漫游消息任务 open func startFilterRoamingMessagesTask() { weak var weakSelf = self workQueue.async { @@ -138,52 +151,24 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } } - private func removeRevokeAtMessageInWorkqueue(messages: [NIMMessage]) { - let currentAccid = NIMSDK.shared().loginManager.currentAccount() - weak var weakSelf = self - var isAtMessageChange = false - var temDic = getMessageDic() - messages.forEach { message in - if message.status == .read { - return - } - if let remoteExt = message.remoteExt, let dic = remoteExt[yxAitMsg] as? [String: AnyObject] { - if dic[atAllKey] != nil, message.from != currentAccid { - isAtMessageChange = weakSelf?.removeRecord(message: message, record: &temDic) ?? false - return - } - if dic[currentAccid] != nil { - isAtMessageChange = weakSelf?.removeRecord(message: message, record: &temDic) ?? false - return - } - } - } - if isAtMessageChange == true { - atMessageChangeNoti() - } - } - @discardableResult - private func filterAtMessageInWorkqueue(messages: [NIMMessage]) -> Bool { - let currentAccid = NIMSDK.shared().loginManager.currentAccount() + private func filterAtMessageInWorkqueue(messages: [V2NIMMessage]) -> Bool { + let currentAccid = IMKitClient.instance.account() weak var weakSelf = self var isExistAtMessage = false var temDic = getMessageDic() - messages.forEach { message in - if message.status == .read { - return - } - if let remoteExt = message.remoteExt, let dic = remoteExt[yxAitMsg] as? [String: AnyObject] { - if dic[atAllKey] != nil, message.from != currentAccid { + for message in messages { + if let serverExtension = message.serverExtension, let remoteExt = NECommonUtil.getDictionaryFromJSONString(serverExtension), let dic = remoteExt[yxAitMsg] as? [String: AnyObject] { + if dic[atAllKey] != nil, message.senderId != currentAccid { weakSelf?.addAtRecord(message: message, record: &temDic) isExistAtMessage = true - return + continue } if dic[currentAccid] != nil { weakSelf?.addAtRecord(message: message, record: &temDic) isExistAtMessage = true - return + continue } } } @@ -192,51 +177,69 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } private func startFilterRoamingMessagesTaskInWorkqueue() { - let sessions = NIMSDK.shared().conversationManager.allRecentSessions() - NELog.infoLog(className(), desc: "startFilterRoamingMessagesTaskInWorkqueue session count : \(sessions?.count ?? 0)") - var temDic = getMessageDic() + var conversations = [V2NIMConversation]() weak var weakSelf = self - var isExistAtMessage = false - print("recent session filter at message") - sessions?.forEach { recentSession in - if recentSession.unreadCount <= 0 { + + getAllConversation(&conversations) { error in + + let workingGroup = DispatchGroup() + let workingQueue = DispatchQueue(label: "at_message_queue") + guard var temDic = weakSelf?.getMessageDic() else { return } - if let session = recentSession.session { - let messages = NIMSDK.shared().conversationManager.messages(in: session, message: nil, limit: 100) - messages?.forEach { message in - if message.status == .read { - return - } - if let remoteExt = message.remoteExt, let dic = remoteExt[yxAitMsg] as? [String: AnyObject] { - if dic[atAllKey] != nil, message.from != currentAccid { - weakSelf?.addAtRecord(message: message, record: &temDic) - isExistAtMessage = true - return - } - if dic[currentAccid] != nil { - weakSelf?.addAtRecord(message: message, record: &temDic) - isExistAtMessage = true - return + guard let accid = weakSelf?.currentAccid else { + return + } + var isExistAtMessage = false + for conversation in conversations { + if conversation.type != .CONVERSATION_TYPE_TEAM { + break + } + weak var weakSelf = self + workingGroup.enter() + workingQueue.async { + let option = V2NIMMessageListOption() + option.limit = 100 + option.conversationId = conversation.conversationId + option.strictMode = false + ChatProvider.shared.getMessageList(option: option) { messages, v2Error in + messages?.forEach { message in + if let serverExtension = message.serverExtension, let remoteExt = NECommonUtil.getDictionaryFromJSONString(serverExtension), let dic = remoteExt[yxAitMsg] as? [String: AnyObject] { + if dic[atAllKey] != nil, message.isSelf == false { + weakSelf?.addAtRecord(message: message, record: &temDic) + isExistAtMessage = true + return + } + if dic[accid] != nil { + weakSelf?.addAtRecord(message: message, record: &temDic) + isExistAtMessage = true + return + } + } } + workingGroup.leave() } } } - } - if isExistAtMessage == true { - writeCacheToDocument(dictionary: temDic) - setMessageDic(temDic) - atMessageChangeNoti() + + workingGroup.notify(queue: workingQueue) { + if isExistAtMessage == true { + weakSelf?.writeCacheToDocument(dictionary: temDic) + weakSelf?.setMessageDic(temDic) + weakSelf?.atMessageChangeNoti() + } + } } } + /// at 消息记录写文件缓存 private func writeCacheToDocument(dictionary: [String: AtMEMessageRecord]) { if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { - let filePath = documentsDirectory.appendingPathComponent("NEIMUIKit/\(currentAccid)_at_message.plist") - NELog.infoLog(className(), desc: "writeCacheToDocument path : \(filePath)") + let filePath = documentsDirectory.appendingPathComponent(imkitDir + "\(currentAccid)_at_message.plist") + NEALog.infoLog(className(), desc: "writeCacheToDocument path : \(filePath)") do { var jsonObject = [String: Any]() - dictionary.forEach { (key: String, value: AtMEMessageRecord) in + for (key, value) in dictionary { if let jsonValue = value.yx_modelToJSONObject() { jsonObject[key] = jsonValue } @@ -244,34 +247,36 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager let jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: []) try jsonData.write(to: filePath) + print("write cache success") } catch { - NELog.infoLog(className(), desc: "write cache error : \(error.localizedDescription)") + NEALog.infoLog(className(), desc: "write cache error : \(error.localizedDescription)") } } } + /// 加载本地缓存文件 private func loadCacheFromDocument() { - NELog.infoLog(className(), desc: "loadCacheFromDocument") + NEALog.infoLog(className(), desc: "loadCacheFromDocument") if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { weak var weakSelf = self - let documentDir = documentsDirectory.appendingPathComponent("NEIMUIKit/") + let documentDir = documentsDirectory.appendingPathComponent(imkitDir) if FileManager.default.fileExists(atPath: documentDir.path) == false { do { try FileManager.default.createDirectory(at: documentDir, withIntermediateDirectories: false) } catch { - NELog.infoLog(className(), desc: "create dir error : \(error.localizedDescription)") + NEALog.infoLog(className(), desc: "create dir error : \(error.localizedDescription)") } } let filePath = documentDir.appendingPathComponent("\(currentAccid)_at_message.plist") if FileManager.default.fileExists(atPath: filePath.path) == false { let success = FileManager.default.createFile(atPath: filePath.absoluteString, contents: nil) - NELog.infoLog(className(), desc: "create file success: \(success) path: \(filePath.absoluteString)") + NEALog.infoLog(className(), desc: "create file success: \(success) path: \(filePath.absoluteString)") } else { do { let data = try Data(contentsOf: filePath) if let jsonObject = try JSONSerialization.jsonObject(with: data, options: []) as? [String: [String: Any]] { var temdDic = weakSelf?.getMessageDic() - jsonObject.forEach { (key: String, value: [String: Any]) in + for (key, value) in jsonObject { if let model = AtMEMessageRecord.yx_model(with: value) { temdDic?[key] = model if let dic = jsonObject[key], let isRead = dic[#keyPath(AtMEMessageRecord.isRead)] as? Bool { @@ -289,17 +294,20 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } } } catch { - NELog.infoLog(className(), desc: "convert to message data to json object error : \(error.localizedDescription)") + NEALog.infoLog(className(), desc: "convert to message data to json object error : \(error.localizedDescription)") } } } } - private func removeRecord(message: NIMMessage, record: inout [String: AtMEMessageRecord]) -> Bool { + /// 移除at记录 + /// - Parameter message: 消息 + /// - Parameter record: at 消息记录缓存 + private func removeRecord(message: V2NIMMessage, record: inout [String: AtMEMessageRecord]) -> Bool { var didRemove = false - if let atMeRecord = record[message.session?.sessionId ?? ""] { - if atMeRecord.atMessages[message.messageId] != nil { - atMeRecord.atMessages.removeValue(forKey: message.messageId) + if let atMeRecord = record[message.conversationId ?? ""] { + if atMeRecord.atMessages[message.messageClientId ?? ""] != nil { + atMeRecord.atMessages.removeValue(forKey: message.messageClientId ?? "") if atMeRecord.atMessages.count <= 0 { atMeRecord.isRead = true didRemove = true @@ -309,34 +317,39 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager return didRemove } - private func addAtRecord(message: NIMMessage, record: inout [String: AtMEMessageRecord]) { - if let atMeRecord = record[message.session?.sessionId ?? ""] { + /// 添加at消息记录 + /// - Parameter message: 消息 + /// - Parameter record: at 消息记录缓存 + private func addAtRecord(message: V2NIMMessage, record: inout [String: AtMEMessageRecord]) { + if let atMeRecord = record[message.conversationId ?? ""] { let lastTime = atMeRecord.lastTime?.doubleValue ?? 0 - if lastTime < message.timestamp { + if lastTime < message.createTime { let atMessage = AtMessageModel() - atMessage.messageId = message.messageId - atMessage.messageTime = NSNumber(value: message.timestamp) - atMeRecord.lastTime = NSNumber(value: message.timestamp) - atMeRecord.atMessages[message.messageId] = NSNumber(value: message.timestamp) + atMessage.messageId = message.messageClientId + atMessage.messageTime = NSNumber(value: message.createTime) + atMeRecord.lastTime = NSNumber(value: message.createTime) + atMeRecord.atMessages[message.messageClientId ?? ""] = NSNumber(value: message.createTime) atMeRecord.isRead = false - if let sessionId = message.session?.sessionId { - record[sessionId] = atMeRecord + if let conversationId = message.conversationId { + record[conversationId] = atMeRecord } } } else { let atMeRecord = AtMEMessageRecord() let atMessage = AtMessageModel() - atMessage.messageId = message.messageId - atMessage.messageTime = NSNumber(value: message.timestamp) - atMeRecord.lastTime = NSNumber(value: message.timestamp) - atMeRecord.atMessages[message.messageId] = NSNumber(value: message.timestamp) + atMessage.messageId = message.messageClientId + atMessage.messageTime = NSNumber(value: message.createTime) + atMeRecord.lastTime = NSNumber(value: message.createTime) + atMeRecord.atMessages[message.messageClientId ?? ""] = NSNumber(value: message.createTime) atMeRecord.isRead = false - if let sessionId = message.session?.sessionId { - record[sessionId] = atMeRecord + if let conversationId = message.conversationId { + record[conversationId] = atMeRecord } } } + /// 获取at消息变更通知 + /// - Parameter isCurrentThread: 是否在当前线程发送 private func atMessageChangeNoti(_ isCurrentThread: Bool = false) { if isCurrentThread == false { DispatchQueue.main.async { @@ -347,7 +360,95 @@ open class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManager } } - open func onRecvMessages(_ messages: [NIMMessage]) { + /// 获取所有会话 + /// - Parameter conversations: 会话列表 + /// - Parameter offset: 偏移量 + /// - Parameter completion: 完成回调 + private func getAllConversation(_ conversations: inout [V2NIMConversation], _ offset: Int64 = 0, _ completion: @escaping (NSError?) -> Void) { + let limit = 20 + var temConversations = conversations + ConversationProvider.shared.getConversationList(offset, limit) { [weak self] result, error in + if let err = error { + completion(err) + } else { + if let datas = result?.conversationList { + temConversations.append(contentsOf: datas) + } + if result?.finished == false, let nextToken = result?.offset { + self?.getAllConversation(&temConversations, nextToken, completion) + } else { + completion(nil) + } + } + } + } + + /// 撤回消息回调 + /// - Parameter revokeNotifications: 撤回通知 + public func onMessageRevokeNotifications(_ revokeNotifications: [V2NIMMessageRevokeNotification]) { + var messageRefers = [V2NIMMessageRefer]() + for notification in revokeNotifications { + if let messageRefer = notification.messageRefer { + messageRefers.append(messageRefer) + } + } + if messageRefers.count > 0 { + removeRevokeAtMessage(messages: messageRefers) + } + } + + /// 移除at消息记录 + /// - Parameter messages: 消息列表 + open func removeRevokeAtMessage(messages: [V2NIMMessageRefer]) { + weak var weakSelf = self + workQueue.async { + weakSelf?.removeRevokeAtMessageInWorkqueue(messageRefers: messages) + } + } + + /// 遍历所有撤回消息判断是否要清除at消息标识(会发送通知通知会话列表) + /// - Parameter messageRefers: 消息索引列表 + private func removeRevokeAtMessageInWorkqueue(messageRefers: [V2NIMMessageRefer]) { + let currentAccid = IMKitClient.instance.account() + weak var weakSelf = self + var isAtMessageChange = false + var temDic = getMessageDic() + for messageRefer in messageRefers { + if messageRefer.senderId != currentAccid { + let removeRetResult = weakSelf?.removeRecordWithMessageref(messageRefer: messageRefer, record: &temDic) + if removeRetResult == true { + isAtMessageChange = true + } + } + } + if isAtMessageChange == true { + atMessageChangeNoti() + DispatchQueue.main.async { + weakSelf?.writeCacheToDocument(dictionary: temDic) + } + } + } + + /// 移除at记录(根据消息指针类,因为撤回的时候拿不到消息对象) + /// - Parameter messageRefer: 消息索引 + /// - Parameter record: at 消息记录缓存 + private func removeRecordWithMessageref(messageRefer: V2NIMMessageRefer, record: inout [String: AtMEMessageRecord]) -> Bool { + var didRemove = false + if let atMeRecord = record[messageRefer.conversationId ?? ""] { + if atMeRecord.atMessages[messageRefer.messageClientId ?? ""] != nil { + atMeRecord.atMessages.removeValue(forKey: messageRefer.messageClientId ?? "") + if atMeRecord.atMessages.count <= 0 { + atMeRecord.isRead = true + didRemove = true + } + } + } + return didRemove + } + + /// 收到消息回调 + /// - Parameter messages: 消息列表 + public func onReceiveMessages(_ messages: [V2NIMMessage]) { filterAtMessage(messages: messages) } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift index d772dd7c..54a514fa 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Cell/ConversationListCell.swift @@ -12,50 +12,45 @@ open class ConversationListCell: NEBaseConversationListCell { super.setupSubviews() NSLayoutConstraint.activate([ - headImge.leftAnchor.constraint( + headImageView.leftAnchor.constraint( equalTo: contentView.leftAnchor, constant: NEConstant.screenInterval ), - headImge.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - headImge.widthAnchor.constraint(equalToConstant: 42), - headImge.heightAnchor.constraint(equalToConstant: 42), + headImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + headImageView.widthAnchor.constraint(equalToConstant: 42), + headImageView.heightAnchor.constraint(equalToConstant: 42), ]) NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - title.rightAnchor.constraint(equalTo: timeLabel.leftAnchor, constant: -5), - title.topAnchor.constraint(equalTo: headImge.topAnchor), + titleLabel.leftAnchor.constraint(equalTo: headImageView.rightAnchor, constant: 12), + titleLabel.rightAnchor.constraint(equalTo: timeLabel.leftAnchor, constant: -5), + titleLabel.topAnchor.constraint(equalTo: headImageView.topAnchor), ]) NSLayoutConstraint.activate([ - notifyMsg.rightAnchor.constraint(equalTo: timeLabel.rightAnchor), - notifyMsg.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 5), - notifyMsg.widthAnchor.constraint(equalToConstant: 13), - notifyMsg.heightAnchor.constraint(equalToConstant: 13), + notifyMsgView.rightAnchor.constraint(equalTo: timeLabel.rightAnchor), + notifyMsgView.topAnchor.constraint(equalTo: timeLabel.bottomAnchor, constant: 5), + notifyMsgView.widthAnchor.constraint(equalToConstant: 13), + notifyMsgView.heightAnchor.constraint(equalToConstant: 13), ]) } override func initSubviewsLayout() { if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .rectangle { - headImge.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius + headImageView.layer.cornerRadius = NEKitConversationConfig.shared.ui.conversationProperties.avatarCornerRadius } else if NEKitConversationConfig.shared.ui.conversationProperties.avatarType == .cycle { - headImge.layer.cornerRadius = 21.0 + headImageView.layer.cornerRadius = 21.0 } else { - headImge.layer.cornerRadius = 21.0 + headImageView.layer.cornerRadius = 21.0 } } - override open func configData(sessionModel: ConversationListModel?) { - super.configData(sessionModel: sessionModel) - - // backgroundColor - if let session = sessionModel?.recentSession?.session { - let isTop = topStickInfos[session] != nil - if isTop { - contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemStickTopBackground ?? UIColor(hexString: "0xF3F5F7") - } else { - contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemBackground ?? .white - } + override open func configureData(_ sessionModel: NEConversationListModel?) { + super.configureData(sessionModel) + if sessionModel?.conversation?.stickTop == true { + contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemStickTopBackground ?? UIColor(hexString: "0xF3F5F7") + } else { + contentView.backgroundColor = NEKitConversationConfig.shared.ui.conversationProperties.itemBackground ?? .white } } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift index 889f9713..0f072b91 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationController.swift @@ -10,6 +10,24 @@ import UIKit @objcMembers open class ConversationController: NEBaseConversationController { + /// 搜索按钮 + public lazy var searchBarButton: UIButton = { + let searchBarButton = UIButton() + searchBarButton.accessibilityIdentifier = "id.titleBarSearchImg" + searchBarButton.setImage(UIImage.ne_imageNamed(name: "chat_search"), for: .normal) + searchBarButton.addTarget(self, action: #selector(searchAction), for: .touchUpInside) + return searchBarButton + }() + + /// 添加按钮 + public lazy var addBarButton: UIButton = { + let addBarButton = UIButton() + addBarButton.accessibilityIdentifier = "id.titleBarMoreImg" + addBarButton.setImage(UIImage.ne_imageNamed(name: "chat_add"), for: .normal) + addBarButton.addTarget(self, action: #selector(didClickAddBtn), for: .touchUpInside) + return addBarButton + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nil, bundle: nil) className = "ConversationController" @@ -17,7 +35,7 @@ open class ConversationController: NEBaseConversationController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -27,18 +45,9 @@ open class ConversationController: NEBaseConversationController { override func initSystemNav() { super.initSystemNav() - let searchBarButton = UIButton() - searchBarButton.accessibilityIdentifier = "id.titleBarSearchImg" - searchBarButton.setImage(UIImage.ne_imageNamed(name: "chat_search"), for: .normal) - searchBarButton.addTarget(self, action: #selector(searchAction), for: .touchUpInside) - let searchBarItem = UIBarButtonItem(customView: searchBarButton) - let addBarButton = UIButton() - addBarButton.accessibilityIdentifier = "id.titleBarMoreImg" - addBarButton.setImage(UIImage.ne_imageNamed(name: "chat_add"), for: .normal) - addBarButton.addTarget(self, action: #selector(didClickAddBtn), for: .touchUpInside) + let searchBarItem = UIBarButtonItem(customView: searchBarButton) let addBarItem = UIBarButtonItem(customView: addBarButton) - let spaceBarItem = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) spaceBarItem.width = NEConstant.screenInterval diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift index 76d9c89a..7ec2bd04 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/Controller/ConversationSearchController.swift @@ -11,13 +11,13 @@ open class SearchSessionHeaderView: SearchSessionBaseView { override open func setupUI() { super.setupUI() NSLayoutConstraint.activate([ - title.topAnchor.constraint(equalTo: contentView.topAnchor), - title.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor), + titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), ]) NSLayoutConstraint.activate([ bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - bottomLine.leftAnchor.constraint(equalTo: title.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 1), ]) @@ -34,7 +34,7 @@ open class ConversationSearchController: NEBaseConversationSearchController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupSubviews() { diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/NormalConversationRouter.swift b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/NormalConversationRouter.swift index ec2026bb..1da936d4 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/NormalConversationRouter.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/NormalUI/NormalConversationRouter.swift @@ -7,6 +7,8 @@ import Foundation public extension ConversationRouter { static func register() { + registerCommon() + Router.shared.register(SearchContactPageRouter) { param in let nav = param["nav"] as? UINavigationController let searchCtrl = ConversationSearchController() @@ -18,11 +20,5 @@ public extension ConversationRouter { let conversation = ConversationController() nav?.pushViewController(conversation, animated: true) } - - Router.shared.register("ClearAtMessageRemind") { param in - if let sessionId = param["sessionId"] as? String { - NEAtMessageManager.instance?.clearAtRecord(sessionId) - } - } } } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift index 847b40ef..89430df5 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Util/NEMessageUtil.swift @@ -4,59 +4,59 @@ // found in the LICENSE file. import Foundation +import NECommonKit import NIMSDK + open class NEMessageUtil { /// last message /// - Parameter message: message /// - Returns: result - open class func messageContent(message: NIMMessage) -> String { - var text = "" - switch message.messageType { - case .text: - if let messageText = message.text { - text = messageText - } - case .tip: + open class func messageContent(_ messageType: V2NIMMessageType, + _ text: String?, + _ attachment: V2NIMMessageAttachment?) -> String { + switch messageType { + case .MESSAGE_TYPE_TEXT: + return text ?? "" + case .MESSAGE_TYPE_TIP: return localizable("tip") - case .audio: - text = localizable("voice") - case .image: - text = localizable("picture") - case .video: - text = localizable("video") - case .location: - text = localizable("location") - case .notification: - text = localizable("notification") - case .file: - text = localizable("file") - case .custom: - text = contentOfCustomMessage(message: message) - case .rtcCallRecord: - let record = message.messageObject as? NIMRtcCallRecordObject - text = (record?.callType == .audio) ? localizable("internet_phone") : - localizable("video_chat") + case .MESSAGE_TYPE_AUDIO: + return localizable("voice") + case .MESSAGE_TYPE_IMAGE: + return localizable("picture") + case .MESSAGE_TYPE_VIDEO: + return localizable("video") + case .MESSAGE_TYPE_LOCATION: + return localizable("location") + " \(text ?? "")" + case .MESSAGE_TYPE_NOTIFICATION: + return localizable("notification") + case .MESSAGE_TYPE_FILE: + return localizable("file") + case .MESSAGE_TYPE_CUSTOM: + return contentOfCustomMessage(attachment) + case .MESSAGE_TYPE_CALL: + if let attachment = attachment as? V2NIMMessageCallAttachment { + return attachment.type == 1 ? localizable("internet_phone") : localizable("video_chat") + } default: - text = localizable("unknown") + return localizable("unknown") } - - return text + return localizable("unknown") } /// 返回自定义消息的外显文案 - static func contentOfCustomMessage(message: NIMMessage?) -> String { - if message?.messageType == .custom, - let object = message?.messageObject as? NIMCustomObject, - let custom = object.attachment as? NECustomAttachment { - if custom.customType == customMultiForwardType { + static func contentOfCustomMessage(_ attachment: V2NIMMessageAttachment?) -> String { + if let customType = NECustomAttachment.typeOfCustomMessage(attachment) { + if customType == customMultiForwardType { return localizable("chat_history") } - if custom.customType == customRichTextType { - if let data = NECustomAttachment.dataOfCustomMessage(message: message), + if customType == customRichTextType { + if let data = NECustomAttachment.dataOfCustomMessage(attachment), let title = data["title"] as? String { return title } } + + return localizable("custom") } return localizable("unknown") } diff --git a/NEMapKit/NEMapKit.podspec b/NEMapKit/NEMapKit.podspec index 021417ed..05b0b2ba 100644 --- a/NEMapKit/NEMapKit.podspec +++ b/NEMapKit/NEMapKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEMapKit' - s.version = '9.7.0' + s.version = '10.1.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/Contents.json new file mode 100644 index 00000000..dc7b64a9 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "chat_loacaiton_img@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "chat_loacaiton_img@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/chat_loacaiton_img@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/chat_loacaiton_img@2x.png new file mode 100644 index 00000000..5e1faa54 Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/chat_loacaiton_img@2x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/chat_loacaiton_img@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/chat_loacaiton_img@3x.png new file mode 100644 index 00000000..8a784580 Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_loacaiton_img.imageset/chat_loacaiton_img@3x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/Contents.json new file mode 100644 index 00000000..aaaf8d3c --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "chat_map_back@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "chat_map_back@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/chat_map_back@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/chat_map_back@2x.png new file mode 100644 index 00000000..8d8cc916 Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/chat_map_back@2x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/chat_map_back@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/chat_map_back@3x.png new file mode 100644 index 00000000..fbd342f9 Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_back.imageset/chat_map_back@3x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/Contents.json new file mode 100644 index 00000000..f16da078 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "map_empty@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "map_empty@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/map_empty@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/map_empty@2x.png new file mode 100644 index 00000000..4f2276ac Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/map_empty@2x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/map_empty@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/map_empty@3x.png new file mode 100644 index 00000000..9848883c Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_empty.imageset/map_empty@3x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/Contents.json new file mode 100644 index 00000000..30781b52 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "chat_map_path@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "chat_map_path@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@2x.png new file mode 100644 index 00000000..6df8b22c Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@2x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@3x.png new file mode 100644 index 00000000..8709b111 Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_path.imageset/chat_map_path@3x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/Contents.json new file mode 100644 index 00000000..235b5576 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "chat_map_select@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "chat_map_select@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/chat_map_select@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/chat_map_select@2x.png new file mode 100644 index 00000000..36add975 Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/chat_map_select@2x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/chat_map_select@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/chat_map_select@3x.png new file mode 100644 index 00000000..4afbe63b Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/chat_map_select.imageset/chat_map_select@3x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/location_point.imageset/Contents.json similarity index 100% rename from NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/Contents.json rename to NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/location_point.imageset/Contents.json diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/location_point@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/location_point.imageset/location_point@2x.png similarity index 100% rename from NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/location_point@2x.png rename to NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/location_point.imageset/location_point@2x.png diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/location_point@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/location_point.imageset/location_point@3x.png similarity index 100% rename from NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/location_point@3x.png rename to NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/location_point.imageset/location_point@3x.png diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/Contents.json new file mode 100644 index 00000000..885ba81c --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "map_select_normal@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "map_select_normal@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/map_select_normal@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/map_select_normal@2x.png new file mode 100644 index 00000000..fb90e93c Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/map_select_normal@2x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/map_select_normal@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/map_select_normal@3x.png new file mode 100644 index 00000000..8fd18fdf Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_normal.imageset/map_select_normal@3x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/Contents.json new file mode 100644 index 00000000..d3b4787a --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "map_reset_select@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "map_reset_select@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@2x.png new file mode 100644 index 00000000..64e31262 Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@2x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@3x.png new file mode 100644 index 00000000..0862b4e5 Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Map/map_reset_select.imageset/map_reset_select@3x.png differ diff --git a/NEMapKit/NEMapKit/Assets/en.lproj/Localizable.strings b/NEMapKit/NEMapKit/Assets/en.lproj/Localizable.strings new file mode 100644 index 00000000..f3789504 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/en.lproj/Localizable.strings @@ -0,0 +1,9 @@ + +"gaode_map"="高德 Map"; +"tencent_map"="腾讯 Map"; +"cancel"="cancel"; +"search_cancel"="cancel"; +"send"="send"; +"search_result_empty"="Not found"; +"search_place"="Search Location"; +"no_location"="Please select the location you want to send"; diff --git a/NEMapKit/NEMapKit/Assets/zh-Hans.lproj/Localizable.strings b/NEMapKit/NEMapKit/Assets/zh-Hans.lproj/Localizable.strings new file mode 100644 index 00000000..3b873609 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/zh-Hans.lproj/Localizable.strings @@ -0,0 +1,10 @@ + +"gaode_map"="高德地图"; +"tencent_map"="腾讯地图"; +"cancel"="取消"; +"search_cancel"="取消"; +"send"="发送"; +"search_result_empty"="未找到你要搜的地址"; +"search_place"="搜索地点"; +"no_location"="请选择你要发送的位置信息"; + diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift b/NEMapKit/NEMapKit/Classes/Controller/NELocationViewController.swift similarity index 59% rename from NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift rename to NEMapKit/NEMapKit/Classes/Controller/NELocationViewController.swift index 2c51c0c9..83577b2f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift +++ b/NEMapKit/NEMapKit/Classes/Controller/NELocationViewController.swift @@ -3,59 +3,198 @@ // found in the LICENSE file. import NEChatKit +import NEChatUIKit +import NECommonKit import NECoreKit import UIKit @objcMembers -open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDelegate { - // 地图展示类型 +open class NELocationViewController: UIViewController, NELocationBottomViewDelegate { + /// 地图展示类型 public var mapType: NEMapType? + /// 当前定位点 public var currentPoint = CGPoint(x: 0, y: 0) + /// 地理位置标题 public var locationTitle: String? + /// 地理位置子标题 public var subTitle: String? - private let reuseId = "NEMapAddressCell" + /// 底部弹出位置列表高度约束控制变量 private var tableViewBottomConstraint: NSLayoutConstraint? - + /// 搜索框位置控制变量 private var searchViewConstraint: NSLayoutConstraint? - // 记录键盘弹起状态 + /// 记录键盘弹起状态 private var foldKeyBoard = true - + /// 底部地理位置列表默认高度 private let defaultTableHeight: CGFloat = 230 + /// 位置列表选中索引 + private var currentIndex: Int = 0 + /// 地图视图 + private var mapView: UIView? + /// 地理位置列表数据源 + private var locations = [NELocaitonModel]() + /// 当前选中的地理位置数据模型 + public var currentModel: NELocaitonModel? - var completion: NEPositionSelectCompletion? + // 顶部返回按钮 + lazy var backButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(mapCoreLoader.loadImage("chat_map_back"), for: .normal) + button.setImage(mapCoreLoader.loadImage("chat_map_back"), for: .highlighted) + button.addTarget(self, action: #selector(backBackClick), for: .touchUpInside) + return button + }() - private var currentIndex: Int = 0 + // 顶部取消按钮 + lazy var cancelButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitleColor(.white, for: .normal) + button.setTitle(mapLocalizable("cancel"), for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 16) + button.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside) + return button + }() - private var mapView: UIView? + // 输入框取消搜索按钮 + lazy var searchCancelButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitle(mapLocalizable("search_cancel"), for: .normal) + button.setTitleColor(UIColor.ne_emptyTitleColor, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 14.0) + button.addTarget(self, action: #selector(cancelSearch), for: .touchUpInside) + button.isHidden = true + return button + }() + + // 地理位置复位按钮 + lazy var resetButton: ExpandButton = { + let button = ExpandButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(mapCoreLoader.loadImage("map_reset_normal"), for: .normal) + button.setImage(mapCoreLoader.loadImage("map_reset_select"), for: .selected) + button.addTarget(self, action: #selector(resetClick), for: .touchUpInside) + return button + }() - private var locations = [ChatLocaitonModel]() + // 发送按钮 + lazy var sendButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitleColor(.white, for: .normal) + button.setTitle(mapLocalizable("send"), for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 16) + button.backgroundColor = UIColor.ne_normalTheme + button.layer.cornerRadius = 4 + button.addTarget(self, action: #selector(sendBtnClick), for: .touchUpInside) + return button + }() - public var currentModel: ChatLocaitonModel? + // 底部引导分享视图 + lazy var guideBottomView: NELocationGuideBottomView = { + let bottomView = NELocationGuideBottomView(frame: CGRect.zero) + bottomView.translatesAutoresizingMaskIntoConstraints = false + bottomView.delegate = self + return bottomView + }() + + /// 空占位图 + public lazy var emptyImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = mapCoreLoader.loadImage("chat_map_empty") + imageView.isHidden = true + return imageView + }() + + public lazy var emptyLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor.ne_greyText + label.font = UIFont.systemFont(ofSize: 14.0) + label.text = mapLocalizable("search_result_empty") + label.isHidden = true + return label + }() + public lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + tableView.register( + NELocationAddressCell.self, + forCellReuseIdentifier: NELocationAddressCell.className() + ) + tableView.rowHeight = 72 + tableView.backgroundColor = .white + tableView.keyboardDismissMode = .onDrag + return tableView + }() + + // 搜索输入框 + public lazy var searchTextField: SearchTextField = { + let textField = SearchTextField() + textField.translatesAutoresizingMaskIntoConstraints = false + textField.contentMode = .center + textField.leftView = UIImageView(image: mapCoreLoader.loadImage("search")) + textField.leftViewMode = .always + textField.placeholder = mapLocalizable("search_place") + textField.font = UIFont.systemFont(ofSize: 14) + textField.textColor = UIColor.ne_greyText + textField.layer.cornerRadius = 8 + textField.backgroundColor = .ne_lightBackgroundColor + textField.clearButtonMode = .always + textField.returnKeyType = .search + textField.addTarget(self, action: #selector(searchTextFieldChange), for: .editingChanged) + return textField + }() + + // 搜搜输入框背景 + lazy var searchBgView: UIView = { + let bgView = UIView() + bgView.translatesAutoresizingMaskIntoConstraints = false + return bgView + }() + + /// 位置定位图片 + public lazy var pointImage: UIImageView = { + let pointImage = UIImageView() + pointImage.translatesAutoresizingMaskIntoConstraints = false + pointImage.image = mapCoreLoader.loadImage("location_point") + return pointImage + }() + + /// 初始化 public init(type: NEMapType) { mapType = type super.init(nibName: nil, bundle: nil) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) navigationController?.navigationBar.isHidden = true weak var weakSelf = self NEChatDetectNetworkTool.shareInstance.netWorkReachability { status in if status == .notReachable { - weakSelf?.sendBtn.isEnabled = false - weakSelf?.sendBtn.alpha = 0.5 + weakSelf?.sendButton.isEnabled = false + weakSelf?.sendButton.alpha = 0.5 } else { - weakSelf?.sendBtn.isEnabled = true - weakSelf?.sendBtn.alpha = 1.0 + weakSelf?.sendButton.isEnabled = true + weakSelf?.sendButton.alpha = 1.0 } } } override open func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) navigationController?.navigationBar.isHidden = false } @@ -64,66 +203,59 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe setupSubviews() } + /// UI 初始化 func setupSubviews() { - if let mapDelagate = NEChatKitClient.instance.delegate { - weak var weakSelf = self - NELog.infoLog(className(), desc: "toSearchCurrentUserLocation setupSubviews call") - toSearchCurrentUserLocation() + weak var weakSelf = self - if let mapView = mapDelagate.getMapView?() as? UIView { - view.addSubview(mapView) - self.mapView = mapView - } - // 配置地图控制器参数 - if let type = mapType { - mapDelagate.setupMapController?(mapType: type.rawValue) - } - mapDelagate.didmoveMap?(completion: { - weakSelf?.resetBtn.isSelected = true - print("user move map") - }) + // 获取地图视图加载 + if let mapView = NEMapClient.shared().getMapView() as? UIView { + view.addSubview(mapView) + self.mapView = mapView + } - if mapType == .detail, let map = mapView { - resetBtn.isSelected = true - mapDelagate.setMapviewLocation?(lat: currentPoint.x, lng: currentPoint.y, mapview: map) - } else { - if let map = mapView { - let pointImage = UIImageView() - pointImage.translatesAutoresizingMaskIntoConstraints = false - pointImage.image = coreLoader.loadImage("location_point") - map.addSubview(pointImage) - NSLayoutConstraint.activate([ - pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), - pointImage.centerYAnchor.constraint(equalTo: map.centerYAnchor, constant: -17), - ]) - } - } + // 配置地图控制器参数 + if let type = mapType { + NEMapClient.shared().setupMapController(withMapType: type.rawValue) + } + // 地图触摸移动回调,如果有触摸重新定位到当前按钮变为可点击状态 + NEMapClient.shared().didmoveMap { + weakSelf?.resetButton.isSelected = true + } + + if mapType == .detail, let map = mapView { + resetButton.isSelected = true + NEMapClient.shared().setMapviewLocationWithLat(currentPoint.x, lng: currentPoint.y, mapview: map) } else { - view.addSubview(emptyTip) - NSLayoutConstraint.activate([ - emptyTip.centerYAnchor.constraint(equalTo: view.centerYAnchor), - emptyTip.centerXAnchor.constraint(equalTo: view.centerXAnchor), - ]) + toSearchCurrentUserLocation() + + if let map = mapView { + map.addSubview(pointImage) + NSLayoutConstraint.activate([ + pointImage.centerXAnchor.constraint(equalTo: map.centerXAnchor), + pointImage.centerYAnchor.constraint(equalTo: map.centerYAnchor, constant: -17), + ]) + } } if mapType == .detail { - NEChatKitClient.instance.delegate?.setCustomAnnotation?(image: coreLoader.loadImage("location_point"), lat: currentPoint.x, lng: currentPoint.y) + NEMapClient.shared().setCustomAnnotationWith(mapCoreLoader.loadImage("location_point"), lat: currentPoint.x, lng: currentPoint.y) addDetailSubviews() } else { - NEChatKitClient.instance.delegate?.setCustomAnnotation?(image: nil, lat: 0, lng: 0) + NEMapClient.shared().setCustomAnnotationWith(nil, lat: 0, lng: 0) addSearchSubviews() } } + // 初始化详情类型视图 func addDetailSubviews() { - view.addSubview(backBtn) + view.addSubview(backButton) view.addSubview(guideBottomView) NSLayoutConstraint.activate([ - backBtn.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 28), - backBtn.widthAnchor.constraint(equalToConstant: 32), - backBtn.heightAnchor.constraint(equalToConstant: 32), - backBtn.topAnchor.constraint(equalTo: view.topAnchor, constant: kNavigationHeight), + backButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 28), + backButton.widthAnchor.constraint(equalToConstant: 32), + backButton.heightAnchor.constraint(equalToConstant: 32), + backButton.topAnchor.constraint(equalTo: view.topAnchor, constant: kNavigationHeight), ]) NSLayoutConstraint.activate([ @@ -133,18 +265,19 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe guideBottomView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - guideBottomView.title.text = locationTitle - guideBottomView.subtitle.text = subTitle + guideBottomView.titleLabel.text = locationTitle + guideBottomView.subtitleLabel.text = subTitle - view.addSubview(resetBtn) + view.addSubview(resetButton) NSLayoutConstraint.activate([ - resetBtn.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), - resetBtn.bottomAnchor.constraint(equalTo: guideBottomView.topAnchor, constant: -209), - resetBtn.widthAnchor.constraint(equalToConstant: 70), - resetBtn.heightAnchor.constraint(equalToConstant: 70), + resetButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), + resetButton.bottomAnchor.constraint(equalTo: guideBottomView.topAnchor, constant: -209), + resetButton.widthAnchor.constraint(equalToConstant: 70), + resetButton.heightAnchor.constraint(equalToConstant: 70), ]) } + // 搜索输入UI布局 func addSearchSubviews() { // 添加键盘监听 NotificationCenter.default.addObserver(self, @@ -156,24 +289,24 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe selector: #selector(keyBoardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) - view.addSubview(cancelBtn) - view.addSubview(sendBtn) + view.addSubview(cancelButton) + view.addSubview(sendButton) view.addSubview(tableView) - view.addSubview(resetBtn) + view.addSubview(resetButton) view.addSubview(searchBgView) NSLayoutConstraint.activate([ - cancelBtn.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16), - cancelBtn.widthAnchor.constraint(equalToConstant: 64), - cancelBtn.heightAnchor.constraint(equalToConstant: 32), - cancelBtn.topAnchor.constraint(equalTo: view.topAnchor, constant: kNavigationHeight), + cancelButton.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 16), + cancelButton.widthAnchor.constraint(equalToConstant: 64), + cancelButton.heightAnchor.constraint(equalToConstant: 32), + cancelButton.topAnchor.constraint(equalTo: view.topAnchor, constant: kNavigationHeight), ]) NSLayoutConstraint.activate([ - sendBtn.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16), - sendBtn.widthAnchor.constraint(equalToConstant: 64), - sendBtn.heightAnchor.constraint(equalToConstant: 32), - sendBtn.topAnchor.constraint(equalTo: view.topAnchor, constant: kNavigationHeight), + sendButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -16), + sendButton.widthAnchor.constraint(equalToConstant: 64), + sendButton.heightAnchor.constraint(equalToConstant: 32), + sendButton.topAnchor.constraint(equalTo: view.topAnchor, constant: kNavigationHeight), ]) tableViewBottomConstraint = tableView.heightAnchor.constraint(equalToConstant: defaultTableHeight) @@ -191,23 +324,23 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe searchBgView.heightAnchor.constraint(equalToConstant: 60), ]) - tableView.addSubview(emptyImage) + tableView.addSubview(emptyImageView) NSLayoutConstraint.activate([ - emptyImage.centerXAnchor.constraint(equalTo: tableView.centerXAnchor), - emptyImage.centerYAnchor.constraint(equalTo: tableView.centerYAnchor), + emptyImageView.centerXAnchor.constraint(equalTo: tableView.centerXAnchor), + emptyImageView.centerYAnchor.constraint(equalTo: tableView.centerYAnchor), ]) tableView.addSubview(emptyLabel) NSLayoutConstraint.activate([ emptyLabel.centerXAnchor.constraint(equalTo: tableView.centerXAnchor), - emptyLabel.topAnchor.constraint(equalTo: emptyImage.bottomAnchor, constant: 8.0), + emptyLabel.topAnchor.constraint(equalTo: emptyImageView.bottomAnchor, constant: 8.0), ]) NSLayoutConstraint.activate([ - resetBtn.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), - resetBtn.bottomAnchor.constraint(equalTo: searchBgView.topAnchor, constant: 0), - resetBtn.widthAnchor.constraint(equalToConstant: 70), - resetBtn.heightAnchor.constraint(equalToConstant: 70), + resetButton.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), + resetButton.bottomAnchor.constraint(equalTo: searchBgView.topAnchor, constant: 0), + resetButton.widthAnchor.constraint(equalToConstant: 70), + resetButton.heightAnchor.constraint(equalToConstant: 70), ]) if let map = mapView { map.translatesAutoresizingMaskIntoConstraints = false @@ -220,7 +353,7 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } searchBgView.addSubview(searchTextField) - searchBgView.addSubview(searchCancelBtn) + searchBgView.addSubview(searchCancelButton) searchViewConstraint = searchTextField.rightAnchor.constraint(equalTo: searchBgView.rightAnchor, constant: -12) searchViewConstraint?.isActive = true @@ -230,19 +363,20 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe searchTextField.heightAnchor.constraint(equalToConstant: 32), ]) NSLayoutConstraint.activate([ - searchCancelBtn.rightAnchor.constraint(equalTo: searchBgView.rightAnchor, constant: -12), - searchCancelBtn.centerYAnchor.constraint(equalTo: searchTextField.centerYAnchor), - searchCancelBtn.widthAnchor.constraint(equalToConstant: 52), - searchCancelBtn.heightAnchor.constraint(equalToConstant: 56), + searchCancelButton.rightAnchor.constraint(equalTo: searchBgView.rightAnchor, constant: -12), + searchCancelButton.centerYAnchor.constraint(equalTo: searchTextField.centerYAnchor), + searchCancelButton.widthAnchor.constraint(equalToConstant: 52), + searchCancelButton.heightAnchor.constraint(equalToConstant: 56), ]) searchBgView.backgroundColor = .white } // MARK: 键盘通知相关操作 + // 键盘弹出 func keyBoardWillShow(_ notification: Notification) { foldKeyBoard = false - searchCancelBtn.isHidden = false + searchCancelButton.isHidden = false searchViewConstraint?.constant = -64 let keyboardRect = (notification .userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue @@ -252,6 +386,7 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe }) } + // 键盘隐藏 func keyBoardWillHide(_ notification: Notification) { foldKeyBoard = true } @@ -260,180 +395,59 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe tableViewBottomConstraint?.constant = defaultTableHeight + offset } - lazy var backBtn: UIButton = { - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setImage(coreLoader.loadImage("chat_map_back"), for: .normal) - button.setImage(coreLoader.loadImage("chat_map_back"), for: .highlighted) - button.addTarget(self, action: #selector(backBackClick), for: .touchUpInside) - return button - }() - - lazy var cancelBtn: ExpandButton = { - let button = ExpandButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitleColor(.white, for: .normal) - button.setTitle(chatLocalizable("cancel"), for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 16) - button.addTarget(self, action: #selector(cancelBtnClick), for: .touchUpInside) - return button - }() - - lazy var searchCancelBtn: ExpandButton = { - let button = ExpandButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitle(chatLocalizable("search_cancel"), for: .normal) - button.setTitleColor(UIColor.ne_emptyTitleColor, for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 14.0) - button.addTarget(self, action: #selector(cancelSearch), for: .touchUpInside) - button.isHidden = true - return button - }() - - lazy var resetBtn: ExpandButton = { - let button = ExpandButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setImage(coreLoader.loadImage("map_reset_normal"), for: .normal) - button.setImage(coreLoader.loadImage("map_reset_select"), for: .selected) - button.addTarget(self, action: #selector(resetClick), for: .touchUpInside) - return button - }() - - lazy var sendBtn: UIButton = { - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - button.setTitleColor(.white, for: .normal) - button.setTitle(chatLocalizable("send"), for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 16) - button.backgroundColor = UIColor.ne_blueText - button.layer.cornerRadius = 4 - button.addTarget(self, action: #selector(sendBtnClick), for: .touchUpInside) - return button - }() - - lazy var guideBottomView: NEMapGuideBottomView = { - let bottomView = NEMapGuideBottomView(frame: CGRect.zero) - bottomView.translatesAutoresizingMaskIntoConstraints = false - bottomView.delegate = self - return bottomView - }() - - lazy var emptyImage: UIImageView = { - let image = UIImageView() - image.translatesAutoresizingMaskIntoConstraints = false - image.image = coreLoader.loadImage("chat_map_empty") - image.isHidden = true - return image - }() - - lazy var emptyLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = UIColor.ne_greyText - label.font = UIFont.systemFont(ofSize: 14.0) - label.text = chatLocalizable("search_result_empty") - label.isHidden = true - return label - }() - - lazy var emptyTip: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = UIFont.systemFont(ofSize: 16.0) - label.text = chatLocalizable("no_map_plugin") - label.textColor = UIColor.ne_greyText - return label - }() - - private lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .plain) - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.register( - NEMapAddressCell.self, - forCellReuseIdentifier: reuseId - ) - tableView.rowHeight = 72 - tableView.backgroundColor = .white - tableView.keyboardDismissMode = .onDrag - return tableView - }() - - private lazy var searchTextField: SearchTextField = { - let textField = SearchTextField() - textField.translatesAutoresizingMaskIntoConstraints = false - textField.contentMode = .center - textField.leftView = UIImageView(image: UIImage - .ne_imageNamed(name: "search")) - textField.leftViewMode = .always - textField.placeholder = chatLocalizable("search_place") - textField.font = UIFont.systemFont(ofSize: 14) - textField.textColor = UIColor.ne_greyText - textField.layer.cornerRadius = 8 - textField.backgroundColor = .ne_lightBackgroundColor - textField.clearButtonMode = .always - textField.returnKeyType = .search - textField.addTarget(self, action: #selector(searchTextFieldChange), for: .editingChanged) - return textField - }() - - lazy var searchBgView: UIView = { - let bgView = UIView() - bgView.translatesAutoresizingMaskIntoConstraints = false - return bgView - }() - - func resetClick() { + // 地理位置复位点击 + open func resetClick() { if let map = mapView { - resetBtn.isSelected = false + resetButton.isSelected = false if mapType == .detail, let map = mapView { - NEChatKitClient.instance.delegate?.setMapCenter?(mapview: map) + NEMapClient.shared().setMapCenterWithMapview(map) return } searchTextField.text = nil toSearchLocalWithMapView() - NEChatKitClient.instance.delegate?.setMapCenter?(mapview: map) + NEMapClient.shared().setMapCenterWithMapview(map) + currentIndex = 0 tableView.reloadData() } } - func cancelSearch() { + // 取消搜索点击 + open func cancelSearch() { UIApplication.shared.keyWindow?.endEditing(true) UIView.animate(withDuration: 0.25, animations: { - self.searchCancelBtn.isHidden = true + self.searchCancelButton.isHidden = true self.searchViewConstraint?.constant = -12 self.tableViewBottomConstraint?.constant = self.defaultTableHeight }) searchTextField.text = "" - NELog.infoLog(className(), desc: "toSearchCurrentUserLocation cancel earch call") + NEALog.infoLog(className(), desc: "toSearchCurrentUserLocation cancel earch call") toSearchCurrentUserLocation() - NEChatKitClient.instance.delegate?.setMapCenter?(mapview: mapView) + NEMapClient.shared().setMapCenterWithMapview(mapView as Any) } func showEmptyView() { - emptyImage.isHidden = false + emptyImageView.isHidden = false emptyLabel.isHidden = false } func hideEmptyView() { - emptyImage.isHidden = true + emptyImageView.isHidden = true emptyLabel.isHidden = true } - func backBackClick() { + open func backBackClick() { navigationController?.popViewController(animated: true) } - func cancelBtnClick() { + open func cancelBtnClick() { navigationController?.popViewController(animated: true) } - func sendBtnClick() { - var model: ChatLocaitonModel? + /// 发送点击 + open func sendBtnClick() { + var model: NELocaitonModel? if model == nil { if locations.count > currentIndex { @@ -446,18 +460,19 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } if let m = model { + // 地理位置回调 navigationController?.popViewController(animated: false) DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: DispatchWorkItem(block: { - if let block = self.completion { - block(m) + if let params = m.yx_modelToJSONObject() as? [String: Any] { + Router.shared.use(NERouterUrl.LocationSearchResult, parameters: params) } })) } else { - showToast(chatLocalizable("no_location")) + showToast(mapLocalizable("no_location")) } } - func searchTextFieldChange(textfield: SearchTextField) { + open func searchTextFieldChange(textfield: SearchTextField) { guard let searchText = textfield.text else { return } @@ -471,54 +486,56 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe if textRange == nil || ((textRange?.isEmpty) == nil) { weak var weakSelf = self if let text = textfield.text { - NEChatKitClient.instance.delegate?.searchPosition?(key: text, completion: { models, error in + NEMapClient.shared().searchPosition(withKey: text) { models, error in + weakSelf?.loadModels(models: models) - }) + } } } if searchText.count <= 0 { if let map = mapView { - NEChatKitClient.instance.delegate?.setMapCenter?(mapview: map) + NEMapClient.shared().setMapCenterWithMapview(map) } toSearchLocalWithMapView() } } - func toSearchLocalWithMapView() { + open func toSearchLocalWithMapView() { guard let map = mapView else { return } weak var weakSelf = self - NEChatKitClient.instance.delegate?.searchMapCenter?(mapview: map, completion: { models, error in + NEMapClient.shared().searchMapCenter(withMapview: map) { models, error in if let text = weakSelf?.searchTextField.text, text.count > 0 { return } + weakSelf?.resetButton.isSelected = false weakSelf?.loadModels(models: models) - }) + } } - func toSearchCurrentUserLocation() { + open func toSearchCurrentUserLocation() { weak var weakSelf = self - let className = className() - NEChatKitClient.instance.delegate?.searchRoundPosition?(completion: { models, error in - NELog.infoLog(className, desc: "toSearchCurrentUserLocation end : \(models) error: \(error?.localizedDescription ?? "") current text input : \(weakSelf?.searchTextField.text ?? "")") + NEMapClient.shared().searchRoundPosition { models, error in if let text = weakSelf?.searchTextField.text, text.count > 0 { return } + weakSelf?.resetButton.isSelected = false weakSelf?.loadModels(models: models) - }) + } } // MARK: NEMapGuideBottomViewDelegate + // 点击跳转三方应用 open func didClickGuide() { - showBottomSelectAlert(firstContent: chatLocalizable("gaode_map"), secondContent: chatLocalizable("tencent_map")) { value in + showBottomSelectAlert(firstContent: mapLocalizable("gaode_map"), secondContent: mapLocalizable("tencent_map")) { value in if value == 0 { if let gaodeApp = URL(string: "iosamap://") { + // 高德 if UIApplication.shared.canOpenURL(gaodeApp) == true { if let url = "iosamap://viewMap?sourceApplication=yunxin_im&backScheme=im_uikit&poiname=\(self.locationTitle ?? "")&lat=\(self.currentPoint.x)&lon=\(self.currentPoint.y)&dev=1".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - print("jump url : ", url) if let jumpUrl = URL(string: url) { if #available(iOS 10.0, *) { UIApplication.shared.open(jumpUrl) @@ -528,7 +545,7 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } } - } else if let url = URL(string: "https://itunes.apple.com/us/app/gao-tu-zhuan-ye-shou-ji-tu/id461703208?mt=8") { + } else if let url = URL(string: aMapDownloadUrl) { if #available(iOS 10.0, *) { UIApplication.shared.open(url) } else { @@ -537,10 +554,10 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } } } else if value == 1 { + // 腾讯 if let gaodeApp = URL(string: "qqmap://") { if UIApplication.shared.canOpenURL(gaodeApp) == true { if let url = "qqmap://map/marker?marker=coord:\(self.currentPoint.x),\(self.currentPoint.y);title:\(self.locationTitle ?? "")".addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - print("jump url : ", url) if let jumpUrl = URL(string: url) { if #available(iOS 10.0, *) { UIApplication.shared.open(jumpUrl) @@ -550,7 +567,7 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } } - } else if let url = URL(string: "https://apps.apple.com/cn/app/%E8%85%BE%E8%AE%AF%E5%9C%B0%E5%9B%BE-%E8%B7%AF%E7%BA%BF%E8%A7%84%E5%88%92-%E5%AF%BC%E8%88%AA%E6%89%93%E8%BD%A6%E5%87%BA%E8%A1%8C/id481623196") { + } else if let url = URL(string: tencentMapDownloadUrl) { if #available(iOS 10.0, *) { UIApplication.shared.open(url) } else { @@ -562,18 +579,28 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } } - open func loadModels(models: [ChatLocaitonModel]) { + open func loadModels(models: [NELocaitonModel]) { currentIndex = 0 locations.removeAll() if let keyword = searchTextField.text, keyword.count > 0 { - models.forEach { model in - model.attribute = model.title.highlight(keyWords: keyword, highlightColor: UIColor.ne_blueText) + for model in models { + model.attribute = model.title.highlight(keyWords: keyword, highlightColor: UIColor.ne_normalTheme) } } else { - models.forEach { model in + for model in models { model.attribute = NSMutableAttributedString(string: model.title) } } + if models.count > currentIndex { + let model = models[currentIndex] + if let map = mapView { + NEMapClient.shared().setMapviewLocationWithLat(model.lat, lng: model.lng, mapview: map) + } + if searchTextField.text?.count ?? 0 > 0 { + resetButton.isSelected = true + } + } + locations.append(contentsOf: models) tableView.reloadData() if models.count > 0 { @@ -594,7 +621,7 @@ open class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomViewDe } } -extension NEDetailMapController: UITableViewDelegate, UITableViewDataSource { +extension NELocationViewController: UITableViewDelegate, UITableViewDataSource { open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { locations.count } @@ -602,26 +629,29 @@ extension NEDetailMapController: UITableViewDelegate, UITableViewDataSource { open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell( - withIdentifier: reuseId, + withIdentifier: NELocationAddressCell.className(), for: indexPath - ) as! NEMapAddressCell + ) as! NELocationAddressCell let model = locations[indexPath.row] cell.configure(model, currentIndex == indexPath.row) return cell } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if let preiousCell = tableView.cellForRow(at: IndexPath(row: currentIndex, section: 0)) as? NEMapAddressCell { - preiousCell.selectImg.isHidden = true + if let preiousCell = tableView.cellForRow(at: IndexPath(row: currentIndex, section: 0)) as? NELocationAddressCell { + preiousCell.selectImgView.isHidden = true } - if let currentCell = tableView.cellForRow(at: indexPath) as? NEMapAddressCell { - currentCell.selectImg.isHidden = false + if let currentCell = tableView.cellForRow(at: indexPath) as? NELocationAddressCell { + currentCell.selectImgView.isHidden = false } let model = locations[indexPath.row] if let map = mapView { - NEChatKitClient.instance.delegate?.setMapviewLocation?(lat: model.lat, lng: model.lng, mapview: map) + NEMapClient.shared().setMapviewLocationWithLat(model.lat, lng: model.lng, mapview: map) } currentIndex = indexPath.row + if indexPath.row != 0 { + resetButton.isSelected = true + } refreshCurrentCache() } } diff --git a/NEMapKit/NEMapKit/Classes/NELocaitonModel.h b/NEMapKit/NEMapKit/Classes/NELocaitonModel.h new file mode 100644 index 00000000..ac24978e --- /dev/null +++ b/NEMapKit/NEMapKit/Classes/NELocaitonModel.h @@ -0,0 +1,21 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NELocaitonModel : NSObject + +@property(nonatomic, strong) NSString *title; +@property(nonatomic, strong) NSString *address; +@property(nonatomic, strong) NSString *city; +@property(nonatomic, assign) CGFloat lat; +@property(nonatomic, assign) CGFloat lng; +@property(nonatomic, assign) NSInteger distance; +@property(nonatomic, strong) NSMutableAttributedString *attribute; + +@end + +NS_ASSUME_NONNULL_END diff --git a/NEMapKit/NEMapKit/Classes/NELocaitonModel.m b/NEMapKit/NEMapKit/Classes/NELocaitonModel.m new file mode 100644 index 00000000..7c62edb2 --- /dev/null +++ b/NEMapKit/NEMapKit/Classes/NELocaitonModel.m @@ -0,0 +1,9 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +#import "NELocaitonModel.h" + +@implementation NELocaitonModel + +@end diff --git a/NEMapKit/NEMapKit/Classes/NEMapClient.h b/NEMapKit/NEMapKit/Classes/NEMapClient.h index b07b0040..9618534c 100644 --- a/NEMapKit/NEMapKit/Classes/NEMapClient.h +++ b/NEMapKit/NEMapKit/Classes/NEMapClient.h @@ -3,6 +3,7 @@ // found in the LICENSE file. #import +#import "NELocaitonModel.h" NS_ASSUME_NONNULL_BEGIN @@ -10,7 +11,62 @@ NS_ASSUME_NONNULL_BEGIN + (instancetype)shared; -- (void)setupMapClientWithAppkey:(NSString *)appkey; +/// 设置插件初始化 +/// @param appkey appkey +- (void)setupMapClientWithAppkey:(NSString *)appkey + __attribute__((deprecated("- (void)setupMapClientWithAppkey:(NSString *)appkey " + "withServerKey:(NSString *)serverKey instead"))); + +/// 设置插件初始化 +/// @param appkey appkey +/// @param serverKey serverKey +- (void)setupMapClientWithAppkey:(NSString *)appkey withServerKey:(NSString *)serverKey; + +/// 定位地理位置中心 +/// @param mapview 高德地图View +- (void)setMapCenterWithMapview:(id)mapview; + +/// 根据关键字搜索地理位置 +/// @param key 地理位置关键字 +/// @param completion 搜索结果回调 +- (void)searchPositionWithKey:(NSString *)key + completion:(void (^)(NSArray *_Nonnull, + NSError *_Nullable))completion; + +/// 搜索当前地图中心位置附近地理位置 +/// @param mapview 地图 view +- (void)searchMapCenterWithMapview:(id)mapview + completion:(void (^)(NSArray *_Nonnull, + NSError *_Nullable))completion; + +/// 设置搜索地图中心位置回调(当用户拖动地图时需要不断回调) +/// @param completion 回调 +- (void)searchRoundPositionWithCompletion:(void (^)(NSArray *_Nonnull, + NSError *_Nullable))completion; + +/// 设置地图定位到某个为止 +/// @param lat 维度 +/// @param lng 精度 +/// @param mapview 地图视图控件 +- (void)setMapviewLocationWithLat:(double)lat lng:(double)lng mapview:(id)mapview; + +/// 获取地图控件(缺省参数) +/// @return mapview 地图视图 +- (id)getMapView; + +/// 设置地图默认参数(地图缺省参数) +/// @param mapType 预留扩展,暂时无用 +- (void)setupMapControllerWithMapType:(NSInteger)mapType; + +/// 位置移动回调 +/// @param completion 回到block +- (void)didmoveMapWithCompletion:(void (^)(void))completion; + +/// 设置地图自定义定位图标 +/// @param image 自定义图片 +/// @param lat 纬度 +/// @param lng 精度 +- (void)setCustomAnnotationWithImage:(nullable UIImage *)image lat:(double)lat lng:(double)lng; @end diff --git a/NEMapKit/NEMapKit/Classes/NEMapClient.m b/NEMapKit/NEMapKit/Classes/NEMapClient.m index f4c06c41..17dab3c1 100644 --- a/NEMapKit/NEMapKit/Classes/NEMapClient.m +++ b/NEMapKit/NEMapKit/Classes/NEMapClient.m @@ -12,14 +12,14 @@ #import #import -typedef void (^SearchCompletion)(NSArray *, NSError *); +#import +#import + +typedef void (^SearchCompletion)(NSArray *, NSError *); typedef void (^MapMoveCompletion)(void); -@interface NEMapClient () +@interface NEMapClient () @property(nonatomic, strong) MAMapView *mapView; @@ -39,6 +39,8 @@ @interface NEMapClient () *_Nonnull param) { + NSObject *param1 = [param objectForKey:@"nav"]; + NSInteger type = NEMapTypeDetail; + NSNumber *typeValue = [param objectForKey:@"type"]; + if (typeValue != nil && ![typeValue isKindOfClass:[NSNull class]]) { + type = typeValue.integerValue; + } + if ([param1 isKindOfClass:[UINavigationController class]]) { + UINavigationController *nav = (UINavigationController *)param1; + NELocationViewController *controller = + [[NELocationViewController alloc] initWithType:type]; + if (type == NEMapTypeDetail) { + double lat = 0; + double lng = 0; + NSNumber *latValue = param[@"lat"]; + if (latValue != nil && ![latValue isKindOfClass:NSNull.class]) { + lat = latValue.doubleValue; + } + NSNumber *lngValue = param[@"lng"]; + if (lngValue != nil && ![lngValue isKindOfClass:NSNull.class]) { + lng = lngValue.doubleValue; + } + NSString *title = param[@"locationTitle"]; + NSString *subTitle = param[@"subTitle"]; + controller.currentPoint = CGPointMake(lat, lng); + controller.locationTitle = title; + controller.subTitle = subTitle; + } + + [nav pushViewController:controller animated:YES]; + } + }]; +} + - (void)setupMapClientWithAppkey:(NSString *)appkey { [[NEChatKitClient instance] addMapDelegate:self]; [self setupMapSdkConfigWithAppkey:appkey]; @@ -98,7 +138,7 @@ - (void)setCustomAnnotationWithImage:(UIImage *)image lat:(double)lat lng:(doubl } - (void)searchPositionWithKey:(NSString *)key - completion:(void (^)(NSArray *_Nonnull, + completion:(void (^)(NSArray *_Nonnull, NSError *_Nullable))completion { if (key.length <= 0) { return; @@ -154,8 +194,6 @@ - (id)getCellMapView { mapView.showsCompass = NO; // 隐藏比例尺 mapView.showsScale = NO; - // mapView.maxZoomLevel = 5; - // mapView.showsUserLocation = YES; mapView.userTrackingMode = MAUserTrackingModeNone; mapView.zoomEnabled = NO; @@ -177,7 +215,7 @@ - (void)setMapCenterWithMapview:(id)mapview { } } -- (void)searchRoundPositionWithCompletion:(void (^)(NSArray *_Nonnull, +- (void)searchRoundPositionWithCompletion:(void (^)(NSArray *_Nonnull, NSError *_Nullable))completion { self.searchRoundBlock = completion; self.needSearchRound = YES; @@ -193,7 +231,7 @@ - (void)searchRoundPositionWithLat:(double)lat lng:(double)lng { } - (void)searchMapCenterWithMapview:(id)mapview - completion:(void (^)(NSArray *_Nonnull, + completion:(void (^)(NSArray *_Nonnull, NSError *_Nullable))completion { if ([mapview isKindOfClass:[MAMapView class]]) { self.searchRoundBlock = completion; @@ -228,9 +266,9 @@ - (void)onGeocodeSearchDone:(AMapGeocodeSearchRequest *)request } - (void)parseAndPassWithData:(NSArray *)datas { - NSMutableArray *mutaData = [[NSMutableArray alloc] init]; + NSMutableArray *mutaData = [[NSMutableArray alloc] init]; for (AMapPOI *poi in datas) { - ChatLocaitonModel *model = [[ChatLocaitonModel alloc] init]; + NELocaitonModel *model = [[NELocaitonModel alloc] init]; [mutaData addObject:model]; model.title = poi.name; model.address = poi.address; @@ -252,7 +290,6 @@ - (void)parseAndPassWithData:(NSArray *)datas { - (void)mapView:(MAMapView *)mapView didUpdateUserLocation:(MAUserLocation *)userLocation updatingLocation:(BOOL)updatingLocation { - // NSLog(@"didUpdateUserLocation : %d", updatingLocation); if (updatingLocation && self.needSearchRound) { AMapReGeocodeSearchRequest *regeoRequest = [[AMapReGeocodeSearchRequest alloc] init]; regeoRequest.location = [AMapGeoPoint locationWithLatitude:userLocation.coordinate.latitude @@ -299,4 +336,12 @@ - (MAAnnotationView *)mapView:(MAMapView *)mapView viewForAnnotation:(id String { + mapCoreLoader.localizable(key) +} + +let tencentMapDownloadUrl = "https://apps.apple.com/cn/app/%E8%85%BE%E8%AE%AF%E5%9C%B0%E5%9B%BE-%E8%B7%AF%E7%BA%BF%E8%A7%84%E5%88%92-%E5%AF%BC%E8%88%AA%E6%89%93%E8%BD%A6%E5%87%BA%E8%A1%8C/id481623196" + +let aMapDownloadUrl = "https://itunes.apple.com/us/app/gao-tu-zhuan-ye-shou-ji-tu/id461703208?mt=8" + +public class MapCoreLoader: NSObject { + public var bundle: Bundle? + + override public init() { + super.init() + if let bundleURL = Bundle.main.url(forResource: "NEMapKit", withExtension: "bundle") { + bundle = Bundle(url: bundleURL) + } + } + + public func localizable(_ key: String) -> String { + let value = NEChatUIKitClient.instance.getLanguage(key: key) ?? "" + return value + } + + public func loadImage(_ name: String) -> UIImage? { + let image = NEChatUIKitClient.instance.getImageSource(imageName: name) + return image + } +} diff --git a/NEMapKit/NEMapKit/Classes/View/NELocationAddressCell.swift b/NEMapKit/NEMapKit/Classes/View/NELocationAddressCell.swift new file mode 100644 index 00000000..e8db469b --- /dev/null +++ b/NEMapKit/NEMapKit/Classes/View/NELocationAddressCell.swift @@ -0,0 +1,123 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import UIKit + +@objcMembers +open class NELocationAddressCell: UITableViewCell { + /// 位置指示图片 + public lazy var locationImgView: UIImageView = { + let locationImageView = UIImageView(image: mapCoreLoader.loadImage("chat_loacaiton_img")) + locationImageView.translatesAutoresizingMaskIntoConstraints = false + return locationImageView + }() + + /// 选中图片 + public lazy var selectImgView: UIImageView = { + let imgView = UIImageView(image: mapCoreLoader.loadImage("chat_map_select")) + imgView.translatesAutoresizingMaskIntoConstraints = false + return imgView + }() + + /// 位置主标题 + public lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor.ne_darkText + label.font = UIFont.systemFont(ofSize: 16) + label.text = "" + return label + }() + + /// 位子副标题 + public lazy var subTitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor.ne_emptyTitleColor + label.font = UIFont.systemFont(ofSize: 14) + label.text = "" + return label + }() + + /// 分割线视图 + public lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.ne_navLineColor + return view + }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + setupSubviews() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func setupSubviews() { + selectionStyle = .none + contentView.addSubview(locationImgView) + contentView.addSubview(selectImgView) + contentView.addSubview(titleLabel) + contentView.addSubview(subTitleLabel) + contentView.addSubview(bottomLine) + + NSLayoutConstraint.activate([ + locationImgView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 15), + locationImgView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 17), + locationImgView.heightAnchor.constraint(equalToConstant: 18), + locationImgView.widthAnchor.constraint(equalToConstant: 18), + ]) + + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: locationImgView.rightAnchor, constant: 7), + titleLabel.centerYAnchor.constraint(equalTo: locationImgView.centerYAnchor), + titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -70), + ]) + + NSLayoutConstraint.activate([ + subTitleLabel.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), + subTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), + subTitleLabel.rightAnchor.constraint(equalTo: titleLabel.rightAnchor), + ]) + + NSLayoutConstraint.activate([ + selectImgView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -13), + selectImgView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + ]) + + NSLayoutConstraint.activate([ + bottomLine.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 12), + bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -12), + bottomLine.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -1), + bottomLine.heightAnchor.constraint(equalToConstant: 1), + ]) + } + + func configure(_ model: NELocaitonModel, _ select: Bool) { + titleLabel.attributedText = model.attribute + var distanceStr = "" + if model.distance > 0 { + if model.distance <= 1000 { + distanceStr = "\(model.distance)m" + } else { + let kilometer = model.distance / 1000 + distanceStr = "\(kilometer)km" + } + subTitleLabel.text = "\(distanceStr)\(mapLocalizable("distance_inner"))|\(model.address)" + } else { + subTitleLabel.text = model.address + } + + if select == true { + selectImgView.isHidden = false + } else { + selectImgView.isHidden = true + } + } +} diff --git a/NEMapKit/NEMapKit/Classes/View/NELocationGuideBottomView.swift b/NEMapKit/NEMapKit/Classes/View/NELocationGuideBottomView.swift new file mode 100644 index 00000000..9d898612 --- /dev/null +++ b/NEMapKit/NEMapKit/Classes/View/NELocationGuideBottomView.swift @@ -0,0 +1,86 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objc +public protocol NELocationBottomViewDelegate: NSObjectProtocol { + func didClickGuide() +} + +@objcMembers +open class NELocationGuideBottomView: UIView { + public weak var delegate: NELocationBottomViewDelegate? + + /// 引导按钮 + lazy var guideButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setImage(mapCoreLoader.loadImage("chat_map_path"), for: .normal) + button.setImage(mapCoreLoader.loadImage("chat_map_path"), for: .highlighted) + button.addTarget(self, action: #selector(guideBtnClick), for: .touchUpInside) + return button + }() + + /// 位置标题 + lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 16) + label.textColor = UIColor.ne_darkText + label.text = "" + return label + }() + + /// 位置副标题 + lazy var subtitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 14) + label.textColor = UIColor.ne_emptyTitleColor + label.text = "" + return label + }() + + override public init(frame: CGRect) { + super.init(frame: frame) + setupSubviews() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func setupSubviews() { + backgroundColor = .white + addSubview(guideButton) + addSubview(titleLabel) + addSubview(subtitleLabel) + + NSLayoutConstraint.activate([ + guideButton.topAnchor.constraint(equalTo: topAnchor, constant: 16), + guideButton.rightAnchor.constraint(equalTo: rightAnchor, constant: -12), + guideButton.widthAnchor.constraint(equalToConstant: 40), + guideButton.heightAnchor.constraint(equalToConstant: 40), + ]) + + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: 12), + titleLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -52), + titleLabel.topAnchor.constraint(equalTo: topAnchor, constant: 16), + ]) + + NSLayoutConstraint.activate([ + subtitleLabel.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), + subtitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), + subtitleLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -52), + ]) + } + + func guideBtnClick() { + if let delegate = delegate { + delegate.didClickGuide() + } + } +} diff --git a/NERtcCallUIKit/NERtcCallUIKit.podspec b/NERtcCallUIKit/NERtcCallUIKit.podspec index 3bef760f..1f25fc58 100644 --- a/NERtcCallUIKit/NERtcCallUIKit.podspec +++ b/NERtcCallUIKit/NERtcCallUIKit.podspec @@ -8,16 +8,16 @@ Pod::Spec.new do |s| s.name = 'NERtcCallUIKit' - s.version = '2.2.0' + s.version = '2.4.0' s.summary = 'Netease XKit' s.homepage = 'http://netease.im' s.license = { :'type' => "Copyright", :'text' => " Copyright 2022 Netease " } s.author = 'yunxin engineering department' - s.ios.deployment_target = '10.0' + s.ios.deployment_target = '11.0' s.source = { :http => "" } s.source_files = 'NERtcCallUIKit/Classes/**/*' s.resource = 'NERtcCallUIKit/Assets/**/*' - s.dependency 'NERtcCallKit/NOS_Special','2.2.0' + s.dependency 'NERtcCallKit/NOS_Special','2.4.0' s.dependency 'SDWebImage' s.dependency 'NECoreKit' s.dependency 'NECommonKit' diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_connecting_en.mp3 b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_connecting_en.mp3 new file mode 100644 index 00000000..d78ec8e8 Binary files /dev/null and b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_connecting_en.mp3 differ diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_no_response_en.mp3 b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_no_response_en.mp3 new file mode 100644 index 00000000..4e64ff76 Binary files /dev/null and b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_no_response_en.mp3 differ diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_busy_en.mp3 b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_busy_en.mp3 new file mode 100644 index 00000000..be2b8d29 Binary files /dev/null and b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_busy_en.mp3 differ diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_reject_en.mp3 b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_reject_en.mp3 new file mode 100644 index 00000000..eb8bfa49 Binary files /dev/null and b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_peer_reject_en.mp3 differ diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_ring_en.mp3 b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_ring_en.mp3 new file mode 100644 index 00000000..fe16a857 Binary files /dev/null and b/NERtcCallUIKit/NERtcCallUIKit/Assets/avchat_ring_en.mp3 differ diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/en.lproj/Localizable.strings b/NERtcCallUIKit/NERtcCallUIKit/Assets/en.lproj/Localizable.strings index c684d74d..eb883c1f 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Assets/en.lproj/Localizable.strings +++ b/NERtcCallUIKit/NERtcCallUIKit/Assets/en.lproj/Localizable.strings @@ -4,40 +4,40 @@ // found in the LICENSE file. -"switch_to_audio"="Switch to audio"; -"switch_to_video"="Switch to video"; +"switch_to_audio"="Switch to Voice"; +"switch_to_video"="Switch to Video"; "accept_failed"="Answer Failed"; -"network_error"="Network error, try later"; -"switch_error"="Switch Error"; -"remote_cancel"="Cancelled by the opposite"; -"remote_busy"="Busy-line"; -"remote_timeout"="No Responding"; -"remote_reject"="Reject"; -"other_client_accept"="Accept by other client"; -"other_client_reject"="Reject by other client"; +"network_error"="Network exception, please try again later"; +"switch_error"="Failed to switch"; +"remote_cancel"="The call was canceled"; +"remote_busy"="The subscriber you dialed is busy"; +"remote_timeout"="Timeout and no response"; +"remote_reject"="The user has declined your call invitation"; +"other_client_accept"="The other client has answered the call"; +"other_client_reject"="The other client has rejected the call"; "permission"="Permission Require"; "reject"="Reject"; "agree"="Agree"; -"audio_to_video"="Opposite requests to switch to video, have to turn on your camera"; -"video_to_audio"="Opposite requests to switch to audio, will turn off your camera directly"; -"reject_tip"="Request Denied"; +"audio_to_video"="A request to convert video to audio will turn off your camera directly"; +"The other party requests to convert audio to video, which requires turning on your camera"; +"reject_tip"="The invitation was rejected."; -"invite_audio_call"="Invite for audio call"; -"invite_video_call"="Invite for video call"; +"invite_audio_call"="Invite you to voice call..."; +"invite_video_call"="Invite you to video call..."; -"waitting_remote_response"="Waiting for response"; +"waitting_remote_response"="Awaiting response..."; "call_cancel"="Cancel"; "call_reject"="Reject"; "call_accept"="Accept"; -"call_micro_phone"="Microphone"; +"call_micro_phone"="Mic"; "call_speaker"="Speaker"; -"waitting_remote_accept"="Waitting to connect"; +"waitting_remote_accept"="Waiting for the call..."; "calling"="Calling"; "cancel_failed"="The invitation has been accepted and cannot be canceled"; "operation_failed"="operation failed"; -"device_not_support"="The current device does not support virtualization"; +"device_not_support"="This device does not support the blurring feature"; -"connecting"="Connecting…"; +"connecting"="Connecting..."; diff --git a/NERtcCallUIKit/NERtcCallUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NERtcCallUIKit/NERtcCallUIKit/Assets/zh-Hans.lproj/Localizable.strings index a34ae8e3..7487f0ff 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NERtcCallUIKit/NERtcCallUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -11,19 +11,19 @@ "remote_cancel"="对方取消"; "remote_busy"="对方占线"; "remote_timeout"="对方超时未响应"; -"remote_reject"="对方已拒绝"; -"other_client_accept"="已被其他端接受"; -"other_client_reject"="已被其他端拒绝"; +"remote_reject"="对方已经拒绝"; +"other_client_accept"="其他端已经接听"; +"other_client_reject"="其他端已经拒绝"; "permission"="权限请求"; "reject"="拒绝"; "agree"="同意"; "audio_to_video"="对方请求将音频转为视频,需要打开您的摄像头。"; "video_to_audio"="对方请求将视频转为音频,将直接关闭您的摄像头。"; -"reject_tip"="对方拒绝了您请求"; +"reject_tip"="对方拒绝了您的请求"; -"invite_audio_call"="邀请您音频通话"; -"invite_video_call"="邀请您视频通话"; +"invite_audio_call"="邀请您音频通话..."; +"invite_video_call"="邀请您视频通话..."; "waitting_remote_response"="正在等待对方响应..."; "call_cancel"="取消"; @@ -31,12 +31,12 @@ "call_accept"="接听"; "call_micro_phone"="麦克风"; "call_speaker"="扬声器"; -"waitting_remote_accept"="等待对方接听……"; +"waitting_remote_accept"="等待对方接听..."; "calling"="正在呼叫"; "cancel_failed"="邀请已接受无法取消"; "operation_failed"="操作失败"; -"device_not_support"="当前设备不支持虚化"; +"device_not_support"="该设备不支持虚化功能"; -"connecting"="正在接通中…"; +"connecting"="正在接通中..."; diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.h b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.h index 8245cf7d..4070de1d 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.h +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.h @@ -83,8 +83,6 @@ NS_ASSUME_NONNULL_BEGIN - (void)refreshVideoView; -- (NSString *)localizableWithKey:(NSString *)key; - @end NS_ASSUME_NONNULL_END diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.m index 78aed788..f9849b0b 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallUIStateController.m @@ -6,6 +6,7 @@ #import #import #import +#import "NECallKitUtil.h" #import "NECallViewController.h" @interface NECallUIStateController () @@ -127,7 +128,7 @@ - (UILabel *)subTitleLabel { _subTitleLabel = [[UILabel alloc] init]; _subTitleLabel.font = [UIFont boldSystemFontOfSize:self.subTitleFontSize]; _subTitleLabel.textColor = [UIColor whiteColor]; - _subTitleLabel.text = [self localizableWithKey:@"waitting_remote_response"]; + _subTitleLabel.text = [NECallKitUtil localizableWithKey:@"waitting_remote_response"]; _subTitleLabel.textAlignment = NSTextAlignmentRight; _subTitleLabel.translatesAutoresizingMaskIntoConstraints = NO; } @@ -137,7 +138,7 @@ - (UILabel *)subTitleLabel { - (NECustomButton *)cancelBtn { if (!_cancelBtn) { _cancelBtn = [[NECustomButton alloc] init]; - _cancelBtn.titleLabel.text = [self localizableWithKey:@"call_cancel"]; + _cancelBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"call_cancel"]; _cancelBtn.imageView.image = [UIImage imageNamed:@"call_cancel" inBundle:self.bundle compatibleWithTraitCollection:nil]; @@ -152,7 +153,7 @@ - (NECustomButton *)cancelBtn { - (NECustomButton *)rejectBtn { if (!_rejectBtn) { _rejectBtn = [[NECustomButton alloc] init]; - _rejectBtn.titleLabel.text = [self localizableWithKey:@"call_reject"]; + _rejectBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"call_reject"]; _rejectBtn.imageView.image = [UIImage imageNamed:@"call_cancel" inBundle:self.bundle compatibleWithTraitCollection:nil]; @@ -167,7 +168,7 @@ - (NECustomButton *)rejectBtn { - (NECustomButton *)acceptBtn { if (!_acceptBtn) { _acceptBtn = [[NECustomButton alloc] init]; - _acceptBtn.titleLabel.text = [self localizableWithKey:@"call_accept"]; + _acceptBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"call_accept"]; _acceptBtn.imageView.image = [UIImage imageNamed:@"call_accept" inBundle:self.bundle compatibleWithTraitCollection:nil]; @@ -183,7 +184,7 @@ - (NECustomButton *)acceptBtn { - (NECustomButton *)microphoneBtn { if (nil == _microphoneBtn) { _microphoneBtn = [[NECustomButton alloc] init]; - _microphoneBtn.titleLabel.text = [self localizableWithKey:@"call_micro_phone"]; + _microphoneBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"call_micro_phone"]; _microphoneBtn.imageView.image = [UIImage imageNamed:@"micro_phone" inBundle:self.bundle compatibleWithTraitCollection:nil]; @@ -202,7 +203,7 @@ - (NECustomButton *)microphoneBtn { - (NECustomButton *)speakerBtn { if (nil == _speakerBtn) { _speakerBtn = [[NECustomButton alloc] init]; - _speakerBtn.titleLabel.text = [self localizableWithKey:@"call_speaker"]; + _speakerBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"call_speaker"]; _speakerBtn.imageView.image = [UIImage imageNamed:@"speaker_off" inBundle:self.bundle compatibleWithTraitCollection:nil]; @@ -223,7 +224,7 @@ - (UILabel *)centerSubtitleLabel { _centerSubtitleLabel = [[UILabel alloc] init]; _centerSubtitleLabel.textColor = [UIColor whiteColor]; _centerSubtitleLabel.font = [UIFont systemFontOfSize:self.subTitleFontSize]; - _centerSubtitleLabel.text = [self localizableWithKey:@"waitting_remote_accept"]; + _centerSubtitleLabel.text = [NECallKitUtil localizableWithKey:@"waitting_remote_accept"]; _centerSubtitleLabel.textAlignment = NSTextAlignmentCenter; _centerSubtitleLabel.translatesAutoresizingMaskIntoConstraints = NO; } @@ -307,9 +308,10 @@ - (void)refreshUI { } - (void)setupVideoCallingUI { - self.titleLabel.text = [NSString stringWithFormat:@"%@ %@", [self localizableWithKey:@"calling"], - self.callParam.remoteShowName]; - self.subTitleLabel.text = [self localizableWithKey:@"waitting_remote_accept"]; + self.titleLabel.text = + [NSString stringWithFormat:@"%@ %@", [NECallKitUtil localizableWithKey:@"calling"], + self.callParam.remoteShowName]; + self.subTitleLabel.text = [NECallKitUtil localizableWithKey:@"waitting_remote_accept"]; if (self.callParam.remoteAvatar.length <= 0) { UIView *cover = [self getDefaultHeaderView:self.callParam.remoteUserAccid @@ -332,9 +334,9 @@ - (void)setupVideoCallingUI { - (void)setupAudioCallingUI { self.centerTitleLabel.text = - [NSString stringWithFormat:@"%@ %@", [self localizableWithKey:@"calling"], + [NSString stringWithFormat:@"%@ %@", [NECallKitUtil localizableWithKey:@"calling"], self.callParam.remoteShowName]; - self.centerSubtitleLabel.text = [self localizableWithKey:@"waitting_remote_accept"]; + self.centerSubtitleLabel.text = [NECallKitUtil localizableWithKey:@"waitting_remote_accept"]; if (self.callParam.remoteAvatar.length <= 0) { UIView *cover = [self getDefaultHeaderView:self.callParam.remoteUserAccid font:[UIFont systemFontOfSize:self.titleFontSize] @@ -369,8 +371,8 @@ - (void)setupCalledUI { - (NSString *)getInviteText { return (self.callParam.callType == NERtcCallTypeAudio - ? [self localizableWithKey:@"invite_audio_call"] - : [self localizableWithKey:@"invite_video_call"]); + ? [NECallKitUtil localizableWithKey:@"invite_audio_call"] + : [NECallKitUtil localizableWithKey:@"invite_video_call"]); } - (void)refreshVideoView { @@ -383,7 +385,7 @@ - (void)refreshVideoView { NSLog(@"show my big view"); self.smallVideoView.maskView.hidden = !self.mainController.isRemoteMute; self.bigVideoView.maskView.hidden = !self.operationView.cameraBtn.selected; - self.bigVideoView.userID = self.callParam.currentUserAccid; + self.bigVideoView.userID = NIMSDK.sharedSDK.loginManager.currentAccount; self.smallVideoView.userID = self.callParam.remoteUserAccid; } else { [[NECallEngine sharedInstance] setupLocalView:self.smallVideoView.videoView]; @@ -392,7 +394,7 @@ - (void)refreshVideoView { self.bigVideoView.maskView.hidden = !self.mainController.isRemoteMute; self.smallVideoView.maskView.hidden = !self.operationView.cameraBtn.selected; self.bigVideoView.userID = self.callParam.remoteUserAccid; - self.smallVideoView.userID = self.callParam.currentUserAccid; + self.smallVideoView.userID = NIMSDK.sharedSDK.loginManager.currentAccount; } } @@ -418,9 +420,4 @@ - (UIView *)getDefaultHeaderView:(NSString *)accid return headerView; } -- (NSString *)localizableWithKey:(NSString *)key { - return [self.bundle localizedStringForKey:key value:nil table:@"Localizable"]; - ; -} - @end diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m index ac4761b6..4dfc8977 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m @@ -10,6 +10,7 @@ #import #import #include +#import "NECallKitUtil.h" #import "NECallUIStateController.h" #import "NECustomButton.h" #import "NEExpandButton.h" @@ -147,7 +148,7 @@ - (void)setupSDK { }); } weakSelf.videoCallingController.bigVideoView.userID = - weakSelf.callParam.currentUserAccid; + NIMSDK.sharedSDK.loginManager.currentAccount; } if (error) { @@ -307,7 +308,7 @@ - (void)setSwitchAudioStyle { self.mediaSwitchBtn.imageView.image = [UIImage imageNamed:@"switch_audio" inBundle:self.bundle compatibleWithTraitCollection:nil]; - self.mediaSwitchBtn.titleLabel.text = [self localizableWithKey:@"switch_to_audio"]; + self.mediaSwitchBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"switch_to_audio"]; self.mediaSwitchBtn.tag = NERtcCallTypeAudio; [self showVideoView]; [self setUrl:self.callParam.remoteAvatar withPlaceholder:@"avator"]; @@ -318,7 +319,7 @@ - (void)setSwitchVideoStyle { self.mediaSwitchBtn.imageView.image = [UIImage imageNamed:@"switch_video" inBundle:self.bundle compatibleWithTraitCollection:nil]; - self.mediaSwitchBtn.titleLabel.text = [self localizableWithKey:@"switch_to_video"]; + self.mediaSwitchBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"switch_to_video"]; self.mediaSwitchBtn.tag = NERtcCallTypeVideo; [self hideVideoView]; [self setUrl:self.callParam.remoteAvatar withPlaceholder:@"avator"]; @@ -354,13 +355,13 @@ - (void)updateUIonStatus:(NERtcCallStatus)status { self.mediaSwitchBtn.imageView.image = [UIImage imageNamed:@"switch_audio" inBundle:self.bundle compatibleWithTraitCollection:nil]; - self.mediaSwitchBtn.titleLabel.text = [self localizableWithKey:@"switch_to_audio"]; + self.mediaSwitchBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"switch_to_audio"]; [self setSwitchAudioStyle]; } else { self.mediaSwitchBtn.imageView.image = [UIImage imageNamed:@"switch_video" inBundle:self.bundle compatibleWithTraitCollection:nil]; - self.mediaSwitchBtn.titleLabel.text = [self localizableWithKey:@"switch_to_video"]; + self.mediaSwitchBtn.titleLabel.text = [NECallKitUtil localizableWithKey:@"switch_to_video"]; [self setSwitchVideoStyle]; } __weak typeof(self) weakSelf = self; @@ -483,7 +484,7 @@ - (void)rejectEvent:(UIButton *)button { - (void)acceptEvent:(UIButton *)button { if ([[NetManager shareInstance] isClose] == YES) { - [self.view ne_makeToast:[self localizableWithKey:@"network_error"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"network_error"]]; return; } @@ -491,26 +492,27 @@ - (void)acceptEvent:(UIButton *)button { self.calledController.acceptBtn.userInteractionEnabled = NO; __weak typeof(self) weakSelf = self; - [[NECallEngine sharedInstance] accept:^(NSError *_Nullable error, - NECallInfo *_Nullable callInfo) { - weakSelf.calledController.rejectBtn.userInteractionEnabled = YES; - weakSelf.calledController.acceptBtn.userInteractionEnabled = YES; - if (error) { - if (error.code != 10420) { - [weakSelf.preiousWindow - ne_makeToast:[NSString stringWithFormat:@"%@ %@", - [weakSelf localizableWithKey:@"accept_failed"], - error.localizedDescription]]; - } - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), - dispatch_get_main_queue(), ^{ - [weakSelf destroy]; - }); - } else { - self.calledController.connectingLabel.hidden = NO; - [self stopCurrentPlaying]; - } - }]; + [[NECallEngine sharedInstance] + accept:^(NSError *_Nullable error, NECallInfo *_Nullable callInfo) { + weakSelf.calledController.rejectBtn.userInteractionEnabled = YES; + weakSelf.calledController.acceptBtn.userInteractionEnabled = YES; + if (error) { + if (error.code != 10420) { + [weakSelf.preiousWindow + ne_makeToast:[NSString stringWithFormat:@"%@ %@", + [NECallKitUtil + localizableWithKey:@"accept_failed"], + error.localizedDescription]]; + } + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), + dispatch_get_main_queue(), ^{ + [weakSelf destroy]; + }); + } else { + self.calledController.connectingLabel.hidden = NO; + [self stopCurrentPlaying]; + } + }]; } - (void)switchCameraBtn:(UIButton *)button { @@ -538,7 +540,7 @@ - (void)cameraBtnClick:(UIButton *)button { [[NECallEngine sharedInstance] muteLocalVideo:button.selected]; } [self changeDefaultImage:button.selected]; - [self cameraAvailble:!button.selected userId:self.callParam.currentUserAccid]; + [self cameraAvailble:!button.selected userId:NIMSDK.sharedSDK.loginManager.currentAccount]; } - (void)hangupBtnClick:(UIButton *)button { @@ -576,7 +578,7 @@ - (void)speakerBtnClick:(UIButton *)button { [self setAudioOutputToSpeaker]; } } else { - [self.view ne_makeToast:[self localizableWithKey:@"operation_failed"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"operation_failed"]]; } } @@ -597,7 +599,7 @@ - (void)virtualBackgroundBtnClick:(UIButton *)button { - (void)operationSwitchClick:(UIButton *)btn { if ([[NetManager shareInstance] isClose] == YES) { - [self.view ne_makeToast:[self localizableWithKey:@"network_error"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"network_error"]]; return; } __weak typeof(self) weakSelf = self; @@ -624,10 +626,10 @@ - (void)operationSwitchClick:(UIButton *)btn { } } else { [weakSelf.view - ne_makeToast:[NSString - stringWithFormat:@"%@: %@", - [weakSelf localizableWithKey:@"switch_error"], - error]]; + ne_makeToast:[NSString stringWithFormat:@"%@: %@", + [NECallKitUtil + localizableWithKey:@"switch_error"], + error]]; } }]; } @@ -637,13 +639,13 @@ - (void)operationSpeakerClick:(UIButton *)btn { if (ret == 0) { btn.selected = !btn.selected; } else { - [self.view ne_makeToast:[self localizableWithKey:@"operation_failed"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"operation_failed"]]; } } - (void)mediaClick:(UIButton *)btn { if ([[NetManager shareInstance] isClose] == YES) { - [self.view ne_makeToast:[self localizableWithKey:@"network_error"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"network_error"]]; return; } __weak typeof(self) weakSelf = self; @@ -670,10 +672,10 @@ - (void)mediaClick:(UIButton *)btn { } } else { [weakSelf.view - ne_makeToast:[NSString - stringWithFormat:@"%@ : %@", - [weakSelf localizableWithKey:@"switch_error"], - error]]; + ne_makeToast:[NSString stringWithFormat:@"%@ : %@", + [NECallKitUtil + localizableWithKey:@"switch_error"], + error]]; } }]; } @@ -745,35 +747,37 @@ - (void)onCallConnected:(NECallInfo *)info { - (void)onCallEnd:(NECallEndInfo *)info { switch (info.reasonCode) { case TerminalCodeTimeOut: - [self playRingWithType:CRTNoResponseRing]; + if (self.callParam.isCaller == YES) { + [self playRingWithType:CRTNoResponseRing]; + } if ([[NetManager shareInstance] isClose] == YES) { [self destroy]; return; } if (self.callParam.isCaller == YES) { - [self showToastWithContent:[self localizableWithKey:@"remote_timeout"]]; + [self showToastWithContent:[NECallKitUtil localizableWithKey:@"remote_timeout"]]; } break; case TerminalCodeBusy: - [self showToastWithContent:[self localizableWithKey:@"remote_busy"]]; + [self showToastWithContent:[NECallKitUtil localizableWithKey:@"remote_busy"]]; [self playRingWithType:CRTBusyRing]; break; case TerminalCalleeCancel: - [self showToastWithContent:[self localizableWithKey:@"remote_cancel"]]; + [self showToastWithContent:[NECallKitUtil localizableWithKey:@"remote_cancel"]]; break; case TerminalCallerRejcted: - [self showToastWithContent:[self localizableWithKey:@"remote_reject"]]; + [self showToastWithContent:[NECallKitUtil localizableWithKey:@"remote_reject"]]; [self playRingWithType:CRTRejectRing]; break; case TerminalOtherRejected: - [self.preiousWindow ne_makeToast:[self localizableWithKey:@"other_client_reject"]]; + [self.preiousWindow ne_makeToast:[NECallKitUtil localizableWithKey:@"other_client_reject"]]; break; case TerminalOtherAccepted: - [self.preiousWindow ne_makeToast:[self localizableWithKey:@"other_client_accept"]]; + [self.preiousWindow ne_makeToast:[NECallKitUtil localizableWithKey:@"other_client_accept"]]; break; case TerminalCallerCancel: @@ -800,16 +804,16 @@ - (void)onCallTypeChange:(NECallTypeChangeInfo *)info { return; } UIAlertController *alert = [UIAlertController - alertControllerWithTitle:[self localizableWithKey:@"permission"] + alertControllerWithTitle:[NECallKitUtil localizableWithKey:@"permission"] message:info.callType == NECallTypeVideo - ? [self localizableWithKey:@"audio_to_video"] - : [self localizableWithKey:@"video_to_audio"] + ? [NECallKitUtil localizableWithKey:@"audio_to_video"] + : [NECallKitUtil localizableWithKey:@"video_to_audio"] preferredStyle:UIAlertControllerStyleAlert]; self.alert = alert; __weak typeof(self) weakSelf = self; UIAlertAction *rejectAction = [UIAlertAction - actionWithTitle:[self localizableWithKey:@"reject"] + actionWithTitle:[NECallKitUtil localizableWithKey:@"reject"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnull action) { NESwitchParam *param = [[NESwitchParam alloc] init]; @@ -824,7 +828,7 @@ - (void)onCallTypeChange:(NECallTypeChangeInfo *)info { }]; }]; UIAlertAction *agreeAction = [UIAlertAction - actionWithTitle:[self localizableWithKey:@"agree"] + actionWithTitle:[NECallKitUtil localizableWithKey:@"agree"] style:UIAlertActionStyleDefault handler:^(UIAlertAction *_Nonnull action) { NESwitchParam *param = [[NESwitchParam alloc] init]; @@ -866,7 +870,7 @@ - (void)onCallTypeChange:(NECallTypeChangeInfo *)info { break; case NECallSwitchStateReject: [self hideBannerView]; - [self.view ne_makeToast:[self localizableWithKey:@"reject_tip"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"reject_tip"]]; break; default: break; @@ -952,8 +956,8 @@ - (NSString *)timeFormatted:(int)totalSeconds { - (NSString *)getInviteText { return (self.callParam.callType == NECallTypeAudio - ? [self localizableWithKey:@"invite_audio_call"] - : [self localizableWithKey:@"invite_video_call"]); + ? [NECallKitUtil localizableWithKey:@"invite_audio_call"] + : [NECallKitUtil localizableWithKey:@"invite_video_call"]); } - (void)hideBannerView { @@ -1043,7 +1047,7 @@ - (UIView *)bannerView { ]]; label.adjustsFontSizeToFitWidth = YES; - label.text = [self localizableWithKey:@"waitting_remote_response"]; + label.text = [NECallKitUtil localizableWithKey:@"waitting_remote_response"]; } return _bannerView; } @@ -1267,10 +1271,6 @@ - (UIView *)getDefaultHeaderView:(NSString *)accid return headerView; } -- (NSString *)localizableWithKey:(NSString *)key { - return [self.bundle localizedStringForKey:key value:nil table:@"Localizable"]; -} - #pragma mark CallEngine Key Value - (BOOL)isGlobalInit { @@ -1287,7 +1287,7 @@ - (void)onNERtcEngineVirtualBackgroundSourceEnabled:(BOOL)enabled reason: (NERtcVirtualBackgroundSourceStateReason)reason { if (reason == kNERtcVirtualBackgroundSourceStateReasonDeviceNotSupported) { - [self.view ne_makeToast:[self localizableWithKey:@"device_not_support"]]; + [self.view ne_makeToast:[NECallKitUtil localizableWithKey:@"device_not_support"]]; self.operationView.virtualBtn.selected = NO; } } diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECalledViewController.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECalledViewController.m index c2e1d247..533aa452 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECalledViewController.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECalledViewController.m @@ -4,6 +4,7 @@ #import "NECalledViewController.h" #import +#import "NECallKitUtil.h" @interface NECalledViewController () @@ -54,7 +55,7 @@ - (void)setupUI { self.connectingLabel = [[UILabel alloc] init]; self.connectingLabel.translatesAutoresizingMaskIntoConstraints = NO; - self.connectingLabel.text = [self localizableWithKey:@"connecting"]; + self.connectingLabel.text = [NECallKitUtil localizableWithKey:@"connecting"]; self.connectingLabel.textColor = [UIColor whiteColor]; self.connectingLabel.font = [UIFont systemFontOfSize:14]; self.connectingLabel.hidden = YES; diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.h b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.h index b0879b0a..9d7a5d41 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.h +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.h @@ -1,8 +1,9 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. #import +#import "NECallUIKitConfig.h" NS_ASSUME_NONNULL_BEGIN @@ -24,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN @property(nonatomic, strong, nullable) NSString *noResponseFilePath; /// 初始化 -- (instancetype)initWithBundle:(NSBundle *)bundle; +- (instancetype)initWithBundle:(NSBundle *)bundle language:(NECallUILanguage)language; @end diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.m index 314b8597..a995f213 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NERingFile.m @@ -1,21 +1,53 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. #import "NERingFile.h" +#import "NERtcCallUIKit.h" @implementation NERingFile -- (instancetype)initWithBundle:(NSBundle *)bundle { +- (instancetype)initWithBundle:(NSBundle *)bundle language:(NECallUILanguage)language { self = [super init]; if (self) { - self.callerRingFilePath = [bundle pathForResource:@"avchat_connecting" ofType:@"mp3"]; - self.calleeRingFilePath = [bundle pathForResource:@"avchat_ring" ofType:@"mp3"]; - self.busyRingFilePath = [bundle pathForResource:@"avchat_peer_busy" ofType:@"mp3"]; - self.rejectRingFilePath = [bundle pathForResource:@"avchat_peer_reject" ofType:@"mp3"]; - self.noResponseFilePath = [bundle pathForResource:@"avchat_no_response" ofType:@"mp3"]; + switch (language) { + case NECallUILanguageEn: + [self setEnWithBundle:bundle]; + break; + case NECallUILanguageZhHans: + [self setZhWithBundle:bundle]; + break; + case NECallUILanguageAuto: { + NSString *language = [NSLocale preferredLanguages].firstObject; + if ([language hasPrefix:@"en"]) { + [self setEnWithBundle:bundle]; + } else { + // 非英文情况下全部使用默认中文 + [self setZhWithBundle:bundle]; + } + break; + } + default: + break; + } } return self; } +- (void)setZhWithBundle:(NSBundle *)bundle { + self.callerRingFilePath = [bundle pathForResource:@"avchat_connecting" ofType:@"mp3"]; + self.calleeRingFilePath = [bundle pathForResource:@"avchat_ring" ofType:@"mp3"]; + self.busyRingFilePath = [bundle pathForResource:@"avchat_peer_busy" ofType:@"mp3"]; + self.rejectRingFilePath = [bundle pathForResource:@"avchat_peer_reject" ofType:@"mp3"]; + self.noResponseFilePath = [bundle pathForResource:@"avchat_no_response" ofType:@"mp3"]; +} + +- (void)setEnWithBundle:(NSBundle *)bundle { + self.callerRingFilePath = [bundle pathForResource:@"avchat_connecting_en" ofType:@"mp3"]; + self.calleeRingFilePath = [bundle pathForResource:@"avchat_ring_en" ofType:@"mp3"]; + self.busyRingFilePath = [bundle pathForResource:@"avchat_peer_busy_en" ofType:@"mp3"]; + self.rejectRingFilePath = [bundle pathForResource:@"avchat_peer_reject_en" ofType:@"mp3"]; + self.noResponseFilePath = [bundle pathForResource:@"avchat_no_response_en" ofType:@"mp3"]; +} + @end diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NEUICallParam.h b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NEUICallParam.h index 14907900..4bdd6aef 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NEUICallParam.h +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Model/NEUICallParam.h @@ -14,9 +14,6 @@ NS_ASSUME_NONNULL_BEGIN /// 被叫accid @property(nonatomic, strong) NSString *remoteUserAccid; -/// 主叫accid -@property(nonatomic, strong) NSString *currentUserAccid; - /// 通话页面被叫显示名称 @property(nonatomic, strong) NSString *remoteShowName; diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.h b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.h index c0e8c2fe..a1cc4ce7 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.h +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.h @@ -1,8 +1,9 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. #import +#import "NECallUIKitConfig.h" NS_ASSUME_NONNULL_BEGIN @@ -10,6 +11,10 @@ NS_ASSUME_NONNULL_BEGIN + (UIColor *)colorWithHexString:(NSString *)hexString; ++ (NSString *)localizableWithKey:(NSString *)key; + ++ (void)setLanguage:(NECallUILanguage)language; + @end NS_ASSUME_NONNULL_END diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.m index 7a733988..510057dc 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallKitUtil.m @@ -3,6 +3,9 @@ // found in the LICENSE file. #import "NECallKitUtil.h" +#import "NERtcCallUIKit.h" + +static NECallUILanguage _language = NECallUILanguageAuto; @implementation NECallKitUtil @@ -19,4 +22,31 @@ + (UIColor *)colorWithHexString:(NSString *)hexString { return color; } ++ (void)setLanguage:(NECallUILanguage)language { + _language = language; +} + ++ (NSString *)localizableWithKey:(NSString *)key { + switch (_language) { + case NECallUILanguageZhHans: { + NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle bundleForClass:[NERtcCallUIKit class]] + pathForResource:@"zh-Hans" + ofType:@"lproj"]]; + return [bundle localizedStringForKey:key value:nil table:nil]; + } + case NECallUILanguageEn: { + NSBundle *bundle = [NSBundle bundleWithPath:[[NSBundle bundleForClass:[NERtcCallUIKit class]] + pathForResource:@"en" + ofType:@"lproj"]]; + return [bundle localizedStringForKey:key value:nil table:nil]; + } + case NECallUILanguageAuto: + default: + break; + } + return [[NSBundle bundleForClass:NERtcCallUIKit.class] localizedStringForKey:key + value:nil + table:@"Localizable"]; +} + @end diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallUIKitConfig.h b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallUIKitConfig.h index 09c9a244..20e82325 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallUIKitConfig.h +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/NECallUIKitConfig.h @@ -7,6 +7,16 @@ NS_ASSUME_NONNULL_BEGIN +/// 国际化类型 +typedef NS_ENUM(NSInteger, NECallUILanguage) { + /// 根据系统设置切换 + NECallUILanguageAuto = 0, + /// 简体中文 + NECallUILanguageZhHans, + /// 英文 + NECallUILanguageEn, +}; + @interface NECallUIConfig : NSObject /// 是否禁止音频通话转视频通话,默认YES,支持转换 @@ -42,6 +52,9 @@ NS_ASSUME_NONNULL_BEGIN /// 是否开启被叫预览,默认NO,不开启 @property(nonatomic, assign) BOOL enableCalleePreview; +/// 国际化配置 +@property(nonatomic, assign) NECallUILanguage language; + @end @interface NECallUIKitConfig : NSObject diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m index c4901e82..5ab92056 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m @@ -119,6 +119,10 @@ - (void)setupWithConfig:(NECallUIKitConfig *)config { self.transcodingDelegate = instance; } } + + self.bundle = [NSBundle bundleForClass:NERtcCallUIKit.class]; + self.ringFile = [[NERingFile alloc] initWithBundle:self.bundle language:config.uiConfig.language]; + [NECallKitUtil setLanguage:config.uiConfig.language]; } - (instancetype)init { @@ -152,29 +156,21 @@ - (instancetype)init { selector:@selector(appDidEnterForeground) name:UIApplicationWillEnterForegroundNotification object:nil]; - - self.bundle = [NSBundle bundleForClass:NERtcCallUIKit.class]; - self.ringFile = [[NERingFile alloc] initWithBundle:self.bundle]; self.smallVideoSize = CGSizeMake(90, 160); self.smallAudioSize = CGSizeMake(70, 70); } return self; } -- (NSString *)localizableWithKey:(NSString *)key { - return [self.bundle localizedStringForKey:key value:nil table:@"Localizable"]; -} - - (void)registerRouter { [[Router shared] register:@"imkit://callkit.page" closure:^(NSDictionary *_Nonnull param) { if ([[NetManager shareInstance] isClose] == YES) { [UIApplication.sharedApplication.keyWindow - ne_makeToast:[self localizableWithKey:@"network_error"]]; + ne_makeToast:[NECallKitUtil localizableWithKey:@"network_error"]]; return; } NEUICallParam *callParam = [[NEUICallParam alloc] init]; - callParam.currentUserAccid = [param objectForKey:@"currentUserAccid"]; callParam.remoteUserAccid = [param objectForKey:@"remoteUserAccid"]; callParam.remoteShowName = [param objectForKey:@"remoteShowName"]; callParam.remoteAvatar = [param objectForKey:@"remoteAvatar"]; @@ -323,7 +319,6 @@ - (void)onReceiveInvited:(NEInviteInfo *)info { ? imUser.userInfo.mobile : imUser.userInfo.nickName; callParam.remoteAvatar = imUser.userInfo.avatarUrl; - callParam.currentUserAccid = NIMSDK.sharedSDK.loginManager.currentAccount; callParam.enableAudioToVideo = self.config.uiConfig.enableAudioToVideo; callParam.enableVideoToAudio = self.config.uiConfig.enableVideoToAudio; callParam.enableVirtualBackground = self.config.uiConfig.enableVirtualBackground; @@ -380,7 +375,6 @@ - (void)showCalled:(NIMUser *)imUser callType:(NECallType)type attachment:(NSStr callParam.remoteUserAccid = imUser.userId; callParam.remoteShowName = imUser.userInfo.mobile; callParam.remoteAvatar = imUser.userInfo.avatarUrl; - callParam.currentUserAccid = NIMSDK.sharedSDK.loginManager.currentAccount; callParam.enableVideoToAudio = self.config.uiConfig.enableVideoToAudio; callParam.enableAudioToVideo = self.config.uiConfig.enableAudioToVideo; callParam.callType = type; @@ -432,6 +426,7 @@ - (UINavigationController *)getKeyWindowNav { } window.frame = [[UIScreen mainScreen] bounds]; window.windowLevel = UIWindowLevelStatusBar - 1; + window.backgroundColor = [UIColor clearColor]; self.keywindow = window; self.preiousKeywindow = UIApplication.sharedApplication.keyWindow; YXAlogInfo(@"create new window %@", self.keywindow); @@ -445,7 +440,7 @@ - (UINavigationController *)getKeyWindowNav { nav.view.backgroundColor = [UIColor clearColor]; [nav.navigationBar setHidden:YES]; self.keywindow.rootViewController = nav; - self.keywindow.window.backgroundColor = [UIColor clearColor]; + self.keywindow.backgroundColor = [UIColor clearColor]; [self.keywindow makeKeyAndVisible]; return nav; } @@ -776,7 +771,7 @@ - (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPict #pragma mark - Version + (NSString *)version { - return @"2.2.0"; + return @"2.4.0"; } @end diff --git a/NETeamUIKit/NETeamUIKit.podspec b/NETeamUIKit/NETeamUIKit.podspec index c0f5c3d1..eadf72e4 100644 --- a/NETeamUIKit/NETeamUIKit.podspec +++ b/NETeamUIKit/NETeamUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NETeamUIKit' - s.version = '9.7.0' + s.version = '10.1.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings b/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings index 62b596f1..f31f2f5d 100644 --- a/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings +++ b/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings @@ -36,7 +36,6 @@ "discuss_info"="Temp Group Info"; "group_info"="Group Info"; "discuss_introduce"="Temp Group Introduce"; -"search_friend"="Search Contact"; "no_result"="No Result"; "discuss_name"="Temp Group Name"; "dissolute_team_chat"="Wether to disband Group"; @@ -60,6 +59,7 @@ "who_edit_team_info"="Edit group info permission"; "who_edit_user_info"="Add member permission"; "who_at_all"="@all permission"; +"who_can_top_message"="Top message permission"; "who_mark"="Pin permission"; "who_user_join_noti"="Group notification recipients"; "manage_manger"="Manager management"; @@ -84,4 +84,4 @@ "remove_member_tip"="This member will leave group after remove"; "member_select_no_member"="No member"; "remove_failed"="remove failed"; -"failed_operation"="操作失败"; +"failed_operation"="Operation failed"; diff --git a/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings index 58405ed7..8dfdc8a2 100644 --- a/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -36,7 +36,6 @@ "discuss_info"="讨论组信息"; "group_info"="群信息"; "discuss_introduce"="讨论组介绍"; -"search_friend"="搜索"; "no_result"="暂无结果"; "discuss_name"="讨论组名称"; "dissolute_team_chat"="是否解散群聊?"; @@ -57,6 +56,7 @@ "who_edit_team_info"="谁可以编辑群信息"; "who_edit_user_info"="谁可以添加群成员"; "who_at_all"="谁可以@所有人"; +"who_can_top_message"="谁可以置顶消息"; "who_mark"="谁可以标记"; "who_user_join_noti"="谁可以收到用户进群通知"; "manage_manger"="管理管理员"; diff --git a/NETeamUIKit/NETeamUIKit/Classes/NEBaseTeamRouter.swift b/NETeamUIKit/NETeamUIKit/Classes/Common/NEBaseTeamRouter.swift similarity index 57% rename from NETeamUIKit/NETeamUIKit/Classes/NEBaseTeamRouter.swift rename to NETeamUIKit/NETeamUIKit/Classes/Common/NEBaseTeamRouter.swift index 60f68364..dfdb2b81 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NEBaseTeamRouter.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Common/NEBaseTeamRouter.swift @@ -4,18 +4,18 @@ import Foundation import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK @objcMembers 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", - "https://s.netease.im/safe/ABg8YjyQWvcqO6sAAAAAAAABAAA?_im_url=1", - "https://s.netease.im/safe/ABg8YkCQWvcqO6sAAAAAAAABAAA?_im_url=1", - "https://s.netease.im/safe/ABg8YkSQWvcqO6sAAAAAAAABAAA?_im_url=1"] + public static var iconUrls = ["https://yx-web-nosdn.netease.im/common/2425b4cc058e5788867d63c322feb7ac/groupAvatar1.png", + "https://yx-web-nosdn.netease.im/common/62c45692c9771ab388d43fea1c9d2758/groupAvatar2.png", + "https://yx-web-nosdn.netease.im/common/d1ed3c21d3f87a41568d17197760e663/groupAvatar3.png", + "https://yx-web-nosdn.netease.im/common/e677d8551deb96723af2b40b821c766a/groupAvatar4.png", + "https://yx-web-nosdn.netease.im/common/fd6c75bb6abca9c810d1292e66d5d87e/groupAvatar5.png"] public static var iconUrlsFun = ["https://nim-nosdn.netease.im/MjYxNDkzNzE=/bmltYV8xNDIxMTk0NzAzMzhfMTY4NDgyNzc0MTczNV8yY2FlMjczZS01MDk0LTQ5NWMtODMzMS1mYTBmMTE1NmEyNDQ=", "https://nim-nosdn.netease.im/MjYxNDkzNzE=/bmltYV8xNDIxMTk0NzAzMzhfMTY4NDgyNzc0MTczNV9jYWJmNjViNy1kMGM3LTRiNDEtYmVmMi1jYjhiNzRjY2EwY2M=", @@ -34,31 +34,31 @@ open class TeamRouter: NSObject { let iconUrl = (param["url"] as? String) ?? iconUrls[Int(arc4random()) % iconUrls.count] - let option = NIMCreateTeamOption() - option.type = .advanced - option.avatarUrl = iconUrl - option.name = name - option.joinMode = .noAuth - option.inviteMode = .all - option.beInviteMode = .noAuth - option.updateInfoMode = .all - option.updateClientCustomMode = .all + let param = V2NIMCreateTeamParams() + param.name = name + param.teamType = .TEAM_TYPE_NORMAL + param.avatar = iconUrl + param.joinMode = .TEAM_JOIN_MODE_FREE + param.inviteMode = .TEAM_INVITE_MODE_ALL + param.agreeMode = .TEAM_AGREE_MODE_NO_AUTH + param.updateInfoMode = .TEAM_UPDATE_INFO_MODE_ALL + param.updateExtensionMode = .TEAM_UPDATE_EXTENSION_MODE_ALL var disucssFlag = [String: Any]() disucssFlag[discussTeamKey] = true let jsonString = NECommonUtil.getJSONStringFromDictionary(disucssFlag) if jsonString.count > 0 { - option.clientCustomInfo = jsonString + param.serverExtension = jsonString } - repo.createAdvanceTeam(accids, option) { error, teamid, failedIds in + repo.createTeam(param, accids, nil, nil) { createResult, error in var result = [String: Any]() - if let err = error { + if let err = error as? NSError { result["code"] = err.code result["msg"] = err.localizedDescription } else { result["code"] = 0 result["msg"] = "ok" - result["teamId"] = teamid + result["teamId"] = createResult?.team?.teamId } Router.shared.use(TeamCreateDiscussResult, parameters: result, closure: nil) } @@ -76,33 +76,42 @@ open class TeamRouter: NSObject { let iconUrl = (param["url"] as? String) ?? iconUrls[Int(arc4random()) % iconUrls.count] - let option = NIMCreateTeamOption() - option.type = .advanced - option.avatarUrl = iconUrl - option.name = name - option.beInviteMode = .noAuth + let param = V2NIMCreateTeamParams() + param.name = name + param.avatar = iconUrl + param.teamType = .TEAM_TYPE_NORMAL + param.agreeMode = .TEAM_AGREE_MODE_NO_AUTH + param.updateExtensionMode = .TEAM_UPDATE_EXTENSION_MODE_ALL - repo.createAdvanceTeam(accids, option) { error, teamid, failedIds in + repo.createTeam(param, [], nil, nil) { creatResult, error in var result = [String: Any]() - if let err = error { + if let err = error as? NSError { result["code"] = err.code result["msg"] = err.localizedDescription } else { result["code"] = 0 result["msg"] = "ok" - result["teamId"] = teamid + result["teamId"] = creatResult?.team?.teamId - repo.sendCreateAdavanceNoti( - teamid ?? "", - localizable("create_senior_team_noti") - ) { error in + // 先插入【成功创建群聊】消息 + ChatRepo.shared.sendCreateAdavanceNoti(creatResult?.team?.teamId ?? "", .TEAM_TYPE_NORMAL, localizable("create_senior_team_noti")) { error in print("send noti message : ", error as Any) + + // 再邀请用户 + repo.inviteMembers(creatResult?.team?.teamId ?? "", .TEAM_TYPE_NORMAL, accids) { error, members in + print("invite users : \(accids)", error as Any) + } } } + Router.shared.use(TeamCreateSeniorResult, parameters: result, closure: nil) print("creat senior reuslt : ", result) } } } + + if IMKitConfigCenter.shared.onlineStatusEnable { + _ = NEEventSubscribeManager.shared + } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift b/NETeamUIKit/NETeamUIKit/Classes/Common/TeamConstant.swift similarity index 86% rename from NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift rename to NETeamUIKit/NETeamUIKit/Classes/Common/TeamConstant.swift index 04f5cdb1..2369e84a 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Common/TeamConstant.swift @@ -7,8 +7,9 @@ import Foundation @_exported import NEChatKit @_exported import NECommonKit @_exported import NECommonUIKit -@_exported import NECoreIMKit +@_exported import NECoreIM2Kit @_exported import NECoreKit + let coreLoader = CoreLoader() func localizable(_ key: String) -> String { coreLoader.localizable(key) @@ -16,9 +17,6 @@ func localizable(_ key: String) -> String { public let ModuleName = "NETeamUIKit" -// 邀请入群 选择人数限制 -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/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunHistoryMessageCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunHistoryMessageCell.swift index 254ed1b6..f68e88e6 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunHistoryMessageCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunHistoryMessageCell.swift @@ -1,5 +1,6 @@ import NIMSDK + // Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. @@ -7,44 +8,52 @@ import UIKit @objcMembers open class FunHistoryMessageCell: NEBaseHistoryMessageCell { - override func setupSubviews() { + override open func setupSubviews() { super.setupSubviews() rangeTextColor = .funTeamThemeColor - headImge.layer.cornerRadius = 4 + headView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ - headImge.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), - headImge.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 18), - headImge.widthAnchor.constraint(equalToConstant: 32), - headImge.heightAnchor.constraint(equalToConstant: 32), + headView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 16), + headView.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 18), + headView.widthAnchor.constraint(equalToConstant: 32), + headView.heightAnchor.constraint(equalToConstant: 32), ]) - title.font = .systemFont(ofSize: 12) - title.textColor = .funTeamHistoryCellTitleTextColor + titleLabel.font = .systemFont(ofSize: 12) + titleLabel.textColor = .funTeamHistoryCellTitleTextColor NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - title.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), - title.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + titleLabel.leftAnchor.constraint(equalTo: headView.rightAnchor, constant: 12), + titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), ]) - subTitle.font = .systemFont(ofSize: 15) - subTitle.textColor = .ne_darkText + subTitleLabel.font = .systemFont(ofSize: 15) + subTitleLabel.textColor = .ne_darkText NSLayoutConstraint.activate([ - subTitle.leftAnchor.constraint(equalTo: title.leftAnchor), - subTitle.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), - subTitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), + subTitleLabel.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), + subTitleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), + subTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), ]) NSLayoutConstraint.activate([ bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), - bottomLine.leftAnchor.constraint(equalTo: headImge.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: headView.leftAnchor), bottomLine.bottomAnchor.constraint(equalTo: bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 0.5), ]) NSLayoutConstraint.activate([ timeLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), - timeLabel.centerYAnchor.constraint(equalTo: title.centerYAnchor), + timeLabel.centerYAnchor.constraint(equalTo: titleLabel.centerYAnchor), ]) } + + override open func configData(message: HistoryMessageModel?) { + super.configData(message: message) + guard let searchStr = searchText, let fullText = message?.imMessage?.text else { return } + let windowWidth = UIScreen.main.bounds.width + let maxWidth = windowWidth - 16 - 32 - 12 - 50 + truncateTextForLabel(subTitleLabel, maxWidth, searchStr, fullText) + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamArrowSettingCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamArrowSettingCell.swift index 32986031..84376b62 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamArrowSettingCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamArrowSettingCell.swift @@ -27,8 +27,8 @@ open class FunTeamArrowSettingCell: NEBaseTeamArrowSettingCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamDefaultIconCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamDefaultIconCell.swift index f87e0eb5..02defa0b 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamDefaultIconCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamDefaultIconCell.swift @@ -10,17 +10,17 @@ open class FunTeamDefaultIconCell: NEBaseTeamDefaultIconCell { override func setupUI() { super.setupUI() NSLayoutConstraint.activate([ - selectBack.rightAnchor.constraint(equalTo: contentView.rightAnchor), - selectBack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - selectBack.widthAnchor.constraint(equalToConstant: 56.0), - selectBack.heightAnchor.constraint(equalToConstant: 56.0), + selectBackView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + selectBackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + selectBackView.widthAnchor.constraint(equalToConstant: 56.0), + selectBackView.heightAnchor.constraint(equalToConstant: 56.0), ]) NSLayoutConstraint.activate([ - iconImage.centerXAnchor.constraint(equalTo: selectBack.centerXAnchor), - iconImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - iconImage.heightAnchor.constraint(equalToConstant: 40), - iconImage.widthAnchor.constraint(equalToConstant: 40), + iconImageView.centerXAnchor.constraint(equalTo: selectBackView.centerXAnchor), + iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconImageView.heightAnchor.constraint(equalToConstant: 40), + iconImageView.widthAnchor.constraint(equalToConstant: 40), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamManagerMemberCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamManagerMemberCell.swift index 9a0380d6..17b0a489 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamManagerMemberCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamManagerMemberCell.swift @@ -5,15 +5,6 @@ import UIKit class FunTeamManagerMemberCell: FunTeamMemberCell { -// lazy var removeLabel: UILabel = { -// let label = UILabel() -// label.translatesAutoresizingMaskIntoConstraints = false -// label.text = localizable("team_member_remove") -// label.textColor = .funTeamRemoveLabelColor -// label.font = UIFont.systemFont(ofSize: 14.0) -// return label -// }() - override func awakeFromNib() { super.awakeFromNib() // Initialization code diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamMemberCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamMemberCell.swift index ae063e2b..9658a80b 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamMemberCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamMemberCell.swift @@ -59,9 +59,9 @@ open class FunTeamMemberCell: NEBaseTeamMemberCell { } func setOwnerStyle() { - ownerLabel.textColor = UIColor.funTeamMemberOwnerFlagColor - ownerLabel.backgroundColor = UIColor.funTeamMemberOwnerFlagColor.withAlphaComponent(0.1) - ownerLabel.layer.borderColor = UIColor.funTeamMemberOwnerFlagColor.cgColor + ownerLabel.textColor = UIColor.funTeamThemeColor + ownerLabel.backgroundColor = UIColor.funTeamThemeColor.withAlphaComponent(0.1) + ownerLabel.layer.borderColor = UIColor.funTeamThemeColor.cgColor ownerWidth?.constant = 48 } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingHeaderCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingHeaderCell.swift index feeee974..da5aa492 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingHeaderCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingHeaderCell.swift @@ -30,12 +30,12 @@ open class FunTeamSettingHeaderCell: NEBaseTeamSettingHeaderCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), ]) NSLayoutConstraint.activate([ - headerView.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), + headerView.centerYAnchor.constraint(equalTo: arrowView.centerYAnchor), headerView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -44.0), headerView.widthAnchor.constraint(equalToConstant: 42.0), headerView.heightAnchor.constraint(equalToConstant: 42.0), diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingLabelArrowCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingLabelArrowCell.swift index 35ca529b..f794fd7b 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingLabelArrowCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingLabelArrowCell.swift @@ -18,8 +18,8 @@ open class FunTeamSettingLabelArrowCell: FunTeamArrowSettingCell { super.setupUI() contentView.addSubview(arrowLabel) NSLayoutConstraint.activate([ - arrowLabel.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), - arrowLabel.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -4), + arrowLabel.centerYAnchor.constraint(equalTo: arrowView.centerYAnchor), + arrowLabel.rightAnchor.constraint(equalTo: arrowView.leftAnchor, constant: -4), ]) } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingSelectCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingSelectCell.swift index 77fc90ee..f73b7af6 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingSelectCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamSettingSelectCell.swift @@ -15,7 +15,7 @@ open class FunTeamSettingSelectCell: NEBaseTeamSettingSelectCell { super.init(coder: coder) } - override func setupUI() { + override open func setupUI() { super.setupUI() contentView.updateLayoutConstraint(firstItem: dividerLine, seconedItem: contentView, attribute: .left, constant: 16) contentView.updateLayoutConstraint(firstItem: dividerLine, seconedItem: contentView, attribute: .right, constant: 0) @@ -33,8 +33,8 @@ open class FunTeamSettingSelectCell: NEBaseTeamSettingSelectCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -16), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamUserCell.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamUserCell.swift index d692aac4..641a628e 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamUserCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Cell/FunTeamUserCell.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -12,12 +12,12 @@ import UIKit open class FunTeamUserCell: NEBaseTeamUserCell { override func setupUI() { super.setupUI() - userHeader.layer.cornerRadius = 4 + userHeaderView.layer.cornerRadius = 4 NSLayoutConstraint.activate([ - userHeader.rightAnchor.constraint(equalTo: contentView.rightAnchor), - userHeader.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - userHeader.widthAnchor.constraint(equalToConstant: 36.0), - userHeader.heightAnchor.constraint(equalToConstant: 36.0), + userHeaderView.rightAnchor.constraint(equalTo: contentView.rightAnchor), + userHeaderView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + userHeaderView.widthAnchor.constraint(equalToConstant: 36.0), + userHeaderView.heightAnchor.constraint(equalToConstant: 36.0), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamAvatarViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamAvatarViewController.swift index 3a5c4631..01175c5c 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamAvatarViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamAvatarViewController.swift @@ -14,7 +14,7 @@ open class FunTeamAvatarViewController: NEBaseTeamAvatarViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { @@ -29,53 +29,53 @@ open class FunTeamAvatarViewController: NEBaseTeamAvatarViewController { view.backgroundColor = .funTeamBackgroundColor NSLayoutConstraint.activate([ - headerBack.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight), - headerBack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), - headerBack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), - headerBack.heightAnchor.constraint(equalToConstant: 128.0), + headerBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight), + headerBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), + headerBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), + headerBackView.heightAnchor.constraint(equalToConstant: 128.0), ]) NSLayoutConstraint.activate([ - photoImage.centerXAnchor.constraint(equalTo: headerView.rightAnchor), - photoImage.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), + photoImageView.rightAnchor.constraint(equalTo: headerView.rightAnchor), + photoImageView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), ]) NSLayoutConstraint.activate([ - defaultHeaderBack.leftAnchor.constraint(equalTo: headerBack.leftAnchor), - defaultHeaderBack.rightAnchor.constraint(equalTo: headerBack.rightAnchor), - defaultHeaderBack.topAnchor.constraint( - equalTo: headerBack.bottomAnchor, + defaultHeaderBackView.leftAnchor.constraint(equalTo: headerBackView.leftAnchor), + defaultHeaderBackView.rightAnchor.constraint(equalTo: headerBackView.rightAnchor), + defaultHeaderBackView.topAnchor.constraint( + equalTo: headerBackView.bottomAnchor, constant: 8.0 ), - defaultHeaderBack.heightAnchor.constraint(equalToConstant: 124.0), + defaultHeaderBackView.heightAnchor.constraint(equalToConstant: 124.0), ]) NSLayoutConstraint.activate([ - tag.leftAnchor.constraint(equalTo: defaultHeaderBack.leftAnchor, constant: 16.0), - tag.topAnchor.constraint(equalTo: defaultHeaderBack.topAnchor, constant: 16.0), - tag.heightAnchor.constraint(equalToConstant: 18), + tagLabel.leftAnchor.constraint(equalTo: defaultHeaderBackView.leftAnchor, constant: 16.0), + tagLabel.topAnchor.constraint(equalTo: defaultHeaderBackView.topAnchor, constant: 16.0), + tagLabel.heightAnchor.constraint(equalToConstant: 18), ]) - iconCollection.register( + iconsCollectionView.register( FunTeamDefaultIconCell.self, forCellWithReuseIdentifier: "\(FunTeamDefaultIconCell.self)" ) NSLayoutConstraint.activate([ - iconCollection.topAnchor.constraint(equalTo: tag.bottomAnchor, constant: 0), - iconCollection.leftAnchor.constraint( - equalTo: defaultHeaderBack.leftAnchor, + iconsCollectionView.topAnchor.constraint(equalTo: tagLabel.bottomAnchor, constant: 0), + iconsCollectionView.leftAnchor.constraint( + equalTo: defaultHeaderBackView.leftAnchor, constant: 16 ), - iconCollection.rightAnchor.constraint( - equalTo: defaultHeaderBack.rightAnchor, + iconsCollectionView.rightAnchor.constraint( + equalTo: defaultHeaderBackView.rightAnchor, constant: -16 ), - iconCollection.heightAnchor.constraint(equalToConstant: 90.0), + iconsCollectionView.heightAnchor.constraint(equalToConstant: 90.0), ]) } override open func uploadPhoto() { - if changePermission() { + if getChangePermission() { showCustomBottomAlert(self) } } @@ -86,7 +86,8 @@ open class FunTeamAvatarViewController: NEBaseTeamAvatarViewController { withReuseIdentifier: "\(FunTeamDefaultIconCell.self)", for: indexPath ) as? FunTeamDefaultIconCell { - cell.iconImage.image = coreLoader.loadImage("fun_icon_\(indexPath.row)") + cell.iconImageView.image = coreLoader.loadImage("fun_icon_\(indexPath.row)") + cell.iconImageView.accessibilityIdentifier = "id.default\(indexPath.row + 1)" return cell } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamHistoryMessageController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamHistoryMessageController.swift index 427153d4..21025c87 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamHistoryMessageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamHistoryMessageController.swift @@ -11,18 +11,18 @@ open class FunTeamHistoryMessageController: NEBaseTeamHistoryMessageController { public lazy var searchView: FunSearchView = { let view = FunSearchView() view.translatesAutoresizingMaskIntoConstraints = false - view.searchBotton.setImage(UIImage.ne_imageNamed(name: "funSearch"), for: .normal) - view.searchBotton.setTitle(localizable("search"), for: .normal) + view.searchButton.setImage(UIImage.ne_imageNamed(name: "funSearch"), for: .normal) + view.searchButton.setTitle(localizable("search"), for: .normal) return view }() - override public init(session: NIMSession?) { - super.init(session: session) + override public init(teamId: String?) { + super.init(teamId: teamId) tag = "FunTeamHistoryMessageController" } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -85,7 +85,7 @@ open class FunTeamHistoryMessageController: NEBaseTeamHistoryMessageController { withIdentifier: "\(NSStringFromClass(FunHistoryMessageCell.self))", for: indexPath ) as! NEBaseHistoryMessageCell - let cellModel = viewmodel.searchResultInfos?[indexPath.row] + let cellModel = viewModel.searchResultInfos?[indexPath.row] cell.searchText = searchStr cell.configData(message: cellModel) return cell diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamInfoViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamInfoViewController.swift index 3278d2ff..9e399574 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamInfoViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamInfoViewController.swift @@ -7,9 +7,9 @@ import UIKit @objcMembers open class FunTeamInfoViewController: NEBaseTeamInfoViewController { - override init(team: NIMTeam?) { + override init(team: V2NIMTeam?) { super.init(team: team) - cellClassDic = [ + registerCellDic = [ SettingCellType.SettingArrowCell.rawValue: FunTeamArrowSettingCell.self, SettingCellType.SettingHeaderCell.rawValue: FunTeamSettingHeaderCell.self, ] @@ -17,12 +17,12 @@ open class FunTeamInfoViewController: NEBaseTeamInfoViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() - viewmodel.cellDatas.forEach { cellModel in + for cellModel in viewModel.cellDatas { cellModel.cornerType = .none if cellModel.type == SettingCellType.SettingArrowCell.rawValue { cellModel.rowHeight = 56 @@ -42,7 +42,7 @@ open class FunTeamInfoViewController: NEBaseTeamInfoViewController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] if let cell = tableView.dequeueReusableCell( withIdentifier: "\(model.type)", for: indexPath @@ -54,15 +54,15 @@ open class FunTeamInfoViewController: NEBaseTeamInfoViewController { } override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] if indexPath.row == 0 { let avatar = FunTeamAvatarViewController() avatar.team = team weak var weakSelf = self avatar.block = { if let t = weakSelf?.team { - weakSelf?.viewmodel.getData(t) - weakSelf?.contentTable.reloadData() + weakSelf?.viewModel.getData(t) + weakSelf?.contentTableView.reloadData() } } navigationController?.pushViewController(avatar, animated: true) diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManageController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerController.swift similarity index 51% rename from NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManageController.swift rename to NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerController.swift index 0ad8eec9..4b4d73d8 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerController.swift @@ -5,7 +5,7 @@ import UIKit @objcMembers -open class FunTeamManageController: NEBaseTeamManageController { +open class FunTeamManagerController: NEBaseTeamManagerController { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) cellClassDic = [ @@ -16,7 +16,7 @@ open class FunTeamManageController: NEBaseTeamManageController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { @@ -26,9 +26,10 @@ open class FunTeamManageController: NEBaseTeamManageController { navigationView.backgroundColor = .ne_lightBackgroundColor } + /// 加载数据,在子类中重新设置样式 override open func reloadSectionData() { - viewmodel.sectionData.forEach { setionModel in - setionModel.cellModels.forEach { cellModel in + for setionModel in viewModel.sectionData { + for cellModel in setionModel.cellModels { cellModel.cornerType = .none if cellModel.rowHeight > 70 { cellModel.rowHeight = 78 @@ -39,29 +40,9 @@ open class FunTeamManageController: NEBaseTeamManageController { } } - override open func getFooterView() -> UIView? { - let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - footer.addSubview(button) - button.backgroundColor = .white - button.clipsToBounds = true - button.setTitleColor(NEConstant.hexRGB(0xE6605C), for: .normal) - button.titleLabel?.font = NEConstant.defaultTextFont(16.0) - button.setTitle(localizable("transfer_owner"), for: .normal) - button.addTarget(self, action: #selector(transferOwner), for: .touchUpInside) - NSLayoutConstraint.activate([ - button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 0), - button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: 0), - button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), - button.heightAnchor.constraint(equalToConstant: 56), - ]) - return footer - } - override open func didManagerClick() { let controller = FunTeamManagerListController() - controller.teamId = viewmodel.teamInfoModel?.team?.teamId + controller.teamId = viewModel.teamInfoModel?.team?.teamId navigationController?.pushViewController(controller, animated: true) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift index 62c25c82..0c0eeb52 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamManagerListController.swift @@ -13,9 +13,10 @@ open class FunTeamManagerListController: NEBaseTeamManagerListController { cellClassDic = [0: FunTeamArrowSettingCell.self, 1: FunTeamManagerMemberCell.self] } - lazy var emptyView: NEEmptyDataView = { + public lazy var emptyView: NEEmptyDataView = { let view = NEEmptyDataView(imageName: "fun_user_empty", content: localizable("no_manager_member"), frame: CGRect.zero) view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false view.isHidden = true return view }() @@ -58,8 +59,8 @@ open class FunTeamManagerListController: NEBaseTeamManagerListController { cell.delegate = self cell.index = indexPath.row cell.configure(viewmodel.managers[indexPath.row]) - if let type = viewmodel.currentMember?.type, type == .manager { - cell.removeBtn.isHidden = true + if let type = viewmodel.currentMember?.memberRole, type == .TEAM_MEMBER_ROLE_MANAGER { + cell.removeButton.isHidden = true cell.removeLabel.isHidden = true } return cell @@ -78,6 +79,7 @@ open class FunTeamManagerListController: NEBaseTeamManagerListController { if indexPath.section == 0 { let selectController = FunTeamMemberSelectController() selectController.teamId = teamId + selectController.selectMemberBlock = { [weak self] datas in self?.didAddManagers(datas) } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift index 02a31097..eabcbfac 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMemberSelectController.swift @@ -24,7 +24,7 @@ open class FunTeamMemberSelectController: NEBaseTeamMemberSelectController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "\(indexPath.section)", for: indexPath) as! FunTeamMemberSelectCell - let member = viewmodel.showDatas[indexPath.row] + let member = viewModel.showDatas[indexPath.row] cell.configureMember(member) return cell } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift index 1976eeaa..a0705676 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamMembersController.swift @@ -3,6 +3,7 @@ // found in the LICENSE file. import NECommonKit +import NIMSDK import UIKit @objcMembers @@ -17,15 +18,15 @@ open class FunTeamMembersController: NEBaseTeamMembersController { override open func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .funTeamBackgroundColor - contentTable.register(FunTeamMemberCell.self, forCellReuseIdentifier: "\(FunTeamMemberCell.self)") - view.insertSubview(searchGrayBackView, belowSubview: back) + contentTableView.register(FunTeamMemberCell.self, forCellReuseIdentifier: "\(FunTeamMemberCell.self)") + view.insertSubview(searchGrayBackView, belowSubview: backView) NSLayoutConstraint.activate([ searchGrayBackView.leftAnchor.constraint(equalTo: view.leftAnchor), searchGrayBackView.rightAnchor.constraint(equalTo: view.rightAnchor), searchGrayBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - searchGrayBackView.bottomAnchor.constraint(equalTo: contentTable.topAnchor), + searchGrayBackView.bottomAnchor.constraint(equalTo: contentTableView.topAnchor), ]) - back.backgroundColor = UIColor.white + backView.backgroundColor = UIColor.white searchTextField.backgroundColor = UIColor.white emptyView.setEmptyImage(name: "fun_user_empty") @@ -39,19 +40,19 @@ open class FunTeamMembersController: NEBaseTeamMembersController { if let model = getRealModel(indexPath.row) { cell.configure(model) var isShowRemove = false - if isOwner(model.nimUser?.userId) { + if isOwner(model.nimUser?.user?.accountId) { cell.ownerLabel.isHidden = false cell.ownerLabel.text = localizable("team_owner") cell.setOwnerStyle() - } else if model.teamMember?.type == .manager { + } else if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { cell.ownerLabel.isHidden = false cell.ownerLabel.text = localizable("team_manager") cell.setManagerStyle() - if isOwner(IMKitClient.instance.imAccid()) { + if isOwner(IMKitClient.instance.account()) { isShowRemove = true } } else { - if isOwner(IMKitClient.instance.imAccid()) || viewmodel.currentMember?.type == .manager { + if isOwner(IMKitClient.instance.account()) || viewModel.currentMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { isShowRemove = true } cell.ownerLabel.isHidden = true @@ -59,14 +60,30 @@ open class FunTeamMembersController: NEBaseTeamMembersController { cell.index = indexPath.row cell.delegate = self cell.configure(model) - cell.removeBtn.isHidden = !isShowRemove + cell.removeButton.isHidden = !isShowRemove cell.removeLabel.isHidden = !isShowRemove + + if IMKitConfigCenter.shared.onlineStatusEnable { + cell.headerView.alpha = 0.5 + + if let accountId = model.nimUser?.user?.accountId { + if accountId == IMKitClient.instance.account() { + cell.headerView.alpha = 1.0 + } else if let event = viewModel.onLineEventDic[accountId] { + if event.value == NIMSubscribeEventOnlineValue.login.rawValue { + cell.headerView.alpha = 1.0 + } + } + } + } } + if isLastRow(indexPath.row) { cell.dividerLine.isHidden = true } else { cell.dividerLine.isHidden = false } + return cell } return UITableViewCell() @@ -78,11 +95,11 @@ open class FunTeamMembersController: NEBaseTeamMembersController { func isLastRow(_ index: Int) -> Bool { if let text = searchTextField.text, text.count > 0 { - if searchDatas.count - 1 == index { + if viewModel.searchDatas.count - 1 == index { return true } } - if viewmodel.datas.count - 1 == index { + if viewModel.datas.count - 1 == index { return true } return false diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamNameViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamNameViewController.swift index c4d43514..d49ffa52 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamNameViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamNameViewController.swift @@ -24,10 +24,10 @@ open class FunTeamNameViewController: NEBaseTeamNameViewController { ]) NSLayoutConstraint.activate([ - textView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), - textView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -32), - textView.centerYAnchor.constraint(equalTo: backView.centerYAnchor, constant: 0), - textView.heightAnchor.constraint(equalToConstant: 60), + textInputView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + textInputView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -32), + textInputView.centerYAnchor.constraint(equalTo: backView.centerYAnchor, constant: 0), + textInputView.heightAnchor.constraint(equalToConstant: 60), ]) NSLayoutConstraint.activate([ @@ -39,15 +39,15 @@ open class FunTeamNameViewController: NEBaseTeamNameViewController { } override open func disableSubmit() { - rightNavBtn.setTitleColor(.funTeamThemeDisableColor, for: .normal) - rightNavBtn.isEnabled = false + rightNavButton.setTitleColor(.funTeamThemeDisableColor, for: .normal) + rightNavButton.isEnabled = false navigationView.moreButton.setTitleColor(.funTeamThemeDisableColor, for: .normal) navigationView.moreButton.isEnabled = false } override open func enableSubmit() { - rightNavBtn.setTitleColor(.funTeamThemeColor, for: .normal) - rightNavBtn.isEnabled = true + rightNavButton.setTitleColor(.funTeamThemeColor, for: .normal) + rightNavButton.isEnabled = true navigationView.moreButton.setTitleColor(.funTeamThemeColor, for: .normal) navigationView.moreButton.isEnabled = true } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift index f9fe667e..c0522288 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/Controller/FunTeamSettingViewController.swift @@ -3,12 +3,63 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @objcMembers open class FunTeamSettingViewController: NEBaseTeamSettingViewController { + /// 顶部背景视图 + lazy var backView: UIView = { + let backView = UIView() + backView.frame = CGRect(x: 0, y: 0, width: NEConstant.screenWidth, height: 188) + return backView + }() + + /// 圆角视图 + lazy var cornerView: UIView = { + let cornerView = UIView() + return cornerView + }() + + /// 群信息跳转下一级页面指示箭头 + lazy var arrowImageView: UIImageView = { + let arrowImageView = UIImageView() + arrowImageView.translatesAutoresizingMaskIntoConstraints = false + arrowImageView.image = coreLoader.loadImage("arrowRight") + return arrowImageView + }() + + /// 分割线 + lazy var dividerLineView: UIView = { + let dividerLineView = UIView() + dividerLineView.translatesAutoresizingMaskIntoConstraints = false + dividerLineView.backgroundColor = NEConstant.hexRGB(0xF5F8FC) + return dividerLineView + }() + + /// 成员列表跳转下一级页面指示箭头 + public var memberArrowImageView: UIImageView = { + let memberArrowImageView = UIImageView() + memberArrowImageView.translatesAutoresizingMaskIntoConstraints = false + memberArrowImageView.image = coreLoader.loadImage("arrowRight") + return memberArrowImageView + }() + + /// 群成员列表按钮 + public var memberListButton: UIButton = { + let memberListButton = UIButton() + memberListButton.translatesAutoresizingMaskIntoConstraints = false + return memberListButton + }() + + /// 群信息页面跳转按钮 + public var infoButton: UIButton = { + let infoButton = UIButton() + infoButton.translatesAutoresizingMaskIntoConstraints = false + return infoButton + }() + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) className = "FunTeamSettingViewController" @@ -20,12 +71,12 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func reloadSectionData() { - viewmodel.sectionData.forEach { setionModel in - setionModel.cellModels.forEach { cellModel in + for setionModel in viewModel.sectionData { + for cellModel in setionModel.cellModels { cellModel.cornerType = .none if cellModel.type == SettingCellType.SettingSelectCell.rawValue { cellModel.rowHeight = 78 @@ -39,85 +90,61 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { override open func setupUI() { super.setupUI() view.backgroundColor = .funTeamBackgroundColor - teamHeader.layer.cornerRadius = 4.0 - addBtn.setImage(coreLoader.loadImage("fun_add"), for: .normal) + teamHeaderView.layer.cornerRadius = 4.0 + addButton.setImage(coreLoader.loadImage("fun_add"), for: .normal) navigationController?.navigationBar.backgroundColor = .white navigationView.backgroundColor = .white navigationView.titleBarBottomLine.isHidden = false } + /// 获取顶部 override open func getHeaderView() -> UIView { - let back = UIView() - back.frame = CGRect(x: 0, y: 0, width: NEConstant.screenWidth, height: 188) - let cornerView = UIView() - back.addSubview(cornerView) + backView.addSubview(cornerView) cornerView.backgroundColor = .white cornerView.clipsToBounds = true cornerView.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - cornerView.leftAnchor.constraint(equalTo: back.leftAnchor, constant: 0), - cornerView.rightAnchor.constraint(equalTo: back.rightAnchor, constant: 0), - cornerView.topAnchor.constraint(equalTo: back.topAnchor), - cornerView.bottomAnchor.constraint(equalTo: back.bottomAnchor), + cornerView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 0), + cornerView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: 0), + cornerView.topAnchor.constraint(equalTo: backView.topAnchor), + cornerView.bottomAnchor.constraint(equalTo: backView.bottomAnchor), ]) - cornerView.addSubview(teamHeader) + cornerView.addSubview(teamHeaderView) NSLayoutConstraint.activate([ - teamHeader.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16), - teamHeader.topAnchor.constraint(equalTo: cornerView.topAnchor, constant: 16), - teamHeader.widthAnchor.constraint(equalToConstant: 50), - teamHeader.heightAnchor.constraint(equalToConstant: 50), + teamHeaderView.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16), + teamHeaderView.topAnchor.constraint(equalTo: cornerView.topAnchor, constant: 16), + teamHeaderView.widthAnchor.constraint(equalToConstant: 50), + teamHeaderView.heightAnchor.constraint(equalToConstant: 50), ]) - if let url = viewmodel.teamInfoModel?.team?.avatarUrl, !url.isEmpty { - print("icon url : ", url) - teamHeader.sd_setImage(with: URL(string: url), completed: nil) - } else { - if let tid = teamId { - if let name = viewmodel.teamInfoModel?.team?.getShowName() { - teamHeader.setTitle(name) - } - teamHeader.backgroundColor = UIColor.colorWithString(string: "\(tid)") - } - } - teamNameLabel.text = viewmodel.teamInfoModel?.team?.getShowName() + setTeamHeaderInfo() cornerView.addSubview(teamNameLabel) NSLayoutConstraint.activate([ - teamNameLabel.leftAnchor.constraint(equalTo: teamHeader.rightAnchor, constant: 16), - teamNameLabel.centerYAnchor.constraint(equalTo: teamHeader.centerYAnchor), + teamNameLabel.leftAnchor.constraint(equalTo: teamHeaderView.rightAnchor, constant: 16), + teamNameLabel.centerYAnchor.constraint(equalTo: teamHeaderView.centerYAnchor), teamNameLabel.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -50), ]) - let arrow = UIImageView() - arrow.translatesAutoresizingMaskIntoConstraints = false - arrow.image = coreLoader.loadImage("arrowRight") - cornerView.addSubview(arrow) + cornerView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: teamHeader.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -16), + arrowImageView.centerYAnchor.constraint(equalTo: teamHeaderView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -16), ]) - let line = UIView() - line.translatesAutoresizingMaskIntoConstraints = false - line.backgroundColor = NEConstant.hexRGB(0xF5F8FC) - cornerView.addSubview(line) + cornerView.addSubview(dividerLineView) NSLayoutConstraint.activate([ - line.heightAnchor.constraint(equalToConstant: 1.0), - line.rightAnchor.constraint(equalTo: cornerView.rightAnchor), - line.leftAnchor.constraint(equalTo: teamHeader.leftAnchor, constant: 0), - line.topAnchor.constraint(equalTo: teamHeader.bottomAnchor, constant: 16.0), + dividerLineView.heightAnchor.constraint(equalToConstant: 1.0), + dividerLineView.rightAnchor.constraint(equalTo: cornerView.rightAnchor), + dividerLineView.leftAnchor.constraint(equalTo: teamHeaderView.leftAnchor, constant: 0), + dividerLineView.topAnchor.constraint(equalTo: teamHeaderView.bottomAnchor, constant: 16.0), ]) - let memberLabel = UILabel() - cornerView.addSubview(memberLabel) - memberLabel.translatesAutoresizingMaskIntoConstraints = false - memberLabel.textColor = NEConstant.hexRGB(0x333333) - memberLabel.font = NEConstant.defaultTextFont(16.0) cornerView.addSubview(memberLabel) NSLayoutConstraint.activate([ - memberLabel.leftAnchor.constraint(equalTo: line.leftAnchor), - memberLabel.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 16), + memberLabel.leftAnchor.constraint(equalTo: dividerLineView.leftAnchor), + memberLabel.topAnchor.constraint(equalTo: dividerLineView.bottomAnchor, constant: 16), ]) if teamSettingType == .Senior { @@ -126,73 +153,66 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { memberLabel.text = localizable("discuss_mebmer") } - let memberArrow = UIImageView() - cornerView.addSubview(memberArrow) - memberArrow.translatesAutoresizingMaskIntoConstraints = false - memberArrow.image = coreLoader.loadImage("arrowRight") + cornerView.addSubview(memberArrowImageView) NSLayoutConstraint.activate([ - memberArrow.rightAnchor.constraint(equalTo: arrow.rightAnchor, constant: 0), - memberArrow.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), + memberArrowImageView.rightAnchor.constraint(equalTo: arrowImageView.rightAnchor, constant: 0), + memberArrowImageView.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), ]) - let memberListBtn = UIButton() - cornerView.addSubview(memberListBtn) - memberListBtn.translatesAutoresizingMaskIntoConstraints = false + cornerView.addSubview(memberListButton) NSLayoutConstraint.activate([ - memberListBtn.leftAnchor.constraint(equalTo: memberLabel.leftAnchor), - memberListBtn.rightAnchor.constraint(equalTo: memberArrow.rightAnchor), - memberListBtn.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), - memberListBtn.heightAnchor.constraint(equalToConstant: 50), + memberListButton.leftAnchor.constraint(equalTo: memberLabel.leftAnchor), + memberListButton.rightAnchor.constraint(equalTo: memberArrowImageView.rightAnchor), + memberListButton.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), + memberListButton.heightAnchor.constraint(equalToConstant: 50), ]) - memberListBtn.addTarget(self, action: #selector(toMemberList), for: .touchUpInside) + memberListButton.addTarget(self, action: #selector(toMemberList), for: .touchUpInside) cornerView.addSubview(memberCountLabel) NSLayoutConstraint.activate([ - memberCountLabel.rightAnchor.constraint(equalTo: memberArrow.leftAnchor, constant: -8), - memberCountLabel.centerYAnchor.constraint(equalTo: memberArrow.centerYAnchor), + memberCountLabel.rightAnchor.constraint(equalTo: memberArrowImageView.leftAnchor, constant: -8), + memberCountLabel.centerYAnchor.constraint(equalTo: memberArrowImageView.centerYAnchor), ]) - memberCountLabel.text = "\(viewmodel.teamInfoModel?.team?.memberNumber ?? 0)" + memberCountLabel.text = "\(viewModel.teamInfoModel?.team?.memberCount ?? 0)" - cornerView.addSubview(addBtn) - addBtnWidth = addBtn.widthAnchor.constraint(equalToConstant: 36) - addBtnWidth?.isActive = true - addBtnLeftMargin = addBtn.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0) + cornerView.addSubview(addButton) + addButtonWidth = addButton.widthAnchor.constraint(equalToConstant: 36) + addButtonWidth?.isActive = true + addButtonLeftMargin = addButton.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0) NSLayoutConstraint.activate([ - addBtnLeftMargin!, - addBtn.topAnchor.constraint(equalTo: memberListBtn.bottomAnchor, constant: 0), + addButtonLeftMargin!, + addButton.topAnchor.constraint(equalTo: memberListButton.bottomAnchor, constant: 0), ]) - addBtn.addTarget(self, action: #selector(addUser), for: .touchUpInside) + addButton.addTarget(self, action: #selector(addUser), for: .touchUpInside) - if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, - let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, let member = viewmodel.memberInTeam, inviteMode == .manager, member.type != .manager { - addBtnWidth?.constant = 0 - addBtn.isHidden = true + if viewModel.isNormalTeam() == false, viewModel.isOwner() == false, + let inviteMode = viewModel.teamInfoModel?.team?.inviteMode, let member = viewModel.memberInTeam, inviteMode == .TEAM_INVITE_MODE_MANAGER, member.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + addButtonWidth?.constant = 0 + addButton.isHidden = true } setupUserInfoCollection(cornerView) - let infoBtn = UIButton() - infoBtn.translatesAutoresizingMaskIntoConstraints = false - cornerView.addSubview(infoBtn) + cornerView.addSubview(infoButton) NSLayoutConstraint.activate([ - infoBtn.leftAnchor.constraint(equalTo: teamHeader.leftAnchor), - infoBtn.topAnchor.constraint(equalTo: teamHeader.topAnchor), - infoBtn.bottomAnchor.constraint(equalTo: teamHeader.bottomAnchor), - infoBtn.rightAnchor.constraint(equalTo: arrow.rightAnchor), + infoButton.leftAnchor.constraint(equalTo: teamHeaderView.leftAnchor), + infoButton.topAnchor.constraint(equalTo: teamHeaderView.topAnchor), + infoButton.bottomAnchor.constraint(equalTo: teamHeaderView.bottomAnchor), + infoButton.rightAnchor.constraint(equalTo: arrowImageView.rightAnchor), ]) - infoBtn.addTarget(self, action: #selector(toInfoView), for: .touchUpInside) + infoButton.addTarget(self, action: #selector(toInfoView), for: .touchUpInside) - return back + return backView } override open func getFooterView() -> UIView? { guard let title = getBottomText() else { return nil } - let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) + let footerView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false - footer.addSubview(button) + footerView.addSubview(button) button.backgroundColor = .white button.clipsToBounds = true button.setTitleColor(NEConstant.hexRGB(0xE6605C), for: .normal) @@ -200,44 +220,44 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { button.setTitle(title, for: .normal) button.addTarget(self, action: #selector(removeTeamForMyself), for: .touchUpInside) NSLayoutConstraint.activate([ - button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 0), - button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: 0), - button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), + button.leftAnchor.constraint(equalTo: footerView.leftAnchor, constant: 0), + button.rightAnchor.constraint(equalTo: footerView.rightAnchor, constant: 0), + button.topAnchor.constraint(equalTo: footerView.topAnchor, constant: 12), button.heightAnchor.constraint(equalToConstant: 56), ]) - return footer + return footerView } override open func setupUserInfoCollection(_ cornerView: UIView) { - cornerView.addSubview(userinfoCollection) + cornerView.addSubview(userinfoCollectionView) NSLayoutConstraint.activate([ - userinfoCollection.leftAnchor.constraint(equalTo: addBtn.rightAnchor, constant: 16), - userinfoCollection.centerYAnchor.constraint(equalTo: addBtn.centerYAnchor), - userinfoCollection.rightAnchor.constraint( + userinfoCollectionView.leftAnchor.constraint(equalTo: addButton.rightAnchor, constant: 16), + userinfoCollectionView.centerYAnchor.constraint(equalTo: addButton.centerYAnchor), + userinfoCollectionView.rightAnchor.constraint( equalTo: cornerView.rightAnchor, constant: -16 ), - userinfoCollection.heightAnchor.constraint(equalToConstant: 36), + userinfoCollectionView.heightAnchor.constraint(equalToConstant: 36), ]) - userinfoCollection.register( + userinfoCollectionView.register( FunTeamUserCell.self, forCellWithReuseIdentifier: "\(FunTeamUserCell.self)" ) } override open func checkoutAddShowOrHide() { - if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, - let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, inviteMode == .manager { - if let member = viewmodel.memberInTeam, member.type == .manager { - addBtn.isHidden = false - addBtnWidth?.constant = 36.0 - addBtnLeftMargin?.constant = 16 + if viewModel.isNormalTeam() == false, viewModel.isOwner() == false, + let inviteMode = viewModel.teamInfoModel?.team?.inviteMode, inviteMode == .TEAM_INVITE_MODE_MANAGER { + if let member = viewModel.memberInTeam, member.memberRole == .TEAM_MEMBER_ROLE_MANAGER { + addButton.isHidden = false + addButtonWidth?.constant = 36.0 + addButtonLeftMargin?.constant = 16 checkMemberCountLimit() } else { - addBtn.isHidden = true - addBtnWidth?.constant = 0 - addBtnLeftMargin?.constant = 0 + addButton.isHidden = true + addButtonWidth?.constant = 0 + addButtonLeftMargin?.constant = 0 } } else { checkMemberCountLimit() @@ -245,29 +265,27 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { } func checkMemberCountLimit() { - if viewmodel.teamInfoModel?.team?.level == viewmodel.teamInfoModel?.team?.memberNumber { - addBtn.isHidden = true - addBtnWidth?.constant = 0 - addBtnLeftMargin?.constant = 0 + if viewModel.teamInfoModel?.team?.memberLimit == viewModel.teamInfoModel?.team?.memberCount { + addButton.isHidden = true + addButtonWidth?.constant = 0 + addButtonLeftMargin?.constant = 0 } else { - addBtn.isHidden = false - addBtnWidth?.constant = 36.0 - addBtnLeftMargin?.constant = 16 + addButton.isHidden = false + addButtonWidth?.constant = 36.0 + addButtonLeftMargin?.constant = 16 } } - // MARK: objc 方法 - override open func toInfoView() { - let info = FunTeamInfoViewController(team: viewmodel.teamInfoModel?.team) + let info = FunTeamInfoViewController(team: viewModel.teamInfoModel?.team) navigationController?.pushViewController(info, animated: true) } override open func didClickChangeNick() { let nick = FunTeamNameViewController() nick.type = .NickName - nick.team = viewmodel.teamInfoModel?.team - nick.teamMember = viewmodel.memberInTeam + nick.team = viewModel.teamInfoModel?.team + nick.teamMember = viewModel.memberInTeam navigationController?.pushViewController(nick, animated: true) } @@ -305,15 +323,14 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { } Router.shared.use( SearchMessageRouter, - parameters: ["nav": navigationController as Any, "teamId": tid], + parameters: ["nav": navigationController as Any, "teamId": tid, "teamInfo": viewModel.teamInfoModel as Any], closure: nil ) } override open func didClickTeamManage() { - let manageTeam = FunTeamManageController() - manageTeam.managerUsers = getManaterUsers() - manageTeam.viewmodel.teamInfoModel = viewmodel.teamInfoModel + let manageTeam = FunTeamManagerController() + manageTeam.viewModel.teamInfoModel = viewModel.teamInfoModel navigationController?.pushViewController(manageTeam, animated: true) } @@ -323,7 +340,7 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { withReuseIdentifier: "\(FunTeamUserCell.self)", for: indexPath ) as? FunTeamUserCell { - if let user = viewmodel.teamInfoModel?.users[indexPath.row] { + if let user = viewModel.teamInfoModel?.users[indexPath.row] { cell.user = user } return cell @@ -341,7 +358,7 @@ open class FunTeamSettingViewController: NEBaseTeamSettingViewController { } override open func toMemberList() { - let memberController = FunTeamMembersController(teamId: viewmodel.teamInfoModel?.team?.teamId) + let memberController = FunTeamMembersController(teamId: viewModel.teamInfoModel?.team?.teamId) navigationController?.pushViewController(memberController, animated: true) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamRouter.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamRouter.swift index f1e62df0..8cd03ce9 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamRouter.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamRouter.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK @@ -22,9 +22,11 @@ public extension TeamRouter { Router.shared.register(SearchMessageRouter) { param in let nav = param["nav"] as? UINavigationController - if let tid = param["teamId"] as? String { - let session = NIMSession(tid, type: .team) - let searchMsgCtrl = FunTeamHistoryMessageController(session: session) + if let teamId = param["teamId"] as? String { + let searchMsgCtrl = FunTeamHistoryMessageController(teamId: teamId) + if let info = param["teamInfo"] as? NETeamInfoModel { + searchMsgCtrl.viewModel.teamInfoModel = info + } nav?.pushViewController(searchMsgCtrl, animated: true) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamUIColor.swift b/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamUIColor.swift index 21e9376a..ee7b5515 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamUIColor.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/FunUI/FunTeamUIColor.swift @@ -7,10 +7,9 @@ import NECommonKit extension UIColor { static let funTeamMemberDividerLine = UIColor(hexString: "#E4E9F2") - static let funTeamMemberOwnerFlagColor = UIColor(hexString: "#58BE6B") static let funTeamMebmerSearchBg = UIColor(hexString: "0xF1F1F6") - static let funTeamThemeColor = UIColor(hexString: "#58BE6B") + static let funTeamThemeColor = UIColor.ne_funTheme static let funTeamThemeDisableColor = UIColor(hexString: "#58BE6B", 0.5) static let funTeamBackgroundColor = UIColor(hexString: "#EDEDED") static let funTeamLineBorderColor = UIColor(hexString: "#E5E5E5") diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/HistoryMessageCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/HistoryMessageCell.swift index 2c77cba5..421efc49 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/HistoryMessageCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/HistoryMessageCell.swift @@ -1,5 +1,6 @@ import NIMSDK + // Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. @@ -7,33 +8,33 @@ import UIKit @objcMembers open class HistoryMessageCell: NEBaseHistoryMessageCell { - override func setupSubviews() { + override open func setupSubviews() { super.setupSubviews() NSLayoutConstraint.activate([ - headImge.leftAnchor.constraint( + headView.leftAnchor.constraint( equalTo: contentView.leftAnchor, constant: NEConstant.screenInterval ), - headImge.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: -5), - headImge.widthAnchor.constraint(equalToConstant: 36), - headImge.heightAnchor.constraint(equalToConstant: 36), + headView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor, constant: -5), + headView.widthAnchor.constraint(equalToConstant: 36), + headView.heightAnchor.constraint(equalToConstant: 36), ]) NSLayoutConstraint.activate([ - title.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - title.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - title.topAnchor.constraint(equalTo: headImge.topAnchor), + titleLabel.leftAnchor.constraint(equalTo: headView.rightAnchor, constant: 12), + titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + titleLabel.topAnchor.constraint(equalTo: headView.topAnchor), ]) NSLayoutConstraint.activate([ - subTitle.leftAnchor.constraint(equalTo: headImge.rightAnchor, constant: 12), - subTitle.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), - subTitle.topAnchor.constraint(equalTo: title.bottomAnchor, constant: 6), + subTitleLabel.leftAnchor.constraint(equalTo: headView.rightAnchor, constant: 12), + subTitleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -50), + subTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6), ]) NSLayoutConstraint.activate([ bottomLine.rightAnchor.constraint(equalTo: contentView.rightAnchor), - bottomLine.leftAnchor.constraint(equalTo: headImge.leftAnchor), + bottomLine.leftAnchor.constraint(equalTo: headView.leftAnchor), bottomLine.bottomAnchor.constraint(equalTo: bottomAnchor), bottomLine.heightAnchor.constraint(equalToConstant: 0.5), ]) @@ -46,4 +47,12 @@ open class HistoryMessageCell: NEBaseHistoryMessageCell { timeLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } + + override open func configData(message: HistoryMessageModel?) { + super.configData(message: message) + guard let searchStr = searchText, let fullText = message?.imMessage?.text else { return } + let windowWidth = UIScreen.main.bounds.width + let maxWidth = windowWidth - NEConstant.screenInterval - 36 - 12 - 50 + truncateTextForLabel(subTitleLabel, maxWidth, searchStr, fullText) + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamArrowSettingCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamArrowSettingCell.swift index e14d218a..d918cb72 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamArrowSettingCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamArrowSettingCell.swift @@ -16,8 +16,8 @@ open class TeamArrowSettingCell: NEBaseTeamArrowSettingCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamDefaultIconCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamDefaultIconCell.swift index 04e15474..e2377abf 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamDefaultIconCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamDefaultIconCell.swift @@ -11,17 +11,17 @@ open class TeamDefaultIconCell: NEBaseTeamDefaultIconCell { override func setupUI() { super.setupUI() NSLayoutConstraint.activate([ - selectBack.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - selectBack.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - selectBack.widthAnchor.constraint(equalToConstant: 48.0), - selectBack.heightAnchor.constraint(equalToConstant: 48.0), + selectBackView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + selectBackView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + selectBackView.widthAnchor.constraint(equalToConstant: 48.0), + selectBackView.heightAnchor.constraint(equalToConstant: 48.0), ]) NSLayoutConstraint.activate([ - iconImage.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), - iconImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - iconImage.heightAnchor.constraint(equalToConstant: 32), - iconImage.widthAnchor.constraint(equalToConstant: 32), + iconImageView.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), + iconImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + iconImageView.heightAnchor.constraint(equalToConstant: 32), + iconImageView.widthAnchor.constraint(equalToConstant: 32), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingHeaderCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingHeaderCell.swift index 21dd5155..575545af 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingHeaderCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingHeaderCell.swift @@ -19,12 +19,12 @@ open class TeamSettingHeaderCell: NEBaseTeamSettingHeaderCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), ]) NSLayoutConstraint.activate([ - headerView.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), + headerView.centerYAnchor.constraint(equalTo: arrowView.centerYAnchor), headerView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -64.0), headerView.widthAnchor.constraint(equalToConstant: 42.0), headerView.heightAnchor.constraint(equalToConstant: 42.0), diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.swift index f4a52175..ff3b7752 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingLabelArrowCell.swift @@ -19,8 +19,8 @@ open class TeamSettingLabelArrowCell: TeamArrowSettingCell { super.setupUI() contentView.addSubview(arrowLabel) NSLayoutConstraint.activate([ - arrowLabel.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), - arrowLabel.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -4), + arrowLabel.centerYAnchor.constraint(equalTo: arrowView.centerYAnchor), + arrowLabel.rightAnchor.constraint(equalTo: arrowView.leftAnchor, constant: -4), ]) } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingSelectCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingSelectCell.swift index e5273e3a..4812feb3 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingSelectCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamSettingSelectCell.swift @@ -7,7 +7,7 @@ import UIKit @objcMembers open class TeamSettingSelectCell: NEBaseTeamSettingSelectCell { - override func setupUI() { + override open func setupUI() { super.setupUI() NSLayoutConstraint.activate([ titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 36), @@ -22,8 +22,8 @@ open class TeamSettingSelectCell: NEBaseTeamSettingSelectCell { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamUserCell.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamUserCell.swift index 28b23b25..cfaa8932 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamUserCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Cell/TeamUserCell.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @@ -13,12 +13,12 @@ import UIKit open class TeamUserCell: NEBaseTeamUserCell { override func setupUI() { super.setupUI() - userHeader.layer.cornerRadius = 16.0 + userHeaderView.layer.cornerRadius = 16.0 NSLayoutConstraint.activate([ - userHeader.leftAnchor.constraint(equalTo: contentView.leftAnchor), - userHeader.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - userHeader.widthAnchor.constraint(equalToConstant: 32.0), - userHeader.heightAnchor.constraint(equalToConstant: 32.0), + userHeaderView.leftAnchor.constraint(equalTo: contentView.leftAnchor), + userHeaderView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + userHeaderView.widthAnchor.constraint(equalToConstant: 32.0), + userHeaderView.heightAnchor.constraint(equalToConstant: 32.0), ]) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamAvatarViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamAvatarViewController.swift index 09e24d8c..346faf7d 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamAvatarViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamAvatarViewController.swift @@ -21,55 +21,55 @@ open class TeamAvatarViewController: NEBaseTeamAvatarViewController { navigationView.backButton.setTitleColor(.ne_greyText, for: .normal) navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor - headerBack.layer.cornerRadius = 8.0 + headerBackView.layer.cornerRadius = 8.0 NSLayoutConstraint.activate([ - headerBack.topAnchor.constraint(equalTo: view.topAnchor, constant: 12.0 + NEConstant.navigationAndStatusHeight), - headerBack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - headerBack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - headerBack.heightAnchor.constraint(equalToConstant: 128.0), + headerBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 12.0 + NEConstant.navigationAndStatusHeight), + headerBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + headerBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + headerBackView.heightAnchor.constraint(equalToConstant: 128.0), ]) NSLayoutConstraint.activate([ - photoImage.rightAnchor.constraint(equalTo: headerView.rightAnchor), - photoImage.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), + photoImageView.rightAnchor.constraint(equalTo: headerView.rightAnchor), + photoImageView.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), ]) let gesture = UITapGestureRecognizer() headerView.addGestureRecognizer(gesture) gesture.addTarget(self, action: #selector(uploadPhoto)) - defaultHeaderBack.layer.cornerRadius = 8.0 + defaultHeaderBackView.layer.cornerRadius = 8.0 NSLayoutConstraint.activate([ - defaultHeaderBack.leftAnchor.constraint(equalTo: headerBack.leftAnchor), - defaultHeaderBack.rightAnchor.constraint(equalTo: headerBack.rightAnchor), - defaultHeaderBack.topAnchor.constraint( - equalTo: headerBack.bottomAnchor, + defaultHeaderBackView.leftAnchor.constraint(equalTo: headerBackView.leftAnchor), + defaultHeaderBackView.rightAnchor.constraint(equalTo: headerBackView.rightAnchor), + defaultHeaderBackView.topAnchor.constraint( + equalTo: headerBackView.bottomAnchor, constant: 12.0 ), - defaultHeaderBack.heightAnchor.constraint(equalToConstant: 114.0), + defaultHeaderBackView.heightAnchor.constraint(equalToConstant: 114.0), ]) NSLayoutConstraint.activate([ - tag.leftAnchor.constraint(equalTo: defaultHeaderBack.leftAnchor, constant: 16.0), - tag.topAnchor.constraint(equalTo: defaultHeaderBack.topAnchor, constant: 15.0), + tagLabel.leftAnchor.constraint(equalTo: defaultHeaderBackView.leftAnchor, constant: 16.0), + tagLabel.topAnchor.constraint(equalTo: defaultHeaderBackView.topAnchor, constant: 15.0), ]) - iconCollection.register( + iconsCollectionView.register( TeamDefaultIconCell.self, forCellWithReuseIdentifier: "\(TeamDefaultIconCell.self)" ) NSLayoutConstraint.activate([ - iconCollection.topAnchor.constraint(equalTo: tag.bottomAnchor, constant: 16.0), - iconCollection.leftAnchor.constraint( - equalTo: defaultHeaderBack.leftAnchor, + iconsCollectionView.topAnchor.constraint(equalTo: tagLabel.bottomAnchor, constant: 16.0), + iconsCollectionView.leftAnchor.constraint( + equalTo: defaultHeaderBackView.leftAnchor, constant: 18 ), - iconCollection.rightAnchor.constraint( - equalTo: defaultHeaderBack.rightAnchor, + iconsCollectionView.rightAnchor.constraint( + equalTo: defaultHeaderBackView.rightAnchor, constant: -18.0 ), - iconCollection.heightAnchor.constraint(equalToConstant: 48.0), + iconsCollectionView.heightAnchor.constraint(equalToConstant: 48.0), ]) } @@ -79,8 +79,8 @@ open class TeamAvatarViewController: NEBaseTeamAvatarViewController { withReuseIdentifier: "\(TeamDefaultIconCell.self)", for: indexPath ) as? TeamDefaultIconCell { - cell.iconImage.image = coreLoader.loadImage("icon_\(indexPath.row)") - cell.iconImage.accessibilityIdentifier = "id.default\(indexPath.row + 1)" + cell.iconImageView.image = coreLoader.loadImage("icon_\(indexPath.row)") + cell.iconImageView.accessibilityIdentifier = "id.default\(indexPath.row + 1)" return cell } return UICollectionViewCell() diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift index 9ab49b1f..0ccf7ec0 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamHistoryMessageController.swift @@ -8,15 +8,15 @@ import UIKit @objcMembers open class TeamHistoryMessageController: NEBaseTeamHistoryMessageController { - override public init(session: NIMSession?) { - super.init(session: session) + override public init(teamId: String?) { + super.init(teamId: teamId) tag = "TeamHistoryMessageController" navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupSubviews() { @@ -49,7 +49,7 @@ open class TeamHistoryMessageController: NEBaseTeamHistoryMessageController { withIdentifier: "\(NSStringFromClass(HistoryMessageCell.self))", for: indexPath ) as! NEBaseHistoryMessageCell - let cellModel = viewmodel.searchResultInfos?[indexPath.row] + let cellModel = viewModel.searchResultInfos?[indexPath.row] cell.searchText = searchStr cell.configData(message: cellModel) return cell diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift index 04528f6a..70a3fb2d 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamInfoViewController.swift @@ -8,9 +8,9 @@ import UIKit @objcMembers open class TeamInfoViewController: NEBaseTeamInfoViewController { - override public init(team: NIMTeam?) { + override public init(team: V2NIMTeam?) { super.init(team: team) - cellClassDic = [ + registerCellDic = [ SettingCellType.SettingArrowCell.rawValue: TeamArrowSettingCell.self, SettingCellType.SettingHeaderCell.rawValue: TeamSettingHeaderCell.self, ] @@ -20,7 +20,7 @@ open class TeamInfoViewController: NEBaseTeamInfoViewController { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { @@ -32,7 +32,7 @@ open class TeamInfoViewController: NEBaseTeamInfoViewController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] if let cell = tableView.dequeueReusableCell( withIdentifier: "\(model.type)", for: indexPath @@ -44,15 +44,15 @@ open class TeamInfoViewController: NEBaseTeamInfoViewController { } override open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] if indexPath.row == 0 { let avatar = TeamAvatarViewController() avatar.team = team weak var weakSelf = self avatar.block = { if let t = weakSelf?.team { - weakSelf?.viewmodel.getData(t) - weakSelf?.contentTable.reloadData() + weakSelf?.viewModel.getData(t) + weakSelf?.contentTableView.reloadData() } } navigationController?.pushViewController(avatar, animated: true) @@ -72,7 +72,7 @@ open class TeamInfoViewController: NEBaseTeamInfoViewController { override open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.cellDatas[indexPath.row] + let model = viewModel.cellDatas[indexPath.row] return model.rowHeight } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManageController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManageController.swift deleted file mode 100644 index 9685464b..00000000 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManageController.swift +++ /dev/null @@ -1,55 +0,0 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NIMSDK -import UIKit - -@objcMembers -open class TeamManageController: NEBaseTeamManageController { - override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { - super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) - navigationView.backgroundColor = .ne_lightBackgroundColor - navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor - cellClassDic = [ - SettingCellType.SettingArrowCell.rawValue: TeamSettingLabelArrowCell.self, - SettingCellType.SettingSwitchCell.rawValue: TeamSettingSwitchCell.self, - SettingCellType.SettingSelectCell.rawValue: TeamSettingSelectCell.self, - ] - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override open func viewDidLoad() { - super.viewDidLoad() - } - - override open func getFooterView() -> UIView? { - let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) - let button = UIButton() - button.translatesAutoresizingMaskIntoConstraints = false - footer.addSubview(button) - button.backgroundColor = .white - button.clipsToBounds = true - button.setTitleColor(NEConstant.hexRGB(0xE6605C), for: .normal) - button.titleLabel?.font = NEConstant.defaultTextFont(16.0) - button.setTitle(localizable("transfer_owner"), for: .normal) - button.addTarget(self, action: #selector(transferOwner), for: .touchUpInside) - button.layer.cornerRadius = 8.0 - NSLayoutConstraint.activate([ - button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 20), - button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: -20), - button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), - button.heightAnchor.constraint(equalToConstant: 40), - ]) - return footer - } - - override open func didManagerClick() { - let controller = TeamManagerListController() - controller.teamId = viewmodel.teamInfoModel?.team?.teamId - navigationController?.pushViewController(controller, animated: true) - } -} diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerController.swift new file mode 100644 index 00000000..82568a93 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerController.swift @@ -0,0 +1,34 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NIMSDK +import UIKit + +@objcMembers +open class TeamManagerController: NEBaseTeamManagerController { + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + navigationView.backgroundColor = .ne_lightBackgroundColor + navigationController?.navigationBar.backgroundColor = .ne_lightBackgroundColor + cellClassDic = [ + SettingCellType.SettingArrowCell.rawValue: TeamSettingLabelArrowCell.self, + SettingCellType.SettingSwitchCell.rawValue: TeamSettingSwitchCell.self, + SettingCellType.SettingSelectCell.rawValue: TeamSettingSelectCell.self, + ] + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + } + + override open func didManagerClick() { + let controller = TeamManagerListController() + controller.teamId = viewModel.teamInfoModel?.team?.teamId + navigationController?.pushViewController(controller, animated: true) + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift index 7f367af2..49e001a0 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamManagerListController.swift @@ -16,6 +16,7 @@ open class TeamManagerListController: NEBaseTeamManagerListController { public lazy var emptyView: NEEmptyDataView = { let view = NEEmptyDataView(imageName: "user_empty", content: localizable("no_manager_member"), frame: CGRect.zero) view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false view.isHidden = true return view }() @@ -59,8 +60,8 @@ open class TeamManagerListController: NEBaseTeamManagerListController { cell.delegate = self cell.index = indexPath.row cell.configure(viewmodel.managers[indexPath.row]) - if let type = viewmodel.currentMember?.type, type == .manager { - cell.removeBtn.isHidden = true + if let type = viewmodel.currentMember?.memberRole, type == .TEAM_MEMBER_ROLE_MANAGER { + cell.removeButton.isHidden = true cell.removeLabel.isHidden = true } return cell diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.swift index 0333e5a6..d8f6e790 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMemberSelectController.swift @@ -23,7 +23,7 @@ open class TeamMemberSelectController: NEBaseTeamMemberSelectController { override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "\(indexPath.section)", for: indexPath) as! TeamMemberSelectCell - let member = viewmodel.showDatas[indexPath.row] + let member = viewModel.showDatas[indexPath.row] cell.configureMember(member) return cell } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.swift index 0e5c509e..5d400643 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamMembersController.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 NIMSDK import UIKit @objcMembers @@ -10,8 +11,8 @@ open class TeamMembersController: NEBaseTeamMembersController { super.viewDidLoad() navigationView.backgroundColor = .white navigationController?.navigationBar.backgroundColor = .white - back.backgroundColor = .ne_backcolor - contentTable.register(TeamMemberCell.self, forCellReuseIdentifier: "\(TeamMemberCell.self)") + backView.backgroundColor = .ne_backcolor + contentTableView.register(TeamMemberCell.self, forCellReuseIdentifier: "\(TeamMemberCell.self)") } override open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -21,19 +22,19 @@ open class TeamMembersController: NEBaseTeamMembersController { ) as? TeamMemberCell { if let model = getRealModel(indexPath.row) { var isShowRemove = false - if isOwner(model.nimUser?.userId) { + if isOwner(model.nimUser?.user?.accountId) { cell.ownerLabel.isHidden = false cell.ownerLabel.text = localizable("team_owner") cell.ownerWidth?.constant = 40 - } else if model.teamMember?.type == .manager { + } else if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { cell.ownerLabel.isHidden = false cell.ownerLabel.text = localizable("team_manager") cell.ownerWidth?.constant = 52 - if isOwner(IMKitClient.instance.imAccid()) { + if isOwner(IMKitClient.instance.account()) { isShowRemove = true } } else { - if isOwner(IMKitClient.instance.imAccid()) || viewmodel.currentMember?.type == .manager { + if isOwner(IMKitClient.instance.account()) || viewModel.currentMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { isShowRemove = true } cell.ownerLabel.isHidden = true @@ -41,9 +42,24 @@ open class TeamMembersController: NEBaseTeamMembersController { cell.index = indexPath.row cell.delegate = self cell.configure(model) - cell.removeBtn.isHidden = !isShowRemove + cell.removeButton.isHidden = !isShowRemove cell.removeLabel.isHidden = !isShowRemove + + if IMKitConfigCenter.shared.onlineStatusEnable { + cell.headerView.alpha = 0.5 + + if let account = model.nimUser?.user?.accountId { + if account == IMKitClient.instance.account() { + cell.headerView.alpha = 1.0 + } else if let event = viewModel.onLineEventDic[account] { + if event.value == NIMSubscribeEventOnlineValue.login.rawValue { + cell.headerView.alpha = 1.0 + } + } + } + } } + return cell } return UITableViewCell() diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamNameViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamNameViewController.swift index 09b03938..ac12ed85 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamNameViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamNameViewController.swift @@ -27,10 +27,10 @@ open class TeamNameViewController: NEBaseTeamNameViewController { ]) NSLayoutConstraint.activate([ - textView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), - textView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -32), - textView.topAnchor.constraint(equalTo: backView.topAnchor, constant: 0), - textView.heightAnchor.constraint(equalToConstant: 44), + textInputView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + textInputView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -40), + textInputView.topAnchor.constraint(equalTo: backView.topAnchor, constant: 0), + textInputView.heightAnchor.constraint(equalToConstant: 44), ]) NSLayoutConstraint.activate([ diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift index e60ab70d..67fb2011 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/Controller/TeamSettingViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -22,89 +22,116 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { ] } + /// 背景视图 + public lazy var backView: UIView = { + let backView = UIView() + backView.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: 172) + return backView + }() + + /// 圆角视图 + public lazy var cornerView: UIView = { + let cornerView = UIView() + cornerView.backgroundColor = .white + cornerView.clipsToBounds = true + cornerView.translatesAutoresizingMaskIntoConstraints = false + cornerView.layer.cornerRadius = 8.0 + return cornerView + }() + + /// 群信息跳转下一级页面指示箭头 + lazy var arrowImageView: UIImageView = { + let arrowImageView = UIImageView() + arrowImageView.translatesAutoresizingMaskIntoConstraints = false + arrowImageView.image = coreLoader.loadImage("arrowRight") + return arrowImageView + }() + + /// 分割线 + lazy var dividerLineView: UIView = { + let dividerLineView = UIView() + dividerLineView.translatesAutoresizingMaskIntoConstraints = false + dividerLineView.backgroundColor = NEConstant.hexRGB(0xF5F8FC) + return dividerLineView + }() + + /// 成员列表跳转下一级页面指示箭头 + public var memberArrowImageView: UIImageView = { + let memberArrowImageView = UIImageView() + memberArrowImageView.translatesAutoresizingMaskIntoConstraints = false + memberArrowImageView.image = coreLoader.loadImage("arrowRight") + return memberArrowImageView + }() + + /// 群成员列表按钮 + public var memberListButton: UIButton = { + let memberListButton = UIButton() + memberListButton.translatesAutoresizingMaskIntoConstraints = false + return memberListButton + }() + + /// 群信息页面跳转按钮 + public var infoButton: UIButton = { + let infoButton = UIButton() + infoButton.translatesAutoresizingMaskIntoConstraints = false + return infoButton + }() + public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func setupUI() { super.setupUI() - teamHeader.layer.cornerRadius = 21.0 - addBtn.setImage(coreLoader.loadImage("add"), for: .normal) + teamHeaderView.layer.cornerRadius = 21.0 + addButton.setImage(coreLoader.loadImage("add"), for: .normal) } + /// 获取顶部视图 override open func getHeaderView() -> UIView { - let back = UIView() - back.frame = CGRect(x: 0, y: 0, width: view.frame.size.width, height: 172) - let cornerView = UIView() - back.addSubview(cornerView) - cornerView.backgroundColor = .white - cornerView.clipsToBounds = true - cornerView.translatesAutoresizingMaskIntoConstraints = false - cornerView.layer.cornerRadius = 8.0 + backView.addSubview(cornerView) NSLayoutConstraint.activate([ - cornerView.leftAnchor.constraint(equalTo: back.leftAnchor, constant: 20), - cornerView.rightAnchor.constraint(equalTo: back.rightAnchor, constant: -20), - cornerView.bottomAnchor.constraint(equalTo: back.bottomAnchor), + cornerView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 20), + cornerView.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -20), + cornerView.bottomAnchor.constraint(equalTo: backView.bottomAnchor), cornerView.heightAnchor.constraint(equalToConstant: 160), ]) - cornerView.addSubview(teamHeader) + cornerView.addSubview(teamHeaderView) NSLayoutConstraint.activate([ - teamHeader.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16), - teamHeader.topAnchor.constraint(equalTo: cornerView.topAnchor, constant: 16), - teamHeader.widthAnchor.constraint(equalToConstant: 42), - teamHeader.heightAnchor.constraint(equalToConstant: 42), + teamHeaderView.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16), + teamHeaderView.topAnchor.constraint(equalTo: cornerView.topAnchor, constant: 16), + teamHeaderView.widthAnchor.constraint(equalToConstant: 42), + teamHeaderView.heightAnchor.constraint(equalToConstant: 42), ]) - if let url = viewmodel.teamInfoModel?.team?.avatarUrl, !url.isEmpty { - print("icon url : ", url) - teamHeader.sd_setImage(with: URL(string: url), completed: nil) - } else { - if let tid = teamId { - if let name = viewmodel.teamInfoModel?.team?.getShowName() { - teamHeader.setTitle(name) - } - teamHeader.backgroundColor = UIColor.colorWithString(string: "\(tid)") - } - } - teamNameLabel.text = viewmodel.teamInfoModel?.team?.getShowName() + setTeamHeaderInfo() cornerView.addSubview(teamNameLabel) NSLayoutConstraint.activate([ - teamNameLabel.leftAnchor.constraint(equalTo: teamHeader.rightAnchor, constant: 11), - teamNameLabel.centerYAnchor.constraint(equalTo: teamHeader.centerYAnchor), + teamNameLabel.leftAnchor.constraint(equalTo: teamHeaderView.rightAnchor, constant: 11), + teamNameLabel.centerYAnchor.constraint(equalTo: teamHeaderView.centerYAnchor), teamNameLabel.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -34), ]) - let arrow = UIImageView() - arrow.translatesAutoresizingMaskIntoConstraints = false - arrow.image = coreLoader.loadImage("arrowRight") - cornerView.addSubview(arrow) + cornerView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: teamHeader.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -16), + arrowImageView.centerYAnchor.constraint(equalTo: teamHeaderView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: cornerView.rightAnchor, constant: -16), ]) - let line = UIView() - line.translatesAutoresizingMaskIntoConstraints = false - line.backgroundColor = NEConstant.hexRGB(0xF5F8FC) - cornerView.addSubview(line) + cornerView.addSubview(dividerLineView) NSLayoutConstraint.activate([ - line.heightAnchor.constraint(equalToConstant: 1.0), - line.rightAnchor.constraint(equalTo: cornerView.rightAnchor), - line.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0), - line.topAnchor.constraint(equalTo: teamHeader.bottomAnchor, constant: 12.0), + dividerLineView.heightAnchor.constraint(equalToConstant: 1.0), + dividerLineView.rightAnchor.constraint(equalTo: cornerView.rightAnchor), + dividerLineView.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0), + dividerLineView.topAnchor.constraint(equalTo: teamHeaderView.bottomAnchor, constant: 12.0), ]) - let memberLabel = UILabel() - cornerView.addSubview(memberLabel) - memberLabel.translatesAutoresizingMaskIntoConstraints = false - memberLabel.textColor = NEConstant.hexRGB(0x333333) - memberLabel.font = NEConstant.defaultTextFont(16.0) cornerView.addSubview(memberLabel) NSLayoutConstraint.activate([ - memberLabel.leftAnchor.constraint(equalTo: line.leftAnchor), - memberLabel.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), + memberLabel.leftAnchor.constraint(equalTo: dividerLineView.leftAnchor), + memberLabel.topAnchor.constraint(equalTo: dividerLineView.bottomAnchor, constant: 12), ]) if teamSettingType == .Senior { @@ -113,78 +140,71 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { memberLabel.text = localizable("discuss_mebmer") } - let memberArrow = UIImageView() - cornerView.addSubview(memberArrow) - memberArrow.translatesAutoresizingMaskIntoConstraints = false - memberArrow.image = coreLoader.loadImage("arrowRight") + cornerView.addSubview(memberArrowImageView) NSLayoutConstraint.activate([ - memberArrow.rightAnchor.constraint(equalTo: arrow.rightAnchor), - memberArrow.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), + memberArrowImageView.rightAnchor.constraint(equalTo: arrowImageView.rightAnchor), + memberArrowImageView.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), ]) - let memberListBtn = UIButton() - memberListBtn.accessibilityIdentifier = "id.member" - cornerView.addSubview(memberListBtn) - memberListBtn.translatesAutoresizingMaskIntoConstraints = false + cornerView.addSubview(memberListButton) + memberListButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - memberListBtn.leftAnchor.constraint(equalTo: memberLabel.leftAnchor), - memberListBtn.rightAnchor.constraint(equalTo: memberArrow.rightAnchor), - memberListBtn.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), - memberListBtn.heightAnchor.constraint(equalToConstant: 40), + memberListButton.leftAnchor.constraint(equalTo: memberLabel.leftAnchor), + memberListButton.rightAnchor.constraint(equalTo: memberArrowImageView.rightAnchor), + memberListButton.centerYAnchor.constraint(equalTo: memberLabel.centerYAnchor), + memberListButton.heightAnchor.constraint(equalToConstant: 40), ]) - memberListBtn.addTarget(self, action: #selector(toMemberList), for: .touchUpInside) + memberListButton.addTarget(self, action: #selector(toMemberList), for: .touchUpInside) cornerView.addSubview(memberCountLabel) NSLayoutConstraint.activate([ - memberCountLabel.rightAnchor.constraint(equalTo: memberArrow.leftAnchor, constant: -2), - memberCountLabel.centerYAnchor.constraint(equalTo: memberArrow.centerYAnchor), + memberCountLabel.rightAnchor.constraint(equalTo: memberArrowImageView.leftAnchor, constant: -2), + memberCountLabel.centerYAnchor.constraint(equalTo: memberArrowImageView.centerYAnchor), ]) - memberCountLabel.text = "\(viewmodel.teamInfoModel?.team?.memberNumber ?? 0)" + memberCountLabel.text = "\(viewModel.teamInfoModel?.team?.memberCount ?? 0)" - cornerView.addSubview(addBtn) - addBtnWidth = addBtn.widthAnchor.constraint(equalToConstant: 32) - addBtnWidth?.isActive = true - addBtnLeftMargin = addBtn.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0) + cornerView.addSubview(addButton) + addButtonWidth = addButton.widthAnchor.constraint(equalToConstant: 32) + addButtonWidth?.isActive = true + addButtonLeftMargin = addButton.leftAnchor.constraint(equalTo: cornerView.leftAnchor, constant: 16.0) NSLayoutConstraint.activate([ - addBtnLeftMargin!, - addBtn.topAnchor.constraint(equalTo: memberLabel.bottomAnchor, constant: 12), + addButtonLeftMargin!, + addButton.topAnchor.constraint(equalTo: memberLabel.bottomAnchor, constant: 12), ]) - addBtn.addTarget(self, action: #selector(addUser), for: .touchUpInside) + addButton.addTarget(self, action: #selector(addUser), for: .touchUpInside) - if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, - let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, let member = viewmodel.memberInTeam, inviteMode == .manager, member.type != .manager { - addBtnWidth?.constant = 0 - addBtn.isHidden = true + if viewModel.isNormalTeam() == false, viewModel.isOwner() == false, + let inviteMode = viewModel.teamInfoModel?.team?.inviteMode, let member = viewModel.memberInTeam, inviteMode == .TEAM_INVITE_MODE_MANAGER, member.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + addButtonWidth?.constant = 0 + addButton.isHidden = true } setupUserInfoCollection(cornerView) - let infoBtn = UIButton() - infoBtn.translatesAutoresizingMaskIntoConstraints = false - cornerView.addSubview(infoBtn) + cornerView.addSubview(infoButton) NSLayoutConstraint.activate([ - infoBtn.leftAnchor.constraint(equalTo: teamHeader.leftAnchor), - infoBtn.topAnchor.constraint(equalTo: teamHeader.topAnchor), - infoBtn.bottomAnchor.constraint(equalTo: teamHeader.bottomAnchor), - infoBtn.rightAnchor.constraint(equalTo: arrow.rightAnchor), + infoButton.leftAnchor.constraint(equalTo: teamHeaderView.leftAnchor), + infoButton.topAnchor.constraint(equalTo: teamHeaderView.topAnchor), + infoButton.bottomAnchor.constraint(equalTo: teamHeaderView.bottomAnchor), + infoButton.rightAnchor.constraint(equalTo: arrowImageView.rightAnchor), ]) - infoBtn.addTarget(self, action: #selector(toInfoView), for: .touchUpInside) + infoButton.addTarget(self, action: #selector(toInfoView), for: .touchUpInside) - return back + return backView } override open func checkoutAddShowOrHide() { - if viewmodel.isNormalTeam() == false, viewmodel.isOwner() == false, - let inviteMode = viewmodel.teamInfoModel?.team?.inviteMode, inviteMode == .manager { - if let member = viewmodel.memberInTeam, member.type == .manager { - addBtn.isHidden = false - addBtnWidth?.constant = 36.0 - addBtnLeftMargin?.constant = 16 + if viewModel.isNormalTeam() == false, viewModel.isOwner() == false, + let inviteMode = viewModel.teamInfoModel?.team?.inviteMode, inviteMode == .TEAM_INVITE_MODE_MANAGER { + if let member = viewModel.memberInTeam, member.memberRole == .TEAM_MEMBER_ROLE_MANAGER { + addButton.isHidden = false + addButtonWidth?.constant = 36.0 + addButtonLeftMargin?.constant = 16 checkMemberCountLimit() } else { - addBtn.isHidden = true - addBtnWidth?.constant = 0 - addBtnLeftMargin?.constant = 0 + addButton.isHidden = true + addButtonWidth?.constant = 0 + addButtonLeftMargin?.constant = 0 } } else { checkMemberCountLimit() @@ -192,14 +212,14 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { } func checkMemberCountLimit() { - if viewmodel.teamInfoModel?.team?.level == viewmodel.teamInfoModel?.team?.memberNumber { - addBtn.isHidden = true - addBtnWidth?.constant = 0 - addBtnLeftMargin?.constant = 0 + if viewModel.teamInfoModel?.team?.memberLimit == viewModel.teamInfoModel?.team?.memberCount { + addButton.isHidden = true + addButtonWidth?.constant = 0 + addButtonLeftMargin?.constant = 0 } else { - addBtn.isHidden = false - addBtnWidth?.constant = 36.0 - addBtnLeftMargin?.constant = 16 + addButton.isHidden = false + addButtonWidth?.constant = 36.0 + addButtonLeftMargin?.constant = 16 } } @@ -207,10 +227,10 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { guard let title = getBottomText() else { return nil } - let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) + let footerView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false - footer.addSubview(button) + footerView.addSubview(button) button.backgroundColor = .white button.clipsToBounds = true button.setTitleColor(NEConstant.hexRGB(0xE6605C), for: .normal) @@ -219,27 +239,27 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { button.addTarget(self, action: #selector(removeTeamForMyself), for: .touchUpInside) button.layer.cornerRadius = 8.0 NSLayoutConstraint.activate([ - button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 20), - button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: -20), - button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), + button.leftAnchor.constraint(equalTo: footerView.leftAnchor, constant: 20), + button.rightAnchor.constraint(equalTo: footerView.rightAnchor, constant: -20), + button.topAnchor.constraint(equalTo: footerView.topAnchor, constant: 12), button.heightAnchor.constraint(equalToConstant: 40), ]) - return footer + return footerView } override open func setupUserInfoCollection(_ cornerView: UIView) { - cornerView.addSubview(userinfoCollection) + cornerView.addSubview(userinfoCollectionView) NSLayoutConstraint.activate([ - userinfoCollection.leftAnchor.constraint(equalTo: addBtn.rightAnchor, constant: 15), - userinfoCollection.centerYAnchor.constraint(equalTo: addBtn.centerYAnchor), - userinfoCollection.rightAnchor.constraint( + userinfoCollectionView.leftAnchor.constraint(equalTo: addButton.rightAnchor, constant: 15), + userinfoCollectionView.centerYAnchor.constraint(equalTo: addButton.centerYAnchor), + userinfoCollectionView.rightAnchor.constraint( equalTo: cornerView.rightAnchor, constant: -15 ), - userinfoCollection.heightAnchor.constraint(equalToConstant: 32), + userinfoCollectionView.heightAnchor.constraint(equalToConstant: 32), ]) - userinfoCollection.register( + userinfoCollectionView.register( TeamUserCell.self, forCellWithReuseIdentifier: "\(TeamUserCell.self)" ) @@ -248,15 +268,15 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { // MARK: objc 方法 override open func toInfoView() { - let info = TeamInfoViewController(team: viewmodel.teamInfoModel?.team) + let info = TeamInfoViewController(team: viewModel.teamInfoModel?.team) navigationController?.pushViewController(info, animated: true) } override open func didClickChangeNick() { let nick = TeamNameViewController() nick.type = .NickName - nick.team = viewmodel.teamInfoModel?.team - nick.teamMember = viewmodel.memberInTeam + nick.team = viewModel.teamInfoModel?.team + nick.teamMember = viewModel.memberInTeam navigationController?.pushViewController(nick, animated: true) } @@ -266,15 +286,14 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { } Router.shared.use( SearchMessageRouter, - parameters: ["nav": navigationController as Any, "teamId": tid], + parameters: ["nav": navigationController as Any, "teamId": tid, "teamInfo": viewModel.teamInfoModel as Any], closure: nil ) } override open func didClickTeamManage() { - let manageTeam = TeamManageController() - manageTeam.managerUsers = getManaterUsers() - manageTeam.viewmodel.teamInfoModel = viewmodel.teamInfoModel + let manageTeam = TeamManagerController() + manageTeam.viewModel.teamInfoModel = viewModel.teamInfoModel navigationController?.pushViewController(manageTeam, animated: true) } @@ -284,8 +303,9 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { withReuseIdentifier: "\(TeamUserCell.self)", for: indexPath ) as? TeamUserCell { - if let user = viewmodel.teamInfoModel?.users[indexPath.row] { - if let userId = user.nimUser?.userId, let nimUser = ChatUserCache.getUserInfo(userId) { + if let user = viewModel.teamInfoModel?.users[indexPath.row] { + // 从缓存中获取用户信息 + if let userId = user.nimUser?.user?.accountId, let nimUser = NEFriendUserCache.shared.getFriendInfo(userId) { user.nimUser = nimUser } cell.user = user @@ -302,7 +322,7 @@ open class TeamSettingViewController: NEBaseTeamSettingViewController { } override open func toMemberList() { - let memberController = TeamMembersController(teamId: viewmodel.teamInfoModel?.team?.teamId) + let memberController = TeamMembersController(teamId: viewModel.teamInfoModel?.team?.teamId) navigationController?.pushViewController(memberController, animated: true) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamRouter.swift b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamRouter.swift index fb5d3f14..dd44b9b7 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamRouter.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/NormalUI/NormalTeamRouter.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK @@ -22,9 +22,11 @@ public extension TeamRouter { Router.shared.register(SearchMessageRouter) { param in let nav = param["nav"] as? UINavigationController - if let tid = param["teamId"] as? String { - let session = NIMSession(tid, type: .team) - let searchMsgCtrl = TeamHistoryMessageController(session: session) + if let teamId = param["teamId"] as? String { + let searchMsgCtrl = TeamHistoryMessageController(teamId: teamId) + if let info = param["teamInfo"] as? NETeamInfoModel { + searchMsgCtrl.viewModel.teamInfoModel = info + } nav?.pushViewController(searchMsgCtrl, animated: true) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/Common/NETeamMemberCache.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/Common/NETeamMemberCache.swift new file mode 100644 index 00000000..d1accdc9 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/Common/NETeamMemberCache.swift @@ -0,0 +1,394 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NECoreIM2Kit +import NIMSDK +import UIKit + +@objc public protocol NETeamMemberCacheListener: NSObjectProtocol { + /// 缓存变更回调协议 + @objc optional func memberCacheDidChange() +} + +@objc +@objcMembers +open class NETeamMemberCache: NSObject, NETeamListener, NEIMKitClientListener, NEContactListener { + private let teamMemberCacheMultiDelegate = MultiDelegate(strongReferences: false) + + public static let shared = NETeamMemberCache() + /// 当前缓存的群id + var currentTeamId = "" + + /// 群模块API单例 + let teamRepo = TeamRepo.shared + + /// 通讯录API单例 + let contactRepo = ContactRepo.shared + + /// kit client 单例 + let client = IMKitClient.instance + + /// 缓存 + private var cacheDic = [String: NETeamMemberInfoModel]() + + /// 清理缓存定时器 + var timer: Timer? + + /// 初始化 + override private init() { + super.init() + teamRepo.addTeamListener(self) + client.addLoginListener(self) + contactRepo.addContactListener(self) + + NotificationCenter.default.addObserver(self, selector: #selector(appDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive), name: UIApplication.willResignActiveNotification, object: nil) + } + + deinit { + teamRepo.removeTeamListener(self) + client.removeLoginListener(self) + contactRepo.removeContactListener(self) + + NotificationCenter.default.removeObserver(self) + } + + /// 添加缓存监听 + /// - Parameter listener: 缓存监听 + public func addTeamCacheListener(_ listener: NETeamMemberCacheListener) { + teamMemberCacheMultiDelegate.addDelegate(listener) + } + + /// 移除缓存监听 + /// - Parameter listener: 缓存监听 + public func removeTeamCacheListener(_ listener: NETeamMemberCacheListener) { + teamMemberCacheMultiDelegate.removeDelegate(listener) + } + + /// 应用进入后台时执行的操作 + func appDidEnterBackground() { + clearCache() + } + + /// 应用即将进入后台(包括锁屏) + func appWillResignActive() { + clearCache() + } + + /// 设置缓存(对一个新群设置缓存会移除之前群的缓存,单例只保存一个群的成员缓存) + /// - Parameter teamId: 群id + /// - Parameter members: 群成员数据对象列表 + public func setCacheMembers(_ teamId: String, _ members: [NETeamMemberInfoModel]) { + cacheDic.removeAll() + currentTeamId = teamId + endTimer() + for model in members { + if let accountId = model.teamMember?.accountId { + cacheDic[accountId] = model + } + } + } + + /// 获取缓存 + /// - Parameter teamId: 群id + /// - returns 群成员缓存数据(可能为空) + public func getTeamMemberCache(_ teamId: String) -> [NETeamMemberInfoModel]? { + if currentTeamId == teamId { + var allCacheMembers = Array(cacheDic.values) + allCacheMembers.sort { model1, model2 in + if let time1 = model1.teamMember?.joinTime, let time2 = model2.teamMember?.joinTime { + return time2 > time1 + } + return false + } + if allCacheMembers.count > 0 { + return allCacheMembers + } + } + return nil + } + + /// 好友信息更新 + /// - Parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + if let account = friendInfo.accountId, let model = cacheDic[account] { + model.nimUser = NEUserWithFriend(friend: friendInfo) + updateFinish() + } + } + + /// 用户信息变更 + /// - Parameter users: 变更用户 + public func onUserProfileChanged(_ users: [V2NIMUser]) { + NEALog.infoLog(className(), desc: #function + " onUserProfileChanged count : \(users.count)") + var needUpdate = false + for user in users { + if let accout = user.accountId, let model = cacheDic[accout] { + model.nimUser = NEUserWithFriend(user: user) + needUpdate = true + } + } + if needUpdate == true { + updateFinish() + } + } + + /// 好友删除 + /// - parameter accountId: 账号id + /// - parameter deletionType: 删除类型 + public func onFriendDeleted(_ accountId: String, deletionType: V2NIMFriendDeletionType) { + if let model = cacheDic[accountId] { + model.nimUser?.friend = nil + } + } + + /// 登录状态改变 + /// - Parameter status: 登录状态枚举 + public func onLoginStatus(_ status: V2NIMLoginStatus) { + if status == .LOGIN_STATUS_LOGOUT { + clearCache() + } + } + + /// 加入回调 + public func onTeamJoined(_ team: V2NIMTeam) { + if team.teamId == currentTeamId { + clearCache() + updateFinish() + } + } + + /// 群成员离开回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + onMemberDidRemove(teamMembers) + } + + /// 群成员被踢回调 + /// - Parameter operatorAccountId: 操作者id + /// - Parameter teamMembers: 群成员 + public func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + onMemberDidRemove(teamMembers) + } + + /// 群成员加入回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { + onMemberDidAdd(teamMembers) + } + + /// 群成员更新回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + onMemberDidChanged(teamMembers) + } + + /// 群聊解散回调 + /// - Parameter team: 群对象 + public func onTeamDismissed(_ team: V2NIMTeam) { + if team.teamId == currentTeamId { + clearCache() + } + } + + /// 群成员变更统一处理 + /// - Parameter teamMembers: 群成员 + private func onMemberDidChanged(_ members: [V2NIMTeamMember]) { + for member in members { + if currentTeamId != member.teamId { + continue + } + if let model = cacheDic[member.accountId] { + model.teamMember = member + } + } + updateFinish() + } + + /// 群成员减少统一处理处理 + /// - Parameter teamMembers: 群成员 + private func onMemberDidRemove(_ members: [V2NIMTeamMember]) { + for member in members { + if currentTeamId != member.teamId { + continue + } + cacheDic.removeValue(forKey: member.accountId) + updateFinish() + } + } + + /// 群成员增加统一处理 + /// - Parameter teamMembers: 群成员 + private func onMemberDidAdd(_ members: [V2NIMTeamMember]) { + var allMembmers = [V2NIMTeamMember]() + for member in members { + if currentTeamId != member.teamId { + continue + } + NEALog.infoLog(className(), desc: "team cache did add member \(member.teamNick ?? ""))") + let model = NETeamMemberInfoModel() + model.teamMember = member + cacheDic[member.accountId] = model + allMembmers.append(member) + } + if allMembmers.count > 0 { + let accids = allMembmers.map(\.accountId) + contactRepo.getUserWithFriend(accountIds: accids) { [weak self] users, error in + users?.forEach { user in + if let accountId = user.user?.accountId { + self?.cacheDic[accountId]?.nimUser = user + } + } + self?.updateFinish() + } + } + } + + /// 清除缓存 + public func clearCache() { + currentTeamId = "" + cacheDic.removeAll() + endTimer() + } + + /// 定时移除缓存 + public func triggerClearCache() { + NEALog.infoLog(className(), desc: "triggerClearCache") + clearCache() + } + + /// 启动定时器 + func trigerTimer() { + timer = Timer.scheduledTimer(timeInterval: 300, target: self, selector: #selector(triggerClearCache), userInfo: nil, repeats: false) + } + + /// 停止定时器 + func endTimer() { + timer?.invalidate() + timer = nil + } + + public func updateFinish() { + teamMemberCacheMultiDelegate |> { delegate in + delegate.memberCacheDidChange?() + } + } + + /// 获取所有群成员信息 + /// - Parameter teamId: 群id + /// - Parameter queryType: 查询类型 + /// - Parameter completion: 完成后的回调 + public func getAllTeamMemberInfos(_ teamId: String, + _ queryType: V2NIMTeamMemberRoleQueryType, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamid:\(teamId)") + var memberLists = [V2NIMTeamMember]() + weak var weakSelf = self + getAllTeamMemberWithMaxLimit(teamId, nil, &memberLists, queryType) { members, error in + if let err = error { + completion(err, nil) + } else { + if let teamMembers = members { + let teamInfo = NETeamInfoModel() + weakSelf?.splitMembers(teamMembers, teamInfo) { error, retTeamInfo in + completion(error, retTeamInfo) + } + } + } + } + } + + /// 获取群成员(使用最大分页参数,防止触发频控) + /// - Parameter teamId: 群ID + /// - Parameter completion: 完成回调 + public func getAllTeamMemberWithMaxLimit(_ teamId: String, _ nextToken: String? = nil, _ memberList: inout [V2NIMTeamMember], _ queryType: V2NIMTeamMemberRoleQueryType, _ completion: @escaping ([V2NIMTeamMember]?, NSError?) -> Void) { + NEALog.infoLog(className(), desc: #function + " teamId : \(teamId)") + let option = V2NIMTeamMemberQueryOption() + option.direction = .QUERY_DIRECTION_ASC + option.limit = 1000 + option.onlyChatBanned = false + option.roleQueryType = queryType + if let token = nextToken { + option.nextToken = token + } else { + option.nextToken = "" + } + var temMemberLists = memberList + teamRepo.getTeamMemberList(teamId, .TEAM_TYPE_NORMAL, option) { [weak self] result, error in + if let err = error { + completion(nil, err) + } else { + if let members = result?.memberList { + temMemberLists.append(contentsOf: members) + } + if let finished = result?.finished { + if finished == true { + completion(temMemberLists, nil) + } else { + self?.getAllTeamMemberWithMaxLimit(teamId, result?.nextToken, &temMemberLists, queryType, completion) + } + } + } + } + } + + /// 分页查询群成员信息 + /// - Parameter members: 要查询的群成员列表 + /// - Parameter model : 群信息 + /// - Parameter maxSizeByPage: 单页最大查询数量 + /// - Parameter completion: 完成后的回调 + private func splitMembers(_ members: [V2NIMTeamMember], + _ model: NETeamInfoModel, + _ maxSizeByPage: Int = 150, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", members.count:\(members.count)") + var remaind = [[V2NIMTeamMember]]() + remaind.append(contentsOf: members.chunk(maxSizeByPage)) + fetchTeamMemberUserInfos(&remaind, model, completion) + } + + /// 从云信服务器批量获取用户资料 + /// - Parameter remainUserIds: 用户集合 + /// - Parameter completion: 成功回调 + private func fetchTeamMemberUserInfos(_ remainUserIds: inout [[V2NIMTeamMember]], + _ model: NETeamInfoModel, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", remainUserIds.count:\(remainUserIds.count)") + guard let members = remainUserIds.first else { + completion(nil, model) + return + } + + let accids = members.map(\.accountId) + var temArray = remainUserIds + weak var weakSelf = self + + contactRepo.getUserWithFriend(accountIds: accids) { infos, v2Error in + if let err = v2Error { + completion(err as NSError, model) + } else { + if let users = infos { + for index in 0 ..< members.count { + let memberInfoModel = NETeamMemberInfoModel() + memberInfoModel.teamMember = members[index] + if users.count > index { + let user = users[index] + memberInfoModel.nimUser = user + } + model.users.append(memberInfoModel) + } + } + temArray.removeFirst() + weakSelf?.fetchTeamMemberUserInfos(&temArray, model, completion) + } + } + } + + /// 群信息同步完成回调 + public func onTeamSyncFinished() { + NEALog.infoLog(className(), desc: #function + " onTeamSyncFinished call back happen") + clearCache() + updateFinish() + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift index 8a25f8c0..38636cce 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/NESelectTeamMember.swift @@ -8,5 +8,5 @@ import UIKit @objcMembers open class NESelectTeamMember: NSObject { var isSelected: Bool = false - var member: TeamMemberInfoModel? + var member: NETeamMemberInfoModel? } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift index 608f1dff..92d1b777 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/Model/SettingSectionModel.swift @@ -12,7 +12,7 @@ open class SettingSectionModel: NSObject { // 设置圆角 open func setCornerType() { - cellModels.forEach { model in + for model in cellModels { if model == cellModels.first { model.cornerType = .topLeft.union(.topRight) if model == cellModels.last { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamAvatarViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamAvatarViewController.swift new file mode 100644 index 00000000..ca9b0e6d --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamAvatarViewController.swift @@ -0,0 +1,263 @@ + +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NECommonUIKit +import NIMSDK +import UIKit + +@objcMembers +open class NEBaseTeamAvatarViewController: NEBaseViewController, UICollectionViewDelegate, + UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate { + public typealias SaveCompletion = () -> Void + public var block: SaveCompletion? + public var team: V2NIMTeam? + public let repo = TeamRepo.shared + + /// 头像背景 + public let headerBackView = UIView() + /// 相机提示图片 + public let photoImageView = UIImageView() + /// 缺省背景 + public let defaultHeaderBackView = UIView() + + public let tagLabel = UILabel() + + public var iconUrls = TeamRouter.iconUrls + + public var viewModel = TeamAvatarViewModel() + + public lazy var headerView: NEUserHeaderView = { + let header = NEUserHeaderView(frame: .zero) + header.translatesAutoresizingMaskIntoConstraints = false + header.clipsToBounds = true + header.isUserInteractionEnabled = true + header.accessibilityIdentifier = "id.icon" + return header + }() + + public var headerUrl = "" + + /// 群头像点击按钮 + public lazy var clickButton: UIButton = { + let clickButton = UIButton(type: .custom) + clickButton.translatesAutoresizingMaskIntoConstraints = false + clickButton.backgroundColor = .clear + return clickButton + }() + + public lazy var iconsCollectionView: UICollectionView = { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + flowLayout.minimumLineSpacing = 0 + flowLayout.minimumInteritemSpacing = 0 + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.backgroundColor = .clear + collectionView.showsHorizontalScrollIndicator = false + collectionView.showsVerticalScrollIndicator = false + collectionView.clipsToBounds = false + collectionView.isScrollEnabled = false + return collectionView + }() + + override open func viewDidLoad() { + super.viewDidLoad() + weak var weakSelf = self + viewModel.getCurrentUserTeamMember(team?.teamId) { error in + weakSelf?.setupUI() + if let err = error { + weakSelf?.view.makeToast(err.localizedDescription) + } + } + } + + /// UI 初始化 + open func setupUI() { + title = localizable("modify_headImage") + addRightAction(localizable("save"), #selector(savePhoto), self) + navigationView.setMoreButtonTitle(localizable("save")) + navigationView.addMoreButtonTarget(target: self, selector: #selector(savePhoto)) + + view.backgroundColor = .ne_lightBackgroundColor + + headerBackView.backgroundColor = .white + headerBackView.clipsToBounds = true + headerBackView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(headerBackView) + + /// 当前头像视图背景 + headerBackView.addSubview(headerView) + NSLayoutConstraint.activate([ + headerView.centerXAnchor.constraint(equalTo: headerBackView.centerXAnchor), + headerView.centerYAnchor.constraint(equalTo: headerBackView.centerYAnchor), + headerView.heightAnchor.constraint(equalToConstant: 80.0), + headerView.widthAnchor.constraint(equalToConstant: 80.0), + ]) + if let url = team?.avatar, !url.isEmpty { + headerView.sd_setImage(with: URL(string: url), completed: nil) + headerUrl = url + } + + photoImageView.translatesAutoresizingMaskIntoConstraints = false + photoImageView.image = coreLoader.loadImage("photo") + photoImageView.accessibilityIdentifier = "id.camera" + headerBackView.addSubview(photoImageView) + + headerBackView.addSubview(clickButton) + clickButton.addTarget(self, action: #selector(uploadPhoto), for: .touchUpInside) + NSLayoutConstraint.activate([ + clickButton.leftAnchor.constraint(equalTo: headerView.leftAnchor), + clickButton.topAnchor.constraint(equalTo: headerView.topAnchor), + clickButton.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), + clickButton.rightAnchor.constraint(equalTo: headerView.rightAnchor, constant: 10), + ]) + + defaultHeaderBackView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(defaultHeaderBackView) + defaultHeaderBackView.clipsToBounds = true + defaultHeaderBackView.backgroundColor = .white + + tagLabel.translatesAutoresizingMaskIntoConstraints = false + tagLabel.text = localizable("default_icon") + tagLabel.font = NEConstant.defaultTextFont(16.0) + tagLabel.textColor = NEConstant.hexRGB(0x333333) + defaultHeaderBackView.addSubview(tagLabel) + + defaultHeaderBackView.addSubview(iconsCollectionView) + + for index in 0 ..< iconUrls.count { + let url = iconUrls[index] + if url == headerUrl { + let indexPath = IndexPath(row: index, section: 0) + iconsCollectionView.selectItem(at: indexPath, animated: false, scrollPosition: .right) + } + } + + /// 判断权限决定是否展示保存按钮 + if getChangePermission() == false { + rightNavButton.isHidden = true + navigationView.moreButton.isHidden = true + photoImageView.isHidden = true + defaultHeaderBackView.isHidden = true + } + } + + /// 获取当前是否有修改权限 + func getChangePermission() -> Bool { + if let ownerId = team?.ownerAccountId, IMKitClient.instance.isMe(ownerId) { + return true + } + if let mode = team?.updateInfoMode, mode == .TEAM_UPDATE_INFO_MODE_ALL { + return true + } + if let member = viewModel.currentTeamMember, member.memberRole == .TEAM_MEMBER_ROLE_MANAGER { + return true + } + return false + } + + open func uploadPhoto() { + if getChangePermission() { + showBottomAlert(self) + } + } + + /// 保存相册 + open func savePhoto() { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + + if let tid = team?.teamId { + view.makeToastActivity(.center) + weak var weakSelf = self + weakSelf?.viewModel.updateTeamAvatar(headerUrl, tid, nil) { error in + NEALog.infoLog(ModuleName + " " + self.className(), desc: #function + "CALLBACK " + (error?.localizedDescription ?? "no error")) + weakSelf?.view.hideToastActivity() + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if error?.code == noPermissionOperationCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + if let completion = weakSelf?.block { + completion() + } + weakSelf?.navigationController?.popViewController(animated: true) + } + } + } + } + + open func collectionView(_ collectionView: UICollectionView, + numberOfItemsInSection section: Int) -> Int { + 5 + } + + open func collectionView(_ collectionView: UICollectionView, + cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { + UICollectionViewCell() + } + + open func collectionView(_ collectionView: UICollectionView, + layout collectionViewLayout: UICollectionViewLayout, + sizeForItemAt indexPath: IndexPath) -> CGSize { + .zero + } + + open func collectionView(_ collectionView: UICollectionView, + didSelectItemAt indexPath: IndexPath) { + if iconUrls.count > indexPath.row { + headerUrl = iconUrls[indexPath.row] + headerView.sd_setImage(with: URL(string: headerUrl), completed: nil) + } + } + + open func imagePickerController(_ picker: UIImagePickerController, + didFinishPickingMediaWithInfo info: [UIImagePickerController + .InfoKey: Any]) { + let image: UIImage = info[UIImagePickerController.InfoKey.editedImage] as! UIImage + uploadHeadImage(image: image) + picker.dismiss(animated: true, completion: nil) + } + + /// 上传头像 + /// - Parameter image: 头像 + open func uploadHeadImage(image: UIImage) { + weak var weakSelf = self + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + view.makeToastActivity(.center) + if let imageData = image.jpegData(compressionQuality: 0.6) as NSData?, + var filePath = NEPathUtils.getDirectoryForDocuments(dir: "\(imkitDir)image/") { + filePath += "\(team?.teamId ?? "team")_avatar.jpg" + let succcess = imageData.write(toFile: filePath, atomically: true) + if succcess { + let fileTask = viewModel.createTask(filePath) + viewModel.uploadImageFile(fileTask, nil) { urlString, error in + if error == nil { + // 显示设置的照片 + weakSelf?.headerView.image = image + if let url = urlString { + weakSelf?.headerUrl = url + } + print("upload image success") + } else { + print("upload image failed,error = \(error!)") + } + weakSelf?.view.hideToastActivity() + } + } + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift index 22af08ea..ee11f73d 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamHistoryMessageController.swift @@ -3,51 +3,21 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NECommonKit import NIMSDK import UIKit @objcMembers open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextFieldDelegate, UITableViewDelegate, UITableViewDataSource { - public let viewmodel = TeamSettingViewModel() - public var teamSession: NIMSession? + public let viewModel = TeamHistoryMessageViewModel() + + /// 群id + public var teamId: String? public var searchStr = "" var tag = "TeamHistoryMessageController" - public init(session: NIMSession?) { - super.init(nibName: nil, bundle: nil) - teamSession = session - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override open func viewDidLoad() { - super.viewDidLoad() - setupSubviews() - initialConfig() - } - - open func setupSubviews() { - view.addSubview(tableView) - view.addSubview(searchTextField) - view.addSubview(emptyView) - - NSLayoutConstraint.activate([ - emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), - emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), - emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), - emptyView.topAnchor.constraint(equalTo: tableView.topAnchor), - ]) - } - - open func initialConfig() { - title = localizable("historical_record") - } - - // MARK: lazy method - + /// 历史消息列表 public lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) tableView.translatesAutoresizingMaskIntoConstraints = false @@ -66,6 +36,7 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField return tableView }() + /// 搜索文本框 public lazy var searchTextField: SearchTextField = { let textField = SearchTextField() let leftImageView = UIImageView(image: coreLoader.loadImage("search_icon")) @@ -80,8 +51,9 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField textField.backgroundColor = UIColor(hexString: "0xF2F4F5") textField.clearButtonMode = .always textField.returnKeyType = .search - textField.addTarget(self, action: #selector(searchTextFieldChange), for: .editingChanged) textField.delegate = self + textField.addTarget(self, action: #selector(searchTextChanged), for: .editingChanged) + if let clearButton = textField.value(forKey: "_clearButton") as? UIButton { clearButton.accessibilityIdentifier = "id.clear" } @@ -90,6 +62,7 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField }() + /// 空占位图 public lazy var emptyView: NEEmptyDataView = { let view = NEEmptyDataView( imageName: "emptyView", @@ -97,32 +70,77 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField frame: CGRect.zero ) view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false view.isHidden = true return view }() - // MARK: private method + /// 正在搜索标志,防止多次点击多次搜索 + public var isSearching = false - func searchTextFieldChange(textfield: SearchTextField) { - guard let searchText = textfield.text else { + public init(teamId: String?) { + super.init(nibName: nil, bundle: nil) + self.teamId = teamId + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + weak var weakSelf = self + + viewModel.getTeamInfo(teamId) { team, error in + if team?.isValidTeam == false || team == nil { + weakSelf?.view.makeToast(localizable("team_not_exist")) + } + } + setupSubviews() + initialConfig() + } + + open func setupSubviews() { + view.addSubview(tableView) + view.addSubview(searchTextField) + view.addSubview(emptyView) + + NSLayoutConstraint.activate([ + emptyView.rightAnchor.constraint(equalTo: tableView.rightAnchor), + emptyView.leftAnchor.constraint(equalTo: tableView.leftAnchor), + emptyView.bottomAnchor.constraint(equalTo: tableView.bottomAnchor), + emptyView.topAnchor.constraint(equalTo: tableView.topAnchor), + ]) + } + + open func initialConfig() { + title = localizable("historical_record") + } + + /// 搜索历史消息 + func toSearchHistory() { + guard let searchText = searchTextField.text else { return } if searchText.count <= 0 { - viewmodel.searchResultInfos?.removeAll() + viewModel.searchResultInfos?.removeAll() emptyView.isHidden = true tableView.reloadData() return } - guard let session = teamSession else { + guard let teamId = teamId else { + return + } + if isSearching == true { return } weak var weakSelf = self searchStr = searchText - let option = NIMMessageSearchOption() - option.searchContent = searchText - weakSelf?.viewmodel.searchMessages(session, option: option) { error, messages in - NELog.infoLog( + isSearching = true + weakSelf?.viewModel.searchHistoryMessages(teamId, searchText) { error, messages in + weakSelf?.isSearching = false + NEALog.infoLog( ModuleName + " " + self.tag, desc: "CALLBACK searchMessages " + (error?.localizedDescription ?? "no error") ) @@ -134,7 +152,7 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField } weakSelf?.tableView.reloadData() } else { - NELog.errorLog( + NEALog.errorLog( ModuleName + " " + (weakSelf?.tag ?? "TeamHistoryMessageController"), desc: "❌searchMessages failed, error = \(error!)" ) @@ -142,10 +160,27 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField } } + /// 监听键盘搜索按钮点击 + public func textFieldShouldReturn(_ textField: UITextField) -> Bool { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return false + } + toSearchHistory() + return true + } + + /// 监听键盘内容变化 + func searchTextChanged() { + if searchTextField.text?.isEmpty == true { + toSearchHistory() + } + } + // MARK: UITableViewDelegate, UITableViewDataSource open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewmodel.searchResultInfos?.count ?? 0 + viewModel.searchResultInfos?.count ?? 0 } open func tableView(_ tableView: UITableView, @@ -154,14 +189,13 @@ open class NEBaseTeamHistoryMessageController: NEBaseViewController, UITextField } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let cellModel = viewmodel.searchResultInfos?[indexPath.row] - if cellModel?.imMessage?.session?.sessionType == .team { - if let sid = cellModel?.imMessage?.session?.sessionId, - let message = cellModel?.imMessage { - let session = NIMSession(sid, type: .team) + let cellModel = viewModel.searchResultInfos?[indexPath.row] + if cellModel?.imMessage?.conversationType == .CONVERSATION_TYPE_TEAM { + if let message = cellModel?.imMessage, let conversationId = message.conversationId { Router.shared.use( PushTeamChatVCRouter, - parameters: ["nav": navigationController as Any, "session": session as Any, + parameters: ["nav": navigationController as Any, + "conversationId": conversationId as Any, "anchor": message], closure: nil ) diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamInfoViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamInfoViewController.swift new file mode 100644 index 00000000..3dfb1ee2 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamInfoViewController.swift @@ -0,0 +1,96 @@ + +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NECoreIM2Kit +import NIMSDK +import UIKit + +@objcMembers +open class NEBaseTeamInfoViewController: NEBaseViewController, UITableViewDelegate, + UITableViewDataSource, NETeamInfoDelegate { + public let viewModel = TeamInfoViewModel() + + public var team: V2NIMTeam? + + public var registerCellDic = [Int: NEBaseTeamSettingCell.Type]() + + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 0 + return tableView + }() + + init(team: V2NIMTeam?) { + super.init(nibName: nil, bundle: nil) + self.team = team + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if team?.serverExtension?.contains(discussTeamKey) == true { + title = localizable("discuss_info") + } else { + title = localizable("group_info") + } + } + + override open func viewDidLoad() { + super.viewDidLoad() + viewModel.delegate = self + viewModel.getData(team) + setupUI() + } + + /// UI 初始化 + open func setupUI() { + view.addSubview(contentTableView) + /// 列表视图布局 + NSLayoutConstraint.activate([ + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant + 12), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + /// 列表视图内容注册 + for (key, value) in registerCellDic { + contentTableView.register(value, forCellReuseIdentifier: "\(key)") + } + } + + // MARK: UITableViewDelegate, UITableViewDataSource + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + viewModel.cellDatas.count + } + + /// 数据绑定,在子类中绑定 + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + UITableViewCell() + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {} + + /// 列表高度回调 + open func tableView(_ tableView: UITableView, + heightForRowAt indexPath: IndexPath) -> CGFloat { + let model = viewModel.cellDatas[indexPath.row] + return model.rowHeight + } + + public func teamInfoDidUpdate(_ t: V2NIMTeam) { + team = t + contentTableView.reloadData() + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamIntroduceViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamIntroduceViewController.swift similarity index 58% rename from NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamIntroduceViewController.swift rename to NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamIntroduceViewController.swift index eafa4c6e..3ca27885 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamIntroduceViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamIntroduceViewController.swift @@ -9,25 +9,26 @@ import UIKit @objcMembers open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDelegate { - public var team: NIMTeam? + public var team: V2NIMTeam? public let textLimit = 100 - public let repo = TeamRepo.shared public let backView = UIView() - public let viewmodel = TeamIntroduceViewModel() + public let viewModel = TeamIntroduceViewModel() + /// 介绍输入框 public lazy var textView: UITextView = { - let text = UITextView() - text.translatesAutoresizingMaskIntoConstraints = false - text.textColor = NEConstant.hexRGB(0x333333) - text.font = NEConstant.defaultTextFont(14.0) - text.delegate = self - text.textContainerInset = UIEdgeInsets.zero - text.layoutManager.allowsNonContiguousLayout = false - text.accessibilityIdentifier = "id.introduce" - return text + let textView = UITextView() + textView.translatesAutoresizingMaskIntoConstraints = false + textView.textColor = NEConstant.hexRGB(0x333333) + textView.font = NEConstant.defaultTextFont(14.0) + textView.delegate = self + textView.textContainerInset = UIEdgeInsets.zero + textView.layoutManager.allowsNonContiguousLayout = false + textView.accessibilityIdentifier = "id.introduce" + return textView }() + /// 清除按钮 public lazy var clearButton: UIButton = { let text = UIButton() text.translatesAutoresizingMaskIntoConstraints = false @@ -37,6 +38,7 @@ open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDe return text }() + /// 字数计数显示 public lazy var countLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -48,18 +50,23 @@ open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDe override open func viewDidLoad() { super.viewDidLoad() - viewmodel.getCurrentUserTeamMember(team?.teamId) - setupUI() + weak var weakSelf = self + viewModel.getCurrentUserTeamMember(team?.teamId) { error in + if let err = error { + weakSelf?.view.makeToast(err.localizedDescription) + } + weakSelf?.setupUI() + } } + /// 布局初始化 open func setupUI() { navigationView.setMoreButtonTitle(localizable("save")) navigationView.addMoreButtonTarget(target: self, selector: #selector(saveIntr)) - - if let type = team?.type, type == .advanced { - title = localizable("team_intr") - } else { + if let serverExtension = team?.serverExtension, serverExtension.contains(discussTeamKey) { title = localizable("discuss_introduce") + } else { + title = localizable("team_intr") } backView.backgroundColor = .white @@ -78,26 +85,28 @@ open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDe figureTextCount(team?.intro ?? "") - if changePermission() == false { + if getPermission() == false { textView.isEditable = false - rightNavBtn.isHidden = true + rightNavButton.isHidden = true navigationView.moreButton.isHidden = true } } - func changePermission() -> Bool { - if let ownerId = team?.owner, IMKitClient.instance.isMySelf(ownerId) { + /// 权限改变 + func getPermission() -> Bool { + if let ownerId = team?.ownerAccountId, IMKitClient.instance.isMe(ownerId) { return true } - if let mode = team?.updateInfoMode, mode == .all { + if let mode = team?.updateInfoMode, mode == .TEAM_UPDATE_INFO_MODE_ALL { return true } - if let member = viewmodel.currentTeamMember, member.type == .manager { + if let member = viewModel.currentTeamMember, member.memberRole == .TEAM_MEMBER_ROLE_MANAGER { return true } return false } + /// 保存简介 func saveIntr() { textView.resignFirstResponder() weak var weakSelf = self @@ -105,51 +114,64 @@ open class NEBaseTeamIntroduceViewController: NEBaseViewController, UITextViewDe weakSelf?.showToast(commonLocalizable("network_error")) return } - + // 上传请求 if let teamid = team?.teamId { let text = textView.text ?? "" view.makeToastActivity(.center) - repo.updateTeamIntroduce(text, teamid) { error in - NELog.infoLog( + viewModel.updateTeamIntroduce(teamid, text) { error in + NEALog.infoLog( ModuleName + " " + self.className(), desc: "CALLBACK updateTeamIntroduce " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { + if let err = error { + if err.code == protocolSendFailed { weakSelf?.showToast(commonLocalizable("network_error")) + } else if error?.code == noPermissionOperationCode { + weakSelf?.showToast(localizable("no_permission_tip")) } else { weakSelf?.showToast(localizable("failed_operation")) } } else { - weakSelf?.team?.intro = text weakSelf?.navigationController?.popViewController(animated: true) } } } } + /// 计算当前输入字数 func figureTextCount(_ text: String) { textView.text = text - countLabel.text = "\(text.count)/\(textLimit)" - clearButton.isHidden = !changePermission() || text.count <= 0 + countLabel.text = "\(text.utf16.count)/\(textLimit)" + clearButton.isHidden = !getPermission() || text.utf16.count <= 0 } + /// 清空输入 func clearText() { figureTextCount("") } - // MARK: UITextViewDelegate - + /// 输入文本变更回调 open func textViewDidChange(_ textView: UITextView) { if let _ = textView.markedTextRange { return } - if var text = textView.text { - if text.count > textLimit { - text = String(text.prefix(textLimit)) - } + if let text = textView.text { figureTextCount(text) } } + + /// 文本变更回调 + /// - Parameter textView: 文本控件对象 + /// - Parameter range: 变更范围 + /// - Parameter text: 变更内容 + public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if !text.isEmpty { + let finalStr = (textView.text as NSString).replacingCharacters(in: range, with: text) + if finalStr.utf16.count > textLimit { + return false + } + } + return true + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManageController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManageController.swift deleted file mode 100644 index be2a7f90..00000000 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManageController.swift +++ /dev/null @@ -1,386 +0,0 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NEChatKit -import NECoreIMKit -import NIMSDK -import UIKit - -@objcMembers -open class NEBaseTeamManageController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource, TeamManageViewModelDelegate { - public let viewmodel = TeamManageViewModel() - - public var managerUsers = [TeamMemberInfoModel]() - - public var cellClassDic = [Int: NEBaseTeamSettingCell.Type]() - - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - table - .tableFooterView = - UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) - if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 - } - return table - }() - - override open func viewDidLoad() { - super.viewDidLoad() - - // Do any additional setup after loading the view. - title = localizable("manage_team") - viewmodel.managerUsers = managerUsers - viewmodel.delegate = self - view.backgroundColor = .ne_lightBackgroundColor - view.addSubview(contentTable) - - NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), - contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in - contentTable.register(value, forCellReuseIdentifier: "\(key)") - } - if let tid = viewmodel.teamInfoModel?.team?.teamId { - viewmodel.getTeamInfo(tid) { [weak self] error in - self?.reloadSectionData() - self?.contentTable.reloadData() - } - } - } - - open func reloadSectionData() {} - - // MARK: UITableViewDataSource, UITableViewDelegate - - open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if viewmodel.sectionData.count > section { - let model = viewmodel.sectionData[section] - return model.cellModels.count - } - return 0 - } - - open func numberOfSections(in tableView: UITableView) -> Int { - viewmodel.sectionData.count - } - - open func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] - if let cell = tableView.dequeueReusableCell( - withIdentifier: "\(model.type)", - for: indexPath - ) as? NEBaseTeamSettingCell { - cell.configure(model) - return cell - } - return UITableViewCell() - } - - open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] - if let block = model.cellClick { - block() - } - } - - open func tableView(_ tableView: UITableView, - heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] - return model.rowHeight - } - - open func tableView(_ tableView: UITableView, - heightForHeaderInSection section: Int) -> CGFloat { - if viewmodel.sectionData.count > section { - let model = viewmodel.sectionData[section] - if model.cellModels.count > 0 { - return 12.0 - } - } - return 0 - } - - open func tableView(_ tableView: UITableView, - viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header - } - - open func tableView(_ tableView: UITableView, - heightForFooterInSection section: Int) -> CGFloat { - if section == viewmodel.sectionData.count - 1 { - return 12.0 - } - return 0 - } - - open func getFooterView() -> UIView? { - nil - } - - open func transferOwner() {} - - func updateTeamInfoAllAction(_ model: SettingCellModel) { - weak var weakSelf = self - view.makeToastActivity(.center) - viewmodel.repo - .updateTeamInfoPrivilege(.all, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in - NELog.infoLog( - ModuleName + " " + self.className(), - desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") - ) - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .all - model.subTitle = localizable("team_all") - weakSelf?.contentTable.reloadData() - } - } - } - - open func didUpdateTeamInfoClick(_ model: SettingCellModel) { - weak var weakSelf = self - - let actionSheetController = UIAlertController( - title: nil, - message: nil, - preferredStyle: .actionSheet - ) - - let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in - print("Cancel") - } - cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - actionSheetController.addAction(cancelActionButton) - - let all = UIAlertAction(title: localizable("team_all"), style: .default) { _ in - weakSelf?.updateTeamInfoAllAction(model) - } - all.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - all.accessibilityIdentifier = "id.teamAllMember" - actionSheetController.addAction(all) - - let manager = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { _ in - weakSelf?.updateTeamInfoOwnerAction(model) - } - manager.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - manager.accessibilityIdentifier = "id.teamOwner" - actionSheetController.addAction(manager) - - actionSheetController.fixIpadAction() - - navigationController?.present(actionSheetController, animated: true, completion: nil) - } - - func updateTeamInfoOwnerAction(_ model: SettingCellModel) { - weak var weakSelf = self - view.makeToastActivity(.center) - viewmodel.repo - .updateTeamInfoPrivilege(.manager, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in - NELog.infoLog( - ModuleName + " " + self.className(), - desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") - ) - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .manager - model.subTitle = localizable("team_owner_and_manager") - weakSelf?.contentTable.reloadData() - } - } - } - - func updateInviteModeOwnerAction(_ model: SettingCellModel) { - weak var weakSelf = self - view.makeToastActivity(.center) - viewmodel.repo.updateInviteMode(.manager, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in - NELog.infoLog( - ModuleName + " " + self.className(), - desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") - ) - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .manager - model.subTitle = localizable("team_owner_and_manager") - weakSelf?.contentTable.reloadData() - } - } - } - - func updateInviteModeAllAction(_ model: SettingCellModel) { - weak var weakSelf = self - view.makeToastActivity(.center) - viewmodel.repo.updateInviteMode(.all, viewmodel.teamInfoModel?.team?.teamId ?? "") { error in - NELog.infoLog( - ModuleName + " " + self.className(), - desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") - ) - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .all - model.subTitle = localizable("team_all") - weakSelf?.contentTable.reloadData() - } - } - } - - open func didChangeInviteModeClick(_ model: SettingCellModel) { - weak var weakSelf = self - - let actionSheetController = UIAlertController( - title: nil, - message: nil, - preferredStyle: .actionSheet - ) - - let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in - print("Cancel") - } - cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - actionSheetController.addAction(cancelActionButton) - - let allActionButton = UIAlertAction(title: localizable("team_all"), style: .default) { _ in - weakSelf?.updateInviteModeAllAction(model) - } - - allActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - allActionButton.accessibilityIdentifier = "id.teamAllMember" - actionSheetController.addAction(allActionButton) - - let ownerActionButton = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { _ in - weakSelf?.updateInviteModeOwnerAction(model) - } - ownerActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - ownerActionButton.accessibilityIdentifier = "id.teamOwner" - actionSheetController.addAction(ownerActionButton) - - actionSheetController.fixIpadAction() - navigationController?.present(actionSheetController, animated: true, completion: nil) - } - - open func didAtPermissionClick(_ model: SettingCellModel) { - weak var weakSelf = self - - let actionSheetController = UIAlertController( - title: nil, - message: nil, - preferredStyle: .actionSheet - ) - - let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in - print("Cancel") - } - cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - actionSheetController.addAction(cancelActionButton) - - let all = UIAlertAction(title: localizable("team_all"), style: .default) { _ in - weakSelf?.viewmodel.updateTeamAtPermission(false) { error in - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.sendTipNoti(false) { error in - } - model.subTitle = localizable("team_all") - weakSelf?.contentTable.reloadData() - } - } - } - all.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - all.accessibilityIdentifier = "id.teamAllMember" - actionSheetController.addAction(all) - actionSheetController.fixIpadAction() - - let manager = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { _ in - weakSelf?.viewmodel.updateTeamAtPermission(true) { error in - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { - weakSelf?.showToast(localizable("no_permission_tip")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.viewmodel.sendTipNoti(true) { error in - } - model.subTitle = localizable("team_owner_and_manager") - weakSelf?.contentTable.reloadData() - } - } - } - manager.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") - manager.accessibilityIdentifier = "id.teamOwner" - actionSheetController.addAction(manager) - - navigationController?.present(actionSheetController, animated: true, completion: nil) - } - - open func didManagerClick() {} - - open func didRefreshData() { - reloadSectionData() - contentTable.reloadData() - } - - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ -} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerController.swift new file mode 100644 index 00000000..c7110585 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerController.swift @@ -0,0 +1,484 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECoreIM2Kit +import NIMSDK +import UIKit + +@objcMembers +open class NEBaseTeamManagerController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource, TeamManagerViewModelDelegate { + public let viewModel = TeamManagerViewModel() + + /// UI样式注册(用户可以自定义) + public var cellClassDic = [Int: NEBaseTeamSettingCell.Type]() + + /// 内容视图 + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + tableView + .tableFooterView = + UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + + override open func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. + title = localizable("manage_team") + viewModel.delegate = self + view.backgroundColor = .ne_lightBackgroundColor + view.addSubview(contentTableView) + + if let teamId = viewModel.teamInfoModel?.team?.teamId { + viewModel.getCurrentUserTeamMember(IMKitClient.instance.account(), teamId) { member, error in + } + } + + NSLayoutConstraint.activate([ + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + for (key, value) in cellClassDic { + contentTableView.register(value, forCellReuseIdentifier: "\(key)") + } + } + + /// 页面出现回到(系统类生命周期函数) + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + if let tid = viewModel.teamInfoModel?.team?.teamId { + viewModel.getTeamWithMembers(tid) { [weak self] error in + self?.reloadSectionData() + self?.contentTableView.reloadData() + } + } + } + + /// 从新加载数据回调,在子类中实现 + open func reloadSectionData() {} + + // MARK: UITableViewDataSource, UITableViewDelegate + + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + if viewModel.sectionData.count > section { + let model = viewModel.sectionData[section] + return model.cellModels.count + } + return 0 + } + + open func numberOfSections(in tableView: UITableView) -> Int { + viewModel.sectionData.count + } + + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] + if let cell = tableView.dequeueReusableCell( + withIdentifier: "\(model.type)", + for: indexPath + ) as? NEBaseTeamSettingCell { + cell.configure(model) + return cell + } + return UITableViewCell() + } + + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] + if let block = model.cellClick { + block() + } + } + + open func tableView(_ tableView: UITableView, + heightForRowAt indexPath: IndexPath) -> CGFloat { + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] + return model.rowHeight + } + + open func tableView(_ tableView: UITableView, + heightForHeaderInSection section: Int) -> CGFloat { + if viewModel.sectionData.count > section { + let model = viewModel.sectionData[section] + if model.cellModels.count > 0 { + return 12.0 + } + } + return 0 + } + + open func tableView(_ tableView: UITableView, + viewForHeaderInSection section: Int) -> UIView? { + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView + } + + open func tableView(_ tableView: UITableView, + heightForFooterInSection section: Int) -> CGFloat { + if section == viewModel.sectionData.count - 1 { + return 12.0 + } + return 0 + } + + /// 更新编辑群信息权限为任意群成员可以发at消息 + /// - Parameter model: 数据模型 + func updateEditTeamInfoPermissionToEveryone(_ model: SettingCellModel) { + weak var weakSelf = self + view.makeToastActivity(.center) + viewModel.updateTeamInfoPrivilege(weakSelf?.viewModel.teamInfoModel?.team?.teamId ?? "", .TEAM_UPDATE_INFO_MODE_ALL) { error, team in + NEALog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewModel.teamInfoModel?.team = team + model.subTitle = localizable("team_all") + weakSelf?.contentTableView.reloadData() + } + } + } + + /// 更新编辑群信权限为管理员可发送权限 + /// - Parameter model: 数据模型 + func updateEditTeamInfoPermissionToManager(_ model: SettingCellModel) { + weak var weakSelf = self + view.makeToastActivity(.center) + viewModel.updateTeamInfoPrivilege(viewModel.teamInfoModel?.team?.teamId ?? "", .TEAM_UPDATE_INFO_MODE_MANAGER) { error, team in + NEALog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewModel.teamInfoModel?.team = team + model.subTitle = localizable("team_owner_and_manager") + weakSelf?.contentTableView.reloadData() + } + } + } + + /// 更新邀请模式为管理员可邀请 + /// - Parameter model: 数据模型 + func updateInvitePermissionToManager(_ model: SettingCellModel) { + weak var weakSelf = self + view.makeToastActivity(.center) + viewModel.updateInviteMode(viewModel.teamInfoModel?.team?.teamId ?? "", .TEAM_INVITE_MODE_MANAGER) { error, team in + NEALog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewModel.teamInfoModel?.team = team + model.subTitle = localizable("team_owner_and_manager") + weakSelf?.contentTableView.reloadData() + } + } + } + + /// 更新邀请人权限为所有人 + /// - Parameter model: 数据模型 + func updateInvitePermissionToEveryone(_ model: SettingCellModel) { + if viewModel.teamMember?.memberRole == .TEAM_MEMBER_ROLE_NORMAL { + showToast(localizable("failed_operation")) + return + } + weak var weakSelf = self + view.makeToastActivity(.center) + viewModel.updateInviteMode(viewModel.teamInfoModel?.team?.teamId ?? "", .TEAM_INVITE_MODE_ALL) { error, team in + NEALog.infoLog( + ModuleName + " " + self.className(), + desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") + ) + weakSelf?.view.hideToastActivity() + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + weakSelf?.viewModel.teamInfoModel?.team = team + model.subTitle = localizable("team_all") + weakSelf?.contentTableView.reloadData() + } + } + } + + /// 点击修改群信息权限回调 + open func didUpdateTeamInfoClick(_ model: SettingCellModel) { + weak var weakSelf = self + + let actionSheetController = UIAlertController( + title: nil, + message: nil, + preferredStyle: .actionSheet + ) + + let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in + print("Cancel") + } + cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + actionSheetController.addAction(cancelActionButton) + + let allAction = UIAlertAction(title: localizable("team_all"), style: .default) { [weak self] _ in + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.updateEditTeamInfoPermissionToEveryone(model) + } + allAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + allAction.accessibilityIdentifier = "id.teamAllMember" + actionSheetController.addAction(allAction) + + let managerAction = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { [weak self] _ in + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.updateEditTeamInfoPermissionToManager(model) + } + managerAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + managerAction.accessibilityIdentifier = "id.teamOwner" + actionSheetController.addAction(managerAction) + + actionSheetController.fixIpadAction() + + navigationController?.present(actionSheetController, animated: true, completion: nil) + } + + /// 点击修改邀请权限回调 + open func didChangeInviteModeClick(_ model: SettingCellModel) { + weak var weakSelf = self + + let actionSheetController = UIAlertController( + title: nil, + message: nil, + preferredStyle: .actionSheet + ) + + let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in + print("Cancel") + } + cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + actionSheetController.addAction(cancelActionButton) + + let allAction = UIAlertAction(title: localizable("team_all"), style: .default) { [weak self] _ in + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.updateInvitePermissionToEveryone(model) + } + + allAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + allAction.accessibilityIdentifier = "id.teamAllMember" + actionSheetController.addAction(allAction) + + let managerAction = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { [weak self] _ in + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.updateInvitePermissionToManager(model) + } + managerAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + managerAction.accessibilityIdentifier = "id.teamOwner" + actionSheetController.addAction(managerAction) + + actionSheetController.fixIpadAction() + navigationController?.present(actionSheetController, animated: true, completion: nil) + } + + /// 点击修改at权限回调 + open func didAtPermissionClick(_ model: SettingCellModel) { + weak var weakSelf = self + + let actionSheetController = UIAlertController( + title: nil, + message: nil, + preferredStyle: .actionSheet + ) + + let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in + print("Cancel") + } + cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + actionSheetController.addAction(cancelActionButton) + + let allAction = UIAlertAction(title: localizable("team_all"), style: .default) { [weak self] _ in + + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.viewModel.updateTeamAtAllPermission(false) { error in + if let err = error as? NSError { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + model.subTitle = localizable("team_all") + weakSelf?.contentTableView.reloadData() + } + } + } + allAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + allAction.accessibilityIdentifier = "id.teamAllMember" + actionSheetController.addAction(allAction) + actionSheetController.fixIpadAction() + + let managerAction = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { [weak self] _ in + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.viewModel.updateTeamAtAllPermission(true) { error in + if let err = error as? NSError { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + model.subTitle = localizable("team_owner_and_manager") + weakSelf?.contentTableView.reloadData() + } + } + } + managerAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + managerAction.accessibilityIdentifier = "id.teamOwner" + actionSheetController.addAction(managerAction) + + navigationController?.present(actionSheetController, animated: true, completion: nil) + } + + /// 点击修改置顶权限回调 + open func didTopMessagePermissionClick(_ model: SettingCellModel) { + weak var weakSelf = self + + let actionSheetController = UIAlertController( + title: nil, + message: nil, + preferredStyle: .actionSheet + ) + + let cancelActionButton = UIAlertAction(title: localizable("cancel"), style: .cancel) { _ in + print("Cancel") + } + cancelActionButton.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + actionSheetController.addAction(cancelActionButton) + + let allAction = UIAlertAction(title: localizable("team_all"), style: .default) { [weak self] _ in + + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.viewModel.updateTeamTopMessagePermission(false) { error in + if let err = error as? NSError { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + model.subTitle = localizable("team_all") + weakSelf?.contentTableView.reloadData() + } + } + } + allAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + allAction.accessibilityIdentifier = "id.teamAllMember" + actionSheetController.addAction(allAction) + actionSheetController.fixIpadAction() + + let managerAction = UIAlertAction(title: localizable("team_owner_and_manager"), style: .default) { [weak self] _ in + if self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_OWNER, self?.viewModel.teamMember?.memberRole != .TEAM_MEMBER_ROLE_MANAGER { + self?.showToast(localizable("no_permission_tip")) + return + } + weakSelf?.viewModel.updateTeamTopMessagePermission(true) { error in + if let err = error as? NSError { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == noPermissionCode { + weakSelf?.showToast(localizable("no_permission_tip")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } + } else { + model.subTitle = localizable("team_owner_and_manager") + weakSelf?.contentTableView.reloadData() + } + } + } + managerAction.setValue(UIColor.ne_darkText, forKey: "_titleTextColor") + managerAction.accessibilityIdentifier = "id.teamOwner" + actionSheetController.addAction(managerAction) + + navigationController?.present(actionSheetController, animated: true, completion: nil) + } + + open func didManagerClick() {} + + /// 刷新数据 + open func didRefreshData() { + reloadSectionData() + contentTableView.reloadData() + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift index b804e0d6..bea3795a 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamManagerListController.swift @@ -3,6 +3,7 @@ // found in the LICENSE file. import NEChatKit +import NECommonKit import NECommonUIKit import UIKit @@ -12,23 +13,24 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel let viewmodel = TeamManagerListViewModel() - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.keyboardDismissMode = .onDrag - table.sectionHeaderHeight = 12.0 - table + /// 内容视图 + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.keyboardDismissMode = .onDrag + tableView.sectionHeaderHeight = 12.0 + tableView .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - return table + return tableView }() public var cellClassDic = [Int: UITableViewCell.Type]() // key 值为 table section 值 @@ -52,16 +54,16 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel } } } - view.addSubview(contentTable) + view.addSubview(contentTableView) NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), - contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0), + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 0), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: 0), + contentTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0), ]) - cellClassDic.forEach { (key: Int, value: UITableViewCell.Type) in - contentTable.register(value, forCellReuseIdentifier: "\(key)") + for (key, value) in cellClassDic { + contentTableView.register(value, forCellReuseIdentifier: "\(key)") } } @@ -88,14 +90,14 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel if indexPath.section == 1 { let model = viewmodel.managers[indexPath.row] if let user = model.nimUser { - if IMKitClient.instance.isMySelf(user.userId) { + if IMKitClient.instance.isMe(user.user?.accountId) { Router.shared.use( MeSettingRouter, parameters: ["nav": navigationController as Any], closure: nil ) } else { - if let uid = user.userId { + if let uid = user.user?.accountId { Router.shared.use( ContactUserInfoPageRouter, parameters: ["nav": navigationController as Any, "uid": uid], @@ -107,17 +109,17 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel } } - open func didAddManagers(_ managers: [TeamMemberInfoModel]) { + open func didAddManagers(_ managers: [NETeamMemberInfoModel]) { if let tid = teamId { var uids = [String]() - managers.forEach { member in - if let uid = member.nimUser?.userId { + for member in managers { + if let uid = member.nimUser?.user?.accountId { uids.append(uid) } } viewmodel.addTeamManager(tid, uids) { [weak self] error in - if let err = error { - self?.view.makeToast(err.localizedDescription) + if error != nil { + self?.view.makeToast(localizable("failed_operation")) } else { self?.viewmodel.managers.insert(contentsOf: managers, at: 0) self?.sortAndReloadData() @@ -126,12 +128,20 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel } } - func didClickRemoveButton(_ model: TeamMemberInfoModel?, _ index: Int) { + func didClickRemoveButton(_ model: NETeamMemberInfoModel?, _ index: Int) { print("did click remove button") weak var weakSelf = self // let content = String(format: localizable("confirm_delete_text"), model?.atNameInTeam() ?? "") + localizable("question_mark") + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } showAlert(title: localizable("remove_manager_title"), message: localizable("remove_manager_tip")) { - if let tid = weakSelf?.teamId, let uid = model?.nimUser?.userId { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + if let tid = weakSelf?.teamId, let uid = model?.nimUser?.user?.accountId { weakSelf?.viewmodel.removeTeamManager(tid, [uid]) { error in if let err = error { weakSelf?.view.makeToast(err.localizedDescription) @@ -148,8 +158,8 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel open func getFilters() -> Set { var filters = Set() - viewmodel.managers.forEach { model in - if let uid = model.nimUser?.userId { + for model in viewmodel.managers { + if let uid = model.nimUser?.user?.accountId { filters.insert(uid) } } @@ -159,12 +169,12 @@ open class NEBaseTeamManagerListController: NEBaseViewController, UITableViewDel open func sortAndReloadData() { // 数据源根据时间排序 viewmodel.managers.sort { model1, model2 -> Bool in - if let time1 = model1.teamMember?.createTime, let time2 = model2.teamMember?.createTime { + if let time1 = model1.teamMember?.joinTime, let time2 = model2.teamMember?.joinTime { return time2 > time1 } return false } - contentTable.reloadData() + contentTableView.reloadData() } open func didNeedReloadData() { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift index 86d629ae..f8f4318d 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMemberSelectController.swift @@ -5,41 +5,58 @@ import NECommonKit import UIKit -public typealias NESelectTeamMemberBlock = ([TeamMemberInfoModel]) -> Void +public typealias NESelectTeamMemberBlock = ([NETeamMemberInfoModel]) -> Void @objcMembers open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource, UITextFieldDelegate, TeamMemberSelectViewModelDelegate { public var selectMemberBlock: NESelectTeamMemberBlock? - let viewmodel = TeamMemberSelectViewModel() + let viewModel = TeamMemberSelectViewModel() + /// 群id var teamId: String? public var cellClassDic = [Int: UITableViewCell.Type]() // key 值为 table section 值 - public let searchInput = UITextField() + /// 搜索输入框 + public lazy var searchInput: UITextField = { + let searchInput = UITextField() + searchInput.textColor = UIColor(hexString: "333333") + searchInput.placeholder = localizable("search_member") + searchInput.font = UIFont.systemFont(ofSize: 14.0) + searchInput.returnKeyType = .search + searchInput.delegate = self + searchInput.clearButtonMode = .always + return searchInput + }() + /// 选择数量限制 public var selectCountLimit = 10 - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - table.keyboardDismissMode = .onDrag - table + /// 内容列表 + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + tableView.keyboardDismissMode = .onDrag + tableView .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - return table + return tableView }() + /// 搜索框背景视图 + let searchBackView = UIView() + + /// 数据为空占位图 public lazy var emptyView: NEEmptyDataView = { let view = NEEmptyDataView( imageName: "user_empty", @@ -47,6 +64,7 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe frame: CGRect.zero ) view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false view.isHidden = true return view @@ -56,82 +74,84 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe super.viewDidLoad() // Do any additional setup after loading the view. - viewmodel.delegate = self + viewModel.delegate = self setupUI() if let tid = teamId { - viewmodel.getTeamInfo(tid) { [weak self] error in + viewModel.getTeamInfo(tid) { [weak self] error in if let err = error { self?.view.makeToast(err.localizedDescription) } else { - self?.contentTable.reloadData() + self?.didReloadTableData() + print("获取群信息成功 : ", self?.viewModel.teamInfoModel?.users.count as Any) } } } } + /// 刷新列表 + open func didReloadTableData() { + if viewModel.showDatas.count <= 0 { + emptyView.isHidden = false + } else { + emptyView.isHidden = true + } + contentTableView.reloadData() + } + + let searchImageView: UIImageView = { + let searchImageView = UIImageView() + searchImageView.image = coreLoader.loadImage("search") + searchImageView.translatesAutoresizingMaskIntoConstraints = false + return searchImageView + }() + open func setupUI() { title = localizable("team_member_select") view.backgroundColor = .white - view.addSubview(contentTable) - - let searchBack = UIView() - view.addSubview(searchBack) - searchBack.backgroundColor = UIColor(hexString: "F2F4F5") - searchBack.translatesAutoresizingMaskIntoConstraints = false - searchBack.clipsToBounds = true - searchBack.layer.cornerRadius = 4.0 + view.addSubview(contentTableView) + + view.addSubview(searchBackView) + searchBackView.backgroundColor = UIColor(hexString: "F2F4F5") + searchBackView.translatesAutoresizingMaskIntoConstraints = false + searchBackView.clipsToBounds = true + searchBackView.layer.cornerRadius = 4.0 NSLayoutConstraint.activate([ - searchBack.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - searchBack.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - searchBack.topAnchor.constraint(equalTo: view.topAnchor, constant: 13 + topConstant), - searchBack.heightAnchor.constraint(equalToConstant: 32), + searchBackView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + searchBackView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + searchBackView.topAnchor.constraint(equalTo: view.topAnchor, constant: 13 + topConstant), + searchBackView.heightAnchor.constraint(equalToConstant: 32), ]) - let searchImage = UIImageView() - searchBack.addSubview(searchImage) - searchImage.image = coreLoader.loadImage("search") - searchImage.translatesAutoresizingMaskIntoConstraints = false + searchBackView.addSubview(searchImageView) NSLayoutConstraint.activate([ - searchImage.centerYAnchor.constraint(equalTo: searchBack.centerYAnchor), - searchImage.leftAnchor.constraint(equalTo: searchBack.leftAnchor, constant: 18), - searchImage.widthAnchor.constraint(equalToConstant: 13), - searchImage.heightAnchor.constraint(equalToConstant: 13), + searchImageView.centerYAnchor.constraint(equalTo: searchBackView.centerYAnchor), + searchImageView.leftAnchor.constraint(equalTo: searchBackView.leftAnchor, constant: 18), + searchImageView.widthAnchor.constraint(equalToConstant: 13), + searchImageView.heightAnchor.constraint(equalToConstant: 13), ]) - searchBack.addSubview(searchInput) + searchBackView.addSubview(searchInput) searchInput.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - searchInput.leftAnchor.constraint(equalTo: searchImage.rightAnchor, constant: 5), - searchInput.rightAnchor.constraint(equalTo: searchBack.rightAnchor, constant: -18), - searchInput.topAnchor.constraint(equalTo: searchBack.topAnchor), - searchInput.bottomAnchor.constraint(equalTo: searchBack.bottomAnchor), + searchInput.leftAnchor.constraint(equalTo: searchImageView.rightAnchor, constant: 5), + searchInput.rightAnchor.constraint(equalTo: searchBackView.rightAnchor, constant: -18), + searchInput.topAnchor.constraint(equalTo: searchBackView.topAnchor), + searchInput.bottomAnchor.constraint(equalTo: searchBackView.bottomAnchor), ]) - searchInput.textColor = UIColor(hexString: "333333") - searchInput.placeholder = localizable("search_member") - searchInput.font = UIFont.systemFont(ofSize: 14.0) - searchInput.returnKeyType = .search - searchInput.delegate = self - searchInput.clearButtonMode = .always + if let clearButton = searchInput.value(forKey: "_clearButton") as? UIButton { clearButton.accessibilityIdentifier = "id.clear" } searchInput.accessibilityIdentifier = "id.addFriendAccount" -// NotificationCenter.default.addObserver( -// self, -// selector: #selector(textFieldChange), -// name: UITextField.textDidChangeNotification, -// object: nil -// ) - NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), - contentTable.topAnchor.constraint(equalTo: searchBack.bottomAnchor), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.topAnchor.constraint(equalTo: searchBackView.bottomAnchor), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: UITableViewCell.Type) in - contentTable.register(value, forCellReuseIdentifier: "\(key)") + for (key, value) in cellClassDic { + contentTableView.register(value, forCellReuseIdentifier: "\(key)") } view.addSubview(emptyView) @@ -149,12 +169,7 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe } 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 + viewModel.showDatas.count } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -162,22 +177,17 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let model = viewmodel.showDatas[indexPath.row] + let model = viewModel.showDatas[indexPath.row] guard let cell = tableView.cellForRow(at: indexPath) as? NEBaseTeamMemberSelectCell else { return } - if let member = model.member, let accid = member.teamMember?.userId { - if viewmodel.selectDic[accid] != nil { - viewmodel.selectDic[accid] = nil + if let member = model.member, let accid = member.teamMember?.accountId { + if viewModel.selectDic[accid] != nil { + viewModel.selectDic[accid] = nil model.isSelected = false cell.checkImageView.isHighlighted = false } else { - if viewmodel.selectDic.count >= selectCountLimit { - let toastString = String(format: localizable("select_limit_tip"), selectCountLimit) - view.makeToast(toastString) - return - } - viewmodel.selectDic[accid] = member + viewModel.selectDic[accid] = member model.isSelected = true cell.checkImageView.isHighlighted = true } @@ -200,42 +210,46 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe } open func textFieldShouldClear(_ textField: UITextField) -> Bool { - viewmodel.showDatas = viewmodel.datas - contentTable.reloadData() + viewModel.showDatas = viewModel.datas + didReloadTableData() return true } + /// 文本输入变更 open func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { let finalString = (textField.text! as NSString).replacingCharacters(in: range, with: string) if string.count <= 0 { if finalString.count <= 0 { - viewmodel.showDatas = viewmodel.datas - contentTable.reloadData() + viewModel.showDatas = viewModel.datas + didReloadTableData() } else { - viewmodel.showDatas = viewmodel.searchAllData(finalString) - contentTable.reloadData() + viewModel.showDatas = viewModel.searchAllData(finalString) + didReloadTableData() } } else { - viewmodel.showDatas = viewmodel.searchAllData(finalString) - contentTable.reloadData() + viewModel.showDatas = viewModel.searchAllData(finalString) + didReloadTableData() } return true } + /// 选择成员变更回调,内部根据选择数量来做右上角状态变更 func didChangeSelectMember() { - if viewmodel.selectDic.count > 0 { - let title = localizable("member_select_sure") + "(\(viewmodel.selectDic.count))" + if viewModel.selectDic.count > 0 { + let title = localizable("member_select_sure") + "(\(viewModel.selectDic.count))" navigationView.moreButton.setTitle(title, for: .normal) } else { navigationView.moreButton.setTitle(localizable("member_select_sure"), for: .normal) } } + /// 刷新回调 open func didNeedRefresh() { - contentTable.reloadData() + contentTableView.reloadData() didChangeSelectMember() } + /// 点击确定添加回调 open func didClickSure() { print("sure click") @@ -244,18 +258,18 @@ open class NEBaseTeamMemberSelectController: NEBaseViewController, UITableViewDe return } - if viewmodel.selectDic.count + viewmodel.managerSet.count > selectCountLimit { + if viewModel.selectDic.count + viewModel.managerSet.count > selectCountLimit { view.makeToast(localizable("max_managers_tip")) return } - if viewmodel.selectDic.count <= 0 { + if viewModel.selectDic.count <= 0 { view.makeToast(localizable("member_empty_tip")) return } - var retArray = [TeamMemberInfoModel]() - viewmodel.selectDic.forEach { (key: String, value: TeamMemberInfoModel) in + var retArray = [NETeamMemberInfoModel]() + for (_, value) in viewModel.selectDic { retArray.append(value) } if let block = selectMemberBlock { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift index 48862110..13902ddc 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamMembersController.swift @@ -5,35 +5,37 @@ import NEChatKit import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit +import NIMSDK import UIKit @objcMembers open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource, TeamMemberCellDelegate, TeamMembersViewModelDelegate { + /// 群id public var teamId: String? - - public var memberDatas: [TeamMemberInfoModel]? { + /// 群成员数据 + public var memberDatas: [NETeamMemberInfoModel]? { didSet { - viewmodel.setShowDatas(memberDatas) + viewModel.setShowDatas(memberDatas) } } + /// 创建者account id public var ownerId: String? public var isSenior = false - public var searchDatas = [TeamMemberInfoModel]() - - public let back = UIView() + public let backView = UIView() - let viewmodel = TeamMembersViewModel() + let viewModel = TeamMembersViewModel() + /// 搜索输入控件 public lazy var searchTextField: UITextField = { let field = UITextField() field.translatesAutoresizingMaskIntoConstraints = false - field.placeholder = localizable("search_friend") + field.placeholder = commonLocalizable("search") field.clearButtonMode = .always field.textColor = .ne_greyText field.font = UIFont.systemFont(ofSize: 14.0) @@ -45,120 +47,142 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat return field }() - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - table + /// 群成员列表视图 + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + tableView .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - table.keyboardDismissMode = .onDrag - return table + tableView.keyboardDismissMode = .onDrag + return tableView }() - lazy var emptyView: NEEmptyDataView = { + /// 空占位图 + public lazy var emptyView: NEEmptyDataView = { + // member_select_no_member let view = NEEmptyDataView(imageName: "user_empty", content: localizable("no_result"), frame: .zero) view.translatesAutoresizingMaskIntoConstraints = false + view.isUserInteractionEnabled = false view.isHidden = true return view }() + /// 搜索背景图 + public lazy var searchIconImageView: UIImageView = { + let searchIconImageView = UIImageView() + searchIconImageView.image = coreLoader.loadImage("search_icon") + searchIconImageView.translatesAutoresizingMaskIntoConstraints = false + return searchIconImageView + }() + public init(teamId: String?) { super.init(nibName: nil, bundle: nil) self.teamId = teamId } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override open func viewDidLoad() { super.viewDidLoad() addObserver() + viewModel.delegate = self + viewModel.teamId = teamId - viewmodel.delegate = self - let team = TeamProvider.shared.getTeam(teamId: teamId ?? "") - ownerId = team?.owner - if team?.isDisscuss() == false { - isSenior = true - } - if let tid = team?.teamId { - viewmodel.getMemberInfo(tid) - } - - setupUI() - - TeamRepo.shared.fetchTeamInfo(teamId ?? "") { [weak self] error, teamModel in - if error != nil { - self?.emptyView.isHidden = false - } else { - self?.viewmodel.setShowDatas(teamModel?.users) - self?.didNeedRefreshUI() + weak var weakSelf = self + if let tid = teamId { + weakSelf?.viewModel.getTeamInfo(tid) { teamInfo, error in + weakSelf?.ownerId = teamInfo?.team?.ownerAccountId + if error != nil { + weakSelf?.emptyView.isHidden = false + if let err = error { + weakSelf?.showToast(err.localizedDescription) + } + } else { + if teamInfo?.team?.isDisscuss() == false { + weakSelf?.isSenior = true + weakSelf?.title = localizable("group_memmber") + } else { + weakSelf?.title = localizable("discuss_mebmer") + } + if IMKitConfigCenter.shared.onlineStatusEnable { + if let members = teamInfo?.users { + var subcribeMembers = [NETeamMemberInfoModel]() + for model in members { + if let account = model.teamMember?.accountId { + if account != IMKitClient.instance.account() { + subcribeMembers.append(model) + } + } + } + weakSelf?.viewModel.subcribeMembers(members) { error in + NEALog.infoLog(weakSelf?.className() ?? "", desc: "sub cribe members error : \(error?.localizedDescription ?? "")") + } + } + } + weakSelf?.didNeedRefreshUI() + } } } + setupUI() } + /// UI 初始化 open func setupUI() { - if isSenior { - title = localizable("group_memmber") - } else { - title = localizable("discuss_mebmer") - } + backView.backgroundColor = .clear + backView.translatesAutoresizingMaskIntoConstraints = false + backView.clipsToBounds = true + backView.layer.cornerRadius = 4.0 - back.backgroundColor = .clear - back.translatesAutoresizingMaskIntoConstraints = false - back.clipsToBounds = true - back.layer.cornerRadius = 4.0 - - view.addSubview(back) + view.addSubview(backView) NSLayoutConstraint.activate([ - back.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0 + topConstant), - back.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), - back.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), - back.heightAnchor.constraint(equalToConstant: 32), + backView.topAnchor.constraint(equalTo: view.topAnchor, constant: 8.0 + topConstant), + backView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20), + backView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + backView.heightAnchor.constraint(equalToConstant: 32), ]) - let searchIcon = UIImageView() - searchIcon.image = coreLoader.loadImage("search_icon") - searchIcon.translatesAutoresizingMaskIntoConstraints = false - back.addSubview(searchIcon) + backView.addSubview(searchIconImageView) NSLayoutConstraint.activate([ - searchIcon.centerYAnchor.constraint(equalTo: back.centerYAnchor), - searchIcon.leftAnchor.constraint(equalTo: back.leftAnchor, constant: 16.0), + searchIconImageView.centerYAnchor.constraint(equalTo: backView.centerYAnchor), + searchIconImageView.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16.0), ]) - back.addSubview(searchTextField) + backView.addSubview(searchTextField) NSLayoutConstraint.activate([ - searchTextField.leftAnchor.constraint(equalTo: back.leftAnchor, constant: 36.0), - searchTextField.rightAnchor.constraint(equalTo: back.rightAnchor, constant: -16.0), - searchTextField.topAnchor.constraint(equalTo: back.topAnchor), - searchTextField.bottomAnchor.constraint(equalTo: back.bottomAnchor), + searchTextField.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 36.0), + searchTextField.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -16.0), + searchTextField.topAnchor.constraint(equalTo: backView.topAnchor), + searchTextField.bottomAnchor.constraint(equalTo: backView.bottomAnchor), ]) - view.addSubview(contentTable) + view.addSubview(contentTableView) NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), - contentTable.topAnchor.constraint(equalTo: back.bottomAnchor, constant: 10), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.topAnchor.constraint(equalTo: backView.bottomAnchor, constant: 10), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - contentTable.register(NEBaseTeamMemberCell.self, forCellReuseIdentifier: "\(NEBaseTeamMemberCell.self)") + contentTableView.register(NEBaseTeamMemberCell.self, forCellReuseIdentifier: "\(NEBaseTeamMemberCell.self)") view.addSubview(emptyView) NSLayoutConstraint.activate([ - emptyView.leftAnchor.constraint(equalTo: contentTable.leftAnchor), - emptyView.rightAnchor.constraint(equalTo: contentTable.rightAnchor), - emptyView.topAnchor.constraint(equalTo: contentTable.topAnchor, constant: 50), - emptyView.bottomAnchor.constraint(equalTo: contentTable.bottomAnchor), + emptyView.leftAnchor.constraint(equalTo: contentTableView.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: contentTableView.rightAnchor), + emptyView.topAnchor.constraint(equalTo: contentTableView.topAnchor, constant: 50), + emptyView.bottomAnchor.constraint(equalTo: contentTableView.bottomAnchor), ]) } @@ -169,7 +193,6 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat name: UITextField.textDidChangeNotification, object: nil ) - NotificationCenter.default.addObserver(self, selector: #selector(didNeedRefreshUI), name: NENotificationName.updateFriendInfo, object: nil) } func isOwner(_ userId: String?) -> Bool { @@ -183,31 +206,31 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat } func textChange() { - searchDatas.removeAll() + viewModel.searchDatas.removeAll() if let text = searchTextField.text, text.count > 0 { - viewmodel.datas.forEach { model in - if let uid = model.nimUser?.userId, uid.contains(text) { - searchDatas.append(model) - } else if let nick = model.nimUser?.userInfo?.nickName, nick.contains(text) { - searchDatas.append(model) - } else if let alias = model.nimUser?.alias, alias.contains(text) { - searchDatas.append(model) - } else if let tNick = model.teamMember?.nickname, tNick.contains(text) { - searchDatas.append(model) + for model in viewModel.datas { + if let uid = model.nimUser?.user?.accountId, uid.contains(text) { + viewModel.searchDatas.append(model) + } else if let nick = model.nimUser?.user?.name, nick.contains(text) { + viewModel.searchDatas.append(model) + } else if let alias = model.nimUser?.friend?.alias, alias.contains(text) { + viewModel.searchDatas.append(model) + } else if let tNick = model.teamMember?.teamNick, tNick.contains(text) { + viewModel.searchDatas.append(model) } } - emptyView.isHidden = searchDatas.count > 0 + } else { emptyView.isHidden = true } didNeedRefreshUI() } - func getRealModel(_ index: Int) -> TeamMemberInfoModel? { + func getRealModel(_ index: Int) -> NETeamMemberInfoModel? { if let text = searchTextField.text, text.count > 0 { - return searchDatas[index] + return viewModel.searchDatas[index] } - return viewmodel.datas[index] + return viewModel.datas[index] } deinit { @@ -218,9 +241,9 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { if let text = searchTextField.text, text.count > 0 { - return searchDatas.count + return viewModel.searchDatas.count } - return viewmodel.datas.count + return viewModel.datas.count } open func tableView(_ tableView: UITableView, @@ -231,7 +254,7 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat ) as? NEBaseTeamMemberCell { if let model = getRealModel(indexPath.row) { cell.configure(model) - cell.ownerLabel.isHidden = !isOwner(model.nimUser?.userId) + cell.ownerLabel.isHidden = !isOwner(model.nimUser?.user?.accountId) } return cell } @@ -245,14 +268,14 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if let model = getRealModel(indexPath.row), let user = model.nimUser { - if IMKitClient.instance.isMySelf(user.userId) { + if IMKitClient.instance.isMe(user.user?.accountId) { Router.shared.use( MeSettingRouter, parameters: ["nav": navigationController as Any], closure: nil ) } else { - if let uid = user.userId { + if let uid = user.user?.accountId { Router.shared.use( ContactUserInfoPageRouter, parameters: ["nav": navigationController as Any, "uid": uid], @@ -263,20 +286,20 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat } } - func didClickRemoveButton(_ model: TeamMemberInfoModel?, _ index: Int) { + /// 移除群成员 + /// - Parameter model: 成员信息 + /// - Parameter index: 成员索引 + func didClickRemoveButton(_ model: NETeamMemberInfoModel?, _ index: Int) { print("did click remove button") weak var weakSelf = self - - // 注释掉的暂时留存,后续可能更改提示语 let content = String(format: localizable("confirm_delete_text"), model?.atNameInTeam() ?? "") + localizable("question_mark") - showAlert(title: localizable("remove_manager_title"), message: localizable("remove_member_tip")) { if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { weakSelf?.view.makeToast(commonLocalizable("network_error")) return } - if let tid = weakSelf?.teamId, let uid = model?.nimUser?.userId { - weakSelf?.viewmodel.removeTeamMember(tid, [uid]) { error in + if let tid = weakSelf?.teamId, let uid = model?.nimUser?.user?.accountId { + weakSelf?.viewModel.removeTeamMember(tid, [uid]) { error in if let err = error { if err.code == noPermissionCode { weakSelf?.view.makeToast(localizable("no_permission_tip")) @@ -285,11 +308,17 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat } } else { if let text = weakSelf?.searchTextField.text, text.count > 0 { - weakSelf?.searchDatas.remove(at: index) - weakSelf?.viewmodel.removeModel(model) + weakSelf?.viewModel.searchDatas.remove(at: index) + weakSelf?.viewModel.searchDatas.removeAll(where: { model in + if model.teamMember?.accountId == uid { + return true + } + return false + }) + weakSelf?.viewModel.removeModel(model) weakSelf?.didNeedRefreshUI() } else { - weakSelf?.viewmodel.removeModel(model) + weakSelf?.viewModel.removeModel(model) weakSelf?.didNeedRefreshUI() } } @@ -298,9 +327,21 @@ open class NEBaseTeamMembersController: NEBaseViewController, UITableViewDelegat } } - // 查找移除数据在 data 中的位置 - func didNeedRefreshUI() { - contentTable.reloadData() + if let text = searchTextField.text, text.count > 0 { + emptyView.isHidden = viewModel.searchDatas.count > 0 + } + contentTableView.reloadData() + } + + override open func didMove(toParent parent: UIViewController?) { + super.didMove(toParent: parent) + if IMKitConfigCenter.shared.onlineStatusEnable { + if parent == nil { + viewModel.unSubcribeMembers(viewModel.datas) { [weak self] error in + NEALog.infoLog(self?.className() ?? " ", desc: #function + " un sub scribe member error : \(error?.localizedDescription ?? "")") + } + } + } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamNameViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamNameViewController.swift similarity index 56% rename from NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamNameViewController.swift rename to NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamNameViewController.swift index 237e5d71..93b0e05b 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamNameViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamNameViewController.swift @@ -9,16 +9,22 @@ import UIKit @objcMembers open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegate { - public var team: NIMTeam? + /// 群对象 + public var team: V2NIMTeam? + /// 修改类型 public var type = ChangeType.TeamName - public var teamMember: NIMTeamMember? + /// 群成员 + public var teamMember: V2NIMTeamMember? + /// 数据单例 public var repo = TeamRepo.shared - public let textLimit = 30 - + /// 输入长度限制 + public var textLimit = 30 + /// 背景视图 public let backView = UIView() let viewModel = TeamNameViewModel() + /// 计数文本显示标签 public lazy var countLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -29,16 +35,18 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat return label }() - public lazy var textView: UITextView = { - let text = UITextView() - text.translatesAutoresizingMaskIntoConstraints = false - text.textColor = NEConstant.hexRGB(0x333333) - text.font = NEConstant.defaultTextFont(14.0) - text.delegate = self - text.accessibilityIdentifier = "id.nickname" - return text + /// 名称输入框 + public lazy var textInputView: UITextView = { + let textView = UITextView() + textView.translatesAutoresizingMaskIntoConstraints = false + textView.textColor = NEConstant.hexRGB(0x333333) + textView.font = NEConstant.defaultTextFont(14.0) + textView.delegate = self + textView.accessibilityIdentifier = "id.nickname" + return textView }() + /// 清除按钮 public lazy var clearButton: UIButton = { let text = UIButton() text.translatesAutoresizingMaskIntoConstraints = false @@ -50,16 +58,25 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat override open func viewDidLoad() { super.viewDidLoad() - viewModel.getCurrentUserTeamMember(team?.teamId) + weak var weakSelf = self + viewModel.getCurrentUserTeamMember(team?.teamId) { error in + if let err = error { + weakSelf?.view.makeToast(err.localizedDescription) + } + DispatchQueue.main.async { + weakSelf?.configData() + } + } setupUI() } + /// UI 控件初始化 open func setupUI() { navigationView.setMoreButtonTitle(localizable("save")) navigationView.addMoreButtonTarget(target: self, selector: #selector(saveName)) view.addSubview(backView) - backView.addSubview(textView) + backView.addSubview(textInputView) backView.addSubview(clearButton) backView.addSubview(countLabel) @@ -71,22 +88,24 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat countLabel.rightAnchor.constraint(equalTo: backView.rightAnchor, constant: -16), countLabel.bottomAnchor.constraint(equalTo: backView.bottomAnchor, constant: -8.0), ]) + } + /// 绑定数据 + func configData() { var name = "" - if type == .TeamName, let n = team?.teamName { + if type == .TeamName, let n = team?.name { name = n - if changePermission() == false { - rightNavBtn.isHidden = true + if getEditablePermission() == false { + rightNavButton.isHidden = true navigationView.moreButton.isHidden = true - textView.isEditable = false + textInputView.isEditable = false } } else if type == .NickName { title = localizable("team_nick") - if let n = teamMember?.nickname { + if let n = teamMember?.teamNick { name = n } } - figureTextCount(name) if name.count <= 0, type != .NickName { @@ -94,58 +113,55 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat } } - /* - // MARK: - Navigation - - // In a storyboard-based application, you will often want to do a little preparation before navigation - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - // Get the new view controller using segue.destination. - // Pass the selected object to the new view controller. - } - */ - - func changePermission() -> Bool { + /// 查看是否有编辑权限 + func getEditablePermission() -> Bool { if type == .NickName { return true } - - if let type = team?.type, type == .normal { - return true - } - - if let ownerId = team?.owner, IMKitClient.instance.isMySelf(ownerId) { + if let mode = team?.updateInfoMode, mode == .TEAM_UPDATE_INFO_MODE_ALL { return true } - if let mode = team?.updateInfoMode, mode == .all { + if let ownerId = team?.ownerAccountId, IMKitClient.instance.isMe(ownerId) { return true } - if let member = viewModel.currentTeamMember, member.type == .manager { + + if let member = viewModel.currentTeamMember, member.memberRole == .TEAM_MEMBER_ROLE_MANAGER { return true } return false } + /// 提交按钮显示不可提交状态 open func disableSubmit() { - rightNavBtn.setTitleColor(NEConstant.hexRGBAlpha(0x337EFF, 0.5), for: .normal) - rightNavBtn.isEnabled = false + rightNavButton.setTitleColor(NEConstant.hexRGBAlpha(0x337EFF, 0.5), for: .normal) + rightNavButton.isEnabled = false navigationView.moreButton.setTitleColor(NEConstant.hexRGBAlpha(0x337EFF, 0.5), for: .normal) navigationView.moreButton.isEnabled = false } + /// 提交按钮显示可提交状态 open func enableSubmit() { - rightNavBtn.setTitleColor(NEConstant.hexRGB(0x337EFF), for: .normal) - rightNavBtn.isEnabled = true + rightNavButton.setTitleColor(NEConstant.hexRGB(0x337EFF), for: .normal) + rightNavButton.isEnabled = true navigationView.moreButton.setTitleColor(NEConstant.hexRGB(0x337EFF), for: .normal) navigationView.moreButton.isEnabled = true } + /// 保存群名称 open func saveName() { guard let tid = team?.teamId else { showToast(localizable("failed_operation")) return } - if let text = textView.text, + weak var weakSelf = self + + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + + if let text = textInputView.text, !text.isEmpty { let trimText = text.trimmingCharacters(in: .whitespaces) if trimText.isEmpty { @@ -155,32 +171,30 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat } } - weak var weakSelf = self - if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { - weakSelf?.showToast(commonLocalizable("network_error")) - return - } + textInputView.resignFirstResponder() - textView.resignFirstResponder() if type == .TeamName { - let n = textView.text ?? "" + let n = textInputView.text ?? "" view.makeToastActivity(.center) - repo.updateTeamName(tid, n) { error in + repo.updateTeamName(tid, .TEAM_TYPE_NORMAL, n) { error in weakSelf?.view.hideToastActivity() - if let err = error { + if error != nil { + if error?.code == noPermissionOperationCode { + weakSelf?.showToast(localizable("no_permission_tip")) + return + } weakSelf?.showToast(localizable("failed_operation")) } else { - weakSelf?.team?.teamName = n weakSelf?.navigationController?.popViewController(animated: true) } } - } else if type == .NickName, let uid = teamMember?.userId { - let n = textView.text ?? "" + } else if type == .NickName, let uid = teamMember?.accountId { + let n = textInputView.text ?? "" view.makeToastActivity(.center) - repo.updateMemberNick(tid, uid, n) { error in + repo.updateMemberNick(tid, .TEAM_TYPE_NORMAL, uid, n) { error in weakSelf?.view.hideToastActivity() - if let err = error { + if error != nil { weakSelf?.showToast(localizable("failed_operation")) } else { weakSelf?.navigationController?.popViewController(animated: true) @@ -189,14 +203,17 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat } } + /// 清除文本 func clearText() { figureTextCount("") } + /// 计算显示数量 + /// - Parameter text: 文本内容 func figureTextCount(_ text: String) { - textView.text = text - countLabel.text = "\(text.count)/\(textLimit)" - clearButton.isHidden = !changePermission() || text.count <= 0 + textInputView.text = text + countLabel.text = "\(text.utf16.count)/\(textLimit)" + clearButton.isHidden = !getEditablePermission() || text.utf16.count <= 0 if type == .NickName { return } @@ -207,17 +224,28 @@ open class NEBaseTeamNameViewController: NEBaseViewController, UITextViewDelegat } } - // MARK: UITextViewDelegate - + /// 文本变更回调 + /// - Parameter textView: 文本控件对象 open func textViewDidChange(_ textView: UITextView) { if let _ = textView.markedTextRange { return } - if var text = textView.text { - if text.count > textLimit { - text = String(text.prefix(textLimit)) - } + if let text = textView.text { figureTextCount(text) } } + + /// 文本变更回调 + /// - Parameter textView: 文本控件对象 + /// - Parameter range: 变更范围 + /// - Parameter text: 变更内容 + public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + if !text.isEmpty { + let finalStr = (textView.text as NSString).replacingCharacters(in: range, with: text) + if finalStr.utf16.count > textLimit { + return false + } + } + return true + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamSettingViewController.swift similarity index 56% rename from NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingViewController.swift rename to NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamSettingViewController.swift index 75b580fc..de40f88e 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/NEBaseTeamSettingViewController.swift @@ -4,7 +4,7 @@ // found in the LICENSE file. import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -12,41 +12,44 @@ import UIKit open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UITableViewDataSource, UITableViewDelegate, TeamSettingViewModelDelegate { - public let viewmodel = TeamSettingViewModel() - + /// 数据管理类 + public let viewModel = TeamSettingViewModel() + /// 群id public var teamId: String? - public var addBtnWidth: NSLayoutConstraint? + public var addButtonWidth: NSLayoutConstraint? - public var addBtnLeftMargin: NSLayoutConstraint? + public var addButtonLeftMargin: NSLayoutConstraint? + /// 群类型 public var teamSettingType: TeamSettingType = .Discuss - public var isSeniorDiscuss = false // 是否是高级群扩展的讨论组 + /// 是否是高级群扩展的讨论组 + public var isSeniorDiscuss = false var className = "TeamSettingViewController" public var cellClassDic = [Int: NEBaseTeamSettingCell.Type]() - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - table + public lazy var contentTableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + tableView .tableFooterView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 12)) if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 + tableView.sectionHeaderTopPadding = 0.0 } - return table + return tableView }() - public lazy var teamHeader: NEUserHeaderView = { + public lazy var teamHeaderView: NEUserHeaderView = { let imageView = NEUserHeaderView(frame: .zero) imageView.translatesAutoresizingMaskIntoConstraints = false imageView.clipsToBounds = true @@ -63,6 +66,15 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi return label }() + public lazy var memberLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = NEConstant.hexRGB(0x333333) + label.font = NEConstant.defaultTextFont(16.0) + label.accessibilityIdentifier = "id.member" + return label + }() + public lazy var memberCountLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false @@ -72,21 +84,22 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi return label }() - public lazy var userinfoCollection: UICollectionView = { - let flow = UICollectionViewFlowLayout() - flow.scrollDirection = .horizontal - flow.minimumLineSpacing = 0 - flow.minimumInteritemSpacing = 0 - let collection = UICollectionView(frame: .zero, collectionViewLayout: flow) - collection.translatesAutoresizingMaskIntoConstraints = false - collection.delegate = self - collection.dataSource = self - collection.backgroundColor = .clear - collection.showsHorizontalScrollIndicator = false - return collection + public lazy var userinfoCollectionView: UICollectionView = { + let flowLayout = UICollectionViewFlowLayout() + flowLayout.scrollDirection = .horizontal + flowLayout.minimumLineSpacing = 0 + flowLayout.minimumInteritemSpacing = 0 + let collectionView = UICollectionView(frame: .zero, collectionViewLayout: flowLayout) + collectionView.translatesAutoresizingMaskIntoConstraints = false + collectionView.delegate = self + collectionView.dataSource = self + collectionView.backgroundColor = .clear + collectionView.showsHorizontalScrollIndicator = false + collectionView.isScrollEnabled = false + return collectionView }() - public lazy var addBtn: ExpandButton = { + public lazy var addButton: ExpandButton = { let button = ExpandButton() button.translatesAutoresizingMaskIntoConstraints = false button.accessibilityIdentifier = "id.add" @@ -95,11 +108,11 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi override open func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - if let model = viewmodel.teamInfoModel { - if let url = model.team?.avatarUrl, !url.isEmpty { - teamHeader.sd_setImage(with: URL(string: url)) + if let model = viewModel.teamInfoModel { + if let url = model.team?.avatar, !url.isEmpty { + teamHeaderView.sd_setImage(with: URL(string: url)) } - if let name = model.team?.teamName { + if let name = model.team?.name { teamNameLabel.text = name } } @@ -110,73 +123,112 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi title = localizable("setting") weak var weakSelf = self - viewmodel.delegate = self + viewModel.delegate = self if let tid = teamId { - viewmodel.getTeamInfo(tid) { error in - NELog.infoLog( - ModuleName + " " + self.className, - desc: "CALLBACK getTeamInfo " + (error?.localizedDescription ?? "no error") - ) - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else { - weakSelf?.showToast(localizable("team_not_exist")) - } + viewModel.getCurrentMember(IMKitClient.instance.account(), tid) { member, error in + if let currentMember = member { + weakSelf?.requestSettingData(tid, currentMember) } else { - if let type = weakSelf?.viewmodel.teamInfoModel?.team?.type { - if type == .normal { - weakSelf?.teamSettingType = .Discuss - } else if type == .advanced { - if let custom = weakSelf?.viewmodel.teamInfoModel?.team?.clientCustomInfo, custom.contains(discussTeamKey) { - weakSelf?.teamSettingType = .Discuss - weakSelf?.isSeniorDiscuss = true - } else { - weakSelf?.teamSettingType = .Senior - } - } - } - if let type = weakSelf?.teamSettingType { - weakSelf?.viewmodel.teamSettingType = type + if let err = error { + weakSelf?.showToast(err.localizedDescription) } - weakSelf?.reloadSectionData() - weakSelf?.contentTable.tableHeaderView = weakSelf?.getHeaderView() - weakSelf?.contentTable.tableFooterView = weakSelf?.getFooterView() - weakSelf?.contentTable.reloadData() - weakSelf?.didRefreshUserinfoCollection() - weakSelf?.checkoutAddShowOrHide() } } + } else { + showToast("team id is nil") } - // Do any additional setup after loading the view. setupUI() - - NotificationCenter.default.addObserver(self, selector: #selector(didRefreshUserinfoCollection), name: NENotificationName.updateFriendInfo, object: nil) } open func reloadSectionData() {} open func didRefreshUserinfoCollection() { - userinfoCollection.reloadData() + userinfoCollectionView.reloadData() } + /// 初始化 open func setupUI() { view.backgroundColor = .ne_lightBackgroundColor - view.addSubview(contentTable) + view.addSubview(contentTableView) NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), - contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), + contentTableView.leftAnchor.constraint(equalTo: view.leftAnchor), + contentTableView.rightAnchor.constraint(equalTo: view.rightAnchor), + contentTableView.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant), + contentTableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in - contentTable.register(value, forCellReuseIdentifier: "\(key)") + for (key, value) in cellClassDic { + contentTableView.register(value, forCellReuseIdentifier: "\(key)") } if let pan = navigationController?.interactivePopGestureRecognizer { - contentTable.panGestureRecognizer.require(toFail: pan) + contentTableView.panGestureRecognizer.require(toFail: pan) + } + } + + /// 设置群设置顶部视图展示内容 + open func setTeamHeaderInfo() { + if let url = viewModel.teamInfoModel?.team?.avatar, !url.isEmpty { + print("icon url : ", url) + teamHeaderView.sd_setImage(with: URL(string: url), completed: nil) + } else { + if let tid = teamId { + if let name = viewModel.teamInfoModel?.team?.getShowName() { + teamHeaderView.setTitle(name) + } + teamHeaderView.backgroundColor = UIColor.colorWithString(string: "\(tid)") + } + } + teamNameLabel.text = viewModel.teamInfoModel?.team?.getShowName() + } + + /// 获取群设置数据 + public func requestSettingData(_ tid: String, _ member: V2NIMTeamMember) { + weak var weakSelf = self + viewModel.getTeamWithMembers(tid) { error in + NEALog.infoLog( + ModuleName + " " + self.className, + desc: "CALLBACK getTeamInfo " + (error?.localizedDescription ?? "no error") + ) + if let err = error { + if err.code == protocolSendFailed { + weakSelf?.showToast(commonLocalizable("network_error")) + } else if err.code == teamNotExistCode { + weakSelf?.showToast(localizable("team_not_exist")) + } else { + weakSelf?.showToast(err.localizedDescription) + } + } else { + if let type = weakSelf?.viewModel.teamInfoModel?.team?.teamType { + if type == .TEAM_TYPE_NORMAL { + if let custom = weakSelf?.viewModel.teamInfoModel?.team?.serverExtension, custom.contains(discussTeamKey) { + weakSelf?.teamSettingType = .Discuss + weakSelf?.isSeniorDiscuss = true + } else { + weakSelf?.teamSettingType = .Senior + } + } + } + if let type = weakSelf?.teamSettingType { + weakSelf?.viewModel.teamSettingType = type + } + weakSelf?.resetupUI() + + weakSelf?.viewModel.getAllTeamMemberInfos(tid, .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL) { error in + NEALog.infoLog(weakSelf?.className() ?? "", desc: "CALLBACK getAllTeamMemberInfos \(error?.localizedDescription ?? "no error")") + } + } } } + /// 有数据返回之后重新刷新UI + public func resetupUI() { + reloadSectionData() + contentTableView.tableHeaderView = getHeaderView() + contentTableView.tableFooterView = getFooterView() + contentTableView.reloadData() + didRefreshUserinfoCollection() + checkoutAddShowOrHide() + } + open func getHeaderView() -> UIView { UIView() } @@ -189,51 +241,67 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi if teamSettingType == .Discuss { return localizable("leave_discuss") } else if teamSettingType == .Senior { - return viewmodel.isOwner() ? localizable("dismiss_team") : localizable("leave_team") + return viewModel.isOwner() ? localizable("dismiss_team") : localizable("leave_team") } return nil } open func setupUserInfoCollection(_ cornerView: UIView) {} - // MARK: objc 方法 - open func addUser() { weak var weakSelf = self Router.shared.register(ContactSelectedUsersRouter) { param in print("addUser weak self ", weakSelf as Any) if let accids = param["accids"] as? [String], - let tid = self.viewmodel.teamInfoModel?.team?.teamId, - let beInviteMode = self.viewmodel.teamInfoModel?.team?.beInviteMode, - let type = self.viewmodel.teamInfoModel?.team?.type { - if beInviteMode == .noAuth || type == .normal { - self.didAddUserAndRefreshUI(accids, tid) - } else { - self.didAddUser(accids, tid) + let tid = weakSelf?.viewModel.teamInfoModel?.team?.teamId, + let beInviteMode = weakSelf?.viewModel.teamInfoModel?.team?.agreeMode, + let type = weakSelf?.viewModel.teamInfoModel?.team?.teamType { + if beInviteMode == .TEAM_AGREE_MODE_NO_AUTH || type == .TEAM_TYPE_NORMAL { + weakSelf?.didAddUser(accids, tid) } } } + var param = [String: Any]() param["nav"] = navigationController as Any var filters = Set() - viewmodel.teamInfoModel?.users.forEach { model in - if let uid = model.nimUser?.userId { - filters.insert(uid) + if let tid = teamId, let models = NETeamMemberCache.shared.getTeamMemberCache(tid) { + for model in models { + if let accountId = model.teamMember?.accountId { + filters.insert(accountId) + } + } + } else { + viewModel.teamInfoModel?.users.forEach { model in + if let uid = model.nimUser?.user?.accountId { + filters.insert(uid) + } } } + if filters.count > 0 { param["filters"] = filters } - param["limit"] = inviteNumberLimit - filters.count + param["limit"] = (viewModel.teamInfoModel?.team?.memberLimit ?? inviteNumberLimit + filters.count) - filters.count Router.shared.use(ContactUserSelectRouter, parameters: param, closure: nil) } + /// 退出/解散群聊 open func removeTeamForMyself() { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + showToast(commonLocalizable("network_error")) + return + } + weak var weakSelf = self if teamSettingType == .Senior { - showAlert(message: viewmodel.isOwner() ? localizable("dissolute_team_chat") : localizable("quit_team_chat")) { - if weakSelf?.viewmodel.isOwner() == true { + showAlert(message: viewModel.isOwner() ? localizable("dissolute_team_chat") : localizable("quit_team_chat")) { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + if weakSelf?.viewModel.isOwner() == true { weakSelf?.dismissTeam() } else { weakSelf?.leaveTeam() @@ -241,16 +309,21 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } } else if teamSettingType == .Discuss { showAlert(message: localizable("quit_discuss_chat")) { + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } weakSelf?.leaveDiscuss() } } } + /// 离开讨论组 open func leaveDiscuss() { weak var weakSelf = self - if isSeniorDiscuss == true, viewmodel.isOwner() { + if isSeniorDiscuss == true, viewModel.isOwner() { view.makeToastActivity(.center) - viewmodel.transferTeamOwner { error in + viewModel.transferTeamOwner { error in weakSelf?.view.hideToastActivity() if let err = error as? NSError { weakSelf?.didError(err) @@ -265,8 +338,9 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func toInfoView() {} + /// 跳转成员列表 open func toMemberList() { - let memberController = NEBaseTeamMembersController(teamId: viewmodel.teamInfoModel?.team?.teamId) + let memberController = NEBaseTeamMembersController(teamId: viewModel.teamInfoModel?.team?.teamId) navigationController?.pushViewController(memberController, animated: true) } @@ -274,8 +348,7 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int { - print("numberOfItemsInSection ", viewmodel.teamInfoModel?.users.count as Any) - return viewmodel.teamInfoModel?.users.count ?? 0 + viewModel.teamInfoModel?.users.count ?? 0 } open func collectionView(_ collectionView: UICollectionView, @@ -291,16 +364,16 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { - if let member = viewmodel.teamInfoModel?.users[indexPath.row], + if let member = viewModel.teamInfoModel?.users[indexPath.row], let nimUser = member.nimUser { - if IMKitClient.instance.isMySelf(nimUser.userId) { + if IMKitClient.instance.isMe(nimUser.user?.accountId) { Router.shared.use( MeSettingRouter, parameters: ["nav": navigationController as Any], closure: nil ) } else { - if let uid = nimUser.userId { + if let uid = nimUser.user?.accountId { Router.shared.use( ContactUserInfoPageRouter, parameters: ["nav": navigationController as Any, "uid": uid], @@ -314,20 +387,20 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi // MARK: UITableViewDataSource, UITableViewDelegate open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - if viewmodel.sectionData.count > section { - let model = viewmodel.sectionData[section] + if viewModel.sectionData.count > section { + let model = viewModel.sectionData[section] return model.cellModels.count } return 0 } open func numberOfSections(in tableView: UITableView) -> Int { - viewmodel.sectionData.count + viewModel.sectionData.count } open func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] if let cell = tableView.dequeueReusableCell( withIdentifier: "\(model.type)", for: indexPath @@ -339,7 +412,7 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] if let block = model.cellClick { block() } @@ -347,14 +420,14 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.sectionData[indexPath.section].cellModels[indexPath.row] + let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] return model.rowHeight } open func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { - if viewmodel.sectionData.count > section { - let model = viewmodel.sectionData[section] + if viewModel.sectionData.count > section { + let model = viewModel.sectionData[section] if model.cellModels.count > 0 { return 12.0 } @@ -364,80 +437,53 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView } open func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { - if section == viewmodel.sectionData.count - 1 { + if section == viewModel.sectionData.count - 1 { return 12.0 } return 0 } - func didAddUserAndRefreshUI(_ accids: [String], _ tid: String) { + /// 添加好友 + func didAddUser(_ accids: [String], _ tid: String) { weak var weakSelf = self view.makeToastActivity(.center) - viewmodel.inviterUsers(accids, tid) { error, members in + + viewModel.inviteUsers(accids, tid) { error, members in if let err = error { weakSelf?.view.hideToastActivity() - if err.code == noNetworkCode { + if err.code == protocolSendFailed { weakSelf?.showToast(commonLocalizable("network_error")) - } else if err.code == noPermissionCode { + } else if err.code == noPermissionInviteCode { weakSelf?.showToast(localizable("no_permission_tip")) } else { weakSelf?.showToast(localizable("failed_operation")) } } else { print("add users success : ", members as Any) - if let ms = members, let model = weakSelf?.viewmodel.teamInfoModel { - weakSelf?.viewmodel.repo.splitGroupMember(ms, model) { error, team in - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - weakSelf?.didError(err) - } else { - weakSelf?.refreshMemberCount() - weakSelf?.didRefreshUserinfoCollection() - weakSelf?.checkoutAddShowOrHide() - } - } - } else { - weakSelf?.view.hideToastActivity() - } - } - } - } - - func didAddUser(_ accids: [String], _ tid: String) { - weak var weakSelf = self - view.makeToastActivity(.center) - viewmodel.repo.inviteUser(accids, tid, nil, nil) { error, members in - NELog.infoLog( - ModuleName + " " + self.className(), - desc: "CALLBACK inviteUser " + (error?.localizedDescription ?? "no error") - ) - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - weakSelf?.didError(err) - } else { - weakSelf?.showToast(localizable("invite_has_send")) + weakSelf?.view.hideToastActivity() } } } + /// 解散群聊 func dismissTeam() { if let tid = teamId { weak var weakSelf = self view.makeToastActivity(.center) - viewmodel.dismissTeam(tid) { error in - NELog.infoLog( + viewModel.dismissTeam(tid) { error in + NEALog.infoLog( ModuleName + " " + self.className, desc: "CALLBACK dismissTeam " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { weakSelf?.didError(err) } else { NotificationCenter.default.post(name: NotificationName.popGroupChatVC, object: nil) @@ -446,28 +492,30 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } } + /// 刷新群成员个数 func refreshMemberCount() { - if let count = viewmodel.teamInfoModel?.team?.memberNumber { + if let count = viewModel.teamInfoModel?.team?.memberCount { memberCountLabel.text = "\(count)" } } + /// 离开群聊 func leaveTeam() { if let tid = teamId { view.makeToastActivity(.center) - viewmodel.quitTeam(tid) { [weak self] error in - NELog.infoLog( + viewModel.leaveTeam(tid) { [weak self] error in + NEALog.infoLog( ModuleName + " " + (self?.className ?? "TeamSettingViewController"), desc: "CALLBACK quitTeam " + (error?.localizedDescription ?? "no error") ) self?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { self?.didError(err) } else { DispatchQueue.main.asyncAfter(deadline: .now() + 0.25) { - self?.viewmodel.getTeamInfo(tid) { _ in - if self?.viewmodel.teamInfoModel?.team?.memberNumber == 1, - self?.viewmodel.isOwner() == true { + self?.viewModel.getTeamWithMembers(tid) { _ in + if self?.viewModel.teamInfoModel?.team?.memberCount == 1, + self?.viewModel.isOwner() == true { self?.dismissTeam() } else { NotificationCenter.default.post(name: NotificationName.popGroupChatVC, object: nil) @@ -479,26 +527,33 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi } } - func didClickMark() { + public func didClickMark() { if let tid = teamId { - let session = NIMSession(tid, type: .team) - Router.shared.use(PushPinMessageVCRouter, parameters: ["nav": navigationController as Any, "session": session as Any], closure: nil) + let conversationId = V2NIMConversationIdUtil.teamConversationId(tid) + Router.shared.use(PushPinMessageVCRouter, parameters: ["nav": navigationController as Any, "conversationId": conversationId as Any], closure: nil) } } - func didError(_ error: NSError) { - if error.code == noNetworkCode { + public func didError(_ error: NSError) { + if error.code == protocolSendFailed { showToast(commonLocalizable("network_error")) } else { showToast(localizable("failed_operation")) } } - func didNeedRefreshUI() { - contentTable.reloadData() + public func didShowNoNetworkToast() { + showToast(commonLocalizable("network_error")) + } + + /// 通知页面刷新回调 + public func didNeedRefreshUI() { + reloadSectionData() + contentTableView.reloadData() refreshMemberCount() didRefreshUserinfoCollection() checkoutAddShowOrHide() + setTeamHeaderInfo() } open func didClickTeamManage() {} @@ -508,18 +563,18 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi func updateInviteModeOwnerAction(_ model: SettingCellModel) { weak var weakSelf = self weakSelf?.view.makeToastActivity(.center) - weakSelf?.viewmodel.repo.updateInviteMode(.manager, weakSelf?.teamId ?? "") { error in - NELog.infoLog( + weakSelf?.viewModel.teamRepo.updateInviteMode(weakSelf?.teamId ?? "", .TEAM_TYPE_NORMAL, .TEAM_INVITE_MODE_MANAGER) { error, team in + NEALog.infoLog( ModuleName + " " + self.className(), desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { weakSelf?.didError(err) } else { - weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .manager + weakSelf?.viewModel.teamInfoModel?.team = team model.subTitle = localizable("team_owner") - weakSelf?.contentTable.reloadData() + weakSelf?.contentTableView.reloadData() } } } @@ -527,18 +582,18 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi func updateInviteModeAllAction(_ model: SettingCellModel) { weak var weakSelf = self weakSelf?.view.makeToastActivity(.center) - weakSelf?.viewmodel.repo.updateInviteMode(.all, weakSelf?.teamId ?? "") { error in - NELog.infoLog( + weakSelf?.viewModel.teamRepo.updateInviteMode(weakSelf?.teamId ?? "", .TEAM_TYPE_NORMAL, .TEAM_INVITE_MODE_ALL) { error, team in + NEALog.infoLog( ModuleName + " " + self.className(), desc: "CALLBACK updateInviteMode " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { weakSelf?.didError(err) } else { - weakSelf?.viewmodel.teamInfoModel?.team?.inviteMode = .all + weakSelf?.viewModel.teamInfoModel?.team = team model.subTitle = localizable("team_all") - weakSelf?.contentTable.reloadData() + weakSelf?.contentTableView.reloadData() } } } @@ -579,19 +634,19 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi func updateTeamInfoOwnerAction(_ model: SettingCellModel) { weak var weakSelf = self weakSelf?.view.makeToastActivity(.center) - weakSelf?.viewmodel.repo - .updateTeamInfoPrivilege(.manager, weakSelf?.teamId ?? "") { error in - NELog.infoLog( + weakSelf?.viewModel.teamRepo + .updateTeamInfoMode(weakSelf?.teamId ?? "", .TEAM_TYPE_NORMAL, .TEAM_UPDATE_INFO_MODE_MANAGER) { error, team in + NEALog.infoLog( ModuleName + " " + self.className(), desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { weakSelf?.didError(err) } else { - weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .manager + weakSelf?.viewModel.teamInfoModel?.team = team model.subTitle = localizable("team_owner") - weakSelf?.contentTable.reloadData() + weakSelf?.contentTableView.reloadData() } } } @@ -599,19 +654,19 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi func updateTeamInfoAllAction(_ model: SettingCellModel) { weak var weakSelf = self weakSelf?.view.makeToastActivity(.center) - weakSelf?.viewmodel.repo - .updateTeamInfoPrivilege(.all, weakSelf?.teamId ?? "") { error in - NELog.infoLog( + weakSelf?.viewModel.teamRepo + .updateTeamInfoMode(weakSelf?.teamId ?? "", .TEAM_TYPE_NORMAL, .TEAM_UPDATE_INFO_MODE_ALL) { error, team in + NEALog.infoLog( ModuleName + " " + self.className(), desc: "CALLBACK updateTeamInfoPrivilege " + (error?.localizedDescription ?? "no error") ) weakSelf?.view.hideToastActivity() - if let err = error as? NSError { + if let err = error { weakSelf?.didError(err) } else { - weakSelf?.viewmodel.teamInfoModel?.team?.updateInfoMode = .all + weakSelf?.viewModel.teamInfoModel?.team = team model.subTitle = localizable("team_all") - weakSelf?.contentTable.reloadData() + weakSelf?.contentTableView.reloadData() } } } @@ -652,13 +707,22 @@ open class NEBaseTeamSettingViewController: NEBaseViewController, UICollectionVi open func didClickHistoryMessage() {} - open func getManaterUsers() -> [TeamMemberInfoModel] { - var members = [TeamMemberInfoModel]() - viewmodel.teamInfoModel?.users.forEach { model in - if model.teamMember?.type == .manager { + open func getManagerUsers() -> [NETeamMemberInfoModel] { + var members = [NETeamMemberInfoModel]() + viewModel.teamInfoModel?.users.forEach { model in + if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { members.append(model) } } return members } + + override open func didMove(toParent parent: UIViewController?) { + super.didMove(toParent: parent) + if IMKitConfigCenter.shared.onlineStatusEnable { + if parent == nil { + viewModel.clear() + } + } + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift index d6a5097c..bc686dba 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseHistoryMessageCell.swift @@ -1,64 +1,20 @@ - -import NIMSDK // Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. + +import NECommonKit +import NIMSDK import UIKit @objcMembers open class NEBaseHistoryMessageCell: UITableViewCell { + /// 搜索文案(用户匹配高亮) public var searchText: String? - public var rangeTextColor = UIColor.ne_blueText - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - setupSubviews() - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func setupSubviews() { - selectionStyle = .none - contentView.addSubview(headImge) - contentView.addSubview(title) - contentView.addSubview(subTitle) - contentView.addSubview(bottomLine) - contentView.addSubview(timeLabel) - } - - func configData(message: HistoryMessageModel?) { - guard let resultText = message?.content else { return } - - guard let searchStr = searchText else { return } + /// 高亮颜色 + public var rangeTextColor = UIColor.ne_normalTheme - let attributedStr = NSMutableAttributedString(string: resultText) - // range 表示从索引几开始取几个字符 - let range = attributedStr.mutableString.range(of: searchStr) - attributedStr.addAttribute( - .foregroundColor, - value: rangeTextColor, - range: range - ) - subTitle.attributedText = attributedStr - - title.text = message?.name - timeLabel.text = message?.time - - if let imageName = message?.avatar, !imageName.isEmpty { - headImge.setTitle("") - headImge.sd_setImage(with: URL(string: imageName), completed: nil) - } else { - headImge.setTitle(message?.name ?? "") - headImge.sd_setImage(with: nil, completed: nil) - headImge.backgroundColor = UIColor.colorWithString(string: message?.imMessage?.from) - } - } - - // MARK: lazy Method - - public lazy var headImge: NEUserHeaderView = { + /// 用户头像视图 + public lazy var headView: NEUserHeaderView = { let headView = NEUserHeaderView(frame: .zero) headView.titleLabel.textColor = .white headView.titleLabel.font = NEConstant.defaultTextFont(14) @@ -68,24 +24,29 @@ open class NEBaseHistoryMessageCell: UITableViewCell { return headView }() - public lazy var title: UILabel = { + /// 用户昵称 + public lazy var titleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_darkText label.font = NEConstant.defaultTextFont(14) label.textAlignment = .left + label.accessibilityIdentifier = "id.name" return label }() - public lazy var subTitle: UILabel = { + /// 消息内容 + public lazy var subTitleLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = UIColor.ne_lightText label.font = NEConstant.defaultTextFont(12) label.textAlignment = .left + label.accessibilityIdentifier = "id.message" return label }() + /// 分割线 public lazy var bottomLine: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false @@ -93,12 +54,102 @@ open class NEBaseHistoryMessageCell: UITableViewCell { return view }() + /// 消息时间 public lazy var timeLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = NEConstant.hexRGB(0xCCCCCC) label.font = NEConstant.defaultTextFont(12) label.textAlignment = .right + label.accessibilityIdentifier = "id.time" return label }() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupSubviews() + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + open func setupSubviews() { + selectionStyle = .none + contentView.addSubview(headView) + contentView.addSubview(titleLabel) + contentView.addSubview(subTitleLabel) + contentView.addSubview(bottomLine) + contentView.addSubview(timeLabel) + } + + open func configData(message: HistoryMessageModel?) { + if message?.fullName?.count ?? 0 <= 0 { + message?.fullName = message?.imMessage?.senderId + } + titleLabel.text = message?.fullName + timeLabel.text = message?.time + + if let imageName = message?.avatar, !imageName.isEmpty { + headView.setTitle("") + headView.sd_setImage(with: URL(string: imageName), completed: nil) + } else { + headView.setTitle(message?.shortName ?? "") + headView.sd_setImage(with: nil, completed: nil) + headView.backgroundColor = UIColor.colorWithString(string: message?.imMessage?.senderId) + } + } + + /// 根据label宽度显示关键字段 + /// - Parameter label: 文本控件 + /// - Parameter maxWidth: 最大宽度 + /// - Parameter importantText: 关键字段 + /// - Parameter fullText: 全文本 + public func truncateTextForLabel(_ label: UILabel, _ maxWidth: CGFloat, _ importantText: String, _ fullText: String) { + guard let font = label.font else { + return + } + + var truncatedText = "" + var displayText = fullText + + let importantTextWidth = importantText.width(withConstrainedHeight: label.frame.size.height, font: font) + let fullTextWidth = fullText.width(withConstrainedHeight: label.frame.size.height, font: font) + + if fullTextWidth > maxWidth { + let ellipsis = "..." + var accumulatedWidth: CGFloat = 0 + + for (_, char) in fullText.enumerated() { + let charWidth = String(char).width(withConstrainedHeight: label.frame.size.height, font: font) + if accumulatedWidth + charWidth + importantTextWidth + ellipsis.width(withConstrainedHeight: label.frame.size.height, font: font) <= maxWidth { + truncatedText.append(char) + accumulatedWidth += charWidth + } else { + break + } + } + + displayText = truncatedText + ellipsis + importantText + } + + let attributedStr = NSMutableAttributedString(string: displayText) + + do { + let regex = try NSRegularExpression(pattern: importantText, options: []) + let matches = regex.matches(in: displayText, options: [], range: NSRange(location: 0, length: displayText.count)) + + for match in matches { + let matchRange = match.range + attributedStr.addAttribute( + .foregroundColor, + value: rangeTextColor, + range: matchRange + ) + } + } catch { + print("Regex error: \(error.localizedDescription)") + } + label.attributedText = attributedStr + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift index 1a036a8f..e5553638 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamArrowSettingCell.swift @@ -23,6 +23,6 @@ open class NEBaseTeamArrowSettingCell: NEBaseTeamSettingCell { open func setupUI() { contentView.addSubview(titleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamAvatarViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamAvatarViewController.swift deleted file mode 100644 index 2a93848c..00000000 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamAvatarViewController.swift +++ /dev/null @@ -1,237 +0,0 @@ - -// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NECommonUIKit -import NIMSDK -import UIKit - -@objcMembers -open class NEBaseTeamAvatarViewController: NEBaseViewController, UICollectionViewDelegate, - UICollectionViewDataSource, UICollectionViewDelegateFlowLayout, UINavigationControllerDelegate { - public typealias SaveCompletion = () -> Void - public var block: SaveCompletion? - public var team: NIMTeam? - public let repo = TeamRepo.shared - - public let headerBack = UIView() - public let photoImage = UIImageView() - public let defaultHeaderBack = UIView() - public let tag = UILabel() - public var iconUrls = TeamRouter.iconUrls - - public var viewmodel = TeamAvatarViewModel() - - public lazy var headerView: NEUserHeaderView = { - let header = NEUserHeaderView(frame: .zero) - header.translatesAutoresizingMaskIntoConstraints = false - header.clipsToBounds = true - header.isUserInteractionEnabled = true - header.accessibilityIdentifier = "id.icon" - return header - }() - - public var headerUrl = "" - - public lazy var iconCollection: UICollectionView = { - let flow = UICollectionViewFlowLayout() - flow.scrollDirection = .horizontal - flow.minimumLineSpacing = 0 - flow.minimumInteritemSpacing = 0 - let collection = UICollectionView(frame: .zero, collectionViewLayout: flow) - collection.translatesAutoresizingMaskIntoConstraints = false - collection.delegate = self - collection.dataSource = self - collection.backgroundColor = .clear - collection.showsHorizontalScrollIndicator = false - collection.showsVerticalScrollIndicator = false - collection.clipsToBounds = false - collection.isScrollEnabled = false - return collection - }() - - override open func viewDidLoad() { - super.viewDidLoad() - viewmodel.getCurrentUserTeamMember(team?.teamId) - setupUI() - } - - open func setupUI() { - title = localizable("modify_headImage") - addRightAction(localizable("save"), #selector(savePhoto), self) - navigationView.setMoreButtonTitle(localizable("save")) - navigationView.addMoreButtonTarget(target: self, selector: #selector(savePhoto)) - - view.backgroundColor = .ne_lightBackgroundColor - - headerBack.backgroundColor = .white - headerBack.clipsToBounds = true - headerBack.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(headerBack) - - headerBack.addSubview(headerView) - NSLayoutConstraint.activate([ - headerView.centerXAnchor.constraint(equalTo: headerBack.centerXAnchor), - headerView.centerYAnchor.constraint(equalTo: headerBack.centerYAnchor), - headerView.heightAnchor.constraint(equalToConstant: 80.0), - headerView.widthAnchor.constraint(equalToConstant: 80.0), - ]) - if let url = team?.avatarUrl, !url.isEmpty { - headerView.sd_setImage(with: URL(string: url), completed: nil) - headerUrl = url - } - - photoImage.translatesAutoresizingMaskIntoConstraints = false - photoImage.image = coreLoader.loadImage("photo") - photoImage.accessibilityIdentifier = "id.camera" - headerBack.addSubview(photoImage) - - let gesture = UITapGestureRecognizer() - headerView.addGestureRecognizer(gesture) - gesture.addTarget(self, action: #selector(uploadPhoto)) - - defaultHeaderBack.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(defaultHeaderBack) - defaultHeaderBack.clipsToBounds = true - defaultHeaderBack.backgroundColor = .white - - tag.translatesAutoresizingMaskIntoConstraints = false - tag.text = localizable("default_icon") - tag.font = NEConstant.defaultTextFont(16.0) - tag.textColor = NEConstant.hexRGB(0x333333) - defaultHeaderBack.addSubview(tag) - - defaultHeaderBack.addSubview(iconCollection) - - for index in 0 ..< iconUrls.count { - let url = iconUrls[index] - if url == headerUrl { - let indexPath = IndexPath(row: index, section: 0) - iconCollection.selectItem(at: indexPath, animated: false, scrollPosition: .right) - } - } - - if changePermission() == false { - rightNavBtn.isHidden = true - navigationView.moreButton.isHidden = true - photoImage.isHidden = true - defaultHeaderBack.isHidden = true - } - } - - func changePermission() -> Bool { - if let type = team?.type, type == .normal { - return true - } - if let ownerId = team?.owner, IMKitClient.instance.isMySelf(ownerId) { - return true - } - if let mode = team?.updateInfoMode, mode == .all { - return true - } - if let member = viewmodel.currentTeamMember, member.type == .manager { - return true - } - return false - } - - // MARK: objc 方法 - - open func uploadPhoto() { - if changePermission() { - showBottomAlert(self) - } - } - - open func savePhoto() { - print("save photo") - if let tid = team?.teamId { - view.makeToastActivity(.center) - weak var weakSelf = self - weakSelf?.repo.updateTeamIcon(headerUrl, tid) { error in - NELog.infoLog(ModuleName + " " + self.className(), desc: #function + "CALLBACK " + (error?.localizedDescription ?? "no error")) - weakSelf?.view.hideToastActivity() - if let err = error as? NSError { - if err.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) - } else { - weakSelf?.showToast(localizable("failed_operation")) - } - } else { - weakSelf?.team?.avatarUrl = weakSelf?.headerUrl - if let completion = weakSelf?.block { - completion() - } - weakSelf?.navigationController?.popViewController(animated: true) - } - } - } - } - - // MAKR: UICollectionViewDelegate, UICollectionViewDataSource,UICollectionViewDelegateFlowLayout - open func collectionView(_ collectionView: UICollectionView, - numberOfItemsInSection section: Int) -> Int { - 5 - } - - open func collectionView(_ collectionView: UICollectionView, - cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { - UICollectionViewCell() - } - - open func collectionView(_ collectionView: UICollectionView, - layout collectionViewLayout: UICollectionViewLayout, - sizeForItemAt indexPath: IndexPath) -> CGSize { - .zero - } - - open func collectionView(_ collectionView: UICollectionView, - didSelectItemAt indexPath: IndexPath) { - if iconUrls.count > indexPath.row { - headerUrl = iconUrls[indexPath.row] - // headerView.image = coreLoader.loadImage("icon_\(indexPath.row)") - headerView.sd_setImage(with: URL(string: headerUrl), completed: nil) - } - } - - // MARK: UINavigationControllerDelegate - - open func imagePickerController(_ picker: UIImagePickerController, - didFinishPickingMediaWithInfo info: [UIImagePickerController - .InfoKey: Any]) { - let image: UIImage = info[UIImagePickerController.InfoKey.editedImage] as! UIImage - uploadHeadImage(image: image) - picker.dismiss(animated: true, completion: nil) - } - - open func uploadHeadImage(image: UIImage) { - weak var weakSelf = self - if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { - weakSelf?.showToast(commonLocalizable("network_error")) - return - } - view.makeToastActivity(.center) - if let imageData = image.jpegData(compressionQuality: 0.6) as NSData? { - let filePath = NSHomeDirectory().appending("/Documents/") - .appending(IMKitClient.instance.imAccid()) - let succcess = imageData.write(toFile: filePath, atomically: true) - if succcess { - NIMSDK.shared().resourceManager - .upload(filePath, progress: nil) { urlString, error in - if error == nil { - // 显示设置的照片 - weakSelf?.headerView.image = image - if let url = urlString { - weakSelf?.headerUrl = url - } - print("upload image success") - } else { - print("upload image failed,error = \(error!)") - } - weakSelf?.view.hideToastActivity() - } - } - } - } -} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamDefaultIconCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamDefaultIconCell.swift index 7f597c84..c38a95e4 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamDefaultIconCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamDefaultIconCell.swift @@ -8,26 +8,16 @@ import UIKit @objcMembers open class NEBaseTeamDefaultIconCell: UICollectionViewCell { - override public init(frame: CGRect) { - super.init(frame: frame) - setupUI() - } - - override public var isSelected: Bool { - didSet { - print("default icon select ", isSelected) - selectBack.isHidden = !isSelected - } - } - - lazy var iconImage: UIImageView = { - let image = UIImageView() - image.translatesAutoresizingMaskIntoConstraints = false - image.contentMode = .scaleAspectFill - return image + /// icon 图片 + public lazy var iconImageView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.contentMode = .scaleAspectFill + return imageView }() - lazy var selectBack: UIView = { + /// 选中背景 + public lazy var selectBackView: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = NEConstant.hexRGB(0xF4F4F4) @@ -37,12 +27,24 @@ open class NEBaseTeamDefaultIconCell: UICollectionViewCell { return view }() + override public init(frame: CGRect) { + super.init(frame: frame) + setupUI() + } + + override public var isSelected: Bool { + didSet { + print("default icon select ", isSelected) + selectBackView.isHidden = !isSelected + } + } + public required init?(coder: NSCoder) { super.init(coder: coder) } func setupUI() { - contentView.addSubview(selectBack) - contentView.addSubview(iconImage) + contentView.addSubview(selectBackView) + contentView.addSubview(iconImageView) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamInfoViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamInfoViewController.swift deleted file mode 100644 index f7f8ee64..00000000 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamInfoViewController.swift +++ /dev/null @@ -1,84 +0,0 @@ - -// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NIMSDK -import UIKit - -@objcMembers -open class NEBaseTeamInfoViewController: NEBaseViewController, UITableViewDelegate, - UITableViewDataSource { - public let viewmodel = TeamInfoViewModel() - - public var team: NIMTeam? - - public var cellClassDic = [Int: NEBaseTeamSettingCell.Type]() - - public lazy var contentTable: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorStyle = .none - table.sectionHeaderHeight = 0 - return table - }() - - init(team: NIMTeam?) { - super.init(nibName: nil, bundle: nil) - self.team = team - } - - public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override open func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - if let type = team?.type, type == .normal { - title = localizable("discuss_info") - } else { - title = localizable("group_info") - } - } - - override open func viewDidLoad() { - super.viewDidLoad() - viewmodel.getData(team) - setupUI() - } - - open func setupUI() { - view.addSubview(contentTable) - NSLayoutConstraint.activate([ - contentTable.leftAnchor.constraint(equalTo: view.leftAnchor), - contentTable.rightAnchor.constraint(equalTo: view.rightAnchor), - contentTable.topAnchor.constraint(equalTo: view.topAnchor, constant: topConstant + 12), - contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), - ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in - contentTable.register(value, forCellReuseIdentifier: "\(key)") - } - } - - // MARK: UITableViewDelegate, UITableViewDataSource - - open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - viewmodel.cellDatas.count - } - - open func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { - UITableViewCell() - } - - open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {} - - open func tableView(_ tableView: UITableView, - heightForRowAt indexPath: IndexPath) -> CGFloat { - let model = viewmodel.cellDatas[indexPath.row] - return model.rowHeight - } -} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift index 59cbf140..83ca66bb 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberCell.swift @@ -7,12 +7,12 @@ import NECommonUIKit import UIKit protocol TeamMemberCellDelegate: NSObject { - func didClickRemoveButton(_ model: TeamMemberInfoModel?, _ index: Int) + func didClickRemoveButton(_ model: NETeamMemberInfoModel?, _ index: Int) } @objcMembers open class NEBaseTeamMemberCell: UITableViewCell { - var currentModel: TeamMemberInfoModel? + var currentModel: NETeamMemberInfoModel? weak var delegate: TeamMemberCellDelegate? @@ -22,7 +22,7 @@ open class NEBaseTeamMemberCell: UITableViewCell { var index = 0 - lazy var headerView: NEUserHeaderView = { + public lazy var headerView: NEUserHeaderView = { let header = NEUserHeaderView(frame: .zero) header.titleLabel.font = NEConstant.defaultTextFont(14) header.titleLabel.textColor = UIColor.white @@ -32,7 +32,7 @@ open class NEBaseTeamMemberCell: UITableViewCell { return header }() - lazy var ownerLabel: UILabel = { + public lazy var ownerLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(12.0) @@ -48,7 +48,7 @@ open class NEBaseTeamMemberCell: UITableViewCell { return label }() - lazy var nameLabel: UILabel = { + public lazy var nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(16.0) @@ -57,7 +57,7 @@ open class NEBaseTeamMemberCell: UITableViewCell { return label }() - lazy var removeLabel: UILabel = { + public lazy var removeLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.text = localizable("team_member_remove") @@ -66,7 +66,7 @@ open class NEBaseTeamMemberCell: UITableViewCell { return label }() - lazy var removeBtn: UIButton = { + public lazy var removeButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false return button @@ -109,31 +109,32 @@ open class NEBaseTeamMemberCell: UITableViewCell { ]) } - func configure(_ model: TeamMemberInfoModel) { - if let userId = model.nimUser?.userId, let user = ChatUserCache.getUserInfo(userId) { - model.nimUser = user + func configure(_ model: NETeamMemberInfoModel) { + // 更新用户信息 + if let userId = model.nimUser?.user?.accountId, let user = NEFriendUserCache.shared.getFriendInfo(userId) { +// model.nimUser = user } currentModel = model - if let url = model.nimUser?.userInfo?.avatarUrl, !url.isEmpty { + if let url = model.nimUser?.user?.avatar, !url.isEmpty { headerView.sd_setImage(with: URL(string: url), completed: nil) headerView.setTitle("") } else { headerView.image = nil - headerView.setTitle(model.showNickInTeam()) - headerView.backgroundColor = UIColor.colorWithString(string: model.nimUser?.userId) + headerView.setTitle(model.showNickInTeam() ?? "") + headerView.backgroundColor = UIColor.colorWithString(string: model.nimUser?.user?.accountId) } nameLabel.text = model.atNameInTeam() } func setupRemoveButton() { - contentView.addSubview(removeBtn) + contentView.addSubview(removeButton) NSLayoutConstraint.activate([ - removeBtn.topAnchor.constraint(equalTo: contentView.topAnchor), - removeBtn.rightAnchor.constraint(equalTo: contentView.rightAnchor), - removeBtn.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), - removeBtn.widthAnchor.constraint(equalToConstant: 100), + removeButton.topAnchor.constraint(equalTo: contentView.topAnchor), + removeButton.rightAnchor.constraint(equalTo: contentView.rightAnchor), + removeButton.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + removeButton.widthAnchor.constraint(equalToConstant: 100), ]) - removeBtn.addTarget(self, action: #selector(didClickRemove), for: .touchUpInside) + removeButton.addTarget(self, action: #selector(didClickRemove), for: .touchUpInside) } func didClickRemove() { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift index b473143d..c3ddbd12 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamMemberSelectCell.swift @@ -16,17 +16,17 @@ open class NEBaseTeamMemberSelectCell: UITableViewCell { return imageView }() - lazy var headerView: NEUserHeaderView = { - let header = NEUserHeaderView(frame: .zero) - header.titleLabel.font = NEConstant.defaultTextFont(14) - header.titleLabel.textColor = UIColor.white - header.clipsToBounds = true - header.translatesAutoresizingMaskIntoConstraints = false - header.accessibilityIdentifier = "id.avatar" - return header + public lazy var headerView: NEUserHeaderView = { + let headerView = NEUserHeaderView(frame: .zero) + headerView.titleLabel.font = NEConstant.defaultTextFont(14) + headerView.titleLabel.textColor = UIColor.white + headerView.clipsToBounds = true + headerView.translatesAutoresizingMaskIntoConstraints = false + headerView.accessibilityIdentifier = "id.avatar" + return headerView }() - lazy var nameLabel: UILabel = { + public lazy var nameLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(16.0) @@ -53,13 +53,13 @@ open class NEBaseTeamMemberSelectCell: UITableViewCell { open func configureMember(_ model: NESelectTeamMember?) { checkImageView.isHighlighted = model?.isSelected ?? false - if let url = model?.member?.nimUser?.userInfo?.avatarUrl, !url.isEmpty { + if let url = model?.member?.nimUser?.user?.avatar, !url.isEmpty { headerView.sd_setImage(with: URL(string: url), completed: nil) headerView.setTitle("") } else { headerView.image = nil headerView.setTitle(model?.member?.showNickInTeam() ?? "") - headerView.backgroundColor = UIColor.colorWithString(string: model?.member?.nimUser?.userId) + headerView.backgroundColor = UIColor.colorWithString(string: model?.member?.nimUser?.user?.accountId) } nameLabel.text = model?.member?.atNameInTeam() } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift index ec8fcff6..14ee89c1 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingCell.swift @@ -18,6 +18,7 @@ public enum TeamSettingType: Int { @objcMembers open class NEBaseTeamSettingCell: CornerCell { + /// 群设置数据模型 var model: SettingCellModel? public lazy var titleLabel: UILabel = { @@ -29,7 +30,7 @@ open class NEBaseTeamSettingCell: CornerCell { return label }() - public lazy var arrow: UIImageView = { + public lazy var arrowView: UIImageView = { let imageView = UIImageView(image: coreLoader.loadImage("arrowRight")) imageView.translatesAutoresizingMaskIntoConstraints = false return imageView diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift index 4668b0fd..b86c4f95 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingHeaderCell.swift @@ -33,13 +33,13 @@ open class NEBaseTeamSettingHeaderCell: NEBaseTeamSettingCell { headerView.setTitle("") } else { headerView.setTitle(model?.defaultHeadData ?? "") - headerView.backgroundColor = UIColor.colorWithString(string: model?.defaultHeadData) + headerView.backgroundColor = UIColor.colorWithString(string: model?.subTitle) } } open func setupUI() { contentView.addSubview(titleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) contentView.addSubview(headerView) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift index a35e0c9d..20945da9 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamSettingSelectCell.swift @@ -7,7 +7,7 @@ import UIKit @objcMembers open class NEBaseTeamSettingSelectCell: NEBaseTeamSettingCell { - lazy var subTitleLabel: UILabel = { + public lazy var subTitleLabel: UILabel = { let label = UILabel() label.textColor = NEConstant.hexRGB(0x999999) label.font = NEConstant.defaultTextFont(14.0) @@ -31,9 +31,9 @@ open class NEBaseTeamSettingSelectCell: NEBaseTeamSettingCell { subTitleLabel.text = model?.subTitle } - func setupUI() { + open func setupUI() { contentView.addSubview(titleLabel) contentView.addSubview(subTitleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift index 15bbe475..63ea2b9a 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/NEBaseTeamUserCell.swift @@ -4,37 +4,47 @@ // found in the LICENSE file. import NECommonKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit @objcMembers open class NEBaseTeamUserCell: UICollectionViewCell { - var user: TeamMemberInfoModel? { + public var user: NETeamMemberInfoModel? { didSet { if let name = user?.showNickInTeam() { - userHeader.setTitle(name) + userHeaderView.setTitle(name) } - if let userId = user?.nimUser?.userId, let nimUser = ChatUserCache.getUserInfo(userId) { - user?.nimUser = nimUser - } - if let url = user?.nimUser?.userInfo?.avatarUrl, !url.isEmpty { - userHeader.sd_setImage(with: URL(string: url), completed: nil) - userHeader.setTitle("") - } else if let id = user?.nimUser?.userId { - userHeader.image = nil - userHeader.backgroundColor = UIColor.colorWithString(string: "\(id)") + + if let userId = user?.nimUser?.user?.accountId { + if let u = NEFriendUserCache.shared.getFriendInfo(userId) { + if let url = u.user?.avatar, !url.isEmpty { + userHeaderView.sd_setImage(with: URL(string: url), completed: nil) + userHeaderView.setTitle("") + } else if let id = u.user?.accountId { + userHeaderView.image = nil + userHeaderView.backgroundColor = UIColor.colorWithString(string: "\(id)") + } + } else { + if let url = user?.nimUser?.user?.avatar, !url.isEmpty { + userHeaderView.sd_setImage(with: URL(string: url), completed: nil) + userHeaderView.setTitle("") + } else if let id = user?.nimUser?.user?.accountId { + userHeaderView.image = nil + userHeaderView.backgroundColor = UIColor.colorWithString(string: "\(id)") + } + } } } } - lazy var userHeader: NEUserHeaderView = { - let header = NEUserHeaderView(frame: .zero) - header.translatesAutoresizingMaskIntoConstraints = false - header.titleLabel.font = NEConstant.defaultTextFont(11.0) - header.clipsToBounds = true - return header + public lazy var userHeaderView: NEUserHeaderView = { + let headerView = NEUserHeaderView(frame: .zero) + headerView.translatesAutoresizingMaskIntoConstraints = false + headerView.titleLabel.font = NEConstant.defaultTextFont(11.0) + headerView.clipsToBounds = true + return headerView }() override public init(frame: CGRect) { @@ -47,6 +57,6 @@ open class NEBaseTeamUserCell: UICollectionViewCell { } func setupUI() { - contentView.addSubview(userHeader) + contentView.addSubview(userHeaderView) } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift index 22af44a5..60aed92c 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingRightCustomCell.swift @@ -12,10 +12,10 @@ open class TeamSettingRightCustomCell: TeamSettingSubtitleCell { if let icon = model?.rightCustomViewIcon, icon.count > 0 { customRightView.isHidden = false customRightView.setImage(coreLoader.loadImage(icon), for: .normal) - arrow.isHidden = true + arrowView.isHidden = true } else { customRightView.isHidden = true - arrow.isHidden = false + arrowView.isHidden = false } } @@ -37,10 +37,10 @@ open class TeamSettingRightCustomCell: TeamSettingSubtitleCell { } public lazy var customRightView: UIButton = { - let btn = UIButton() - btn.translatesAutoresizingMaskIntoConstraints = false - btn.accessibilityIdentifier = "id.accountCopy" - return btn + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.accessibilityIdentifier = "id.accountCopy" + return button }() open func customRightViewClick() { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift index 115bd1b5..31cfb7fe 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamSettingSubtitleCell.swift @@ -9,6 +9,17 @@ import UIKit open class TeamSettingSubtitleCell: NEBaseTeamSettingCell { public var titleWidthAnchor: NSLayoutConstraint? + /// 标题 + public lazy var subTitleLabel: UILabel = { + let label = UILabel() + label.textColor = UIColor(hexString: "0xA6ADB6") + label.font = NEConstant.defaultTextFont(12.0) + label.translatesAutoresizingMaskIntoConstraints = false + label.textAlignment = .right + label.accessibilityIdentifier = "id.subTitleLabel" + return label + }() + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none @@ -16,13 +27,13 @@ open class TeamSettingSubtitleCell: NEBaseTeamSettingCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } open func setupUI() { contentView.addSubview(titleLabel) contentView.addSubview(subTitleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) NSLayoutConstraint.activate([ titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 36), @@ -33,14 +44,14 @@ open class TeamSettingSubtitleCell: NEBaseTeamSettingCell { titleWidthAnchor?.isActive = true NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), - arrow.widthAnchor.constraint(equalToConstant: 7), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + arrowView.widthAnchor.constraint(equalToConstant: 7), ]) NSLayoutConstraint.activate([ subTitleLabel.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: 10), - subTitleLabel.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -10), + subTitleLabel.rightAnchor.constraint(equalTo: arrowView.leftAnchor, constant: -10), subTitleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } @@ -52,14 +63,4 @@ open class TeamSettingSubtitleCell: NEBaseTeamSettingCell { subTitleLabel.text = m.subTitle } } - - public lazy var subTitleLabel: UILabel = { - let label = UILabel() - label.textColor = UIColor(hexString: "0xA6ADB6") - label.font = NEConstant.defaultTextFont(12.0) - label.translatesAutoresizingMaskIntoConstraints = false - label.textAlignment = .right - label.accessibilityIdentifier = "id.subTitleLabel" - return label - }() } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.swift index 2073ab57..e7a8e18c 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamAvatarViewModel.swift @@ -8,13 +8,54 @@ import UIKit @objcMembers open class TeamAvatarViewModel: NSObject { - let repo = ChatRepo.shared - var currentTeamMember: NIMTeamMember? + /// 群API单例 + let teamRepo = TeamRepo.shared + /// 当前用户群成员对象 + var currentTeamMember: V2NIMTeamMember? - func getCurrentUserTeamMember(_ teamId: String?) { + /// 获取当前用户群信息 + /// - Parameter teamId 群id + func getCurrentUserTeamMember(_ teamId: String?, _ completion: @escaping (NSError?) -> Void) { if let tid = teamId { - let currentUserAccid = IMKitClient.instance.imAccid() - currentTeamMember = repo.getTeamMemberList(userId: currentUserAccid, teamId: tid) + let currentUserAccid = IMKitClient.instance.account() + teamRepo.getTeamMember(tid, .TEAM_TYPE_NORMAL, currentUserAccid) { member, error in + self.currentTeamMember = member + completion(error) + } } } + + /// 更新群组头像 + /// - Parameter url: 群组头像Url + /// - Parameter teamId : 群组ID + /// - Parameter antispamConfig: 反垃圾配置 + /// - Parameter completion: 完成后的回调 + public func updateTeamAvatar(_ url: String, + _ teamId: String, + _ antispamConfig: V2NIMAntispamConfig?, + _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", url:\(url)") + teamRepo.updateTeamIcon(teamId, .TEAM_TYPE_NORMAL, url) { error in + completion(error) + } + } + + /// 创建文件上传任务 + /// - Parameter filePath 文件路径 + /// - Parameter sceneName 场景名 + public func createTask(_ filePath: String, + _ sceneName: String? = nil) -> V2NIMUploadFileTask { + ResourceRepo.shared.createUploadFileTask(filePath, sceneName) + } + + /// 上传文件 + /// - Parameter filepath: 上传文件路径 + /// - Parameter progress: 进度回调 + /// - Parameter completion: 完成回调 + public func uploadImageFile(_ fileTask: V2NIMUploadFileTask, + _ progress: ((Float) -> Void)?, + _ completion: ((String?, NSError?) -> Void)?) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", taskId:\(fileTask.taskId)") + ResourceRepo.shared.upload(fileTask, progress, completion) + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamHistoryMessageViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamHistoryMessageViewModel.swift new file mode 100644 index 00000000..d052795d --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamHistoryMessageViewModel.swift @@ -0,0 +1,224 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NECoreIM2Kit +import NIMSDK +import UIKit + +@objcMembers +open class TeamHistoryMessageViewModel: NSObject, NETeamListener, NEContactListener { + /// 群信息 + public var teamInfoModel: NETeamInfoModel? + /// 搜索结果 + public var searchResultInfos: [HistoryMessageModel]? + + /// 群模块API单例 + public let teamRepo = TeamRepo.shared + + /// 通讯录模块API单例 + public let contactRepo = ContactRepo.shared + + /// 消息模块API单例 + public let chatRepo = ChatRepo.shared + + /// 群成员缓存 + public var memberModelCacheDic = [String: NETeamMemberInfoModel]() + + override public init() { + super.init() + teamRepo.addTeamListener(self) + contactRepo.addContactListener(self) + } + + deinit {} + + /// 设置从上一个页面传入的成员 + public func setupCache() { + teamInfoModel?.users.forEach { member in + if let accountId = member.teamMember?.accountId { + memberModelCacheDic[accountId] = member + } + } + } + + /// 消息搜索 + /// - Parameter teamId: 群id + /// - Parameter searchContent: 搜索内容 + open func searchHistoryMessages(_ teamId: String?, _ searchContent: String, _ completion: @escaping (Error?, [HistoryMessageModel]?) -> Void) { + var infoDic = [String: NETeamMemberInfoModel]() + for (key, value) in memberModelCacheDic { + if let accountId = value.teamMember?.accountId { + infoDic[accountId] = value + } + } + + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", searchContent:\(searchContent)") + guard let teamId = teamId else { + completion(NSError(domain: "teamId is nil", code: -1), nil) + return + } + + let param = V2NIMMessageSearchParams() + param.keyword = searchContent + param.teamIds = [teamId] + + weak var weakSelf = self + chatRepo.searchMessages(params: param) { error, messages in + + if error == nil { + // 未找到用户信息信息记录 + var noFindUserSet = Set() + for message in messages ?? [] { + if let uid = message.imMessage?.senderId { + if let member = infoDic[uid] { + message.avatar = member.nimUser?.user?.avatar + message.fullName = member.atNameInTeam() + message.shortName = member.getShortName(member.showNickInTeam() ?? "") + } else { + noFindUserSet.insert(uid) + } + } + } + if noFindUserSet.count > 0 { + let accids = Array(noFindUserSet) + weakSelf?.getSearchMessageMembers(teamId, accids) { error, members in + if let err = error { + completion(err, nil) + } else { + members?.forEach { member in + if let accountId = member.teamMember?.accountId { + infoDic[accountId] = member + weakSelf?.memberModelCacheDic[accountId] = member + } + } + if let historyMessages = messages { + weakSelf?.bindMessageUserInfo(historyMessages, infoDic) + } + weakSelf?.searchResultInfos = messages + completion(nil, messages) + } + } + } else { + weakSelf?.searchResultInfos = messages + completion(nil, messages) + } + } else { + completion(error, nil) + } + } + } + + /// 获取消息对应的用户信息 + public func bindMessageUserInfo(_ messages: [HistoryMessageModel], _ infoDic: [String: NETeamMemberInfoModel]) { + for message in messages { + if let uid = message.imMessage?.senderId { + if let member = infoDic[uid] { + message.avatar = member.nimUser?.user?.avatar + message.fullName = member.atNameInTeam() + message.shortName = member.getShortName(member.showNickInTeam() ?? "") + } + } + } + } + + /// 获取群信息 + public func getTeamInfo(_ teamId: String?, _ completion: @escaping (V2NIMTeam?, NSError?) -> Void) { + guard let tid = teamId else { + return + } + teamRepo.getTeamInfo(tid) { team, error in + completion(team, error) + } + } + + /// 获取搜索消息中关联的群成员信息 + /// - Parameter teamId: 群id + /// - Parameter accounts: 群成员id列表 + /// - Parameter completion: 完成回调 + public func getSearchMessageMembers(_ teamId: String, _ accounts: [String], _ completion: @escaping (NSError?, [NETeamMemberInfoModel]?) -> Void) { + weak var weakSelf = self + teamRepo.getTeamMemberListByIds(teamId, .TEAM_TYPE_NORMAL, accounts) { members, error in + if let err = error { + completion(err, nil) + } else { + if let ms = members { + weakSelf?.getUsersInfo(ms) { error, memberInfos in + var retMembers = [NETeamMemberInfoModel]() + memberInfos?.forEach { member in + retMembers.append(member) + } + completion(nil, retMembers) + } + } else { + completion(nil, nil) + } + } + } + } + + /// 根据成员信息获取用户信息 + /// - Parameter members: 群成员列表 + /// - Parameter completion: 完成回调 + public func getUsersInfo(_ members: [V2NIMTeamMember], _ completion: @escaping (NSError?, [NETeamMemberInfoModel]?) -> Void) { + var memberModels = [NETeamMemberInfoModel]() + var accids = [String]() + + for member in members { + accids.append(member.accountId) + let model = NETeamMemberInfoModel() + model.teamMember = member + memberModels.append(model) + } + + contactRepo.getUserWithFriend(accountIds: accids) { users, v2Error in + + if v2Error != nil { + completion(nil, memberModels) + } else { + var dic = [String: NEUserWithFriend]() + if let us = users { + for user in us { + if let accid = user.user?.accountId { + dic[accid] = user + } + } + for model in memberModels { + if let accid = model.teamMember?.accountId { + if let user = dic[accid] { + model.nimUser = user + } + } + } + completion(nil, memberModels) + } + } + } + } + + /// 好友变更回调 + /// - parameter friendInfo: 好友信息对象 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + if let accountId = friendInfo.accountId { + memberModelCacheDic[accountId]?.nimUser?.friend = friendInfo + } + } + + /// 群成员变更回调 + /// - parameter teamMembers: 群成员信息对象列表 + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + guard let currentTeamId = teamInfoModel?.team?.teamId else { + return + } + // 判断是否有属于当前群 + var currentMembers = [V2NIMTeamMember]() + for member in teamMembers { + if member.teamId == currentTeamId { + currentMembers.append(member) + } + } + for member in currentMembers { + memberModelCacheDic[member.accountId]?.teamMember = member + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift index d3c5a532..ea415ad3 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamInfoViewModel.swift @@ -3,21 +3,45 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NIMSDK +/// 群信息变更回调协议 +@objc public protocol NETeamInfoDelegate: NSObjectProtocol { + /// 群信息变更 + func teamInfoDidUpdate(_ team: V2NIMTeam) +} + @objcMembers -open class TeamInfoViewModel: NSObject { +open class TeamInfoViewModel: NSObject, NETeamListener { + /// UI 列表数据源 var cellDatas = [SettingCellModel]() - func getData(_ team: NIMTeam?) { - NELog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId:\(team?.teamId ?? "nil")") + /// chat kit 群 api 单例 + public let teamRepo = TeamRepo.shared + + /// 群 + public var v2Team: V2NIMTeam? + + /// 代理 + public weak var delegate: NETeamInfoDelegate? + + override public init() { + super.init() + teamRepo.addTeamListener(self) + } + + /// 获取群信息 + /// - Parameter team: 群 + func getData(_ team: V2NIMTeam?) { + v2Team = team + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamId:\(team?.teamId ?? "nil")") cellDatas.removeAll() let headerCell = SettingCellModel() headerCell.cornerType = .topLeft.union(.topRight) headerCell.type = SettingCellType.SettingHeaderCell.rawValue - headerCell.headerUrl = team?.avatarUrl + headerCell.headerUrl = team?.avatar headerCell.rowHeight = 74.0 let nameCell = SettingCellModel() @@ -40,4 +64,13 @@ open class TeamInfoViewModel: NSObject { intrCell.cellName = localizable("team_intr") } } + + /// 群信息更新 + /// - Parameter team: 群 + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + if let teamId = v2Team?.teamId, teamId == team.teamId { + getData(team) + delegate?.teamInfoDidUpdate(team) + } + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.swift index 381db61d..42ca279f 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamIntroduceViewModel.swift @@ -9,13 +9,31 @@ import UIKit @objc @objcMembers open class TeamIntroduceViewModel: NSObject { - let repo = ChatRepo.shared - var currentTeamMember: NIMTeamMember? + /// 群API单例 + public let teamRepo = TeamRepo.shared + /// 群信息 + public var currentTeamMember: V2NIMTeamMember? - func getCurrentUserTeamMember(_ teamId: String?) { + /// 获取当前用户群信息 + /// - Parameter teamId 群id + func getCurrentUserTeamMember(_ teamId: String?, _ completion: @escaping (NSError?) -> Void) { if let tid = teamId { - let currentUserAccid = IMKitClient.instance.imAccid() - currentTeamMember = repo.getTeamMemberList(userId: currentUserAccid, teamId: tid) + let currentUserAccid = IMKitClient.instance.account() + teamRepo.getTeamMember(tid, .TEAM_TYPE_NORMAL, currentUserAccid) { member, error in + self.currentTeamMember = member + completion(error) + } + } + } + + /// 更新群介绍 + /// - Parameter teamId: 群组ID + /// - Parameter introduce: 群介绍 + /// - Parameter completion: 完成后的回调 + public func updateTeamIntroduce(_ teamId: String, _ introduce: String, + _ completion: @escaping (NSError?) -> Void) { + teamRepo.updateTeamIntroduce(teamId, .TEAM_TYPE_NORMAL, introduce) { error in + completion(error) } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManageViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManageViewModel.swift deleted file mode 100644 index ba330f3b..00000000 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManageViewModel.swift +++ /dev/null @@ -1,230 +0,0 @@ -//// Copyright (c) 2022 NetEase, Inc. All rights reserved. -// Use of this source code is governed by a MIT license that can be -// found in the LICENSE file. - -import NEChatKit -import NECommonKit -import NIMSDK -import UIKit - -public protocol TeamManageViewModelDelegate: NSObjectProtocol { - func didChangeInviteModeClick(_ model: SettingCellModel) - func didUpdateTeamInfoClick(_ model: SettingCellModel) - func didAtPermissionClick(_ model: SettingCellModel) - func didManagerClick() - func didRefreshData() -} - -@objc -@objcMembers -open class TeamManageViewModel: NSObject, NIMTeamManagerDelegate { - let repo = TeamRepo.shared - - let chatRepo = ChatRepo.shared - - public var teamInfoModel: TeamInfoModel? - - var sectionData = [SettingSectionModel]() - - public var managerUsers = [TeamMemberInfoModel]() - - var isRequestData = false - - weak var delegate: TeamManageViewModelDelegate? - - override public init() { - super.init() - NIMSDK.shared().teamManager.add(self) - } - - open func getSectionData() { - sectionData.removeAll() - if teamInfoModel?.team?.owner == IMKitClient.instance.imAccid() { - sectionData.append(getTopSection()) - } - sectionData.append(getMidSection()) - } - - open func getTeamInfo(_ tid: String, _ completion: @escaping (Error?) -> Void) { - if isRequestData == true { - return - } - weak var weakSelf = self - isRequestData = true - repo.fetchTeamInfo(tid) { error, teamInfo in - weakSelf?.isRequestData = false - if error == nil { - weakSelf?.managerUsers.removeAll() - } - teamInfo?.users.forEach { model in - if model.teamMember?.type == .manager { - weakSelf?.managerUsers.append(model) - } - } - weakSelf?.teamInfoModel = teamInfo - weakSelf?.getSectionData() - completion(error) - } - } - - open func getTopSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - weak var weakSelf = self - let model = SettingSectionModel() - let manager = SettingCellLabelArrowModel() - manager.cellName = localizable("manage_manger") - manager.type = SettingCellType.SettingArrowCell.rawValue - manager.rowHeight = 56 - manager.arrowLabelText = "\(managerUsers.count)" - model.cellModels.append(contentsOf: [manager]) - model.setCornerType() - manager.cellClick = { - weakSelf?.delegate?.didManagerClick() - } - return model - } - - open func getMidSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className(), desc: #function) - weak var weakSelf = self - let model = SettingSectionModel() - - let editTeam = SettingCellModel() - editTeam.cellName = localizable("who_edit_team_info") - editTeam.type = SettingCellType.SettingSelectCell.rawValue - editTeam.rowHeight = 73 - - if let updateMode = teamInfoModel?.team?.updateInfoMode, updateMode == .all { - editTeam.subTitle = localizable("team_all") - } else { - editTeam.subTitle = localizable("team_owner_and_manager") - } - - editTeam.cellClick = { - weakSelf?.delegate?.didUpdateTeamInfoClick(editTeam) - } - - let invitePermission = SettingCellModel() - invitePermission.cellName = localizable("who_edit_user_info") - invitePermission.type = SettingCellType.SettingSelectCell.rawValue - invitePermission.rowHeight = 73 - if let inviteMode = teamInfoModel?.team?.inviteMode, inviteMode == .all { - invitePermission.subTitle = localizable("team_all") - } else { - invitePermission.subTitle = localizable("team_owner_and_manager") - } - - invitePermission.cellClick = { - weakSelf?.delegate?.didChangeInviteModeClick(invitePermission) - } - - let atAll = SettingCellModel() - atAll.cellName = localizable("who_at_all") - atAll.type = SettingCellType.SettingSelectCell.rawValue - atAll.rowHeight = 73 - atAll.subTitle = localizable("team_owner_and_manager") - atAll.subTitle = getTeamAtPermissionValue() - - atAll.cellClick = { - weakSelf?.delegate?.didAtPermissionClick(atAll) - } - - model.cellModels.append(contentsOf: [editTeam, invitePermission, atAll]) - model.setCornerType() - - return model - } - - open func updateTeamAtPermission(_ isManager: Bool, _ completion: @escaping (Error?) -> Void) { - let value = isManager == true ? allowAtManagerValue : allowAtAllValue - guard let tid = teamInfoModel?.team?.teamId else { - return - } - let latestTeam = repo.getTeam(tid) - if let custom = latestTeam?.clientCustomInfo { - if var dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { - dic[keyAllowAtAll] = value - let info = NECommonUtil.getJSONStringFromDictionary(dic) - repo.updateTeamCustomInfo(info, tid) { error in - completion(error) - } - } - } else { - var dic = [String: Any]() - dic[keyAllowAtAll] = value - let info = NECommonUtil.getJSONStringFromDictionary(dic) - repo.updateTeamCustomInfo(info, tid) { error in - completion(error) - } - } - } - - func getTeamAtPermissionValue() -> String { - if let custom = teamInfoModel?.team?.clientCustomInfo { - if let dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { - if let value = dic[keyAllowAtAll] as? String { - if value == allowAtManagerValue { - return localizable("team_owner_and_manager") - } - } - } - } - return localizable("team_all") - } - - open func onTeamMemberChanged(_ team: NIMTeam) { - updateTeamInfo(team) - } - - open func onTeamUpdated(_ team: NIMTeam) { - updateTeamInfo(team) - } - - func sendTipNoti(_ isManagerPermission: Bool, _ completion: @escaping (Error?) -> Void) { - guard let teamId = teamInfoModel?.team?.teamId else { - return - } - let session = NIMSession(teamId, type: .team) - let tipContent = isManagerPermission ? localizable("team_tip_noti_at_manager") : localizable("team_tip_noti_at_all") - let tipMessage = NIMMessage() - let object = NIMTipObject(attach: nil, callbackExt: nil) - tipMessage.messageObject = object - tipMessage.text = tipContent - let setting = NIMMessageSetting() - setting.shouldBeCounted = false - setting.apnsEnabled = false - tipMessage.setting = setting - chatRepo.sendMessage(message: tipMessage, session: session) { error in - completion(error) - } - } - - private func updateTeamInfo(_ team: NIMTeam) { - guard let tid = teamInfoModel?.team?.teamId else { - return - } - - if isRequestData == true { - return - } - isRequestData = true - repo.fetchTeamInfo(tid) { [weak self] error, info in - if error == nil, info != nil { - self?.teamInfoModel = info - self?.managerUsers.removeAll() - info?.users.forEach { userInfo in - if userInfo.teamMember?.type == .manager { - self?.managerUsers.append(userInfo) - } - } - - self?.sectionData.removeAll() - self?.getSectionData() - self?.delegate?.didRefreshData() - - print("onTeamMemberChanged managers count : ", self?.managerUsers.count as Any) - } - self?.isRequestData = false - } - } -} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift index d768b721..eb8b8a36 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerListViewModel.swift @@ -10,33 +10,42 @@ public protocol TeamManagerListViewModelDelegate: NSObject { func didNeedReloadData() } -open class TeamManagerListViewModel: NSObject, NIMTeamManagerDelegate { - let repo = TeamRepo.shared +@objcMembers +open class TeamManagerListViewModel: NSObject, NETeamListener, NEContactListener { + /// 群API 单例 + public let teamRepo = TeamRepo.shared + /// 当前用户的群成员对象 + public var currentMember: V2NIMTeamMember? - public var currentMember: NIMTeamMember? - - public var managers = [TeamMemberInfoModel]() + public var managers = [NETeamMemberInfoModel]() weak var delegate: TeamManagerListViewModelDelegate? - + /// 群 id public var teamId: String? + /// 是否正在请求数据 + public var isRequest = false + override public init() { super.init() - NIMSDK.shared().teamManager.add(self) - NotificationCenter.default.addObserver(self, selector: #selector(refreshData), name: NENotificationName.updateFriendInfo, object: nil) + teamRepo.addTeamListener(self) + ContactRepo.shared.addContactListener(self) } deinit { - NIMSDK.shared().teamManager.remove(self) + teamRepo.removeTeamListener(self) + ContactRepo.shared.removeContactListener(self) } - open func getManagerDatas(_ tid: String, _ completion: @escaping (Error?) -> Void) { - repo.fetchTeamInfo(tid) { [weak self] error, teamInfo in + /// 获取群成员信息 + /// - Parameter teamId: 群id + /// - Parameter completion: 结果回调 + open func getManagerDatas(_ teamId: String, _ completion: @escaping (Error?) -> Void) { + getTeamInfo(teamId) { [weak self] model, error in if error == nil { self?.managers.removeAll() - teamInfo?.users.forEach { model in - if model.teamMember?.type == .manager { + model?.users.forEach { model in + if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { self?.managers.append(model) } } @@ -47,23 +56,48 @@ open class TeamManagerListViewModel: NSObject, NIMTeamManagerDelegate { } } + /// 获当前登录用户的群成员信息 + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getCurrentUserTeamMember(_ teamId: String, _ completion: @escaping (NSError?) -> Void) { + weak var weakSelf = self + teamRepo.getTeamMember(teamId, .TEAM_TYPE_NORMAL, IMKitClient.instance.account()) { member, error in + weakSelf?.currentMember = member + completion(error) + } + } + + /// 添加管理员 + /// - Parameter teamId: 群id + /// - Parameter uids: 用户id + /// - Parameter completion: 结果回调 open func addTeamManager(_ teamId: String, _ uids: [String], _ completion: @escaping (Error?) -> Void) { - repo.addTeamManagers(teamId, uids) { error in + teamRepo.addManagers(teamId, .TEAM_TYPE_NORMAL, uids) { error in completion(error) } } + /// 移除管理员 + /// - Parameter teamId: 群id + /// - Parameter uids: 用户id + /// - Parameter completion: 结果回调 open func removeTeamManager(_ teamId: String, _ uids: [String], _ completion: @escaping (Error?) -> Void) { - repo.removeTeamManagers(teamId, uids) { error in + teamRepo.removeManagers(teamId, .TEAM_TYPE_NORMAL, uids) { error in completion(error) } } + /// 获取当前群成员信息 + /// - Parameter teamId: 群id open func getCurrentMember(_ teamId: String) { - currentMember = repo.getMemberInfo(IMKitClient.instance.imAccid(), teamId) + weak var weakSelf = self + teamRepo.getTeamMember(teamId, .TEAM_TYPE_NORMAL, IMKitClient.instance.account()) { member, error in + weakSelf?.currentMember = member + } } - @objc func refreshData() { + /// 刷新数据 + func refreshData() { guard let tid = teamId else { return } @@ -74,16 +108,226 @@ open class TeamManagerListViewModel: NSObject, NIMTeamManagerDelegate { } } - public func onTeamMemberChanged(_ team: NIMTeam) { - guard let tid = teamId else { + /// 群成员离开 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员被踢 + /// - Parameter operatorAccountId: 操作者id + /// - Parameter teamMembers: 群成员 + public func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员加入 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + public func onTeamLeft(_ team: V2NIMTeam, isKicked: Bool) {} + + /// 群信息更新 + /// - Parameter team: 群对象 + private func onTeamMemberChanged(_ members: [V2NIMTeamMember]) { + var isCurrentTeam = false + for member in members { + if let currentTid = teamId, currentTid == member.teamId { + isCurrentTeam = true + break + } + } + + if isCurrentTeam == true { + guard let tid = teamId else { + return + } + + getManagerDatas(tid) { [weak self] error in + if error == nil { + self?.delegate?.didNeedReloadData() + } + } + } + } + + /// 好友信息变更 + /// - parameter friendInfo: 好友信息 + public func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + for memberInfo in managers { + if memberInfo.teamMember?.accountId == friendInfo.accountId { + let user = NEUserWithFriend(friend: friendInfo) + memberInfo.nimUser = user + delegate?.didNeedReloadData() + } + } + } + + /// 获取群信息(包含管理员) + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getTeamInfo(_ teamId: String, _ completion: @escaping (NETeamInfoModel?, NSError?) -> Void) { + weak var weakSelf = self + if isRequest == true { + return + } + isRequest = true + getCurrentUserTeamMember(teamId) { error in + if let err = error { + weakSelf?.isRequest = false + completion(nil, err) + } else { + weakSelf?.teamRepo.getTeamInfo(teamId) { team, error in + if let err = error { + weakSelf?.isRequest = false + completion(nil, err) + } else { + let model = NETeamInfoModel() + model.team = team + weakSelf?.getTeamManagers(model, .TEAM_MEMBER_ROLE_QUERY_TYPE_MANAGER) { error, teamInfo in + weakSelf?.isRequest = false + if let err = error { + completion(nil, err) + } else { + if let datas = teamInfo?.users { + weakSelf?.managers.removeAll() + weakSelf?.managers.append(contentsOf: datas) + weakSelf?.managers.sort(by: { model1, model2 in + if let time1 = model1.teamMember?.joinTime, let time2 = model2.teamMember?.joinTime { + return time2 > time1 + } + return false + }) + } + completion(teamInfo, error) + } + } + } + } + } + } + } + + /// 获取群管理员 + /// - Parameter teamModel:群信息对象 + /// - Parameter queryType: 查询类型 + /// - Parameter completion: 完成后的回调 + private func getTeamManagers(_ teamInfo: NETeamInfoModel, + _ queryType: V2NIMTeamMemberRoleQueryType, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamid:\(teamInfo.team?.teamId ?? "")") + guard let teamId = teamInfo.team?.teamId else { return } - if tid != team.teamId { + + weak var weakSelf = self + var memberLists = [V2NIMTeamMember]() + + weakSelf?.getAllTeamManagerInfos(teamId, nil, &memberLists, queryType) { ms, error in + if let e = error { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "CALLBACK fetchTeamMember \(String(describing: error))") + completion(e, nil) + } else { + if let members = ms { + weakSelf?.splitTeamManagers(members, teamInfo, 150) { error, model in + completion(error, model) + } + } else { + completion(error, teamInfo) + } + } + } + } + + /// 分页查询群成员信息 + /// - Parameter members: 要查询的群成员列表 + /// - Parameter model : 群信息 + /// - Parameter maxSizeByPage: 单页最大查询数量 + /// - Parameter completion: 完成后的回调 + private func splitTeamManagers(_ members: [V2NIMTeamMember], + _ model: NETeamInfoModel, + _ maxSizeByPage: Int = 150, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", members.count:\(members.count)") + var remaind = [[V2NIMTeamMember]]() + remaind.append(contentsOf: members.chunk(maxSizeByPage)) + fetchManagersInfo(&remaind, model, completion) + } + + /// 从云信服务器批量获取用户资料 + /// - Parameter remainUserIds: 用户集合 + /// - Parameter model: 群信息 + /// - Parameter completion: 成功回调 + private func fetchManagersInfo(_ remainUserIds: inout [[V2NIMTeamMember]], + _ model: NETeamInfoModel, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", remainUserIds.count:\(remainUserIds.count)") + guard let members = remainUserIds.first else { + completion(nil, model) return } - getManagerDatas(tid) { [weak self] error in - if error != nil { - self?.delegate?.didNeedReloadData() + + let accids = members.map(\.accountId) + var temArray = remainUserIds + weak var weakSelf = self + + ContactRepo.shared.getUserWithFriend(accountIds: accids) { infos, v2Error in + if let err = v2Error { + completion(err as NSError, model) + } else { + if let users = infos { + for index in 0 ..< members.count { + let memberInfoModel = NETeamMemberInfoModel() + memberInfoModel.teamMember = members[index] + if users.count > index { + let user = users[index] + memberInfoModel.nimUser = user + } + model.users.append(memberInfoModel) + } + } + temArray.removeFirst() + weakSelf?.fetchManagersInfo(&temArray, model, completion) + } + } + } + + /// 获取群管理员 + /// - Parameter teamId: 群ID + /// - Parameter nextToken: 下一页标识 + /// - Parameter completion: 完成回调 + private func getAllTeamManagerInfos(_ teamId: String, _ nextToken: String? = nil, _ memberList: inout [V2NIMTeamMember], _ queryType: V2NIMTeamMemberRoleQueryType, _ completion: @escaping ([V2NIMTeamMember]?, NSError?) -> Void) { + let option = V2NIMTeamMemberQueryOption() + option.limit = 100 + option.direction = .QUERY_DIRECTION_ASC + option.onlyChatBanned = false + option.roleQueryType = queryType + if let token = nextToken { + option.nextToken = token + } else { + option.nextToken = "" + } + var temMemberLists = memberList + teamRepo.getTeamMemberList(teamId, .TEAM_TYPE_NORMAL, option) { [weak self] result, error in + if let err = error { + completion(nil, err) + } else { + if let members = result?.memberList { + temMemberLists.append(contentsOf: members) + } + if let finished = result?.finished { + if finished == true { + completion(temMemberLists, nil) + } else { + self?.getAllTeamManagerInfos(teamId, result?.nextToken, &temMemberLists, queryType, completion) + } + } } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerViewModel.swift new file mode 100644 index 00000000..aa2422f0 --- /dev/null +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamManagerViewModel.swift @@ -0,0 +1,429 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatKit +import NECommonKit +import NIMSDK +import UIKit + +public protocol TeamManagerViewModelDelegate: NSObjectProtocol { + /// 邀请权限变更回调 + /// - Parameter model: 设置数据模型 + func didChangeInviteModeClick(_ model: SettingCellModel) + /// 群信息修改权限变更回调 + /// - Parameter model: 设置数据模型 + func didUpdateTeamInfoClick(_ model: SettingCellModel) + /// at权限变更回调 + /// - Parameter model: 设置数据模型 + func didAtPermissionClick(_ model: SettingCellModel) + /// 置顶权限变更回调 + /// - Parameter model: 设置数据模型 + func didTopMessagePermissionClick(_ model: SettingCellModel) + /// 管理员点击回调 + func didManagerClick() + /// 通知页面刷新回调 + func didRefreshData() +} + +@objc +@objcMembers +open class TeamManagerViewModel: NSObject, NETeamListener { + /// 群API单例 + public let teamRepo = TeamRepo.shared + /// 聊天API单例 + public let chatRepo = ChatRepo.shared + /// 群信息 + public var teamInfoModel: NETeamInfoModel? + /// UI 分区数据 + public var sectionData = [SettingSectionModel]() + /// 管理员数据 + public var managerUsers = [V2NIMTeamMember]() + + var isRequestData = false + + public weak var delegate: TeamManagerViewModelDelegate? + + /// 当前用户的群成员对象 + public var teamMember: V2NIMTeamMember? + + override public init() { + super.init() + teamRepo.addTeamListener(self) + } + + deinit { + teamRepo.removeTeamListener(self) + } + + /// 获取UI分组数据 + open func getSectionData() { + sectionData.removeAll() + if teamInfoModel?.team?.ownerAccountId == IMKitClient.instance.account() { + sectionData.append(getFirstSection()) + } + sectionData.append(getSecondSection()) + } + + /// 获取当前用户在群中的信息 + /// - Parameter userId: 用户id + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getCurrentUserTeamMember(_ userId: String, _ teamId: String?, completion: @escaping (V2NIMTeamMember?, NSError?) -> Void) { + if let tid = teamId { + teamRepo.getTeamMember(tid, .TEAM_TYPE_NORMAL, userId) { [weak self] member, error in + if let currentMember = member { + self?.teamMember = currentMember + completion(currentMember, nil) + } else { + completion(member, error) + } + } + } + } + + /// 获取管理员信息 + + /// 群信息(包含群成员) + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + open func getTeamWithMembers(_ teamId: String, _ completion: @escaping (Error?) -> Void) { + if isRequestData == true { + return + } + weak var weakSelf = self + isRequestData = true + + teamRepo.getTeamInfo(teamId) { team, error in + if let err = error { + weakSelf?.isRequestData = false + completion(err) + } else { + var memberList = [V2NIMTeamMember]() + weakSelf?.getAllTeamManagers(teamId, nil, &memberList, .TEAM_MEMBER_ROLE_QUERY_TYPE_MANAGER) { members, error in + if let err = error { + weakSelf?.isRequestData = false + completion(err) + } else { + weakSelf?.isRequestData = false + let model = NETeamInfoModel() + model.team = team + weakSelf?.teamInfoModel = model + weakSelf?.managerUsers.removeAll() + if let managers = members { + for member in managers { + if member.memberRole != .TEAM_MEMBER_ROLE_OWNER { + weakSelf?.managerUsers.append(member) + } + } + } + weakSelf?.sectionData.removeAll() + weakSelf?.getSectionData() + completion(nil) + } + } + } + } + } + + /// 获取顶部section(管理员数量) + open func getFirstSection() -> SettingSectionModel { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + weak var weakSelf = self + let model = SettingSectionModel() + let manager = SettingCellLabelArrowModel() + manager.cellName = localizable("manage_manger") + manager.type = SettingCellType.SettingArrowCell.rawValue + manager.rowHeight = 56 + manager.arrowLabelText = "\(managerUsers.count)" + model.cellModels.append(contentsOf: [manager]) + model.setCornerType() + manager.cellClick = { + weakSelf?.delegate?.didManagerClick() + } + return model + } + + /// 获取中间section数据(权限) + open func getSecondSection() -> SettingSectionModel { + NEALog.infoLog(ModuleName + " " + className(), desc: #function) + weak var weakSelf = self + let model = SettingSectionModel() + + // 谁可以编辑群信息 + let editTeamPermission = SettingCellModel() + editTeamPermission.cellName = localizable("who_edit_team_info") + editTeamPermission.type = SettingCellType.SettingSelectCell.rawValue + editTeamPermission.rowHeight = 73 + + if let updateMode = teamInfoModel?.team?.updateInfoMode, updateMode == .TEAM_UPDATE_INFO_MODE_ALL { + editTeamPermission.subTitle = localizable("team_all") + } else { + editTeamPermission.subTitle = localizable("team_owner_and_manager") + } + + editTeamPermission.cellClick = { + weakSelf?.delegate?.didUpdateTeamInfoClick(editTeamPermission) + } + + // 谁可以添加群成员 + let invitePermission = SettingCellModel() + invitePermission.cellName = localizable("who_edit_user_info") + invitePermission.type = SettingCellType.SettingSelectCell.rawValue + invitePermission.rowHeight = 73 + if let inviteMode = teamInfoModel?.team?.inviteMode, inviteMode == .TEAM_INVITE_MODE_ALL { + invitePermission.subTitle = localizable("team_all") + } else { + invitePermission.subTitle = localizable("team_owner_and_manager") + } + + invitePermission.cellClick = { + weakSelf?.delegate?.didChangeInviteModeClick(invitePermission) + } + + // 谁可以 @所有人 + let atAllPermission = SettingCellModel() + atAllPermission.cellName = localizable("who_at_all") + atAllPermission.type = SettingCellType.SettingSelectCell.rawValue + atAllPermission.rowHeight = 73 + atAllPermission.subTitle = localizable("team_owner_and_manager") + atAllPermission.subTitle = getTeamAtAllPermissionValue() + + atAllPermission.cellClick = { + weakSelf?.delegate?.didAtPermissionClick(atAllPermission) + } + + model.cellModels.append(contentsOf: [editTeamPermission, invitePermission, atAllPermission]) + + if IMKitConfigCenter.shared.topEnable { + // 谁可以置顶消息 + let topMessagePermission = SettingCellModel() + topMessagePermission.cellName = localizable("who_can_top_message") + topMessagePermission.type = SettingCellType.SettingSelectCell.rawValue + topMessagePermission.rowHeight = 73 + topMessagePermission.subTitle = localizable("team_owner_and_manager") + topMessagePermission.subTitle = getTeamTopMessagePermissionValue() + + topMessagePermission.cellClick = { + weakSelf?.delegate?.didTopMessagePermissionClick(topMessagePermission) + } + + model.cellModels.append(topMessagePermission) + } + + // 设置 section 圆角 + model.setCornerType() + + return model + } + + /// 更新at权限 + /// - Parameter isManager: 是否只有管理员能at,false 允许所有人发送at消息 + /// - Parameter completion: 完成回调 + open func updateTeamAtAllPermission(_ isManager: Bool, _ completion: @escaping (Error?) -> Void) { + let value = isManager == true ? allowAtManagerValue : allowAtAllValue + guard let tid = teamInfoModel?.team?.teamId else { + return + } + weak var weakSelf = self + teamRepo.getTeamInfo(tid) { team, error in + if let custom = team?.serverExtension { + if var dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { + dic[keyAllowAtAll] = value + dic["lastOpt"] = keyAllowAtAll + let info = NECommonUtil.getJSONStringFromDictionary(dic) + weakSelf?.teamRepo.updateTeamExtension(tid, .TEAM_TYPE_NORMAL, info) { error in + completion(error) + } + } + } else { + var dic = [String: Any]() + dic[keyAllowAtAll] = value + dic["lastOpt"] = keyAllowAtAll + let info = NECommonUtil.getJSONStringFromDictionary(dic) + weakSelf?.teamRepo.updateTeamExtension(tid, .TEAM_TYPE_NORMAL, info) { error in + completion(error) + } + } + } + } + + /// 获取at权限值 + func getTeamAtAllPermissionValue() -> String { + if let custom = teamInfoModel?.team?.serverExtension { + if let dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { + if let value = dic[keyAllowAtAll] as? String { + if value == allowAtManagerValue { + return localizable("team_owner_and_manager") + } + } + } + } + return localizable("team_all") + } + + /// 获取置顶消息权限值 + func getTeamTopMessagePermissionValue() -> String { + if let custom = teamInfoModel?.team?.serverExtension { + if let dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { + if let value = dic[keyAllowTopMessage] as? String { + if value == allowAtAllValue { + return localizable("team_all") + } + } + } + } + return localizable("team_owner_and_manager") + } + + /// 更新置顶权限 + /// - Parameter isManager: 是否只有管理员能置顶消息,false 允许所有人置顶消息 + /// - Parameter completion: 完成回调 + open func updateTeamTopMessagePermission(_ isManager: Bool, _ completion: @escaping (Error?) -> Void) { + let value = isManager == true ? allowAtManagerValue : allowAtAllValue + guard let tid = teamInfoModel?.team?.teamId else { + return + } + weak var weakSelf = self + teamRepo.getTeamInfo(tid) { team, error in + if let custom = team?.serverExtension { + if var dic = NECommonUtil.getDictionaryFromJSONString(custom) as? [String: Any] { + dic[keyAllowTopMessage] = value + dic["lastOpt"] = keyAllowTopMessage + let info = NECommonUtil.getJSONStringFromDictionary(dic) + weakSelf?.teamRepo.updateTeamExtension(tid, .TEAM_TYPE_NORMAL, info) { error in + completion(error) + } + } + } else { + var dic = [String: Any]() + dic[keyAllowTopMessage] = value + dic["lastOpt"] = keyAllowTopMessage + let info = NECommonUtil.getJSONStringFromDictionary(dic) + weakSelf?.teamRepo.updateTeamExtension(tid, .TEAM_TYPE_NORMAL, info) { error in + completion(error) + } + } + } + } + + /// 群成员离开 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员加入 + /// - Parameter operatorAccountId: 操作者 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群信息更新 + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + updateTeamInfo(team.teamId) + } + + /// 更改群组更新信息的权限 + /// - Parameter teamId : 群组ID + /// - Parameter mode : 群信息修改权限 + /// - Parameter completion: 完成后的回调 + public func updateTeamInfoPrivilege(_ teamId: String, _ mode: V2NIMTeamUpdateInfoMode, + _ completion: @escaping (NSError?, V2NIMTeam?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", mode:\(mode.rawValue)") + teamRepo.updateTeamInfoMode(teamId, .TEAM_TYPE_NORMAL, mode) { error, team in + completion(error, team) + } + } + + /// 更新群组邀请他人方式 + /// - Parameter teamId: 群组ID + /// - Parameter mode: 邀请模式 + /// - Parameter completion: 完成后的回调 + public func updateInviteMode(_ teamId: String, _ mode: V2NIMTeamInviteMode, + _ completion: @escaping (NSError?, V2NIMTeam?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", mode:\(mode.rawValue)") + teamRepo.updateInviteMode(teamId, .TEAM_TYPE_NORMAL, mode) { error, team in + completion(error, team) + } + } + + /// 群信息更新 + /// - Parameter teamId: 群id + private func updateTeamInfo(_ teamId: String) { + guard let tid = teamInfoModel?.team?.teamId else { + return + } + + if tid != teamId { + return + } + + getTeamWithMembers(teamId) { [weak self] error in + if error == nil { + self?.delegate?.didRefreshData() + } + } + } + + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + print("team manage onTeamMemberInfoUpdated") + onTeamMemberChanged(teamMembers) + } + + /// 处理群成员变更 + /// - Parameter members: 群成员 + private func onTeamMemberChanged(_ members: [V2NIMTeamMember]) { + var isCurrentTeam = false + for member in members { + if let currentTid = teamInfoModel?.team?.teamId, currentTid == member.teamId { + isCurrentTeam = true + } + if member.accountId == IMKitClient.instance.account() { + teamMember = member + } + } + + if isCurrentTeam == true { + if let currentTid = teamInfoModel?.team?.teamId { + print("team manage updateTeamInfo") + updateTeamInfo(currentTid) + } + } + } + + /// 获取群管理员 + /// - Parameter teamId: 群ID + /// - Parameter nextToken: 下一页标识 + /// - Parameter completion: 完成回调 + private func getAllTeamManagers(_ teamId: String, _ nextToken: String? = nil, _ memberList: inout [V2NIMTeamMember], _ queryType: V2NIMTeamMemberRoleQueryType, _ completion: @escaping ([V2NIMTeamMember]?, NSError?) -> Void) { + let option = V2NIMTeamMemberQueryOption() + option.limit = 100 + option.direction = .QUERY_DIRECTION_ASC + option.onlyChatBanned = false + option.roleQueryType = queryType + if let token = nextToken { + option.nextToken = token + } else { + option.nextToken = "" + } + var temMemberLists = memberList + teamRepo.getTeamMemberList(teamId, .TEAM_TYPE_NORMAL, option) { [weak self] result, error in + if let err = error { + completion(nil, err) + } else { + if let members = result?.memberList { + temMemberLists.append(contentsOf: members) + } + if let finished = result?.finished { + if finished == true { + completion(temMemberLists, nil) + } else { + self?.getAllTeamManagers(teamId, result?.nextToken, &temMemberLists, queryType, completion) + } + } + } + } + } +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift index 7b863baa..5ccf0f25 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMemberSelectViewModel.swift @@ -3,7 +3,7 @@ // found in the LICENSE file. import NEChatKit -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -11,67 +11,109 @@ public protocol TeamMemberSelectViewModelDelegate: NSObject { func didNeedRefresh() } -class TeamMemberSelectViewModel: NSObject, NIMTeamManagerDelegate { - let repo = TeamRepo.shared - +class TeamMemberSelectViewModel: NSObject, NETeamListener, NETeamMemberCacheListener { + /// 群API单例 + let teamRepo = TeamRepo.shared + /// 选中成员数据 var datas = [NESelectTeamMember]() var showDatas = [NESelectTeamMember]() - - var teamInfoModel: TeamInfoModel? - + /// 群信息 + var teamInfoModel: NETeamInfoModel? + /// 代理 weak var delegate: TeamMemberSelectViewModelDelegate? - - var selectDic = [String: TeamMemberInfoModel]() // key 值为用户 id - + /// 当前选中的数据 + var selectDic = [String: NETeamMemberInfoModel]() // key 值为用户 id + /// 是否正在发送请求 var isRequest = false - + /// 管理员account id 存放 var managerSet = Set() override init() { super.init() - NIMSDK.shared().teamManager.add(self) + teamRepo.addTeamListener(self) + NETeamMemberCache.shared.addTeamCacheListener(self) } deinit { - NIMSDK.shared().teamManager.remove(self) + teamRepo.removeTeamListener(self) + NETeamMemberCache.shared.removeTeamCacheListener(self) } - func getTeamInfo(_ teamId: String, _ completion: @escaping (Error?) -> Void) { + /// 群信息(包含群成员) + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getTeamInfo(_ teamId: String, _ completion: @escaping (NSError?) -> 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() + teamRepo.getTeamInfo(teamId) { team, error in + if let err = error { + weakSelf?.isRequest = false + completion(err) + } else { + let teamInfo = NETeamInfoModel() + teamInfo.team = team + if let members = NETeamMemberCache.shared.getTeamMemberCache(teamId), team?.memberCount == members.count { + teamInfo.users = members + weakSelf?.teamInfoModel = teamInfo + weakSelf?.datas.removeAll() + weakSelf?.showDatas.removeAll() + weakSelf?.getData() + weakSelf?.isRequest = false + completion(nil) + } else { + var memberLists = [V2NIMTeamMember]() + + weakSelf?.getSelectMemberInfos(teamId, nil, &memberLists, .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL) { ms, error in + if error != nil { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "CALLBACK fetchTeamMember \(String(describing: error))") + weakSelf?.isRequest = false + completion(nil) + } else { + if let members = ms { + weakSelf?.splitSelectMembers(members, teamInfo, 150) { error, model in + if let users = model?.users, users.count > 0 { + NEALog.infoLog(weakSelf?.className() ?? "", desc: "set team member cache success.") + NETeamMemberCache.shared.setCacheMembers(teamId, users) + model?.users = users + } + weakSelf?.teamInfoModel = model + weakSelf?.isRequest = false + weakSelf?.getData() + completion(error) + } + } else { + weakSelf?.isRequest = false + completion(error) + } + } + } + } } - weakSelf?.teamInfoModel = teamInfo - weakSelf?.getData() - completion(error) } } + /// 获取选择器数据 func getData() { var temFilters = Set() - selectDic.forEach { (key: String, value: TeamMemberInfoModel) in + for (key, _) in selectDic { temFilters.insert(key) } managerSet.removeAll() teamInfoModel?.users.forEach { [weak self] userModel in - if let uid = userModel.nimUser?.userId { + if let uid = userModel.nimUser?.user?.accountId { temFilters.remove(uid) - if uid == IMKitClient.instance.imAccid() { + if uid == IMKitClient.instance.account() { return } - if uid == self?.teamInfoModel?.team?.owner { + if uid == self?.teamInfoModel?.team?.ownerAccountId { return } - if userModel.teamMember?.type == .manager { + if userModel.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { self?.managerSet.insert(uid) self?.selectDic.removeValue(forKey: uid) return @@ -82,12 +124,12 @@ class TeamMemberSelectViewModel: NSObject, NIMTeamManagerDelegate { self?.datas.append(selectMember) self?.showDatas.append(selectMember) } - temFilters.forEach { uid in + for uid in temFilters { selectDic.removeValue(forKey: uid) } - datas.forEach { member in - if let accid = member.member?.nimUser?.userId { - if selectDic.contains(where: { (key: String, value: TeamMemberInfoModel) in + for member in datas { + if let accid = member.member?.nimUser?.user?.accountId { + if selectDic.contains(where: { (key: String, value: NETeamMemberInfoModel) in key == accid }) { member.isSelected = true @@ -96,39 +138,221 @@ class TeamMemberSelectViewModel: NSObject, NIMTeamManagerDelegate { } } + /// 数据缓存变更 + func memberCacheDidChange() { + if let tid = teamInfoModel?.team?.teamId { + print("memberCacheDidChange tid \(tid)") + weak var weakSelf = self + getTeamInfo(tid) { error in + if error == nil { + self.delegate?.didNeedRefresh() + } else { + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + "memberCacheDidChange get team info error \(error?.localizedDescription ?? ""))") + } + } + } + } + + /// 搜索所有数据 + /// - Parameter searchText: 搜索关键字 func searchAllData(_ searchText: String) -> [NESelectTeamMember] { let result = datas.filter { findContainStr(searchText, $0) } return result } + /// 所有展示数据 + /// - Parameter searchText: 搜索关键字 func searchShowData(_ searchText: String) -> [NESelectTeamMember] { let result = showDatas.filter { findContainStr(searchText, $0) } return result } + /// 判断选择器对象是否包含搜索字段 func findContainStr(_ text: String, _ selectModel: NESelectTeamMember) -> Bool { - if let uid = selectModel.member?.nimUser?.userId, uid.contains(text) { + if let uid = selectModel.member?.nimUser?.user?.accountId, uid.contains(text) { return true - } else if let nick = selectModel.member?.nimUser?.userInfo?.nickName, nick.contains(text) { + } else if let nick = selectModel.member?.nimUser?.user?.name, nick.contains(text) { return true - } else if let alias = selectModel.member?.nimUser?.alias, alias.contains(text) { + } else if let alias = selectModel.member?.nimUser?.friend?.alias, alias.contains(text) { return true - } else if let tNick = selectModel.member?.teamMember?.nickname, tNick.contains(text) { + } else if let tNick = selectModel.member?.teamMember?.teamNick, tNick.contains(text) { return true } return false } - func onTeamMemberChanged(_ team: NIMTeam) { - guard let tid = teamInfoModel?.team?.teamId else { + /// 群成员离开回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员被踢回调 + /// - Parameter operatorAccountId: 操作者id + /// - Parameter teamMembers: 群成员 + public func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员加入回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员更新回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员变更统一处理 + /// - Parameter teamMembers: 群成员 + private func onTeamMemberChanged(_ members: [V2NIMTeamMember]) { + var isCurrentTeam = false + for member in members { + if let currentTid = teamInfoModel?.team?.teamId, currentTid == member.teamId { + isCurrentTeam = true + break + } + } + + if isCurrentTeam == true { + if let tid = teamInfoModel?.team?.teamId { + getTeamInfo(tid) { [weak self] error in + if error == nil { + self?.delegate?.didNeedRefresh() + } + } + } + } + } + + /// 获取群成员 + /// - Parameter queryType: 查询类型 + /// - Parameter teamModel:群信息对象 + /// - Parameter completion: 完成后的回调 + private func getTeamMembers(_ teamInfo: NETeamInfoModel, + _ queryType: V2NIMTeamMemberRoleQueryType, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamid:\(teamInfo.team?.teamId ?? "")") + guard let teamId = teamInfo.team?.teamId else { return } - if tid != team.teamId { + + weak var weakSelf = self + + if let members = NETeamMemberCache.shared.getTeamMemberCache(teamId) { + teamInfo.users = members + completion(nil, teamInfo) + NEALog.infoLog(weakSelf?.className() ?? "", desc: "load team member from cache success.") return } - getTeamInfo(tid) { [weak self] error in - if error == nil { - self?.delegate?.didNeedRefresh() + + var memberLists = [V2NIMTeamMember]() + + weakSelf?.getSelectMemberInfos(teamId, nil, &memberLists, queryType) { ms, error in + if let e = error { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "CALLBACK fetchTeamMember \(String(describing: error))") + completion(e, nil) + } else { + if let members = ms { + weakSelf?.splitSelectMembers(members, teamInfo, 150) { error, model in + if let users = model?.users, users.count > 0 { + NEALog.infoLog(weakSelf?.className() ?? "", desc: "set team member cache success.") + NETeamMemberCache.shared.setCacheMembers(teamId, users) + } + completion(error, model) + } + } else { + completion(error, teamInfo) + } + } + } + } + + /// 分页查询群成员信息 + /// - Parameter members: 要查询的群成员列表 + /// - Parameter model : 群信息 + /// - Parameter maxSizeByPage: 单页最大查询数量 + /// - Parameter completion: 完成后的回调 + private func splitSelectMembers(_ members: [V2NIMTeamMember], + _ model: NETeamInfoModel, + _ maxSizeByPage: Int = 150, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", members.count:\(members.count)") + var remaind = [[V2NIMTeamMember]]() + remaind.append(contentsOf: members.chunk(maxSizeByPage)) + fetchSelectUsersInfo(&remaind, model, completion) + } + + /// 从云信服务器批量获取用户资料 + /// - Parameter remainUserIds: 用户集合 + /// - Parameter model: 群信息 + /// - Parameter completion: 成功回调 + private func fetchSelectUsersInfo(_ remainUserIds: inout [[V2NIMTeamMember]], + _ model: NETeamInfoModel, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", remainUserIds.count:\(remainUserIds.count)") + guard let members = remainUserIds.first else { + completion(nil, model) + return + } + + let accids = members.map(\.accountId) + var temArray = remainUserIds + weak var weakSelf = self + + ContactRepo.shared.getUserWithFriend(accountIds: accids) { infos, v2Error in + if let err = v2Error { + completion(err as NSError, model) + } else { + if let users = infos { + for index in 0 ..< members.count { + let memberInfoModel = NETeamMemberInfoModel() + memberInfoModel.teamMember = members[index] + if users.count > index { + let user = users[index] + memberInfoModel.nimUser = user + } + model.users.append(memberInfoModel) + } + } + temArray.removeFirst() + weakSelf?.fetchSelectUsersInfo(&temArray, model, completion) + } + } + } + + /// 获取群成员 + /// - Parameter teamId: 群ID + /// - Parameter completion: 完成回调 + private func getSelectMemberInfos(_ teamId: String, _ nextToken: String? = nil, _ memberList: inout [V2NIMTeamMember], _ queryType: V2NIMTeamMemberRoleQueryType, _ completion: @escaping ([V2NIMTeamMember]?, NSError?) -> Void) { + let option = V2NIMTeamMemberQueryOption() + option.limit = 1000 + option.onlyChatBanned = false + option.direction = .QUERY_DIRECTION_ASC + option.roleQueryType = queryType + if let token = nextToken { + option.nextToken = token + } else { + option.nextToken = "" + } + var temMemberLists = memberList + teamRepo.getTeamMemberList(teamId, .TEAM_TYPE_NORMAL, option) { [weak self] result, error in + if let err = error { + completion(nil, err) + } else { + if let members = result?.memberList { + temMemberLists.append(contentsOf: members) + } + if let finished = result?.finished { + if finished == true { + completion(temMemberLists, nil) + } else { + self?.getSelectMemberInfos(teamId, result?.nextToken, &temMemberLists, queryType, completion) + } + } } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift index 055f439f..fecb2744 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamMembersViewModel.swift @@ -3,6 +3,7 @@ // found in the LICENSE file. import NEChatKit +import NECoreIM2Kit import NIMSDK import UIKit @@ -10,33 +11,78 @@ protocol TeamMembersViewModelDelegate: NSObject { func didNeedRefreshUI() } -class TeamMembersViewModel: NSObject { +class TeamMembersViewModel: NSObject, NETeamListener, NEContactListener, NEConversationListener, NETeamMemberCacheListener, NEEventListener { + /// 是否正在请求数据 + public var isRequest = false + /// 群id + var teamId: String? + weak var delegate: TeamMembersViewModelDelegate? - var datas = [TeamMemberInfoModel]() + var datas = [NETeamMemberInfoModel]() + + /// 搜索结果数据 + public var searchDatas = [NETeamMemberInfoModel]() + + let teamRepo = TeamRepo.shared + + public var currentMember: V2NIMTeamMember? + + /// 在线状态记录 + var onLineEventDic = [String: NIMSubscribeEvent]() + + override init() { + super.init() + teamRepo.addTeamListener(self) + ContactRepo.shared.addContactListener(self) + ConversationRepo.shared.addListener(self) + NETeamMemberCache.shared.addTeamCacheListener(self) + if IMKitConfigCenter.shared.onlineStatusEnable { + EventSubscribeRepo.shared.addListener(self) + } + } - let repo = TeamRepo.shared - public var currentMember: NIMTeamMember? + deinit { + teamRepo.removeTeamListener(self) + ContactRepo.shared.removeContactListener(self) + ConversationRepo.shared.removeListener(self) + NETeamMemberCache.shared.removeTeamCacheListener(self) + if IMKitConfigCenter.shared.onlineStatusEnable { + EventSubscribeRepo.shared.removeListener(self) + } + } - func getMemberInfo(_ teamId: String) { - currentMember = repo.getMemberInfo(IMKitClient.instance.imAccid(), teamId) + /// 获取群成员信息 + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getMemberInfo(_ teamId: String, _ completion: @escaping (NSError?) -> Void) { + weak var weakSelf = self + teamRepo.getTeamMember(teamId, .TEAM_TYPE_NORMAL, IMKitClient.instance.account()) { member, error in + weakSelf?.currentMember = member + completion(error) + } } + /// 移除群成员 + /// - Parameter teamdId: 群id + /// - Parameter uids: 用户id func removeTeamMember(_ teamdId: String, _ uids: [String], _ completion: @escaping (NSError?) -> Void) { - repo.removeMembers(teamdId, uids) { error in + teamRepo.removeMembers(teamdId, .TEAM_TYPE_NORMAL, uids) { error in completion(error as NSError?) } } - func setShowDatas(_ memberDatas: [TeamMemberInfoModel]?) { - var owner: TeamMemberInfoModel? - var managers = [TeamMemberInfoModel]() - var normalMembers = [TeamMemberInfoModel]() + /// 设置成员数据 + /// - Parameter memberDatas: 成员数据 + func setShowDatas(_ memberDatas: [NETeamMemberInfoModel]?) { + var owner: NETeamMemberInfoModel? + var managers = [NETeamMemberInfoModel]() + var normalMembers = [NETeamMemberInfoModel]() memberDatas?.forEach { model in - if model.teamMember?.type == .owner { + if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_OWNER { owner = model - } else if model.teamMember?.type == .manager { + } else if model.teamMember?.memberRole == .TEAM_MEMBER_ROLE_MANAGER { managers.append(model) } else { normalMembers.append(model) @@ -49,14 +95,14 @@ class TeamMembersViewModel: NSObject { } // managers 根据 时间排序 排序 managers.sort { model1, model2 in - if let time1 = model1.teamMember?.createTime, let time2 = model2.teamMember?.createTime { + if let time1 = model1.teamMember?.joinTime, let time2 = model2.teamMember?.joinTime { return time2 > time1 } return false } // normalMembers 根据 时间排序 排序 normalMembers.sort { model1, model2 in - if let time1 = model1.teamMember?.createTime, let time2 = model2.teamMember?.createTime { + if let time1 = model1.teamMember?.joinTime, let time2 = model2.teamMember?.joinTime { return time2 > time1 } return false @@ -66,12 +112,14 @@ class TeamMembersViewModel: NSObject { delegate?.didNeedRefreshUI() } - func removeModel(_ model: TeamMemberInfoModel?) { + /// 移除成员数据(UI数据源) + /// - Parameter model: 成员数据 + func removeModel(_ model: NETeamMemberInfoModel?) { guard let rmModel = model else { return } datas.removeAll(where: { model in - if let rmUid = rmModel.nimUser?.userId, let uid = model.nimUser?.userId { + if let rmUid = rmModel.nimUser?.user?.accountId, let uid = model.nimUser?.user?.accountId { if rmUid == uid { return true } @@ -79,4 +127,224 @@ class TeamMembersViewModel: NSObject { return false }) } + + // MARK: - NEContactListener + + /// 好友信息变更回调 + /// - Parameter friendInfo: 好友信息 + func onFriendInfoChanged(_ friendInfo: V2NIMFriend) { + datas.forEach { [weak self] model in + if let accountId = model.nimUser?.user?.accountId, accountId == friendInfo.accountId { + if let tid = self?.teamId {} + return + } + } + } + + /// 群成员信息更新 + /// - Parameter teamMembers: 群成员信息 + func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) {} + + /// 群成员离开 + /// - Parameter teamMembers: 群成员信息 + func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + removeSearchData(teamMembers) + } + + /// 判断离开用户是不是当前搜索展示用户 + /// - Parameter teamMembers: 群成员信息 + public func removeSearchData(_ teamMembers: [V2NIMTeamMember]) { + if searchDatas.count <= 0 { + return + } + var memberSet = Set() + for member in teamMembers { + if let tid = teamId, tid == member.teamId { + memberSet.insert(member.accountId) + } + } + + if memberSet.count <= 0 { + return + } + searchDatas.removeAll { model in + if let accid = model.teamMember?.accountId, memberSet.contains(accid) { + return true + } + return false + } + } + + /// 群成员加入 + /// - Parameter teamMembers: 群成员信息 + func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) {} + + /// 群成员被踢 + /// - Parameter operatorAccountId: 操作者id + /// - Parameter teamMembers: 群成员信息 + func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + removeSearchData(teamMembers) + } + + /// 群成员信息更新统一处理方法 + /// - Parameter teamMembers: 群成员信息 + func changeMembers(_ teamMembers: [V2NIMTeamMember]) { + guard let tid = teamId else { + return + } + var isNeedRefresh = false + + for member in teamMembers { + if member.teamId == tid { + isNeedRefresh = true + break + } + } + + if isNeedRefresh == true { + getTeamInfo(tid) { model, error in + if error == nil { + self.delegate?.didNeedRefreshUI() + } + } + } + } + + /// 获取群信息(包含群成员) + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getTeamInfo(_ teamId: String, _ completion: @escaping (NETeamInfoModel?, NSError?) -> Void) { + weak var weakSelf = self + if isRequest == true { + return + } + isRequest = true + + getMemberInfo(teamId) { error in + if let err = error { + weakSelf?.isRequest = false + completion(nil, err) + } else { + weakSelf?.teamRepo.getTeamInfo(teamId) { team, error in + if let err = error { + weakSelf?.isRequest = false + completion(nil, err) + } else { + let model = NETeamInfoModel() + model.team = team + weakSelf?.getTeamMembers(model, .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL) { error, teamInfo in + weakSelf?.isRequest = false + if let err = error { + completion(nil, err) + } else { + if let datas = teamInfo?.users { + weakSelf?.setShowDatas(datas) + } + completion(teamInfo, error) + } + } + } + } + } + } + } + + /// 获取群成员 + /// - Parameter queryType: 查询类型 + /// - Parameter teamModel:群信息对象 + /// - Parameter completion: 完成后的回调 + private func getTeamMembers(_ teamInfo: NETeamInfoModel, + _ queryType: V2NIMTeamMemberRoleQueryType, + _ completion: @escaping (NSError?, NETeamInfoModel?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamid:\(teamInfo.team?.teamId ?? "")") + guard let teamId = teamInfo.team?.teamId else { + return + } + + weak var weakSelf = self + + if let members = NETeamMemberCache.shared.getTeamMemberCache(teamId), teamInfo.team?.memberCount == members.count { + teamInfo.users = members + completion(nil, teamInfo) + NEALog.infoLog(weakSelf?.className() ?? "", desc: "load team member from cache success.") + return + } + + NETeamMemberCache.shared.getAllTeamMemberInfos(teamId, queryType) { error, teamInfo in + if let err = error { + NEALog.infoLog(ModuleName + " " + (weakSelf?.className() ?? ""), desc: "CALLBACK fetchTeamMember \(String(describing: error))") + completion(err, nil) + } else { + if let members = teamInfo?.users, members.count > 0 { + NEALog.infoLog(weakSelf?.className() ?? "", desc: "set team member cache success.") + NETeamMemberCache.shared.setCacheMembers(teamId, members) + } + completion(error, teamInfo) + } + } + } + + /// 订阅群成员在线状态 + /// - Parameter members: 成员列表 + func subcribeMembers(_ members: [NETeamMemberInfoModel], _ completion: @escaping (NSError?) -> Void) { + var accounts = [String]() + for model in members { + if let accountId = model.teamMember?.accountId { + accounts.append(accountId) + } + } + NEEventSubscribeManager.shared.subscribeUsersOnlineState(accounts) { error in + completion(error) + } + } + + /// 取消订阅群成员 + func unSubcribeMembers(_ members: [NETeamMemberInfoModel], _ completion: @escaping (NSError?) -> Void) { + var accounts = [String]() + for model in members { + if let accountId = model.teamMember?.accountId { + accounts.append(accountId) + } + } + NEEventSubscribeManager.shared.unSubscribeUsersOnlineState(accounts) { error in + completion(error) + } + } + + /// 订阅状态变更回调 + /// - Parameter event: 订阅事件 + func onRecvSubscribeEvents(_ event: [NIMSubscribeEvent]) { + NEALog.infoLog(className(), desc: #function + " event count : \(event.count)") + for e in event { + print("event from : \(e.from ?? "") event value : \(e.value) event type : \(e.type)") + if e.type == NIMSubscribeSystemEventType.online.rawValue, let acountId = e.from { + onLineEventDic[acountId] = e + } + } + delegate?.didNeedRefreshUI() + } + + /// 缓存数据变更回调 + func memberCacheDidChange() { + NEALog.infoLog(className(), desc: #function + " memberCacheDidChange") + guard let tid = teamId else { + return + } + + weak var weakSelf = self + if let members = NETeamMemberCache.shared.getTeamMemberCache(tid) { + getMemberInfo(tid) { error in + if error == nil { + weakSelf?.setShowDatas(members) + weakSelf?.delegate?.didNeedRefreshUI() + } else { + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + " getMemberInfo error:\(String(describing: error))") + } + } + } else { + getTeamInfo(tid) { teamInfo, error in + weakSelf?.delegate?.didNeedRefreshUI() + } + } + } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.swift index bf92a566..eb87f9fb 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamNameViewModel.swift @@ -9,13 +9,18 @@ import UIKit @objc @objcMembers open class TeamNameViewModel: NSObject { - let repo = ChatRepo.shared - var currentTeamMember: NIMTeamMember? + let teamRepo = TeamRepo.shared + var currentTeamMember: V2NIMTeamMember? - func getCurrentUserTeamMember(_ teamId: String?) { + /// 获取当前用户群信息 + /// - Parameter teamId 群id + func getCurrentUserTeamMember(_ teamId: String?, _ completion: @escaping (NSError?) -> Void) { if let tid = teamId { - let currentUserAccid = IMKitClient.instance.imAccid() - currentTeamMember = repo.getTeamMemberList(userId: currentUserAccid, teamId: tid) + let currentUserAccid = IMKitClient.instance.account() + teamRepo.getTeamMember(tid, .TEAM_TYPE_NORMAL, currentUserAccid) { member, error in + self.currentTeamMember = member + completion(error) + } } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift index 7ecec906..7bccb75f 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift @@ -3,11 +3,11 @@ // found in the LICENSE file. import Foundation -import NECoreIMKit +import NECoreIM2Kit import NIMSDK import UIKit -protocol TeamSettingViewModelDelegate: NSObjectProtocol { +public protocol TeamSettingViewModelDelegate: NSObjectProtocol { func didClickChangeNick() func didChangeInviteModeClick(_ model: SettingCellModel) func didUpdateTeamInfoClick(_ model: SettingCellModel) @@ -16,50 +16,67 @@ protocol TeamSettingViewModelDelegate: NSObjectProtocol { func didError(_ error: NSError) func didClickMark() func didClickTeamManage() + func didShowNoNetworkToast() } @objcMembers -open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { +open class TeamSettingViewModel: NSObject, NETeamListener, NEConversationListener { + /// 分区区域数据 public var sectionData = [SettingSectionModel]() public var searchResultInfos: [HistoryMessageModel]? - - public var teamInfoModel: TeamInfoModel? - - public let repo = TeamRepo.shared - - public var memberInTeam: NIMTeamMember? - - weak var delegate: TeamSettingViewModelDelegate? + /// 群信息(包含群成员列表) + public var teamInfoModel: NETeamInfoModel? + /// 群模块接口单例 + public let teamRepo = TeamRepo.shared + /// 通讯录接口单例 + public let contactRepo = ContactRepo.shared + /// 当前用户的群成员信息 + public var memberInTeam: V2NIMTeamMember? + /// 群设置代理 + public weak var delegate: TeamSettingViewModelDelegate? private let className = "TeamSettingViewModel" - + /// 群类型 public var teamSettingType: TeamSettingType = .Discuss + /// 群对应的会话信息 + public var conversation: V2NIMConversation? + /// 会话API单例 + public var conversationRepo = ConversationRepo.shared + /// 是否获取过群设置数据 + public var isRequestSettingData = false + + /// 群成员 + public var allMembersDic = [String: V2NIMTeamMember]() override public init() { super.init() - NIMSDK.shared().teamManager.add(self) + teamRepo.addTeamListener(self) + conversationRepo.addListener(self) } - deinit { - NIMSDK.shared().teamManager.remove(self) + func clear() { + teamRepo.removeTeamListener(self) + conversationRepo.removeListener(self) + NETeamMemberCache.shared.trigerTimer() } func getData() { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) sectionData.removeAll() sectionData.append(getTwoSection()) - print("current team type : ", teamInfoModel?.team?.type.rawValue as Any) - if let type = teamInfoModel?.team?.type, type == .advanced { - if teamInfoModel?.team?.clientCustomInfo?.contains(discussTeamKey) == true { + print("current team type : ", teamInfoModel?.team?.teamType.rawValue as Any) + if let type = teamInfoModel?.team?.teamType, type == .TEAM_TYPE_NORMAL { + if teamInfoModel?.team?.serverExtension?.contains(discussTeamKey) == true { return } sectionData.append(getThreeSection()) } } + // 头像 成员列表 private func getOneSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) let model = SettingSectionModel() let cellModel = SettingCellModel() cellModel.type = SettingCellType.SettingHeaderCell.rawValue @@ -69,13 +86,20 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return model } + // 标记 历史记录 消息提醒 private func getTwoSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) + let model = SettingSectionModel() + guard let tid = teamInfoModel?.team?.teamId else { + NEALog.infoLog(ModuleName + " " + className, desc: #function + " teamId is nil") + return model + } + weak var weakSelf = self - // 标记 + // 标记 置顶 昵称 let mark = SettingCellModel() mark.cellName = localizable("mark") mark.type = SettingCellType.SettingArrowCell.rawValue @@ -96,15 +120,22 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { remind.cellName = localizable("message_remind") remind.type = SettingCellType.SettingSwitchCell.rawValue - if let noti = teamInfoModel?.team?.notifyStateForNewMsg, noti == .all { + let mode = teamRepo.getTeamMuteStatus(tid) + if mode == .TEAM_MESSAGE_MUTE_MODE_OFF { remind.switchOpen = true } + remind.swichChange = { isOpen in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + remind.switchOpen = !isOpen + weakSelf?.delegate?.didShowNoNetworkToast() + weakSelf?.delegate?.didNeedRefreshUI() + return + } if let tid = weakSelf?.teamInfoModel?.team?.teamId { if isOpen == true { - // weakSelf?.repo.updateNoti(.all, tid) - weakSelf?.repo.setTeamNotify(.all, tid) { error in - if let err = error as? NSError { + weakSelf?.teamRepo.setTeamMuteStatus(tid, .TEAM_TYPE_NORMAL, .TEAM_MESSAGE_MUTE_MODE_OFF) { error in + if let err = error { weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) } else { @@ -112,9 +143,8 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { } } } else { - // weakSelf?.repo.updateNoti(.none, tid) - weakSelf?.repo.setTeamNotify(.none, tid) { error in - if let err = error as? NSError { + weakSelf?.teamRepo.setTeamMuteStatus(tid, .TEAM_TYPE_NORMAL, .TEAM_MESSAGE_MUTE_MODE_ON) { error in + if let err = error { weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) } else { @@ -130,23 +160,22 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { setTop.cellName = localizable("session_set_top") setTop.type = SettingCellType.SettingSwitchCell.rawValue - if let tid = teamInfoModel?.team?.teamId { - let session = NIMSession(tid, type: .team) - setTop.switchOpen = repo.isStickTop(session) + if let currentConversation = conversation { + setTop.switchOpen = currentConversation.stickTop } + // 置顶 setTop.swichChange = { isOpen in - if let tid = weakSelf?.teamInfoModel?.team?.teamId { - let session = NIMSession(tid, type: .team) - if isOpen { - // 不存在最近会话的置顶,先创建最近会话 - if weakSelf?.getRecenterSession() == nil { - NELog.infoLog(weakSelf?.className() ?? "", desc: #function + "addRecentetSession") - weakSelf?.addRecentetSession() - } - let params = NIMAddStickTopSessionParams(session: session) - weakSelf?.repo.addStickTop(params: params) { error, info in - NELog.infoLog(weakSelf?.className() ?? "", desc: #function + "addStickTop error : \(error?.localizedDescription ?? "") ") + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + setTop.switchOpen = !isOpen + weakSelf?.delegate?.didShowNoNetworkToast() + weakSelf?.delegate?.didNeedRefreshUI() + return + } + if isOpen { + if let teamId = weakSelf?.teamInfoModel?.team?.teamId, let conversationId = V2NIMConversationIdUtil.teamConversationId(teamId) { + weakSelf?.conversationRepo.setStickTop(conversationId, true) { error in + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + "addStickTop error : \(error?.localizedDescription ?? "") ") if let err = error { weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) @@ -154,30 +183,32 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { setTop.switchOpen = true } } - } else { - if let info = weakSelf?.repo.getTopSessionInfo(session) { - weakSelf?.repo.removeStickTop(params: info) { error, info in - NELog.infoLog(weakSelf?.className() ?? "", desc: #function + "removeStickTop error : \(error?.localizedDescription ?? "") ") - if let err = error { - weakSelf?.delegate?.didNeedRefreshUI() - weakSelf?.delegate?.didError(err) - } else { - setTop.switchOpen = false - } + } + } else { + if let teamId = weakSelf?.teamInfoModel?.team?.teamId, let conversationId = V2NIMConversationIdUtil.teamConversationId(teamId) { + weakSelf?.conversationRepo.setStickTop(conversationId, false) { error in + NEALog.infoLog(weakSelf?.className() ?? "", desc: #function + "removeStickTop error : \(error?.localizedDescription ?? "") ") + if let err = error { + weakSelf?.delegate?.didNeedRefreshUI() + weakSelf?.delegate?.didError(err) + } else { + setTop.switchOpen = false } } } } } + // 群昵称 let nick = SettingCellModel() nick.cellName = localizable("team_nick") nick.type = SettingCellType.SettingArrowCell.rawValue nick.cellClick = { weakSelf?.delegate?.didClickChangeNick() } - - model.cellModels.append(mark) + if IMKitConfigCenter.shared.pinEnable { + model.cellModels.append(mark) + } model.cellModels.append(history) model.cellModels.append(remind) model.cellModels.append(setTop) @@ -189,9 +220,9 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return model } - // 群昵称/群禁言 + // 群昵称 群禁言 private func getThreeSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) let model = SettingSectionModel() weak var weakSelf = self @@ -199,14 +230,25 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { forbiddenWords.cellName = localizable("team_no_speak") forbiddenWords.type = SettingCellType.SettingSwitchCell.rawValue - if let mute = teamInfoModel?.team?.inAllMuteMode() { - forbiddenWords.switchOpen = mute + if let chatBanndedMode = teamInfoModel?.team?.chatBannedMode { + if chatBanndedMode == .TEAM_CHAT_BANNED_MODE_BANNED_ALL || chatBanndedMode == .TEAM_CHAT_BANNED_MODE_BANNED_NORMAL { + forbiddenWords.switchOpen = true + } else { + forbiddenWords.switchOpen = false + } } + forbiddenWords.swichChange = { isOpen in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + forbiddenWords.switchOpen = !isOpen + weakSelf?.delegate?.didShowNoNetworkToast() + weakSelf?.delegate?.didNeedRefreshUI() + return + } if let tid = weakSelf?.teamInfoModel?.team?.teamId { - weakSelf?.repo.muteAllMembers(isOpen, tid) { error in + weakSelf?.teamRepo.setTeamChatBannedMode(tid, .TEAM_TYPE_NORMAL, isOpen ? .TEAM_CHAT_BANNED_MODE_BANNED_NORMAL : .TEAM_CHAT_BANNED_MODE_NONE) { error in print("update mute error : ", error as Any) - if let err = error as? NSError { + if let err = error { forbiddenWords.switchOpen = !isOpen weakSelf?.delegate?.didNeedRefreshUI() weakSelf?.delegate?.didError(err) @@ -236,8 +278,9 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return model } + // 邀请 修改群信息 private func getFourSection() -> SettingSectionModel { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) weak var weakSelf = self let model = SettingSectionModel() @@ -246,7 +289,7 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { invitePermission.type = SettingCellType.SettingSelectCell.rawValue invitePermission.rowHeight = 73 - if let inviteMode = teamInfoModel?.team?.inviteMode, inviteMode == .all { + if let inviteMode = teamInfoModel?.team?.inviteMode, inviteMode == .TEAM_INVITE_MODE_ALL { invitePermission.subTitle = localizable("team_all") } else { invitePermission.subTitle = localizable("team_owner") @@ -260,7 +303,7 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { modifyPermission.cellName = localizable("modify_team_info_permission") modifyPermission.type = SettingCellType.SettingSelectCell.rawValue modifyPermission.rowHeight = 73 - if let updateMode = teamInfoModel?.team?.updateInfoMode, updateMode == .all { + if let updateMode = teamInfoModel?.team?.updateInfoMode, updateMode == .TEAM_UPDATE_INFO_MODE_ALL { modifyPermission.subTitle = localizable("team_all") } else { modifyPermission.subTitle = localizable("team_owner") @@ -278,81 +321,214 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return model } - func getTeamInfo(_ teamId: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") + /// 获取群(内部获取群成员) + /// - Parameter teamId: 群id + /// - Parameter completion: 回调 + func getTeamWithMembers(_ teamId: String, _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") weak var weakSelf = self - repo.fetchTeamInfo(teamId) { error, teamInfo in - weakSelf?.teamInfoModel = teamInfo + if isRequestSettingData == true { + return + } + getTeamInfoWithSomeMembers(teamId) { error, finished in + weakSelf?.isRequestSettingData = false if error == nil { weakSelf?.getData() - weakSelf?.getCurrentMember(IMKitClient.instance.imAccid(), teamId) } completion(error) } } - public func dismissTeam(_ teamId: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") - repo.dismissTeam(teamId, completion) + /// 获取所有群成员信息并缓存 + /// - Parameter teamId: 群id + /// - Parameter queryType: 查询类型 + /// - Parameter completion: 完成后的回调 + public func getAllTeamMemberInfos(_ teamId: String, + _ queryType: V2NIMTeamMemberRoleQueryType, + _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className(), desc: #function + ", teamid:\(teamId)") + if NETeamMemberCache.shared.getTeamMemberCache(teamId) != nil { + NETeamMemberCache.shared.endTimer() + NEALog.infoLog(className(), desc: "load team member from cache success.") + completion(nil) + return + } + weak var weakSelf = self + NETeamMemberCache.shared.getAllTeamMemberInfos(teamId, queryType) { error, teamInfo in + if let err = error { + completion(err) + } else { + if let members = teamInfo?.users { + NEALog.infoLog(weakSelf?.className() ?? "", desc: "set team member cache success.") + NETeamMemberCache.shared.setCacheMembers(teamId, members) + } + } + } } - open func quitTeam(_ teamId: String, _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") - repo.quitTeam(teamId, completion) + /// 获取群信息(只获取第一页群成员) + func getTeamInfoWithSomeMembers(_ teamId: String, _ completion: @escaping (NSError?, Bool?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") + weak var weakSelf = self + if let cid = V2NIMConversationIdUtil.teamConversationId(teamId) { + conversationRepo.getConversation(cid) { conversation, error in + if error == nil { + weakSelf?.conversation = conversation + } + weakSelf?.teamRepo.getTeamInfo(teamId) { team, error in + if let err = error { + completion(err, false) + } else { + let teamInfo = NETeamInfoModel() + teamInfo.team = team + weakSelf?.teamInfoModel = teamInfo + let option = V2NIMTeamMemberQueryOption() + option.nextToken = "" + option.limit = 20 + option.direction = .QUERY_DIRECTION_ASC + option.onlyChatBanned = false + option.roleQueryType = .TEAM_MEMBER_ROLE_QUERY_TYPE_ALL + + weakSelf?.teamRepo.getTeamMemberList(teamId, .TEAM_TYPE_NORMAL, option) { result, error in + if let members = result?.memberList { + weakSelf?.getUserInfo(members) { error, models in + + if let err = error { + completion(err, result?.finished) + } else { + if let users = models { + weakSelf?.teamInfoModel?.users = users + } + completion(nil, result?.finished) + } + } + } + } + } + } + } + } } - open func getTopSessionInfo(_ session: NIMSession) -> NIMStickTopSessionInfo { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(session.sessionId)") - return repo.getTopSessionInfo(session) + /// 根据成员信息获取用户信息 + public func getUserInfo(_ members: [V2NIMTeamMember], _ completion: @escaping (NSError?, [NETeamMemberInfoModel]?) -> Void) { + var accids = [String]() + var memberModels = [NETeamMemberInfoModel]() + for member in members { + accids.append(member.accountId) + let model = NETeamMemberInfoModel() + model.teamMember = member + memberModels.append(model) + } + + ContactRepo.shared.getUserWithFriend(accountIds: accids) { users, v2Error in + + if v2Error != nil { + completion(nil, memberModels) + } else { + var dic = [String: NEUserWithFriend]() + if let us = users { + for user in us { + if let accid = user.user?.accountId { + dic[accid] = user + } + } + for model in memberModels { + if let accid = model.teamMember?.accountId { + if let user = dic[accid] { + model.nimUser = user + } + } + } + completion(nil, memberModels) + } + } + } + } + + /// 解散群聊 + /// - Parameter teamId : 群id + /// - Parameter completion: 完成回调 + public func dismissTeam(_ teamId: String, _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") + teamRepo.dismissTeam(teamId, .TEAM_TYPE_NORMAL, completion) } - open func removeStickTop(params: NIMStickTopSessionInfo, - _ completion: @escaping (NSError?, NIMStickTopSessionInfo?) - -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(params.session.sessionId)") - repo.removeStickTop(params: params, completion) + /// 退出群 + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + open func leaveTeam(_ teamId: String, _ completion: @escaping (NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", teamId:\(teamId)") + teamRepo.leaveTeam(teamId, .TEAM_TYPE_NORMAL, completion) } - @discardableResult - func getCurrentMember(_ userId: String, _ teamId: String?) -> NIMTeamMember? { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId:\(userId)") - if memberInTeam == nil, let tid = teamId { - memberInTeam = repo.getMemberInfo(userId, tid) + /// 取消置顶 + /// - Parameter completion: 完成回调 + open func removeStickTop(_ completion: @escaping (NSError?) + -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function) + if let teamId = teamInfoModel?.team?.teamId, let conversationId = V2NIMConversationIdUtil.teamConversationId(teamId) { + conversationRepo.setStickTop(conversationId, true) { error in + completion(error) + } + } + } + + /// 获取当前用户在群中的信息 + /// - Parameter userId: 用户id + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + func getCurrentMember(_ userId: String, _ teamId: String?, completion: @escaping (V2NIMTeamMember?, NSError?) -> Void) { + NEALog.infoLog(ModuleName + " " + className, desc: #function + ", userId:\(userId)") + if let tid = teamId { + teamRepo.getTeamMember(tid, .TEAM_TYPE_NORMAL, userId) { [weak self] member, error in + if let currentMember = member { + self?.memberInTeam = currentMember + completion(currentMember, nil) + } else { + completion(member, error) + } + } } - return memberInTeam } + /// 判断是不是创建者 func isOwner() -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function) + NEALog.infoLog(ModuleName + " " + className, desc: #function) - if let accid = teamInfoModel?.team?.owner { - if IMKitClient.instance.isMySelf(accid) { + if let accid = teamInfoModel?.team?.ownerAccountId { + if IMKitClient.instance.isMe(accid) { return true } } return false } + /// 是不是管理员 func isManager() -> Bool { - if let tid = teamInfoModel?.team?.teamId, let currentTeamMebmer = repo.getMemberInfo(IMKitClient.instance.imAccid(), tid) { - if currentTeamMebmer.type == .manager { + if let currentTeamMebmer = memberInTeam { + if currentTeamMebmer.memberRole == .TEAM_MEMBER_ROLE_MANAGER { return true } } return false } - private func sampleMemberId(arr: [TeamMemberInfoModel], owner: String) -> String? { - var index = arc4random_uniform(UInt32(arr.count)) - while arr[Int(index)].teamMember?.userId == owner { - if arr.count == 1 { - return owner + private func sampleMemberId(arr: [NETeamMemberInfoModel], owner: String) -> String? { + let sortArr = arr.sorted { model1, model2 in + (model1.teamMember?.joinTime ?? 0) < (model2.teamMember?.joinTime ?? 0) + } + + for model in sortArr { + if model.teamMember?.accountId != owner { + return model.teamMember?.accountId } - index = arc4random_uniform(UInt32(arr.count)) } - return arr[Int(index)].teamMember?.userId + return owner } + /// 移交群主 + /// - Parameter completion: 完成回调 func transferTeamOwner(_ completion: @escaping (Error?) -> Void) { if isOwner() == false { completion(NSError(domain: "imuikit", code: -1, userInfo: [NSLocalizedDescriptionKey: "not team manager"])) @@ -364,7 +540,7 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { return } - var userId = NIMSDK.shared().loginManager.currentAccount() + var userId = IMKitClient.instance.account() if members.count == 1 { dismissTeam(teamId, completion) return @@ -372,103 +548,139 @@ open class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { userId = sampleOwnerId } - if userId == NIMSDK.shared().loginManager.currentAccount() { + if userId == IMKitClient.instance.account() { dismissTeam(teamId, completion) return } - - NIMSDK.shared().teamManager.transferManager(withTeam: teamId, newOwnerId: userId, isLeave: true) { error in + teamRepo.transferTeam(teamId, .TEAM_TYPE_NORMAL, userId, true) { error in completion(error) } } - open func updateInfoMode(_ mode: NIMTeamUpdateInfoMode, _ teamId: String, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", mode:\(mode.rawValue)") - repo.updateTeamInfoPrivilege(mode, teamId, completion) - } - - open func updateInviteMode(_ mode: NIMTeamInviteMode, _ teamId: String, - _ completion: @escaping (Error?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", mode:\(mode.rawValue)") - repo.updateInviteMode(mode, teamId, completion) - } - + /// 是不是普通群 open func isNormalTeam() -> Bool { - NELog.infoLog(ModuleName + " " + className, desc: #function) - if let type = teamInfoModel?.team?.type, type == .normal { - return true - } - if teamInfoModel?.team?.clientCustomInfo?.contains(discussTeamKey) == true { + NEALog.infoLog(ModuleName + " " + className, desc: #function) + if teamInfoModel?.team?.serverExtension?.contains(discussTeamKey) == true { return true } return false } - open func searchMessages(_ session: NIMSession, option: NIMMessageSearchOption, - _ completion: @escaping (NSError?, [HistoryMessageModel]?) -> Void) { - NELog.infoLog(ModuleName + " " + className, desc: #function + ", session:\(session.sessionId)") - weak var weakSelf = self - repo.searchMessages(session, option: option) { error, messages in - if error == nil { - weakSelf?.searchResultInfos = messages - completion(nil, weakSelf?.searchResultInfos) - } else { - completion(error, nil) - } + /// 群信息更改回调 + /// - Parameter team: 群信息类 + public func onTeamInfoUpdated(_ team: V2NIMTeam) { + if let tid = teamInfoModel?.team?.teamId, tid == team.teamId { + teamInfoModel?.team = team + getData() + delegate?.didNeedRefreshUI() } } - open func onTeamMemberRemoved(_ team: NIMTeam, withMembers memberIDs: [String]?) { - if let accids = memberIDs { - accids.forEach { accid in - if let users = teamInfoModel?.users { - for (i, m) in users.enumerated() { - if m.nimUser?.userId == accid { - teamInfoModel?.users.remove(at: i) - } + /// 群成员离开回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberLeft(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员被踢回调 + /// - Parameter operatorAccountId: 操作者id + /// - Parameter teamMembers: 群成员 + public func onTeamMemberKicked(_ operatorAccountId: String, teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群成员加入回调 + /// - Parameter teamMembers: 群成员 + public func onTeamMemberJoined(_ teamMembers: [V2NIMTeamMember]) { + onTeamMemberChanged(teamMembers) + } + + /// 群信息同步完成回调 + public func onTeamSyncFinished() { + NEALog.infoLog(className(), desc: #function + " team setting viewmo model onTeamSyncFinished ") + if let tid = teamInfoModel?.team?.teamId { + weak var weakSelf = self + getCurrentMember(IMKitClient.instance.account(), tid) { member, error in + weakSelf?.getTeamInfoWithSomeMembers(tid) { error, finished in + if error == nil { + weakSelf?.getData() + weakSelf?.delegate?.didNeedRefreshUI() } } } - delegate?.didNeedRefreshUI() } } - public func onTeamMemberChanged(_ team: NIMTeam) { - if let tid = teamInfoModel?.team?.teamId, tid != team.teamId { - return + /// 群成员更新回调 + /// - Parameter teamMembers: 群成员列表 + public func onTeamMemberInfoUpdated(_ teamMembers: [V2NIMTeamMember]) { + weak var weakSelf = self + for member in teamMembers { + if let currentTid = teamInfoModel?.team?.teamId, currentTid == member.teamId, member.accountId == IMKitClient.instance.account() { + weakSelf?.memberInTeam = member + break + } } - teamInfoModel?.team = team - getCurrentMember(IMKitClient.instance.imAccid(), teamInfoModel?.team?.teamId) - getData() - delegate?.didNeedRefreshUI() + + onTeamMemberChanged(teamMembers) } - open func onTeamUpdated(_ team: NIMTeam) { - if let teamId = teamInfoModel?.team?.teamId, teamId != team.teamId { - return + /// 离开群回调 + /// - Parameter teamMembers: 群成员 + /// - Parameter team: 群信息 + public func onTeamLeft(_ team: V2NIMTeam, isKicked: Bool) {} + + /// 群成员变更统一处理 + /// - Parameter teamMembers: 群成员 + private func onTeamMemberChanged(_ members: [V2NIMTeamMember]) { + var isCurrentTeam = false + for member in members { + if let currentTid = teamInfoModel?.team?.teamId, currentTid == member.teamId { + isCurrentTeam = true + } + + if member.accountId == IMKitClient.instance.account(), let teamId = teamInfoModel?.team?.teamId { + getCurrentMember(IMKitClient.instance.account(), teamId) { [weak self] member, error in + NEALog.infoLog(self?.className() ?? "", desc: "current member : \(self?.memberInTeam?.yx_modelToJSONString() ?? "")") + } + } } - 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) + if isCurrentTeam == true { + guard let tid = teamInfoModel?.team?.teamId else { + return + } + weak var weakSelf = self + getTeamWithMembers(tid) { error in + if error == nil { + weakSelf?.delegate?.didNeedRefreshUI() + } + } } } - public func addRecentetSession() { - if let tid = teamInfoModel?.team?.teamId { - let currentSession = NIMSession(tid, type: .team) - repo.addRecentSession(currentSession) + /// 邀请用户 + /// - Parameter members: 用户id数组 + /// - Parameter teamId: 群id + /// - Parameter completion: 完成回调 + open func inviteUsers(_ members: [String], _ teamId: String, _ completion: @escaping (NSError?, [V2NIMTeamMember]?) -> Void) { + teamRepo.inviteMembers(teamId, .TEAM_TYPE_NORMAL, members) { error, members in + completion(error, members) } } - public func getRecenterSession() -> NIMRecentSession? { - if let tid = teamInfoModel?.team?.teamId { - let currentSession = NIMSession(tid, type: .team) - return repo.getRecentSession(currentSession) + /// 会话变更 + /// - Parameter conversations: 会话 + public func onConversationChanged(_ conversations: [V2NIMConversation]) { + if let currentConversation = conversation { + for changeConversation in conversations { + if currentConversation.conversationId == changeConversation.conversationId { + conversation = changeConversation + getData() + delegate?.didNeedRefreshUI() + break + } + } } - return nil } } diff --git a/Podfile b/Podfile index 30148591..826d290d 100644 --- a/Podfile +++ b/Podfile @@ -9,32 +9,36 @@ target 'app' do #登录组件 pod 'YXLogin', '1.0.0' - #可选UI库 - pod 'NEContactUIKit', '9.7.0' - pod 'NEConversationUIKit', '9.7.0' - pod 'NEChatUIKit', '9.7.0' - pod 'NETeamUIKit', '9.7.0' - - #可选Kit库(和UIKit对应) - pod 'NEChatKit', '9.7.0' - - #基础kit库 - pod 'NECommonUIKit', '9.6.6' - pod 'NECommonKit', '9.6.6' - pod 'NECoreIMKit', '9.6.7' - pod 'NECoreKit', '9.6.6' - - #扩展库 -# pod 'NEMapKit', '9.7.0' + # 不指定 NIM SDK 版本, 可不指定基础库版本 + pod 'NEChatUIKit', '10.1.0' + pod 'NEContactUIKit', '10.1.0' + pod 'NEConversationUIKit', '10.1.0' + pod 'NETeamUIKit', '10.1.0' + pod 'NEMapKit', '10.1.0' + + # 指定 NIM SDK 版本, 建议指定基础库版本 + pod 'lottie-ios', '2.5.3' + pod 'NECoreKit', '9.6.9' + pod 'NECoreIM2Kit', '1.0.0' + pod 'NECommonKit', '9.6.8' + pod 'NECommonUIKit', '9.6.8' + pod 'NEChatKit/NOS_Special', '10.1.0' + + pod 'NEChatUIKit/NOS_Special', '10.1.0' + pod 'NEContactUIKit/NOS_Special', '10.1.0' + pod 'NEConversationUIKit/NOS_Special', '10.1.0' + pod 'NETeamUIKit/NOS_Special', '10.1.0' + pod 'NEMapKit/NOS_Special', '10.1.0' #呼叫组件,音视频通话能力,需要开通 音视频2.0,可选,聊天一面会根据依赖初始化自动显示音视频通话入口 - pod 'NIMSDK_LITE','9.14.2' - pod 'NERtcCallKit/NOS_Special', '2.2.0' - pod 'NERtcCallUIKit/NOS_Special', '2.2.0' - pod 'NERtcSDK', '5.5.2' + pod 'NIMSDK_LITE','10.2.6-beta' + pod 'NERtcCallKit/NOS_Special', '2.4.0' + pod 'NERtcCallUIKit/NOS_Special', '2.4.0' + pod 'NERtcSDK', '5.5.33' - # # 如果需要查看UI部分源码请注释掉以上在线依赖,打开下面的本地依赖 + # 如果需要查看UI部分源码请注释掉以上在线依赖,打开下面的本地依赖 + # 源码依赖时如果需要指定 NIM SDK 版本(Special),建议同样在 podspec 中指定基础库版本 # pod 'NEContactUIKit', :path => 'NEContactUIKit/NEContactUIKit.podspec' # pod 'NEConversationUIKit', :path => 'NEConversationUIKit/NEConversationUIKit.podspec' # pod 'NETeamUIKit', :path => 'NETeamUIKit/NETeamUIKit.podspec' diff --git a/app.xcodeproj/project.pbxproj b/app.xcodeproj/project.pbxproj index b10bcfa1..03ee1bf2 100644 --- a/app.xcodeproj/project.pbxproj +++ b/app.xcodeproj/project.pbxproj @@ -9,9 +9,7 @@ /* Begin PBXBuildFile section */ 181EE5872B234C510043817F /* CustomView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE57E2B234C510043817F /* CustomView.swift */; }; 181EE5882B234C510043817F /* CustomP2PChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE57F2B234C510043817F /* CustomP2PChatViewController.swift */; }; - 181EE58A2B234C510043817F /* CustomContactsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5812B234C510043817F /* CustomContactsViewController.swift */; }; 181EE58B2B234C510043817F /* CustomConversationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5822B234C510043817F /* CustomConversationController.swift */; }; - 181EE58C2B234C510043817F /* CustomAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5832B234C510043817F /* CustomAttachment.swift */; }; 181EE58D2B234C510043817F /* CustomContactTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5842B234C510043817F /* CustomContactTableViewCell.swift */; }; 181EE58E2B234C510043817F /* CustomChatCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5852B234C510043817F /* CustomChatCell.swift */; }; 181EE58F2B234C510043817F /* CustomConversationListCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5862B234C510043817F /* CustomConversationListCell.swift */; }; @@ -41,9 +39,12 @@ 181EE6012B234E540043817F /* CustomTeamSettingSwitchCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5E72B234E540043817F /* CustomTeamSettingSwitchCell.swift */; }; 181EE6022B234E540043817F /* NodeSelectCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5E82B234E540043817F /* NodeSelectCell.swift */; }; 181EE6032B234E540043817F /* VersionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 181EE5E92B234E540043817F /* VersionCell.swift */; }; + 18B05B342BD5FD0300666AD1 /* CustomFunChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B05B322BD5FD0300666AD1 /* CustomFunChatViewController.swift */; }; + 18B05B352BD5FD0300666AD1 /* CustomNormalChatViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B05B332BD5FD0300666AD1 /* CustomNormalChatViewController.swift */; }; 39E9E27728D87E9800A11820 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 39E9E27528D87E9800A11820 /* Localizable.strings */; }; 4B3B9BE6277AFEE50091A74E /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B3B9BE4277AFEE50091A74E /* Main.storyboard */; }; 4B3B9BEB277AFEE70091A74E /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 4B3B9BE9277AFEE70091A74E /* LaunchScreen.storyboard */; }; + 962DCBBD1E7AEAFEF8DF27AE /* Pods_app.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B974281E00A6C11F3A28F391 /* Pods_app.framework */; }; DD141DFC2A56ABFD0091318F /* Constant.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD141DC82A56ABFD0091318F /* Constant.swift */; }; DD141DFD2A56ABFE0091318F /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD141DC92A56ABFD0091318F /* ViewController.swift */; }; DD141DFF2A56ABFE0091318F /* NENavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD141DCB2A56ABFD0091318F /* NENavigationController.swift */; }; @@ -52,7 +53,6 @@ DD141E042A56ABFE0091318F /* NETabBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD141DD02A56ABFD0091318F /* NETabBarController.swift */; }; DD141E052A56ABFE0091318F /* AppKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD141DD12A56ABFD0091318F /* AppKey.swift */; }; DDCE2A652A56BDB800E17751 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DDCE2A642A56BDB800E17751 /* Assets.xcassets */; }; - E3361980803427B31EB33C64 /* Pods_app.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F2833D0D33BD28525D83EAB4 /* Pods_app.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -74,11 +74,10 @@ 01B0A28E2816CF41009065C5 /* app.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = app.entitlements; sourceTree = ""; }; 01C0FC0F27BE754400C32949 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = ""; }; 01C0FC1027BE754500C32949 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = ""; }; + 16DC7E2FA55766E184C10FEA /* Pods-app.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.debug.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.debug.xcconfig"; sourceTree = ""; }; 181EE57E2B234C510043817F /* CustomView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomView.swift; sourceTree = ""; }; 181EE57F2B234C510043817F /* CustomP2PChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomP2PChatViewController.swift; sourceTree = ""; }; - 181EE5812B234C510043817F /* CustomContactsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomContactsViewController.swift; sourceTree = ""; }; 181EE5822B234C510043817F /* CustomConversationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomConversationController.swift; sourceTree = ""; }; - 181EE5832B234C510043817F /* CustomAttachment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomAttachment.swift; sourceTree = ""; }; 181EE5842B234C510043817F /* CustomContactTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomContactTableViewCell.swift; sourceTree = ""; }; 181EE5852B234C510043817F /* CustomChatCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomChatCell.swift; sourceTree = ""; }; 181EE5862B234C510043817F /* CustomConversationListCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomConversationListCell.swift; sourceTree = ""; }; @@ -108,13 +107,15 @@ 181EE5E72B234E540043817F /* CustomTeamSettingSwitchCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomTeamSettingSwitchCell.swift; sourceTree = ""; }; 181EE5E82B234E540043817F /* NodeSelectCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NodeSelectCell.swift; sourceTree = ""; }; 181EE5E92B234E540043817F /* VersionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionCell.swift; sourceTree = ""; }; + 18B05B322BD5FD0300666AD1 /* CustomFunChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomFunChatViewController.swift; sourceTree = ""; }; + 18B05B332BD5FD0300666AD1 /* CustomNormalChatViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CustomNormalChatViewController.swift; sourceTree = ""; }; 39E9E27628D87E9800A11820 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 39E9E27828D87EA000A11820 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; 4B3B9BDB277AFEE50091A74E /* app.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = app.app; sourceTree = BUILT_PRODUCTS_DIR; }; 4B3B9BE5277AFEE50091A74E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 4B3B9BEA277AFEE70091A74E /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 852A4E08916CBE74DF8872B0 /* Pods-app.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.debug.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.debug.xcconfig"; sourceTree = ""; }; - B7F66F12DB299173053C1C33 /* Pods-app.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.release.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.release.xcconfig"; sourceTree = ""; }; + 7B8E90D94B6F032B6A80C9FE /* Pods-app.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-app.release.xcconfig"; path = "Target Support Files/Pods-app/Pods-app.release.xcconfig"; sourceTree = ""; }; + B974281E00A6C11F3A28F391 /* Pods_app.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_app.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DD141DC82A56ABFD0091318F /* Constant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constant.swift; sourceTree = ""; }; DD141DC92A56ABFD0091318F /* ViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; DD141DCB2A56ABFD0091318F /* NENavigationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NENavigationController.swift; sourceTree = ""; }; @@ -123,7 +124,6 @@ DD141DD02A56ABFD0091318F /* NETabBarController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NETabBarController.swift; sourceTree = ""; }; DD141DD12A56ABFD0091318F /* AppKey.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppKey.swift; sourceTree = ""; }; DDCE2A642A56BDB800E17751 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - F2833D0D33BD28525D83EAB4 /* Pods_app.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_app.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -131,7 +131,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - E3361980803427B31EB33C64 /* Pods_app.framework in Frameworks */, + 962DCBBD1E7AEAFEF8DF27AE /* Pods_app.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -141,11 +141,11 @@ 181EE57D2B234C510043817F /* Custom */ = { isa = PBXGroup; children = ( + 18B05B322BD5FD0300666AD1 /* CustomFunChatViewController.swift */, + 18B05B332BD5FD0300666AD1 /* CustomNormalChatViewController.swift */, 181EE57E2B234C510043817F /* CustomView.swift */, 181EE57F2B234C510043817F /* CustomP2PChatViewController.swift */, - 181EE5812B234C510043817F /* CustomContactsViewController.swift */, 181EE5822B234C510043817F /* CustomConversationController.swift */, - 181EE5832B234C510043817F /* CustomAttachment.swift */, 181EE5842B234C510043817F /* CustomContactTableViewCell.swift */, 181EE5852B234C510043817F /* CustomChatCell.swift */, 181EE5862B234C510043817F /* CustomConversationListCell.swift */, @@ -218,21 +218,13 @@ path = Theme; sourceTree = ""; }; - 405473F1290906C67D0BFCE0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - F2833D0D33BD28525D83EAB4 /* Pods_app.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; 4B3B9BD2277AFEE50091A74E = { isa = PBXGroup; children = ( 4B3B9BDD277AFEE50091A74E /* app */, 4B3B9BDC277AFEE50091A74E /* Products */, BF083551FB71DB2F668919FB /* Pods */, - 405473F1290906C67D0BFCE0 /* Frameworks */, + C8F542C0762421B5D64218AA /* Frameworks */, ); sourceTree = ""; }; @@ -262,12 +254,20 @@ BF083551FB71DB2F668919FB /* Pods */ = { isa = PBXGroup; children = ( - 852A4E08916CBE74DF8872B0 /* Pods-app.debug.xcconfig */, - B7F66F12DB299173053C1C33 /* Pods-app.release.xcconfig */, + 16DC7E2FA55766E184C10FEA /* Pods-app.debug.xcconfig */, + 7B8E90D94B6F032B6A80C9FE /* Pods-app.release.xcconfig */, ); path = Pods; sourceTree = ""; }; + C8F542C0762421B5D64218AA /* Frameworks */ = { + isa = PBXGroup; + children = ( + B974281E00A6C11F3A28F391 /* Pods_app.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; DD141DC62A56ABFD0091318F /* Main */ = { isa = PBXGroup; children = ( @@ -289,13 +289,13 @@ isa = PBXNativeTarget; buildConfigurationList = 4B3B9BEF277AFEE70091A74E /* Build configuration list for PBXNativeTarget "app" */; buildPhases = ( - F6BBE706AB53572CEA1E810B /* [CP] Check Pods Manifest.lock */, + A560DA587DC897F27FDFE32F /* [CP] Check Pods Manifest.lock */, 4B3B9BD7277AFEE50091A74E /* Sources */, 4B3B9BD8277AFEE50091A74E /* Frameworks */, 4B3B9BD9277AFEE50091A74E /* Resources */, 39722F9327998A14007BFC8B /* Embed Frameworks */, - DDF8E1695FBE0EE8EC771C05 /* [CP] Embed Pods Frameworks */, - BCB7AB04958A9EAFB3582CAA /* [CP] Copy Pods Resources */, + 264CA3266150FDF885781FE5 /* [CP] Embed Pods Frameworks */, + 0707A69D252FA43BDDD73B34 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -357,7 +357,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - BCB7AB04958A9EAFB3582CAA /* [CP] Copy Pods Resources */ = { + 0707A69D252FA43BDDD73B34 /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -374,7 +374,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-resources.sh\"\n"; showEnvVarsInLog = 0; }; - DDF8E1695FBE0EE8EC771C05 /* [CP] Embed Pods Frameworks */ = { + 264CA3266150FDF885781FE5 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -391,7 +391,7 @@ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-app/Pods-app-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - F6BBE706AB53572CEA1E810B /* [CP] Check Pods Manifest.lock */ = { + A560DA587DC897F27FDFE32F /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -442,11 +442,9 @@ DD141E032A56ABFE0091318F /* AppDelegate.swift in Sources */, 181EE58B2B234C510043817F /* CustomConversationController.swift in Sources */, 181EE5F92B234E540043817F /* IntroduceBrandViewController.swift in Sources */, - 181EE58A2B234C510043817F /* CustomContactsViewController.swift in Sources */, 181EE5F62B234E540043817F /* NENodeViewController.swift in Sources */, 181EE5FB2B234E540043817F /* StyleSelectionCell.swift in Sources */, 181EE5EB2B234E540043817F /* MineSettingViewModel.swift in Sources */, - 181EE58C2B234C510043817F /* CustomAttachment.swift in Sources */, DD141E052A56ABFE0091318F /* AppKey.swift in Sources */, 181EE5EF2B234E540043817F /* MeViewController.swift in Sources */, 181EE5FE2B234E540043817F /* CustomTeamSettingSubtitleCell.swift in Sources */, @@ -457,10 +455,12 @@ DD141E042A56ABFE0091318F /* NETabBarController.swift in Sources */, DD141DFC2A56ABFD0091318F /* Constant.swift in Sources */, 181EE6012B234E540043817F /* CustomTeamSettingSwitchCell.swift in Sources */, + 18B05B342BD5FD0300666AD1 /* CustomFunChatViewController.swift in Sources */, 181EE5F52B234E540043817F /* MessageRemindViewController.swift in Sources */, 181EE5FD2B234E540043817F /* CustomTeamSettingRightCustomCell.swift in Sources */, 181EE5FA2B234E540043817F /* MineTableViewCell.swift in Sources */, 181EE5EC2B234E540043817F /* PersonInfoViewModel.swift in Sources */, + 18B05B352BD5FD0300666AD1 /* CustomNormalChatViewController.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -621,7 +621,7 @@ }; 4B3B9BF0277AFEE70091A74E /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 852A4E08916CBE74DF8872B0 /* Pods-app.debug.xcconfig */; + baseConfigurationReference = 16DC7E2FA55766E184C10FEA /* Pods-app.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -655,7 +655,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 9.6.5; + MARKETING_VERSION = 10.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.netease.yunxin.app.im; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -670,7 +670,7 @@ }; 4B3B9BF1277AFEE70091A74E /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B7F66F12DB299173053C1C33 /* Pods-app.release.xcconfig */; + baseConfigurationReference = 7B8E90D94B6F032B6A80C9FE /* Pods-app.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -704,7 +704,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 9.6.5; + MARKETING_VERSION = 10.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.netease.yunxin.app.im; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = inHouseYunxin; diff --git a/app/Custom/CustomAttachment.swift b/app/Custom/CustomAttachment.swift deleted file mode 100644 index 58cdac24..00000000 --- a/app/Custom/CustomAttachment.swift +++ /dev/null @@ -1,44 +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 NEChatUIKit -import NIMSDK -import UIKit -public class CustomAttachment: NECustomAttachment { - public var goodsName = "name" - - public var goodsURL = "url" - - 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: []) - var content = "" - if let data = jsonData { - content = String(data: data, encoding: .utf8) ?? "" - } - return content - } -} - -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 - } - customAttachment.goodsName = info["goodsName"] as? String ?? "" - customAttachment.goodsURL = info["goodsURL"] as? String ?? "" - - return customAttachment - } -} diff --git a/app/Custom/CustomChatCell.swift b/app/Custom/CustomChatCell.swift index 0625c66f..d564e2a3 100644 --- a/app/Custom/CustomChatCell.swift +++ b/app/Custom/CustomChatCell.swift @@ -4,6 +4,7 @@ import NEChatUIKit import UIKit + class CustomChatCell: NEChatBaseCell { public var testLabel = UILabel() @@ -19,13 +20,13 @@ class CustomChatCell: NEChatBaseCell { } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none - backgroundColor = .clear + backgroundColor = .lightGray testLabel.translatesAutoresizingMaskIntoConstraints = false contentView.addSubview(testLabel) NSLayoutConstraint.activate([ @@ -38,6 +39,6 @@ class CustomChatCell: NEChatBaseCell { override func setModel(_ model: MessageContentModel, _ isSend: Bool) { print("this is custom message") - testLabel.text = "this is custom message" + testLabel.text = model.message?.text } } diff --git a/app/Custom/CustomContactTableViewCell.swift b/app/Custom/CustomContactTableViewCell.swift index e93c4a63..8b56d6e0 100644 --- a/app/Custom/CustomContactTableViewCell.swift +++ b/app/Custom/CustomContactTableViewCell.swift @@ -7,26 +7,26 @@ import NEContactUIKit public class CustomContactTableViewCell: ContactTableViewCell { private lazy var onlineView: UIImageView = { - let notify = UIImageView() - notify.translatesAutoresizingMaskIntoConstraints = false - notify.image = UIImage(named: "about_yunxin") - notify.isHidden = true - return notify + let notifyView = UIImageView() + notifyView.translatesAutoresizingMaskIntoConstraints = false + notifyView.image = UIImage(named: "about_yunxin") + notifyView.isHidden = true + return notifyView }() override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) contentView.addSubview(onlineView) NSLayoutConstraint.activate([ - onlineView.rightAnchor.constraint(equalTo: avatarImage.rightAnchor), - onlineView.bottomAnchor.constraint(equalTo: avatarImage.bottomAnchor), + onlineView.rightAnchor.constraint(equalTo: avatarImageView.rightAnchor), + onlineView.bottomAnchor.constraint(equalTo: avatarImageView.bottomAnchor), onlineView.widthAnchor.constraint(equalToConstant: 12), onlineView.heightAnchor.constraint(equalToConstant: 12), ]) } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } // 根据数据模型设置 cell 内容 diff --git a/app/Custom/CustomContactsViewController.swift b/app/Custom/CustomContactViewController.swift similarity index 86% rename from app/Custom/CustomContactsViewController.swift rename to app/Custom/CustomContactViewController.swift index 2d0dbad6..7594059b 100644 --- a/app/Custom/CustomContactsViewController.swift +++ b/app/Custom/CustomContactViewController.swift @@ -5,10 +5,10 @@ import Foundation import NEContactUIKit -public class CustomContactsViewController: ContactsViewController, NEBaseContactsViewControllerDelegate { +public class CustomContactViewController: ContactViewController, NEBaseContactViewControllerDelegate { override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { /// 是否在通讯录界面显示头部模块 -// NEKitContactConfig.shared.ui.showHeader = false + NEKitContactConfig.shared.ui.showHeader = false /// 通讯录列表头部模块的数据回调 NEKitContactConfig.shared.ui.headerData = { headerData in @@ -21,19 +21,20 @@ public class CustomContactsViewController: ContactsViewController, NEBaseContact } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override public func viewDidLoad() { - // 通过配置项实现自定义 + // 通过配置项实现自定义,该方式不需要继承自 ChatViewController customByConfig() - // 通过重写实现自定义 + // 通过重写实现自定义,该方式需要继承自 ChatViewController // customByOverread() super.viewDidLoad() } + /// 通过配置项实现自定义,该方式不需要继承自 ChatViewController func customByConfig() { /* UI 属性自定义 @@ -107,27 +108,28 @@ public class CustomContactsViewController: ContactsViewController, NEBaseContact viewController.navigationView.backgroundColor = .gray // 顶部bodyTopView中添加自定义view(需要设置bodyTopView的高度) - self.customTopView.btn.setTitle("通过配置项添加", for: .normal) + self.customTopView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyTopView.backgroundColor = .purple viewController.bodyTopView.addSubview(self.customTopView) viewController.bodyTopViewHeight = 80 // 底部bodyBottomView中添加自定义view(需要设置bodyBottomView的高度) - self.customBottomView.btn.setTitle("通过配置项添加", for: .normal) + self.customBottomView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyBottomView.backgroundColor = .purple viewController.bodyBottomView.addSubview(self.customBottomView) viewController.bodyBottomViewHeight = 60 } } + /// 通过重写实现自定义布局(这种方式需要继承,从而拿到父类属性) func customByOverread() { // 顶部bodyTopView中添加自定义view(需要设置bodyTopView的高度) - customTopView.btn.setTitle("通过重写方式添加", for: .normal) + customTopView.button.setTitle("通过重写方式添加", for: .normal) bodyTopView.addSubview(customTopView) bodyTopViewHeight = 80 // 底部bodyBottomView中添加自定义view(需要设置bodyBottomView的高度) - customBottomView.btn.setTitle("通过重写方式添加", for: .normal) + customBottomView.button.setTitle("通过重写方式添加", for: .normal) bodyBottomView.addSubview(customBottomView) bodyBottomViewHeight = 60 } @@ -146,7 +148,7 @@ public class CustomContactsViewController: ContactsViewController, NEBaseContact // 父类加载完数据后会调用此方法,可在此对数据进行二次处理 public func onDataLoaded() { - viewModel.contacts[1].contacts.forEach { info in + for info in viewModel.contacts[1].contacts { info.contactCellType = ContactCellType.ContactCutom.rawValue } } diff --git a/app/Custom/CustomConversationController.swift b/app/Custom/CustomConversationController.swift index 8a302134..21e380dd 100644 --- a/app/Custom/CustomConversationController.swift +++ b/app/Custom/CustomConversationController.swift @@ -15,20 +15,20 @@ open class CustomConversationController: ConversationController, NEBaseConversat } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override public func viewDidLoad() { - // 通过配置项实现自定义 + // 通过配置项实现自定义,该方式不需要继承自 ChatViewController customByConfig() - // 通过重写实现自定义 + // 通过重写实现自定义,该方式需要继承自 ChatViewController // customByOverread() super.viewDidLoad() } - // 通过配置项实现自定义(这种方式不需要继承就可以实现 UI 自定义) + /// 通过配置项实现自定义(这种方式不需要继承就可以实现 UI 自定义) open func customByConfig() { /* UI 属性自定义 @@ -82,22 +82,22 @@ open class CustomConversationController: ConversationController, NEBaseConversat NEKitConversationConfig.shared.ui.conversationProperties.itemDateSize = 14 /// 会话列表 cell 左划置顶按钮文案内容 - NEKitConversationConfig.shared.ui.stickTopBottonTitle = "左侧" + NEKitConversationConfig.shared.ui.stickTopButtonTitle = "左侧" /// 会话列表 cell 左划取消置顶按钮文案内容(会话置顶后生效) - NEKitConversationConfig.shared.ui.stickTopBottonCancelTitle = "左侧1" + NEKitConversationConfig.shared.ui.stickTopButtonCancelTitle = "左侧1" /// 会话列表 cell 左划置顶按钮背景颜色 - NEKitConversationConfig.shared.ui.stickTopBottonBackgroundColor = UIColor.brown + NEKitConversationConfig.shared.ui.stickTopButtonBackgroundColor = UIColor.brown /// 会话列表 cell 左划置顶按钮点击事件 - NEKitConversationConfig.shared.ui.stickTopBottonClick = { model, indexPath in + NEKitConversationConfig.shared.ui.stickTopButtonClick = { model, indexPath in self.showToast("会话列表 cell 左划置顶按钮点击事件") } /// 会话列表 cell 左划删除按钮文案内容 - NEKitConversationConfig.shared.ui.deleteBottonTitle = "右侧" + NEKitConversationConfig.shared.ui.deleteButtonTitle = "右侧" /// 会话列表 cell 左划删除按钮背景颜色 - NEKitConversationConfig.shared.ui.deleteBottonBackgroundColor = UIColor.purple + NEKitConversationConfig.shared.ui.deleteButtonBackgroundColor = UIColor.purple /// 会话列表 cell 左划删除按钮点击事件 - NEKitConversationConfig.shared.ui.deleteBottonClick = { model, indexPath in + NEKitConversationConfig.shared.ui.deleteButtonClick = { model, indexPath in self.showToast("会话列表 cell 左划删除按钮点击事件") } @@ -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?.conversation?.name ?? "会话列表点击事件") + } /* 布局自定义 @@ -130,20 +130,20 @@ open class CustomConversationController: ConversationController, NEBaseConversat viewController.navigationView.backgroundColor = .gray // 顶部bodyTopView中添加自定义view(需要设置bodyTopView的高度) - self.customTopView.btn.setTitle("通过配置项添加", for: .normal) + self.customTopView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyTopView.backgroundColor = .purple viewController.bodyTopView.addSubview(self.customTopView) viewController.bodyTopViewHeight = 80 // 底部bodyBottomView中添加自定义view(需要设置bodyBottomView的高度) - self.customBottomView.btn.setTitle("通过配置项添加", for: .normal) + self.customBottomView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyBottomView.backgroundColor = .purple viewController.bodyBottomView.addSubview(self.customBottomView) viewController.bodyBottomViewHeight = 60 } } - // 通过重写实现自定义布局(这种方式需要继承,拿到父类属性) + /// 通过重写实现自定义布局(这种方式需要继承,拿到父类属性) open func customByOverread() { // 实现协议(重写tabbar点击事件) navigationView.delegate = self @@ -157,18 +157,18 @@ open class CustomConversationController: ConversationController, NEBaseConversat navigationView.addBtn.setImage(UIImage.ne_imageNamed(name: "noNeed_notify"), for: .normal) // 顶部bodyTopView中添加自定义view(需要设置bodyTopView的高度) - customTopView.btn.setTitle("通过重写方式添加", for: .normal) + customTopView.button.setTitle("通过重写方式添加", for: .normal) bodyTopView.addSubview(customTopView) bodyTopViewHeight = 80 // 底部bodyBottomView中添加自定义view(需要设置bodyBottomView的高度) - customBottomView.btn.setTitle("通过重写方式添加", for: .normal) + customBottomView.button.setTitle("通过重写方式添加", for: .normal) bodyBottomView.addSubview(customBottomView) bodyBottomViewHeight = 60 // 自定义占位图文案、背景图片 emptyView.setEmptyImage(image: UIImage()) - emptyView.settingContent(content: "") + emptyView.setText("") } // 通过继承方式重写次最右侧按钮点击事件, 这种方式会覆盖配置项中的点击事件 @@ -195,9 +195,10 @@ open class CustomConversationController: ConversationController, NEBaseConversat // 可自行处理数据 public func onDataLoaded() { - guard let conversationList = viewModel.conversationListArray else { return + for model in viewModel.conversationListData { + model.customType = 1 } - conversationList.forEach { model in + for model in viewModel.stickTopConversations { model.customType = 1 } tableView.reloadData() diff --git a/app/Custom/CustomConversationListCell.swift b/app/Custom/CustomConversationListCell.swift index de8d11e9..9e85ba51 100644 --- a/app/Custom/CustomConversationListCell.swift +++ b/app/Custom/CustomConversationListCell.swift @@ -9,10 +9,10 @@ import UIKit open class CustomConversationListCell: ConversationListCell { // 新增 UI 元素,用于展示在线状态 private lazy var onlineView: UIImageView = { - let notify = UIImageView() - notify.translatesAutoresizingMaskIntoConstraints = false - notify.image = UIImage(named: "about_yunxin") - return notify + let notifyView = UIImageView() + notifyView.translatesAutoresizingMaskIntoConstraints = false + notifyView.image = UIImage(named: "about_yunxin") + return notifyView }() override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -21,20 +21,20 @@ open class CustomConversationListCell: ConversationListCell { // 头像右下角 contentView.addSubview(onlineView) NSLayoutConstraint.activate([ - onlineView.rightAnchor.constraint(equalTo: headImge.rightAnchor), - onlineView.bottomAnchor.constraint(equalTo: headImge.bottomAnchor), + onlineView.rightAnchor.constraint(equalTo: headImageView.rightAnchor), + onlineView.bottomAnchor.constraint(equalTo: headImageView.bottomAnchor), onlineView.widthAnchor.constraint(equalToConstant: 12), onlineView.heightAnchor.constraint(equalToConstant: 12), ]) } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } // 此方法用于数据和 UI 的绑定,可在此处在数据展示前对数据进行处理 - override open func configData(sessionModel: ConversationListModel?) { - super.configData(sessionModel: sessionModel) -// subTitle.text = "[自定义类型文案]" + override open func configureData(_ sessionModel: NEConversationListModel?) { + super.configureData(sessionModel) + // subTitle.text = "[自定义类型文案]" } } diff --git a/app/Custom/CustomConversationListViewController.swift b/app/Custom/CustomConversationListViewController.swift new file mode 100644 index 00000000..11494e40 --- /dev/null +++ b/app/Custom/CustomConversationListViewController.swift @@ -0,0 +1,44 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import Foundation +import NEConversationUIKit + +open class CustomConversationListViewController: ConversationListViewController, ConversationListViewControllerDelegate { + override public init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) { + super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) + delegate = self + + // 自定义cell, [ConversationListModel.customType: 需要注册的自定义cell] + registerCellDic[1] = CustomConversationListCell.self + } + + public required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override open func viewDidLoad() { + super.viewDidLoad() + } + + override open func deleteActionHandler(action: UITableViewRowAction?, indexPath: IndexPath) { + showSingleAlert(message: "override deleteActionHandler") {} + } + + override open func topActionHandler(action: UITableViewRowAction?, indexPath: IndexPath, isTop: Bool) { + showSingleAlert(message: "override topActionHandler") { + super.topActionHandler(action: action, indexPath: indexPath, isTop: isTop) + } + } + + // 可自行处理数据 + public func onDataLoaded() { + guard let conversationList = viewModel.conversationListArray else { return + } + for model in conversationList { + model.customType = 1 + } + tableView.reloadData() + } +} diff --git a/app/Custom/CustomFunChatViewController.swift b/app/Custom/CustomFunChatViewController.swift new file mode 100644 index 00000000..3ccbecb3 --- /dev/null +++ b/app/Custom/CustomFunChatViewController.swift @@ -0,0 +1,43 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatUIKit +import NERtcCallKit +import NIMSDK +import UIKit + +class CustomFunChatViewController: FunP2PChatViewController, NERecordProvider { + /// 话单拦截 + func onRecordSend(_ config: NERecordConfig) { + NEALog.infoLog(className(), desc: "call status : \(NECallEngine.sharedInstance().callStatus)") + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + if NECallEngine.sharedInstance().callStatus == .calling { + return + } + } + + let message = V2NIMMessageCreator.createCallMessage("", type: Int(config.callType.rawValue), channelId: "", status: Int(config.callState.rawValue), durations: []) + if let cid = V2NIMConversationIdUtil.p2pConversationId(config.accId) { + viewModel.chatRepo.sendMessage(message: message, conversationId: cid) { [weak self] result, error, ret in + NEALog.infoLog(self?.className() ?? "", desc: "CustomNormalChatViewController result: \(error?.localizedDescription ?? "")") + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + NECallEngine.sharedInstance().setCall(self) + // Do any additional setup after loading the view. + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ +} diff --git a/app/Custom/CustomNormalChatViewController.swift b/app/Custom/CustomNormalChatViewController.swift new file mode 100644 index 00000000..bef24e59 --- /dev/null +++ b/app/Custom/CustomNormalChatViewController.swift @@ -0,0 +1,42 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import NEChatUIKit +import NERtcCallKit +import NIMSDK +import UIKit + +class CustomNormalChatViewController: P2PChatViewController, NERecordProvider { + /// 话单拦截 + func onRecordSend(_ config: NERecordConfig) { + NEALog.infoLog(className(), desc: "call status : \(NECallEngine.sharedInstance().callStatus)") + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + if NECallEngine.sharedInstance().callStatus == .calling { + return + } + } + let message = V2NIMMessageCreator.createCallMessage("", type: Int(config.callType.rawValue), channelId: "", status: Int(config.callState.rawValue), durations: []) + if let cid = V2NIMConversationIdUtil.p2pConversationId(config.accId) { + viewModel.chatRepo.sendMessage(message: message, conversationId: cid) { [weak self] result, error, ret in + NEALog.infoLog(self?.className() ?? "", desc: "CustomNormalChatViewController result: \(error?.localizedDescription ?? "")") + } + } + } + + override func viewDidLoad() { + super.viewDidLoad() + NECallEngine.sharedInstance().setCall(self) + // Do any additional setup after loading the view. + } + + /* + // MARK: - Navigation + + // In a storyboard-based application, you will often want to do a little preparation before navigation + override func prepare(for segue: UIStoryboardSegue, sender: Any?) { + // Get the new view controller using segue.destination. + // Pass the selected object to the new view controller. + } + */ +} diff --git a/app/Custom/CustomP2PChatViewController.swift b/app/Custom/CustomP2PChatViewController.swift index 91302645..6c3b7cc1 100644 --- a/app/Custom/CustomP2PChatViewController.swift +++ b/app/Custom/CustomP2PChatViewController.swift @@ -5,16 +5,17 @@ import NEChatUIKit import NIMSDK import UIKit + class CustomP2PChatViewController: P2PChatViewController { let customMessageType = 20 override func viewDidLoad() { // 自定义消息cell绑定需要放在 super.viewDidLoad() 之前 NEChatUIKitClient.instance.regsiterCustomCell(["\(customMessageType)": CustomChatCell.self]) - // 通过配置项实现自定义 + // 通过配置项实现自定义,该方式不需要继承自 ChatViewController customByConfig() - // 通过重写实现自定义 + // 通过重写实现自定义,该方式需要继承自 ChatViewController // customByOverread() super.viewDidLoad() @@ -23,30 +24,30 @@ class CustomP2PChatViewController: P2PChatViewController { customMessage() } - /// 通过配置项实现 UI 自定义 + /// 通过配置项实现 UI 自定义,该方式不需要继承自 ChatViewController func customByConfig() { -// NEKitChatConfig.shared.ui.messageProperties.avatarType = .rectangle -// NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius = 8.0 -// NEKitChatConfig.shared.ui.messageProperties.signalBgColor = UIColor.ne_backcolor -// NEKitChatConfig.shared.ui.messageProperties.selfMessageBg = UIColor.ne_greenText -// NEKitChatConfig.shared.ui.messageProperties.receiveMessageBg = UIColor.ne_greenText -// NEKitChatConfig.shared.ui.messageProperties.timeTextColor = UIColor.ne_redText -// NEKitChatConfig.shared.ui.messageProperties.timeTextSize = 18 -// NEKitChatConfig.shared.ui.messageProperties.userNickColor = UIColor.ne_redText -// NEKitChatConfig.shared.ui.messageProperties.userNickTextSize = 8.0 -// NEKitChatConfig.shared.ui.messageProperties.messageTextColor = UIColor.ne_redColor -// NEKitChatConfig.shared.ui.messageProperties.messageTextSize = 12 -// NEKitChatConfig.shared.ui.messageProperties.rightBubbleBg = UIImage(named: "copy_right") -// NEKitChatConfig.shared.ui.messageProperties.leftBubbleBg = UIImage(named: "copy_right") -// NEKitChatConfig.shared.ui.messageProperties.showP2pMessageStatus = false -// NEKitChatConfig.shared.ui.messageProperties.showTeamMessageStatus = false + NEKitChatConfig.shared.ui.messageProperties.avatarType = .rectangle + NEKitChatConfig.shared.ui.messageProperties.avatarCornerRadius = 8.0 + NEKitChatConfig.shared.ui.messageProperties.signalBgColor = UIColor.ne_backcolor + NEKitChatConfig.shared.ui.messageProperties.selfMessageBg = UIColor.ne_greenText + NEKitChatConfig.shared.ui.messageProperties.receiveMessageBg = UIColor.ne_greenText + NEKitChatConfig.shared.ui.messageProperties.timeTextColor = UIColor.ne_redText + NEKitChatConfig.shared.ui.messageProperties.timeTextSize = 18 + NEKitChatConfig.shared.ui.messageProperties.userNickColor = UIColor.ne_redText + NEKitChatConfig.shared.ui.messageProperties.userNickTextSize = 8.0 + NEKitChatConfig.shared.ui.messageProperties.messageTextColor = UIColor.ne_redColor + NEKitChatConfig.shared.ui.messageProperties.messageTextSize = 12 + NEKitChatConfig.shared.ui.messageProperties.rightBubbleBg = UIImage(named: "copy_right") + NEKitChatConfig.shared.ui.messageProperties.leftBubbleBg = UIImage(named: "copy_right") + NEKitChatConfig.shared.ui.messageProperties.showP2pMessageStatus = false + NEKitChatConfig.shared.ui.messageProperties.showTeamMessageStatus = false // NEKitChatConfig.shared.ui.messageProperties.showTitleBar = false // NEKitChatConfig.shared.ui.messageProperties.showTitleBarRightIcon = false -// NEKitChatConfig.shared.ui.messageProperties.titleBarRightRes = UIImage(named: "copy_right") -// NEKitChatConfig.shared.ui.messageProperties.titleBarRightClick = { [weak self] in -// self?.showToast("标题栏右侧图标的点击事件") -// } -// NEKitChatConfig.shared.ui.messageProperties.chatViewBackground = UIColor.ne_redText + NEKitChatConfig.shared.ui.messageProperties.titleBarRightRes = UIImage(named: "copy_right") + NEKitChatConfig.shared.ui.messageProperties.titleBarRightClick = { [weak self] in + self?.showToast("标题栏右侧图标的点击事件") + } + NEKitChatConfig.shared.ui.messageProperties.chatViewBackground = UIColor.ne_redText NEKitChatConfig.shared.ui.messageItemClick = { [weak self] cell, model in self?.showToast("点击了消息: \(String(describing: model?.message?.text))") @@ -131,29 +132,40 @@ class CustomP2PChatViewController: P2PChatViewController { viewController.navigationView.backgroundColor = .gray // 顶部bodyTopView中添加自定义view(需要设置bodyTopView的高度) - self.customTopView.btn.setTitle("通过配置项添加", for: .normal) + self.customTopView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyTopView.backgroundColor = .purple viewController.bodyTopView.addSubview(self.customTopView) viewController.bodyTopViewHeight = 80 // 底部bodyBottomView中添加自定义view(需要设置bodyBottomView的高度) - self.customBottomView.btn.setTitle("通过配置项添加", for: .normal) + self.customBottomView.button.setTitle("通过配置项添加", for: .normal) viewController.bodyBottomView.backgroundColor = .purple viewController.bodyBottomView.addSubview(self.customBottomView) viewController.bodyBottomViewHeight = 60 } + + /// 消息列表发送消息时的视图控制器回调 + /// 回调参数:消息体和消息列表的视图控制器 + /// 返回值:是否继续发送消息 + NEKitChatConfig.shared.ui.onSendMessage = { message, ViewController in + if let text = message.text, text.starts(with: "哈") { + ViewController.showToast(text) + return false + } + return true + } } - /// 通过重写实现自定义 + /// 通过重写实现自定义布局(这种方式需要继承,从而拿到父类属性) func customByOverread() { // 聊天页顶部导航栏下方扩展视图示例 - customTopView.btn.setTitle("通过重写方式添加", for: .normal) + customTopView.button.setTitle("通过重写方式添加", for: .normal) bodyTopView.addSubview(customTopView) bodyTopView.backgroundColor = .yellow bodyTopViewHeight = 80 // 输入框上区域扩展视图示例 - customBottomView.btn.setTitle("通过重写方式添加", for: .normal) + customBottomView.button.setTitle("通过重写方式添加", for: .normal) bodyBottomView.addSubview(customBottomView) bodyBottomView.backgroundColor = .yellow bodyBottomViewHeight = 60 @@ -202,35 +214,36 @@ class CustomP2PChatViewController: P2PChatViewController { /// 自定义消息以及外部扩展 覆盖cell UI 样式示例 func customMessage() { - // 注册自定义消息的解析器 - NIMCustomObject.registerCustomDecoder(CustomAttachmentDecoder()) - // 测试自定义消息发送按钮 - let testBtn = UIButton() - testBtn.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(testBtn) + let testButton = UIButton() + testButton.translatesAutoresizingMaskIntoConstraints = false + testButton.setTitle("发送自定义消息", for: .normal) + testButton.titleLabel?.font = .systemFont(ofSize: 12) + view.addSubview(testButton) NSLayoutConstraint.activate([ - testBtn.widthAnchor.constraint(equalToConstant: 100), - testBtn.heightAnchor.constraint(equalToConstant: 40), - testBtn.centerXAnchor.constraint(equalTo: view.centerXAnchor), - testBtn.centerYAnchor.constraint(equalTo: view.centerYAnchor), + testButton.widthAnchor.constraint(equalToConstant: 120), + testButton.heightAnchor.constraint(equalToConstant: 40), + testButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + testButton.centerYAnchor.constraint(equalTo: view.centerYAnchor), ]) - testBtn.backgroundColor = UIColor.red - testBtn.addTarget(self, action: #selector(sendCustomButton), for: .touchUpInside) + testButton.backgroundColor = UIColor.red + testButton.addTarget(self, action: #selector(sendCustomButton), for: .touchUpInside) } // 自定义标题 - // override func getSessionInfo(session: NIMSession) { - // super.getSessionInfo(session: session) - // title = "小易助手" - // } + override func getSessionInfo(sessionId: String, _ completion: @escaping () -> Void) { + super.getSessionInfo(sessionId: sessionId) { + self.title = "小易助手" + completion() + } + } // 长按消息功能弹窗列表自定义(可针对不同 type 消息自定义长按功能项) - // override func setOperationItems(items: inout [OperationItem], model: MessageContentModel?) { - // if model?.type == .rtcCallRecord { - // items.append(OperationItem.deleteItem()) - // } - // } + override func setOperationItems(items: inout [OperationItem], model: MessageContentModel?) { + if model?.type == .rtcCallRecord { + items.append(OperationItem.replayItem()) + } + } @objc func customClick() { showToast("自定义点击事件") @@ -238,7 +251,7 @@ class CustomP2PChatViewController: P2PChatViewController { func customBottomBar() { let subviews = chatInputView.stackView.subviews - subviews.forEach { view in + for view in subviews { view.removeFromSuperview() chatInputView.stackView.removeArrangedSubview(view) } @@ -258,29 +271,43 @@ class CustomP2PChatViewController: P2PChatViewController { @objc func buttonEvent(_ btn: UIButton) { if btn.tag == 0 { // 表情 - layoutInputView(offset: bottomExanpndHeight) + layoutInputView(offset: bottomExanpndHeight, true) chatInputView.addEmojiView() } else if btn.tag == 1 { // 语音 - layoutInputView(offset: bottomExanpndHeight) + layoutInputView(offset: bottomExanpndHeight, true) chatInputView.addRecordView() } else if btn.tag == 2 { // 照片 goPhotoAlbumWithVideo(self) } else if btn.tag == 3 { // 更多 - layoutInputView(offset: bottomExanpndHeight) + layoutInputView(offset: bottomExanpndHeight, true) chatInputView.addMoreActionView() } } @objc func sendCustomButton() { - let data = ["type": customMessageType] - let attachment = CustomAttachment(customType: customMessageType, cellHeight: 50, data: data) - let message = NIMMessage() - let object = NIMCustomObject() - object.attachment = attachment - message.messageObject = object - - NIMSDK.shared().chatManager.send(message, to: viewmodel.session) { error in - print("send custom message error : ", error?.localizedDescription as Any) + // type 字段必须指定,且不可为 101、102(UIKit 内部已使用),否则解析为【未知消息体】 + let dataDic: [String: Any] = ["type": customMessageType] + let dataJson = NECommonUtil.getJSONStringFromDictionary(dataDic) + let customMessage = MessageUtils.customMessage(text: "this is a custom message, create time:\(Date.timeIntervalSinceReferenceDate)", + rawAttachment: dataJson) + + ChatRepo.shared.sendMessage(message: customMessage, conversationId: viewModel.conversationId) { result, error, pro in + if let err = error { + print("send custom message error : ", err.localizedDescription) + } + } + } + + /// 获取消息模型,可在此处对消息体进行修改 + /// - Parameter model: 模型 + override func getMessageModel(model: any MessageModel) { + super.getMessageModel(model: model) + + // 例如设置自定义消息高度 + if model.type == .custom { + if model.customType == customMessageType { + model.height = 50 + } } } diff --git a/app/Custom/CustomView.swift b/app/Custom/CustomView.swift index 71cd2b0b..c9bb29a1 100644 --- a/app/Custom/CustomView.swift +++ b/app/Custom/CustomView.swift @@ -5,25 +5,25 @@ import UIKit public class CustomView: UIView { - public let btn = UIButton() + public let button = UIButton() override public init(frame: CGRect) { super.init(frame: frame) - btn.translatesAutoresizingMaskIntoConstraints = false - btn.addTarget(self, action: #selector(tapView), for: .touchUpInside) - btn.setTitle("按钮", for: .normal) - btn.backgroundColor = .red - addSubview(btn) + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(tapView), for: .touchUpInside) + button.setTitle("按钮", for: .normal) + button.backgroundColor = .red + addSubview(button) NSLayoutConstraint.activate([ - btn.topAnchor.constraint(equalTo: topAnchor), - btn.bottomAnchor.constraint(equalTo: bottomAnchor), - btn.widthAnchor.constraint(equalToConstant: 200), - btn.centerXAnchor.constraint(equalTo: centerXAnchor), + button.topAnchor.constraint(equalTo: topAnchor), + button.bottomAnchor.constraint(equalTo: bottomAnchor), + button.widthAnchor.constraint(equalToConstant: 200), + button.centerXAnchor.constraint(equalTo: centerXAnchor), ]) } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } @objc func tapView() { diff --git a/app/Main/AppDelegate.swift b/app/Main/AppDelegate.swift index 8668a71b..d80ff9df 100644 --- a/app/Main/AppDelegate.swift +++ b/app/Main/AppDelegate.swift @@ -8,7 +8,7 @@ import NEContactUIKit import YXLogin import NECoreKit import NIMSDK -import NECoreIMKit +import NECoreIM2Kit import NEConversationUIKit import NETeamUIKit import NEChatUIKit @@ -35,9 +35,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD // 初始化NIMSDK let option = NIMSDKOption() + option.v2 = true option.appKey = AppKey.appKey option.apnsCername = AppKey.pushCerName - IMKitClient.instance.setupCoreKitIM(option) + IMKitClient.instance.setupIM(option) // 登录IM之前先初始化 @ 消息监听mananger NEAtMessageManager.setupInstance() @@ -46,7 +47,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD let token = "<#token#>" weak var weakSelf = self - IMKitClient.instance.loginIM(account, token) { error in + IMKitClient.instance.login(account, token, nil) { error in if let err = error { print("login error in app : ", err.localizedDescription) }else { @@ -96,11 +97,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) { - NELog.infoLog("app delegate : ", desc: error.localizedDescription) + NEALog.infoLog("app delegate : ", desc: error.localizedDescription) } func initializePage() { - self.window?.rootViewController = NETabBarController() + self.window?.rootViewController = NETabBarController(true) loadService() } @@ -122,7 +123,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD customVerification() //地图map初始化 - NEMapClient.shared().setupMapClient(withAppkey: AppKey.gaodeMapAppkey) + NEMapClient.shared().setupMapClient(withAppkey: AppKey.gaodeMapAppkey, withServerKey: AppKey.gaodeMapServerAppkey) /* 聊天面板外部扩展示例 // 新增未知类型 @@ -172,11 +173,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Router.shared.register(PushP2pChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let p2pChatVC = P2PChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let p2pChatVC = CustomNormalChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { @@ -185,7 +186,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD return } } - if let remove = param["removeUserVC"] as? Bool, remove { nav?.viewControllers.removeLast() } @@ -196,11 +196,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD Router.shared.register(PushP2pChatVCRouter) { param in print("param:\(param)") let nav = param["nav"] as? UINavigationController - guard let session = param["session"] as? NIMSession else { + guard let conversationId = param["conversationId"] as? String else { return } - let anchor = param["anchor"] as? NIMMessage - let p2pChatVC = FunP2PChatViewController(session: session, anchor: anchor) + let anchor = param["anchor"] as? V2NIMMessage + let p2pChatVC = CustomFunChatViewController(conversationId: conversationId, anchor: anchor) for (i, vc) in (nav?.viewControllers ?? []).enumerated() { if vc.isKind(of: ChatViewController.self) { diff --git a/app/Main/AppKey.swift b/app/Main/AppKey.swift index 234281af..00e6e459 100644 --- a/app/Main/AppKey.swift +++ b/app/Main/AppKey.swift @@ -8,9 +8,11 @@ public struct AppKey { public static let pushCerName = "<#请输入推送证书#>" public static let appKey = "<#请输入appkey#>" public static let gaodeMapAppkey = "<#输入高德地图key#>" + public static let gaodeMapServerAppkey = "<#输入高德地图key#>" #else public static let pushCerName = "<#请输入推送证书#>" public static let appKey = "<#请输入appkey#>" public static let gaodeMapAppkey = "<#输入高德地图key#>" + public static let gaodeMapServerAppkey = "<#输入高德地图key#>" #endif } diff --git a/app/Main/NETabBarController.swift b/app/Main/NETabBarController.swift index bb20aca2..6160aa82 100644 --- a/app/Main/NETabBarController.swift +++ b/app/Main/NETabBarController.swift @@ -1,88 +1,120 @@ - // Copyright (c) 2022 NetEase, Inc. All rights reserved. // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. -import UIKit -import NIMSDK -import NECoreKit -import NECoreIMKit -import NEConversationUIKit -import NETeamUIKit -import NEChatUIKit +import NEChatKit +import NECommonKit import NEContactUIKit +import NEConversationUIKit +import NIMSDK +import UIKit -class NETabBarController: UITabBarController { +class NETabBarController: UITabBarController, NEConversationListener, NEContactListener { private var sessionUnreadCount = 0 private var contactUnreadCount = 0 + /// 是通过切换UI风格触发,需要重置会话是否同步完成标志位,因为不是首次登录,已经同步过,同步完成回调不会再触发,正常单皮肤可忽略此逻辑 + public var isChangeUIType = false { + didSet {} + } + + public init(_ isChangeUI: Bool) { + isChangeUIType = isChangeUI + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + override func viewDidLoad() { super.viewDidLoad() + ContactRepo.shared.addContactListener(self) setUpControllers() setUpSessionBadgeValue() setUpContactBadgeValue() - NIMSDK.shared().conversationManager.add(self) - NIMSDK.shared().systemNotificationManager.add(self) + ConversationRepo.shared.addListener(self) + + NotificationCenter.default.addObserver(self, selector: #selector(clearValidationUnreadCount), name: NENotificationName.clearValidationUnreadCount, object: nil) + } + + deinit { + ConversationRepo.shared.removeListener(self) + ContactRepo.shared.removeContactListener(self) } func setUpControllers() { if NEStyleManager.instance.isNormalStyle() { // chat let chat = ConversationController() + chat.viewModel.syncFinished = isChangeUIType chat.tabBarItem = UITabBarItem( title: NSLocalizedString("message", comment: ""), image: UIImage(named: "chat"), selectedImage: UIImage(named: "chatSelect")?.withRenderingMode(.alwaysOriginal) ) + chat.tabBarItem.accessibilityIdentifier = "id.conversation" let chatNav = NENavigationController(rootViewController: chat) // Contacts - let contactVC = ContactsViewController() + let contactVC = ContactViewController() contactVC.tabBarItem = UITabBarItem( title: NSLocalizedString("contact", comment: ""), image: UIImage(named: "contact"), selectedImage: UIImage(named: "contactSelect")?.withRenderingMode(.alwaysOriginal) ) + contactVC.tabBarItem.accessibilityIdentifier = "id.contact" let contactsNav = NENavigationController(rootViewController: contactVC) // Me let meVC = MeViewController() - meVC.view.backgroundColor = UIColor.white meVC.tabBarItem = UITabBarItem( title: NSLocalizedString("mine", comment: ""), image: UIImage(named: "person"), selectedImage: UIImage(named: "personSelect")?.withRenderingMode(.alwaysOriginal) ) + meVC.tabBarItem.accessibilityIdentifier = "id.mine" + meVC.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#999999")], for: .normal) + meVC.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#337EFF")], for: .selected) let meNav = NENavigationController(rootViewController: meVC) - tabBar.backgroundColor = .white + tabBar.backgroundColor = UIColor(hexString: "#F6F8FA") viewControllers = [chatNav, contactsNav, meNav] selectedIndex = 0 if #available(iOS 13.0, *) { let appearance = UITabBarAppearance() + appearance.stackedLayoutAppearance.normal.iconColor = UIColor(hexString: "#C5C9D2") + appearance.stackedLayoutAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor(hexString: "#999999")] appearance.stackedLayoutAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor(hexString: "#337EFF")] tabBar.standardAppearance = appearance + } else { + tabBar.unselectedItemTintColor = UIColor(hexString: "#C5C9D2") + viewControllers?.forEach { vc in + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#999999")], for: .normal) + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#337EFF")], for: .selected) + } } } else { // chat let chat = FunConversationController() + chat.viewModel.syncFinished = isChangeUIType chat.tabBarItem = UITabBarItem( title: NSLocalizedString("message", comment: ""), image: UIImage(named: "funChat"), selectedImage: UIImage(named: "funChatSelect")?.withRenderingMode(.alwaysOriginal) ) - setFunStyleColor(chat.tabBarItem) + chat.tabBarItem.accessibilityIdentifier = "id.conversation" let chatNav = NENavigationController(rootViewController: chat) // Contacts - let contactVC = FunContactsViewController() + let contactVC = FunContactViewController() contactVC.tabBarItem = UITabBarItem( title: NSLocalizedString("contact", comment: ""), image: UIImage(named: "funContact"), selectedImage: UIImage(named: "funContactSelect")?.withRenderingMode(.alwaysOriginal) ) - setFunStyleColor(contactVC.tabBarItem) + contactVC.tabBarItem.accessibilityIdentifier = "id.contact" let contactsNav = NENavigationController(rootViewController: contactVC) // Me @@ -92,23 +124,36 @@ class NETabBarController: UITabBarController { image: UIImage(named: "funPerson"), selectedImage: UIImage(named: "funPersonSelect")?.withRenderingMode(.alwaysOriginal) ) - setFunStyleColor(meVC.tabBarItem) + meVC.tabBarItem.accessibilityIdentifier = "id.mine" let meNav = NENavigationController(rootViewController: meVC) - tabBar.backgroundColor = .white + tabBar.backgroundColor = UIColor(hexString: "#F6F6F6") + tabBar.unselectedItemTintColor = UIColor(hexString: "#C5C9D2") viewControllers = [chatNav, contactsNav, meNav] + viewControllers?.forEach { vc in + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#999999")], for: .normal) + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.ne_funTheme], for: .selected) + } selectedIndex = 0 if #available(iOS 13.0, *) { let appearance = UITabBarAppearance() - appearance.stackedLayoutAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor(hexString: "#58BE6B")] + appearance.stackedLayoutAppearance.normal.iconColor = UIColor(hexString: "#C5C9D2") + appearance.stackedLayoutAppearance.normal.titleTextAttributes = [.foregroundColor: UIColor(hexString: "#999999")] + appearance.stackedLayoutAppearance.selected.titleTextAttributes = [.foregroundColor: UIColor.ne_funTheme] tabBar.standardAppearance = appearance + } else { + tabBar.unselectedItemTintColor = UIColor(hexString: "#C5C9D2") + viewControllers?.forEach { vc in + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor(hexString: "#999999")], for: .normal) + vc.tabBarItem.setTitleTextAttributes([NSAttributedString.Key.foregroundColor: UIColor.ne_funTheme], for: .selected) + } } } } func setUpSessionBadgeValue() { - sessionUnreadCount = ConversationProvider.shared.allUnreadCount(notify: true) + sessionUnreadCount = ConversationRepo.shared.getTotalUnreadCount() if sessionUnreadCount > 0 { tabBar.showBadgOn(index: 0, tabbarItemNums: 3) } else { @@ -117,11 +162,13 @@ class NETabBarController: UITabBarController { } func setUpContactBadgeValue() { - contactUnreadCount = NIMSDK.shared().systemNotificationManager.allUnreadCount() - if contactUnreadCount > 0 { - tabBar.showBadgOn(index: 1, tabbarItemNums: 3) - } else { - tabBar.hideBadg(on: 1) + ContactRepo.shared.getUnreadApplicationCount { [self] unreadCount, error in + contactUnreadCount = unreadCount + if unreadCount > 0 { + tabBar.showBadgOn(index: 1, tabbarItemNums: 3) + } else { + tabBar.hideBadg(on: 1) + } } } @@ -129,33 +176,27 @@ class NETabBarController: UITabBarController { setUpSessionBadgeValue() } - private func setFunStyleColor(_ item: UITabBarItem) { - item.setTitleTextAttributes([.foregroundColor: UIColor(hexString: "#58BE6B")], for: .selected) - } - - deinit { - NIMSDK.shared().systemNotificationManager.remove(self) - NIMSDK.shared().conversationManager.remove(self) + @objc public func clearValidationUnreadCount() { + setUpContactBadgeValue() } } -extension NETabBarController: NIMConversationManagerDelegate { - func didAdd(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { +// MARK: - V2NIMConversationListener + +extension NETabBarController { + func onConversationChanged(_ conversations: [V2NIMConversation]) { refreshSessionBadge() } - func didUpdate(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { + func onConversationCreated(_ conversation: V2NIMConversation) { refreshSessionBadge() } - func didRemove(_ recentSession: NIMRecentSession, totalUnreadCount: Int) { + func onConversationDeleted(_ conversationIds: [String]) { refreshSessionBadge() } -} -extension NETabBarController: NIMSystemNotificationManagerDelegate { - func onSystemNotificationCountChanged(_ unreadCount: Int) { - contactUnreadCount = unreadCount + func onFriendAddApplication(_ application: V2NIMFriendAddApplication) { setUpContactBadgeValue() } } diff --git a/app/Mine/Controller/ConfigTestViewController.swift b/app/Mine/Controller/ConfigTestViewController.swift index 3300c4bf..07469db0 100644 --- a/app/Mine/Controller/ConfigTestViewController.swift +++ b/app/Mine/Controller/ConfigTestViewController.swift @@ -14,6 +14,21 @@ class ConfigTestViewController: NEBaseViewController, UITableViewDelegate, [SettingCellType.SettingSwitchCell.rawValue: CustomTeamSettingSwitchCell.self] var sectionData = [SettingSectionModel]() + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + override func viewDidLoad() { super.viewDidLoad() sectionData.append(getSectionData()) @@ -36,18 +51,63 @@ class ConfigTestViewController: NEBaseViewController, UITableViewDelegate, let showTeam = SettingCellModel() showTeam.cellName = "显示群聊" showTeam.type = SettingCellType.SettingSwitchCell.rawValue - showTeam.switchOpen = IMKitClient.instance.getConfigCenter().teamEnable + showTeam.switchOpen = IMKitConfigCenter.shared.teamEnable showTeam.swichChange = { isOpen in - IMKitClient.instance.getConfigCenter().teamEnable = isOpen + IMKitConfigCenter.shared.teamEnable = isOpen } model.cellModels.append(showTeam) + let showMessageCollection = SettingCellModel() + showMessageCollection.cellName = "显示收藏" + showMessageCollection.type = SettingCellType.SettingSwitchCell.rawValue + showMessageCollection.switchOpen = IMKitConfigCenter.shared.collectionEnable + showMessageCollection.swichChange = { isOpen in +// IMKitConfigCenter.shared.collectionEnable = isOpen + } + model.cellModels.append(showMessageCollection) + + let showMessagePin = SettingCellModel() + showMessagePin.cellName = "显示标记" + showMessagePin.type = SettingCellType.SettingSwitchCell.rawValue + showMessagePin.switchOpen = IMKitConfigCenter.shared.pinEnable + showMessagePin.swichChange = { isOpen in + IMKitConfigCenter.shared.pinEnable = isOpen + } + model.cellModels.append(showMessagePin) + + let showMessageTop = SettingCellModel() + showMessageTop.cellName = "显示置顶" + showMessageTop.type = SettingCellType.SettingSwitchCell.rawValue + showMessageTop.switchOpen = IMKitConfigCenter.shared.topEnable + showMessageTop.swichChange = { isOpen in +// IMKitConfigCenter.shared.topEnable = isOpen + } + model.cellModels.append(showMessageTop) + + let showOnlineStatus = SettingCellModel() + showOnlineStatus.cellName = "显示在线状态" + showOnlineStatus.type = SettingCellType.SettingSwitchCell.rawValue + showOnlineStatus.switchOpen = IMKitConfigCenter.shared.onlineStatusEnable + showOnlineStatus.swichChange = { isOpen in +// IMKitConfigCenter.shared.onlineStatusEnable = isOpen + } + model.cellModels.append(showOnlineStatus) + + let strangerCallEnable = SettingCellModel() + strangerCallEnable.cellName = "是否允许陌生人音视频通话" + strangerCallEnable.type = SettingCellType.SettingSwitchCell.rawValue + strangerCallEnable.switchOpen = IMKitConfigCenter.shared.strangerCallEnable + strangerCallEnable.swichChange = { isOpen in + IMKitConfigCenter.shared.strangerCallEnable = isOpen + } + model.cellModels.append(strangerCallEnable) + model.setCornerType() return model } func initialConfig() { - title = "配置测试页" + title = "全局配置" if NEStyleManager.instance.isNormalStyle() { view.backgroundColor = .ne_backgroundColor navigationView.backgroundColor = .ne_backgroundColor @@ -87,26 +147,11 @@ class ConfigTestViewController: NEBaseViewController, UITableViewDelegate, tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in + for (key, value) in cellClassDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } - lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 - } - return table - }() - @objc func saveConfig() { NotificationCenter.default.post( name: Notification.Name(CHANGE_UI), @@ -156,8 +201,8 @@ class ConfigTestViewController: NEBaseViewController, UITableViewDelegate, } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView } } diff --git a/app/Mine/Controller/InputPersonInfoController.swift b/app/Mine/Controller/InputPersonInfoController.swift index 1871cd0b..7727ee2d 100644 --- a/app/Mine/Controller/InputPersonInfoController.swift +++ b/app/Mine/Controller/InputPersonInfoController.swift @@ -24,6 +24,31 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { public var callBack: ResultCallBack? private var limitNumberCount = 0 + + lazy var textField: UITextField = { + let text = UITextField() + text.translatesAutoresizingMaskIntoConstraints = false + text.textColor = UIColor(hexString: "0x333333") + text.font = UIFont.systemFont(ofSize: 14) + text.delegate = self + text.clearButtonMode = .always + text.addTarget(self, action: #selector(textFieldChange), for: .editingChanged) + if let clearButton = text.value(forKey: "_clearButton") as? UIButton { + clearButton.accessibilityIdentifier = "id.clear" + } + text.accessibilityIdentifier = "id.nickname" + return text + }() + + lazy var textfieldBgView: UIView = { + let backView = UIView() + backView.backgroundColor = .white + backView.clipsToBounds = true + backView.layer.cornerRadius = 8.0 + backView.translatesAutoresizingMaskIntoConstraints = false + return backView + }() + override func viewDidLoad() { super.viewDidLoad() setupSubviews() @@ -34,10 +59,12 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { })) } + /// 初始化UI(内容区域) func setupSubviews() { view.addSubview(textfieldBgView) textfieldBgView.addSubview(textField) + /// 文本框白色背景 if NEStyleManager.instance.isNormalStyle() { NSLayoutConstraint.activate([ textfieldBgView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 20.0), @@ -55,6 +82,8 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { ]) textfieldBgView.layer.cornerRadius = 0 } + + /// 文本框 NSLayoutConstraint.activate([ textField.leftAnchor.constraint(equalTo: textfieldBgView.leftAnchor, constant: 16), textField.rightAnchor.constraint(equalTo: textfieldBgView.rightAnchor, constant: -12), @@ -62,6 +91,7 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { ]) } + /// 初始化UI(导航栏) func initialConfig() { addRightAction(NSLocalizedString("save", comment: ""), #selector(saveName), self) @@ -81,19 +111,21 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { } } + /// 保存昵称 @objc func saveName() { - weak var weakSelf = self if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { - weakSelf?.showToast(commonLocalizable("network_error")) + showToast(commonLocalizable("network_error")) return } if let block = callBack { block(textField.text ?? "") -// weakSelf?.navigationController?.popViewController(animated: true) +// navigationController?.popViewController(animated: true) } } + /// 配置标题类型 + /// - Parameter editType: 标题类型 func configTitle(editType: EditType) { switch editType { case .nickName: @@ -113,37 +145,11 @@ class InputPersonInfoController: NEBaseViewController, UITextFieldDelegate { } } - // MARK: lazy Method - - lazy var textField: UITextField = { - let text = UITextField() - text.translatesAutoresizingMaskIntoConstraints = false - text.textColor = UIColor(hexString: "0x333333") - text.font = UIFont.systemFont(ofSize: 14) - text.delegate = self - text.clearButtonMode = .always - text.addTarget(self, action: #selector(textFieldChange), for: .editingChanged) - if let clearButton = text.value(forKey: "_clearButton") as? UIButton { - clearButton.accessibilityIdentifier = "id.clear" - } - text.accessibilityIdentifier = "id.nickname" - return text - }() - - lazy var textfieldBgView: UIView = { - let backView = UIView() - backView.backgroundColor = .white - backView.clipsToBounds = true - backView.layer.cornerRadius = 8.0 - backView.translatesAutoresizingMaskIntoConstraints = false - return backView - }() - @objc func textFieldChange() { guard let _ = textField.markedTextRange else { if let text = textField.text, - text.count > limitNumberCount { + text.utf16.count > limitNumberCount { textField.text = String(text.prefix(limitNumberCount)) showToast(String(format: NSLocalizedString("text_count_limit", comment: ""), limitNumberCount)) } diff --git a/app/Mine/Controller/IntroduceBrandViewController.swift b/app/Mine/Controller/IntroduceBrandViewController.swift index 31c3c438..d9ea27ef 100644 --- a/app/Mine/Controller/IntroduceBrandViewController.swift +++ b/app/Mine/Controller/IntroduceBrandViewController.swift @@ -11,75 +11,77 @@ class IntroduceBrandViewController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource { private var viewModel = IntroduceViewModel() + /// 网易云信IM Logo + private lazy var headImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "yunxin_logo")) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.accessibilityIdentifier = "id.aboutLogo" + return imageView + }() + + /// 网易云信 文本 + private lazy var headLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = NSLocalizedString("brand_des", comment: "") + label.font = UIFont.systemFont(ofSize: 20.0) + label.textColor = UIColor(hexString: "333333") + label.accessibilityIdentifier = "id.aboutApp" + return label + }() + + /// 内容列表控件 + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .white + tableView.dataSource = self + tableView.delegate = self + tableView.separatorStyle = .none + tableView.register(VersionCell.self, forCellReuseIdentifier: "VersionCell") + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + override func viewDidLoad() { super.viewDidLoad() viewModel.getData() setupSubviews() } + /// UI 初始化 func setupSubviews() { - view.addSubview(headImage) + view.addSubview(headImageView) view.addSubview(headLabel) view.addSubview(tableView) navigationController?.navigationBar.backgroundColor = .white navigationView.backgroundColor = .white NSLayoutConstraint.activate([ - headImage.centerXAnchor.constraint(equalTo: view.centerXAnchor), - headImage.topAnchor.constraint( + headImageView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + headImageView.topAnchor.constraint( equalTo: view.topAnchor, constant: topConstant + 20 ), - headImage.widthAnchor.constraint(equalToConstant: 72), - headImage.heightAnchor.constraint(equalToConstant: 53), + headImageView.widthAnchor.constraint(equalToConstant: 72), + headImageView.heightAnchor.constraint(equalToConstant: 53), ]) NSLayoutConstraint.activate([ - headLabel.centerXAnchor.constraint(equalTo: headImage.centerXAnchor), - headLabel.topAnchor.constraint(equalTo: headImage.bottomAnchor, constant: 10), + headLabel.centerXAnchor.constraint(equalTo: headImageView.centerXAnchor), + headLabel.topAnchor.constraint(equalTo: headImageView.bottomAnchor, constant: 10), ]) NSLayoutConstraint.activate([ tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), - tableView.topAnchor.constraint(equalTo: headImage.bottomAnchor, constant: 45), + tableView.topAnchor.constraint(equalTo: headImageView.bottomAnchor, constant: 45), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) } - // MARK: lazy method - - private lazy var headImage: UIImageView = { - let image = UIImageView(image: UIImage(named: "yunxin_logo")) - image.translatesAutoresizingMaskIntoConstraints = false - image.accessibilityIdentifier = "id.aboutLogo" - return image - }() - - private lazy var headLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = NSLocalizedString("brand_des", comment: "") - label.font = UIFont.systemFont(ofSize: 20.0) - label.textColor = UIColor(hexString: "333333") - label.accessibilityIdentifier = "id.aboutApp" - return label - }() - - lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .white - table.dataSource = self - table.delegate = self - table.separatorStyle = .none - table.register(VersionCell.self, forCellReuseIdentifier: "VersionCell") - if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 - } - return table - }() - // MARK: UITableViewDelegate, UITableViewDataSource func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -93,7 +95,7 @@ class IntroduceBrandViewController: NEBaseViewController, UITableViewDelegate, for: indexPath ) as? VersionCell { cell.configData(model: model) - if indexPath.row == 0 { + if indexPath.row == 0 || indexPath.row == 1 { cell.cellType = .version } else { cell.cellType = .productIntroduce @@ -104,7 +106,7 @@ class IntroduceBrandViewController: NEBaseViewController, UITableViewDelegate, } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if indexPath.row == 1 { + if indexPath.row == 2 { let ctrl = NEAboutWebViewController(url: "https://netease.im/m/") navigationController?.pushViewController(ctrl, animated: true) } diff --git a/app/Mine/Controller/MeViewController.swift b/app/Mine/Controller/MeViewController.swift index 24e36faa..93c87ec6 100644 --- a/app/Mine/Controller/MeViewController.swift +++ b/app/Mine/Controller/MeViewController.swift @@ -3,20 +3,53 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NEChatKit +import NEChatUIKit import NECommonUIKit -import NECoreIMKit +import NECoreIM2Kit import NECoreKit import NIMSDK import UIKit import YXLogin class MeViewController: UIViewController, UIGestureRecognizerDelegate { - private let mineData = [ + private var mineData = [ [NSLocalizedString("setting", comment: ""): "mine_setting"], [NSLocalizedString("about_yunxin", comment: ""): "about_yunxin"], ] - private let userProvider = UserInfoProvider.shared + private lazy var tableView: UITableView = { + let tableView = UITableView(frame: .zero, style: .plain) + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.separatorStyle = .none + tableView.delegate = self + tableView.dataSource = self + tableView.register( + MineTableViewCell.self, + forCellReuseIdentifier: "\(NSStringFromClass(MineTableViewCell.self))" + ) + tableView.rowHeight = 52 + return tableView + }() + + private lazy var arrowImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "arrow_right")) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.accessibilityIdentifier = "id.rightArrow" + return imageView + }() + + private lazy var personInfoButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.addTarget(self, action: #selector(personInfoButtonClick), for: .touchUpInside) + return button + }() + + @objc func personInfoButtonClick(sender: UIButton) { + let personInfo = PersonInfoViewController() + navigationController?.pushViewController(personInfo, animated: true) + } lazy var headerView: UIView = { let view = UIView(frame: .zero) @@ -33,12 +66,12 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { }() lazy var nameLabel: UILabel = { - let name = UILabel() - name.textColor = .ne_darkText - name.font = UIFont.systemFont(ofSize: 22.0) - name.translatesAutoresizingMaskIntoConstraints = false - name.accessibilityIdentifier = "id.name" - return name + let nameLabel = UILabel() + nameLabel.textColor = .ne_darkText + nameLabel.font = UIFont.systemFont(ofSize: 22.0) + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.accessibilityIdentifier = "id.name" + return nameLabel }() lazy var idLabel: UILabel = { @@ -54,6 +87,9 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { super.viewDidLoad() view.backgroundColor = NEStyleManager.instance.isNormalStyle() ? UIColor(hexString: "#EFF1F4") : UIColor(hexString: "#EDEDED") setupSubviews() + if IMKitConfigCenter.shared.collectionEnable { + mineData.insert([NSLocalizedString("mine_collection", comment: ""): "mine_collection"], at: 1) + } } override func viewWillAppear(_ animated: Bool) { @@ -70,6 +106,7 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { } func setupSubviews() { + // 顶部视图 view.addSubview(header) if #available(iOS 11.0, *) { NSLayoutConstraint.activate([ @@ -126,8 +163,8 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { ]) view.addSubview(tableView) - view.addSubview(arrow) - view.addSubview(personInfoBtn) + view.addSubview(arrowImageView) + view.addSubview(personInfoButton) tableView.backgroundColor = NEStyleManager.instance.isNormalStyle() ? UIColor.white : UIColor.clear NSLayoutConstraint.activate([ @@ -138,15 +175,15 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { ]) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: header.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), + arrowImageView.centerYAnchor.constraint(equalTo: header.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -20), ]) NSLayoutConstraint.activate([ - personInfoBtn.topAnchor.constraint(equalTo: header.topAnchor), - personInfoBtn.leftAnchor.constraint(equalTo: view.leftAnchor), - personInfoBtn.rightAnchor.constraint(equalTo: view.rightAnchor), - personInfoBtn.bottomAnchor.constraint(equalTo: divider.topAnchor), + personInfoButton.topAnchor.constraint(equalTo: header.topAnchor), + personInfoButton.leftAnchor.constraint(equalTo: view.leftAnchor), + personInfoButton.rightAnchor.constraint(equalTo: view.rightAnchor), + personInfoButton.bottomAnchor.constraint(equalTo: divider.topAnchor), ]) view.insertSubview(headerView, belowSubview: header) @@ -158,47 +195,22 @@ class MeViewController: UIViewController, UIGestureRecognizerDelegate { ]) } - func updateUserInfo() { - let user = userProvider.getUserInfo(userId: IMKitClient.instance.imAccid()) - idLabel.text = "\(NSLocalizedString("account", comment: "")):\(user?.userId ?? "")" - nameLabel.text = user?.showName(false) - header.configHeadData(headUrl: user?.userInfo?.avatarUrl, - name: user?.showName(false) ?? "", - uid: user?.userId ?? "") + func setupUserInfo(_ userFriend: NEUserWithFriend?) { + idLabel.text = "\(NSLocalizedString("account", comment: "")):\(userFriend?.user?.accountId ?? "")" + nameLabel.text = userFriend?.showName() + header.configHeadData(headUrl: userFriend?.user?.avatar, + name: userFriend?.showName() ?? "", + uid: userFriend?.user?.accountId ?? "") } - // MAKR: lazy method - private lazy var tableView: UITableView = { - let tableView = UITableView(frame: .zero, style: .plain) - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.register( - MineTableViewCell.self, - forCellReuseIdentifier: "\(NSStringFromClass(MineTableViewCell.self))" - ) - tableView.rowHeight = 52 - return tableView - }() - - private lazy var arrow: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "arrow_right")) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.accessibilityIdentifier = "id.rightArrow" - return imageView - }() - - private lazy var personInfoBtn: UIButton = { - let btn = UIButton() - btn.translatesAutoresizingMaskIntoConstraints = false - btn.addTarget(self, action: #selector(personInfoBtnClick), for: .touchUpInside) - return btn - }() - - @objc func personInfoBtnClick(sender: UIButton) { - let personInfo = PersonInfoViewController() - navigationController?.pushViewController(personInfo, animated: true) + func updateUserInfo() { + if let userFriend = NEFriendUserCache.shared.getFriendInfo(IMKitClient.instance.account()) { + setupUserInfo(userFriend) + } else { + ContactRepo.shared.getUserList(accountIds: [IMKitClient.instance.account()]) { [weak self] users, error in + self?.setupUserInfo(users?.first) + } + } } } @@ -221,23 +233,31 @@ extension MeViewController: UITableViewDelegate, UITableViewDataSource { } public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { -// if indexPath.row == 0 { -// -// }else if indexPath.row == 1{ -// let ctrl = IntroduceBrandViewController() -// navigationController?.pushViewController(ctrl, animated: true) -// }else if indexPath.row == 2{ -// let ctrl = MineSettingViewController() -// navigationController?.pushViewController(ctrl, animated: true) -// } - - if indexPath.row == 0 { - let ctrl = MineSettingViewController() - navigationController?.pushViewController(ctrl, animated: true) - } else if indexPath.row == 1 { - let ctrl = IntroduceBrandViewController() - navigationController?.pushViewController(ctrl, animated: true) - } else if indexPath.row == 2 {} + if IMKitConfigCenter.shared.collectionEnable { + if indexPath.row == 0 { + let ctrl = MineSettingViewController() + navigationController?.pushViewController(ctrl, animated: true) + } else if indexPath.row == 1 { + if NEStyleManager.instance.isNormalStyle() == true { + let collectionCtrl = CollectionMessageController() + navigationController?.pushViewController(collectionCtrl, animated: true) + } else { + let collectionCtrl = FunCollectionMessageController() + navigationController?.pushViewController(collectionCtrl, animated: true) + } + } else if indexPath.row == 2 { + let ctrl = IntroduceBrandViewController() + navigationController?.pushViewController(ctrl, animated: true) + } + } else { + if indexPath.row == 0 { + let ctrl = MineSettingViewController() + navigationController?.pushViewController(ctrl, animated: true) + } else if indexPath.row == 1 { + let ctrl = IntroduceBrandViewController() + navigationController?.pushViewController(ctrl, animated: true) + } + } } public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { diff --git a/app/Mine/Controller/MessageRemindViewController.swift b/app/Mine/Controller/MessageRemindViewController.swift index c0bad2dd..1c860782 100644 --- a/app/Mine/Controller/MessageRemindViewController.swift +++ b/app/Mine/Controller/MessageRemindViewController.swift @@ -14,6 +14,21 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, [SettingCellType.SettingSwitchCell.rawValue: CustomTeamSettingSwitchCell.self] private var viewModel = MessageRemindViewModel() + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + override func viewDidLoad() { super.viewDidLoad() viewModel.getData() @@ -21,6 +36,7 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, initialConfig() } + /// 导航栏配置 func initialConfig() { title = NSLocalizedString("message_remind", comment: "") if NEStyleManager.instance.isNormalStyle() { @@ -32,6 +48,7 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, } } + /// 页面主题元素初始化以及布局 func setupSubviews() { view.addSubview(tableView) if NEStyleManager.instance.isNormalStyle() { @@ -44,26 +61,11 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in + for (key, value) in cellClassDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } - lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 - } - return table - }() - // MARK: UITableViewDelegate, UITableViewDataSource func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -93,6 +95,7 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { // let model = viewModel.sectionData[indexPath.section].cellModels[indexPath.row] // if let block = model.cellClick { + // block() // } } @@ -116,8 +119,8 @@ class MessageRemindViewController: NEBaseViewController, UITableViewDelegate, } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView } } diff --git a/app/Mine/Controller/MineSettingViewController.swift b/app/Mine/Controller/MineSettingViewController.swift index 05b8ddb9..f897d50a 100644 --- a/app/Mine/Controller/MineSettingViewController.swift +++ b/app/Mine/Controller/MineSettingViewController.swift @@ -3,6 +3,7 @@ // Use of this source code is governed by a MIT license that can be // found in the LICENSE file. +import NECoreIM2Kit import NECoreKit import NETeamUIKit import NIMSDK @@ -18,6 +19,25 @@ class MineSettingViewController: NEBaseViewController, UITableViewDataSource, UI private var tag = "MineSettingViewController" private let userDefault = UserDefaults.standard + /// 设置列表 + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.tableFooterView = getFooterView() + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + + /// 退出登录按钮 + let logoutButton = UIButton() + override func viewDidLoad() { super.viewDidLoad() viewModel.delegate = self @@ -50,82 +70,73 @@ class MineSettingViewController: NEBaseViewController, UITableViewDataSource, UI tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in + for (key, value) in cellClassDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } - lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.tableFooterView = getFooterView() - if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 - } - return table - }() - func getFooterView() -> UIView? { - let footer = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) - let button = UIButton() - footer.addSubview(button) - button.backgroundColor = .white - button.clipsToBounds = true - button.setTitleColor(UIColor(hexString: "0xE6605C"), for: .normal) - button.titleLabel?.font = UIFont.systemFont(ofSize: 16) - button.setTitle(title, for: .normal) - button.addTarget(self, action: #selector(loginOutAction), for: .touchUpInside) - button.setTitle(NSLocalizedString("logout", comment: ""), for: .normal) - button.accessibilityIdentifier = "id.logout" + let footerView = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.size.width, height: 64.0)) + footerView.addSubview(logoutButton) + logoutButton.backgroundColor = .white + logoutButton.clipsToBounds = true + logoutButton.setTitleColor(UIColor(hexString: "0xE6605C"), for: .normal) + logoutButton.titleLabel?.font = UIFont.systemFont(ofSize: 16) + logoutButton.setTitle(title, for: .normal) + logoutButton.addTarget(self, action: #selector(loginOutAction), for: .touchUpInside) + logoutButton.setTitle(NSLocalizedString("logout", comment: ""), for: .normal) + logoutButton.accessibilityIdentifier = "id.logout" if NEStyleManager.instance.isNormalStyle() { - button.layer.cornerRadius = 8.0 - button.frame = CGRect(x: 20, y: 12, width: view.frame.size.width - 40, height: 40) + logoutButton.layer.cornerRadius = 8.0 + logoutButton.frame = CGRect(x: 20, y: 12, width: view.frame.size.width - 40, height: 40) } else { - button.translatesAutoresizingMaskIntoConstraints = false + logoutButton.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ - button.leftAnchor.constraint(equalTo: footer.leftAnchor, constant: 0), - button.rightAnchor.constraint(equalTo: footer.rightAnchor, constant: 0), - button.topAnchor.constraint(equalTo: footer.topAnchor, constant: 12), - button.heightAnchor.constraint(equalToConstant: 40), + logoutButton.leftAnchor.constraint(equalTo: footerView.leftAnchor, constant: 0), + logoutButton.rightAnchor.constraint(equalTo: footerView.rightAnchor, constant: 0), + logoutButton.topAnchor.constraint(equalTo: footerView.topAnchor, constant: 12), + logoutButton.heightAnchor.constraint(equalToConstant: 40), ]) } - return footer + return footerView } @objc func loginOutAction() { + weak var weakSelf = self AuthorManager.shareInstance()? .logout( withConfirm: NSLocalizedString("want_to_logout", comment: ""), withCompletion: { [weak self] user, error in if error != nil { + NEALog.infoLog(self?.className() ?? "", desc: "logout author manager error : \(error?.localizedDescription ?? "")") self?.view.makeToast(error?.localizedDescription) - NELog.errorLog( + NEALog.errorLog( self?.tag ?? "", desc: "CALLBACK logout failed,error = \(error!)" ) } else { - IMKitClient.instance.logout { error in + weakSelf?.logoutButton.isEnabled = false + IMKitClient.instance.logoutIM { error in + weakSelf?.logoutButton.isEnabled = true if error != nil { + NEALog.infoLog(self?.className() ?? "", desc: "logout im error : \(error?.localizedDescription ?? "")") self?.view.makeToast(error?.localizedDescription) - NELog.errorLog( + NEALog.errorLog( self?.tag ?? "", - desc: "CALLBACK logout failed,error = \(error!)" + desc: "CALLBACK logout SUCCESS = \(error!)" ) } else { + NEALog.infoLog(self?.className() ?? "", desc: "logout im success ") NotificationCenter.default.post( name: Notification.Name("logout"), object: nil ) - NELog.infoLog( + NEALog.infoLog( self?.tag ?? "", desc: "CALLBACK logout SUCCESS" ) + NEFriendUserCache.shared.removeAllFriendInfo() } } } @@ -185,9 +196,9 @@ class MineSettingViewController: NEBaseViewController, UITableViewDataSource, UI } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .clear - return header + let headerView = UIView() + headerView.backgroundColor = .clear + return headerView } func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { diff --git a/app/Mine/Controller/NEAboutWebViewController.swift b/app/Mine/Controller/NEAboutWebViewController.swift index 402e6f0a..41065b61 100644 --- a/app/Mine/Controller/NEAboutWebViewController.swift +++ b/app/Mine/Controller/NEAboutWebViewController.swift @@ -22,7 +22,7 @@ class NEAboutWebViewController: NEBaseViewController { } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setUpSubViews() { diff --git a/app/Mine/Controller/NELoginViewController.swift b/app/Mine/Controller/NELoginViewController.swift index 4b298a55..81e0a43c 100644 --- a/app/Mine/Controller/NELoginViewController.swift +++ b/app/Mine/Controller/NELoginViewController.swift @@ -5,7 +5,8 @@ import NEChatUIKit import NECommonKit -import NECoreIMKit +import NECoreIM2Kit +import NIMSDK import UIKit import YXLogin @@ -15,6 +16,66 @@ public class NELoginViewController: UIViewController { public var successLogin: LoginBlock? + lazy var launchIconView: UIImageView = { + let imageView = UIImageView() + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.image = UIImage(named: "launchIcon") + return imageView + }() + + lazy var launchIconLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.text = NSLocalizedString("appName", comment: "") + label.font = UIFont.systemFont(ofSize: 24.0) + label.textColor = UIColor(hexString: "333333") + label.accessibilityIdentifier = "id.appYunxin" + return label + }() + + lazy var loginButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.layer.cornerRadius = 8 + button.backgroundColor = UIColor.ne_normalTheme + button.setTitleColor(UIColor.white, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 15.0) + button.setTitle(NSLocalizedString("register_login", comment: ""), for: .normal) + button.addTarget(self, action: #selector(loginBtnClick), for: .touchUpInside) + button.accessibilityIdentifier = "id.loginButton" + return button + }() + + lazy var emailLoginButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitleColor(UIColor.ne_lightText, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 12.0) + button.setTitle(NSLocalizedString("email_login", comment: ""), for: .normal) + button.addTarget(self, action: #selector(emailLoginBtnClick), for: .touchUpInside) + button.accessibilityIdentifier = "id.emailLogin" + return button + }() + + lazy var dividerLineView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor.ne_lightText + return view + }() + + /// 节点按钮 + lazy var nodeButton: UIButton = { + let button = UIButton() + button.translatesAutoresizingMaskIntoConstraints = false + button.setTitleColor(UIColor.ne_lightText, for: .normal) + button.titleLabel?.font = UIFont.systemFont(ofSize: 12.0) + button.setTitle(NSLocalizedString("node_select", comment: ""), for: .normal) + button.addTarget(self, action: #selector(nodeBtnClick), for: .touchUpInside) + button.accessibilityIdentifier = "id.serverConfig" + return button + }() + override public func viewDidLoad() { super.viewDidLoad() setupUI() @@ -29,61 +90,77 @@ public class NELoginViewController: UIViewController { } func setupUI() { - view.addSubview(launchIcon) + view.addSubview(launchIconView) view.addSubview(launchIconLabel) - view.addSubview(loginBtn) - view.addSubview(emailLoginBtn) - view.addSubview(divideView) - view.addSubview(nodeBtn) + view.addSubview(loginButton) + view.addSubview(emailLoginButton) + view.addSubview(dividerLineView) + view.addSubview(nodeButton) if #available(iOS 11.0, *) { NSLayoutConstraint.activate([ - launchIcon.centerXAnchor.constraint(equalTo: view.centerXAnchor), - launchIcon.topAnchor.constraint( + launchIconView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + launchIconView.topAnchor.constraint( equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 145.0 ), ]) } else { NSLayoutConstraint.activate([ - launchIcon.centerXAnchor.constraint(equalTo: view.centerXAnchor), - launchIcon.topAnchor.constraint(equalTo: view.topAnchor, constant: 145.0), + launchIconView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + launchIconView.topAnchor.constraint(equalTo: view.topAnchor, constant: 145.0), ]) } NSLayoutConstraint.activate([ launchIconLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor), - launchIconLabel.topAnchor.constraint(equalTo: launchIcon.bottomAnchor, constant: -12.0), + launchIconLabel.topAnchor.constraint(equalTo: launchIconView.bottomAnchor, constant: -12.0), ]) NSLayoutConstraint.activate([ - loginBtn.centerXAnchor.constraint(equalTo: view.centerXAnchor), - loginBtn.topAnchor.constraint(equalTo: launchIconLabel.bottomAnchor, constant: 20), - loginBtn.widthAnchor.constraint(equalToConstant: NEConstant.screenWidth - 80), - loginBtn.heightAnchor.constraint(equalToConstant: 44), + loginButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), + loginButton.topAnchor.constraint(equalTo: launchIconLabel.bottomAnchor, constant: 20), + loginButton.widthAnchor.constraint(equalToConstant: NEConstant.screenWidth - 80), + loginButton.heightAnchor.constraint(equalToConstant: 44), ]) NSLayoutConstraint.activate([ - divideView.bottomAnchor.constraint( + dividerLineView.bottomAnchor.constraint( equalTo: view.bottomAnchor, constant: -10 - NEConstant.statusBarHeight ), - divideView.centerXAnchor.constraint(equalTo: view.centerXAnchor), - divideView.widthAnchor.constraint(equalToConstant: 1), - divideView.heightAnchor.constraint(equalToConstant: 10), + dividerLineView.centerXAnchor.constraint(equalTo: view.centerXAnchor), + dividerLineView.widthAnchor.constraint(equalToConstant: 1), + dividerLineView.heightAnchor.constraint(equalToConstant: 10), ]) NSLayoutConstraint.activate([ - emailLoginBtn.centerYAnchor.constraint(equalTo: divideView.centerYAnchor), - emailLoginBtn.rightAnchor.constraint(equalTo: divideView.leftAnchor, constant: -8), + emailLoginButton.centerYAnchor.constraint(equalTo: dividerLineView.centerYAnchor), + emailLoginButton.rightAnchor.constraint(equalTo: dividerLineView.leftAnchor, constant: -8), ]) NSLayoutConstraint.activate([ - nodeBtn.centerYAnchor.constraint(equalTo: divideView.centerYAnchor), - nodeBtn.leftAnchor.constraint(equalTo: divideView.rightAnchor, constant: 8), + nodeButton.centerYAnchor.constraint(equalTo: dividerLineView.centerYAnchor), + nodeButton.leftAnchor.constraint(equalTo: dividerLineView.rightAnchor, constant: 8), ]) } @objc func loginBtnClick(sender: UIButton) { + // login to business server + let config = YXConfig() + config.appKey = AppKey.appKey + config.parentScope = NSNumber(integerLiteral: 2) + config.scope = NSNumber(integerLiteral: 7) + config.supportInternationalize = true + config.type = .phone + #if DEBUG + config.isOnline = false + print("debug") + #else + config.isOnline = true + print("release") + #endif + AuthorManager.shareInstance()?.initAuthor(with: config) + weak var weakSelf = self AuthorManager.shareInstance()?.startLogin(completion: { user, error in if let err = error { @@ -110,6 +187,7 @@ public class NELoginViewController: UIViewController { print("release") #endif AuthorManager.shareInstance()?.initAuthor(with: config) + weak var weakSelf = self AuthorManager.shareInstance()?.startLogin(completion: { user, error in if let err = error { @@ -128,11 +206,16 @@ public class NELoginViewController: UIViewController { private func setupSuccessLogic(_ user: YXUserInfo?) { if let token = user?.imToken, let account = user?.imAccid { weak var weakSelf = self - IMKitClient.instance.loginIM(account, token) { error in + print("login accid : ", account) + print("login token : ", token) + + let option = V2NIMLoginOption() + IMKitClient.instance.login(account, token, option) { error in if let err = error { - print("loginIM error : ", err) + NEALog.infoLog(weakSelf?.className() ?? "", desc: "login IM error : \(err.localizedDescription)") UIApplication.shared.keyWindow?.makeToast(err.localizedDescription) } else { + NEALog.infoLog(weakSelf?.className() ?? "", desc: "login IM Success") ChatRouter.setupInit() if let block = weakSelf?.successLogin { block() @@ -141,64 +224,4 @@ public class NELoginViewController: UIViewController { } } } - - // lazy method - lazy var launchIcon: UIImageView = { - let imageView = UIImageView() - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.image = UIImage(named: "launchIcon") - return imageView - }() - - lazy var launchIconLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.text = NSLocalizedString("appName", comment: "") - label.font = UIFont.systemFont(ofSize: 24.0) - label.textColor = UIColor(hexString: "333333") - label.accessibilityIdentifier = "id.appYunxin" - return label - }() - - lazy var loginBtn: UIButton = { - let btn = UIButton() - btn.translatesAutoresizingMaskIntoConstraints = false - btn.layer.cornerRadius = 8 - btn.backgroundColor = UIColor.ne_blueText - btn.setTitleColor(UIColor.white, for: .normal) - btn.titleLabel?.font = UIFont.systemFont(ofSize: 15.0) - btn.setTitle(NSLocalizedString("register_login", comment: ""), for: .normal) - btn.addTarget(self, action: #selector(loginBtnClick), for: .touchUpInside) - btn.accessibilityIdentifier = "id.loginButton" - return btn - }() - - lazy var emailLoginBtn: UIButton = { - let btn = UIButton() - btn.translatesAutoresizingMaskIntoConstraints = false - btn.setTitleColor(UIColor.ne_lightText, for: .normal) - btn.titleLabel?.font = UIFont.systemFont(ofSize: 12.0) - btn.setTitle(NSLocalizedString("email_login", comment: ""), for: .normal) - btn.addTarget(self, action: #selector(emailLoginBtnClick), for: .touchUpInside) - btn.accessibilityIdentifier = "id.emailLogin" - return btn - }() - - lazy var divideView: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor.ne_lightText - return view - }() - - lazy var nodeBtn: UIButton = { - let btn = UIButton() - btn.translatesAutoresizingMaskIntoConstraints = false - btn.setTitleColor(UIColor.ne_lightText, for: .normal) - btn.titleLabel?.font = UIFont.systemFont(ofSize: 12.0) - btn.setTitle(NSLocalizedString("node_select", comment: ""), for: .normal) - btn.addTarget(self, action: #selector(nodeBtnClick), for: .touchUpInside) - btn.accessibilityIdentifier = "id.serverConfig" - return btn - }() } diff --git a/app/Mine/Controller/NENodeViewController.swift b/app/Mine/Controller/NENodeViewController.swift index 22539d8c..00e6587d 100644 --- a/app/Mine/Controller/NENodeViewController.swift +++ b/app/Mine/Controller/NENodeViewController.swift @@ -12,6 +12,21 @@ class NENodeViewController: NEBaseViewController, UITableViewDataSource, UITable // 记录默认选中的cell private var selectIndex = 0 + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .ne_lightBackgroundColor + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + tableView.sectionHeaderHeight = 12.0 + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + override func viewDidLoad() { super.viewDidLoad() viewModel.getData() @@ -20,11 +35,13 @@ class NENodeViewController: NEBaseViewController, UITableViewDataSource, UITable func setupUI() { title = NSLocalizedString("node_select", comment: "") + navigationView.backgroundColor = .ne_lightBackgroundColor + view.addSubview(tableView) NSLayoutConstraint.activate([ tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), - tableView.topAnchor.constraint(equalTo: view.topAnchor), + tableView.topAnchor.constraint(equalTo: view.topAnchor, constant: NEConstant.navigationAndStatusHeight), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) @@ -46,28 +63,13 @@ class NENodeViewController: NEBaseViewController, UITableViewDataSource, UITable alertController.addAction(cancelAction) let sureAction = UIAlertAction(title: NSLocalizedString("restart", comment: ""), style: .default) { action in // 设置节点 - IMKitClient.instance.getSettingRepo().setNodeValue(isDomestic) + SettingRepo.shared.setNodeValue(isDomestic) exit(0) } alertController.addAction(sureAction) present(alertController, animated: true, completion: nil) } - lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .ne_lightBackgroundColor - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - table.sectionHeaderHeight = 12.0 - if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 - } - return table - }() - // MARK: UITableViewDataSource, UITableViewDelegate func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { @@ -116,8 +118,8 @@ class NENodeViewController: NEBaseViewController, UITableViewDataSource, UITable } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView } } diff --git a/app/Mine/Controller/PersonInfoViewController.swift b/app/Mine/Controller/PersonInfoViewController.swift index 62cb08d9..93d6f8ee 100644 --- a/app/Mine/Controller/PersonInfoViewController.swift +++ b/app/Mine/Controller/PersonInfoViewController.swift @@ -9,9 +9,9 @@ import NIMSDK import UIKit @objcMembers -class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, +class PersonInfoViewController: NEBaseViewController, UINavigationControllerDelegate, PersonInfoViewModelDelegate, UITableViewDelegate, - UITableViewDataSource { + UITableViewDataSource, NEContactListener { public var cellClassDic = [ SettingCellType.SettingSubtitleCell.rawValue: CustomTeamSettingSubtitleCell.self, SettingCellType.SettingHeaderCell.rawValue: CustomTeamSettingHeaderCell.self, @@ -20,11 +20,34 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, private var viewModel = PersonInfoViewModel() private var className = "PersonInfoViewController" + lazy var tableView: UITableView = { + let tableView = UITableView() + tableView.translatesAutoresizingMaskIntoConstraints = false + tableView.backgroundColor = .clear + tableView.dataSource = self + tableView.delegate = self + tableView.separatorColor = .clear + tableView.separatorStyle = .none + if #available(iOS 15.0, *) { + tableView.sectionHeaderTopPadding = 0.0 + } + return tableView + }() + + private lazy var pickerView: BirthdayDatePickerView = { + let picker = BirthdayDatePickerView() + picker.translatesAutoresizingMaskIntoConstraints = false + return picker + }() + override func viewDidLoad() { super.viewDidLoad() - viewModel.getData() - setupSubviews() + ContactRepo.shared.addContactListener(self) initialConfig() + setupSubviews() + viewModel.getData { [weak self] in + self?.tableView.reloadData() + } } func initialConfig() { @@ -39,7 +62,6 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, view.backgroundColor = .funChatBackgroundColor } viewModel.delegate = self - NIMSDK.shared().userManager.add(self) } func setupSubviews() { @@ -54,7 +76,7 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - cellClassDic.forEach { (key: Int, value: NEBaseTeamSettingCell.Type) in + for (key, value) in cellClassDic { tableView.register(value, forCellReuseIdentifier: "\(key)") } } @@ -104,16 +126,14 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, func showDatePicker() { view.addSubview(pickerView) - - weak var weakSelf = self - pickerView.timeCallBack = { time in + pickerView.timeCallBack = { [weak self] time in if let t = time { - weakSelf?.viewModel.updateBirthday(birthDay: t) { error in - if error != nil { - if error?.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) + self?.viewModel.updateSelfBirthday(t) { error in + if let err = error { + if err.code == protocolSendFailed { + self?.showToast(commonLocalizable("network_error")) } else { - weakSelf?.showToast(NSLocalizedString("setting_birthday_failure", comment: "")) + self?.showToast(NSLocalizedString("setting_birthday_failure", comment: "")) } } } @@ -127,36 +147,17 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, ]) } - lazy var tableView: UITableView = { - let table = UITableView() - table.translatesAutoresizingMaskIntoConstraints = false - table.backgroundColor = .clear - table.dataSource = self - table.delegate = self - table.separatorColor = .clear - table.separatorStyle = .none - if #available(iOS 15.0, *) { - table.sectionHeaderTopPadding = 0.0 - } - return table - }() - - private lazy var pickerView: BirthdayDatePickerView = { - let picker = BirthdayDatePickerView() - picker.translatesAutoresizingMaskIntoConstraints = false - return picker - }() - - deinit { - NIMSDK.shared().userManager.remove(self) - } - - // MARK: NIMUserManagerDelegate - - func onUserInfoChanged(_ user: NIMUser) { - if user.userId == IMKitClient.instance.imAccid() { - viewModel.getData() - tableView.reloadData() + /// 用户信息变更回调 + /// - Parameter users: 用户列表 + func onUserProfileChanged(_ users: [V2NIMUser]) { + for user in users { + if user.accountId == IMKitClient.instance.account() { + print("change self user profile : ", user.yx_modelToJSONString() as Any) + viewModel.userInfo = NEUserWithFriend(user: user) + viewModel.refreshData() + tableView.reloadData() + break + } } } @@ -178,29 +179,27 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, } view.makeToastActivity(.center) - if let imageData = image.jpegData(compressionQuality: 0.6) as NSData? { - let filePath = NSHomeDirectory().appending("/Documents/") - .appending(IMKitClient.instance.imAccid()) + if let imageData = image.jpegData(compressionQuality: 0.6) as NSData?, + var filePath = NEPathUtils.getDirectoryForDocuments(dir: "\(imkitDir)image/") { + filePath += "\(IMKitClient.instance.account())_avatar.jpg" let succcess = imageData.write(toFile: filePath, atomically: true) if succcess { - NIMSDK.shared().resourceManager - .upload(filePath, scene: NIMNOSSceneTypeAvatar, - progress: nil) { urlString, error in - if error == nil { - weakSelf?.viewModel.updateAvatar(avatar: urlString ?? "") { error in - if error != nil { - weakSelf?.showToast(NSLocalizedString("setting_head_failure", comment: "")) - } + let fileTask = ResourceRepo.shared.createUploadFileTask(filePath) + ResourceRepo.shared.upload(fileTask, nil) { [weak self] urlString, error in + if error == nil { + self?.viewModel.updateSelfAvatar(urlString ?? "") { [weak self] error in + if error != nil { + self?.showToast(NSLocalizedString("setting_head_failure", comment: "")) } - - } else { - NELog.errorLog( - weakSelf?.className ?? "", - desc: "❌CALLBACK upload image failed,error = \(error!)" - ) } - self.view.hideToastActivity() + } else { + NEALog.errorLog( + weakSelf?.className ?? "", + desc: "❌CALLBACK upload image failed,error = \(error!)" + ) } + self?.view.hideToastActivity() + } } } } @@ -221,11 +220,11 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, ctrl.contentText = name weak var weakSelf = self ctrl.callBack = { editText in - weakSelf?.viewModel.updateNickName(name: editText) { error in + weakSelf?.viewModel.updateSelfNickName(editText) { [weak self] error in if error != nil { - weakSelf?.showToastInWindow(NSLocalizedString("setting_nickname_failure", comment: "")) + self?.showToastInWindow(NSLocalizedString("setting_nickname_failure", comment: "")) } else { - weakSelf?.navigationController?.popViewController(animated: true) + self?.navigationController?.popViewController(animated: true) } } } @@ -233,17 +232,18 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, } func didClickGender() { - var sex = NIMUserGender.unknown + var gender = V2NIMGender.GENDER_UNKNOWN weak var weakSelf = self let block: ((_ value: NSInteger) -> Void) = { value in - sex = value == 0 ? .male : .female - weakSelf?.viewModel.updateSex(sex: sex) { error in + gender = value == 0 ? .GENDER_MALE : .GENDER_FEMALE + + weakSelf?.viewModel.updateSelfSex(gender) { [weak self] error in if error != nil { - if error?.code == noNetworkCode { - weakSelf?.showToast(commonLocalizable("network_error")) + if error?.code == protocolSendFailed { + self?.showToast(commonLocalizable("network_error")) } else { - weakSelf?.showToast(NSLocalizedString("change_gender_failure", comment: "")) + self?.showToast(NSLocalizedString("change_gender_failure", comment: "")) } } } @@ -274,28 +274,39 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, ctrl.contentText = mobile weak var weakSelf = self ctrl.callBack = { editText in - weakSelf?.viewModel.updateMobile(mobile: editText) { error in + weakSelf?.viewModel.updateSelfMobile(editText) { [weak self] error in if error != nil { - weakSelf?.showToastInWindow(NSLocalizedString("change_phone_failure", comment: "")) + self?.showToastInWindow(NSLocalizedString("change_phone_failure", comment: "")) } else { - weakSelf?.navigationController?.popViewController(animated: true) + self?.navigationController?.popViewController(animated: true) } } } navigationController?.pushViewController(ctrl, animated: true) } + func isValidEmail(_ email: String) -> Bool { + let emailRegex = #"^\w+@\w+\.[a-zA-Z]{2,}"# + return NSPredicate(format: "SELF MATCHES %@", emailRegex).evaluate(with: email) + } + func didClickEmail(email: String) { let ctrl = InputPersonInfoController() ctrl.configTitle(editType: .email) ctrl.contentText = email weak var weakSelf = self ctrl.callBack = { editText in - weakSelf?.viewModel.updateEmail(email: editText) { error in + + if editText.count > 0, weakSelf?.isValidEmail(editText) == false { + weakSelf?.showToastInWindow(NSLocalizedString("change_email_failure", comment: "")) + return + } + + weakSelf?.viewModel.updateSelfEmail(editText) { [weak self] error in if error != nil { - weakSelf?.showToastInWindow(NSLocalizedString("change_email_failure", comment: "")) + self?.showToastInWindow(NSLocalizedString("change_email_failure", comment: "")) } else { - weakSelf?.navigationController?.popViewController(animated: true) + self?.navigationController?.popViewController(animated: true) } } } @@ -308,11 +319,11 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, ctrl.contentText = sign weak var weakSelf = self ctrl.callBack = { editText in - weakSelf?.viewModel.updateSign(sign: editText) { error in + weakSelf?.viewModel.updateSelfSign(editText) { [weak self] error in if error != nil { - weakSelf?.showToastInWindow(NSLocalizedString("change_sign_failure", comment: "")) + self?.showToastInWindow(NSLocalizedString("change_sign_failure", comment: "")) } else { - weakSelf?.navigationController?.popViewController(animated: true) + self?.navigationController?.popViewController(animated: true) } } } @@ -320,7 +331,7 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, } func didCopyAccount(account: String) { - showToast(NSLocalizedString("copy_success", comment: "")) + showToast(commonLocalizable("copy_success")) UIPasteboard.general.string = account } @@ -376,8 +387,8 @@ class PersonInfoViewController: NEBaseViewController, NIMUserManagerDelegate, } func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - let header = UIView() - header.backgroundColor = .ne_lightBackgroundColor - return header + let headerView = UIView() + headerView.backgroundColor = .ne_lightBackgroundColor + return headerView } } diff --git a/app/Mine/Controller/StyleSelectionViewController.swift b/app/Mine/Controller/StyleSelectionViewController.swift index 21124fd5..a9f333bd 100644 --- a/app/Mine/Controller/StyleSelectionViewController.swift +++ b/app/Mine/Controller/StyleSelectionViewController.swift @@ -39,16 +39,16 @@ open class StyleSelectionViewController: NEBaseViewController, UICollectionViewD layout.minimumLineSpacing = 0 layout.minimumInteritemSpacing = 0 layout.sectionInset = UIEdgeInsets(top: topMargin, left: 0, bottom: topMargin, right: 0) - let collect = UICollectionView(frame: .zero, collectionViewLayout: layout) - collect.translatesAutoresizingMaskIntoConstraints = false - collect.dataSource = self - collect.delegate = self - collect.isUserInteractionEnabled = true - collect.backgroundColor = .white - collect.contentMode = .center - collect.register(StyleSelectionCell.self, forCellWithReuseIdentifier: "\(StyleSelectionCell.self)") - - return collect + let collectView = UICollectionView(frame: .zero, collectionViewLayout: layout) + collectView.translatesAutoresizingMaskIntoConstraints = false + collectView.dataSource = self + collectView.delegate = self + collectView.isUserInteractionEnabled = true + collectView.backgroundColor = .white + collectView.contentMode = .center + collectView.register(StyleSelectionCell.self, forCellWithReuseIdentifier: "\(StyleSelectionCell.self)") + + return collectView }() override open func viewDidLoad() { @@ -119,7 +119,6 @@ open class StyleSelectionViewController: NEBaseViewController, UICollectionViewD public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) { if indexPath.row == 0 { if NEStyleManager.instance.isNormalStyle() == false { - removeConversationDelegate() NEStyleManager.instance.setNormalStyle() NotificationCenter.default.post( name: Notification.Name(CHANGE_UI), @@ -128,7 +127,6 @@ open class StyleSelectionViewController: NEBaseViewController, UICollectionViewD } } else if indexPath.row == 1 { if NEStyleManager.instance.isNormalStyle() == true { - removeConversationDelegate() NEStyleManager.instance.setFunStyle() NotificationCenter.default.post( name: Notification.Name(CHANGE_UI), @@ -137,14 +135,4 @@ open class StyleSelectionViewController: NEBaseViewController, UICollectionViewD } } } - - public func removeConversationDelegate() { - if let tabcontroller = UIApplication.shared.keyWindow?.rootViewController as? NETabBarController { - tabcontroller.viewControllers?.forEach { controller in - if let nav = controller as? NENavigationController, let conversationController = nav.topViewController as? NEBaseConversationController { - NIMSDK.shared().chatManager.remove(conversationController) - } - } - } - } } diff --git a/app/Mine/View/BirthdayDatePickerView.swift b/app/Mine/View/BirthdayDatePickerView.swift index 907717d5..ed76adf4 100644 --- a/app/Mine/View/BirthdayDatePickerView.swift +++ b/app/Mine/View/BirthdayDatePickerView.swift @@ -11,7 +11,7 @@ public class BirthdayDatePickerView: UIView { public typealias SelectTimeCallBack = (String?) -> Void public var timeCallBack: SelectTimeCallBack? - lazy var cancelBtn: UIButton = { + lazy var cancelButton: UIButton = { let button = UIButton() button.translatesAutoresizingMaskIntoConstraints = false button.setTitle(NSLocalizedString("cancel", comment: ""), for: .normal) @@ -21,7 +21,7 @@ public class BirthdayDatePickerView: UIView { return button }() - lazy var sureBtn: UIButton = { + lazy var sureButton: UIButton = { let button = UIButton(type: .custom) button.translatesAutoresizingMaskIntoConstraints = false button.setTitle(NSLocalizedString("confirm", comment: ""), for: .normal) @@ -56,6 +56,14 @@ public class BirthdayDatePickerView: UIView { return datePicker }() + /// 日期选择器背景视图 + public lazy var pickerBackView: UIView = { + let pickerBackView = UIView() + pickerBackView.translatesAutoresizingMaskIntoConstraints = false + pickerBackView.backgroundColor = .white + return pickerBackView + }() + override init(frame: CGRect) { super.init(frame: frame) backgroundColor = UIColor(white: 0, alpha: 0.25) @@ -65,13 +73,11 @@ public class BirthdayDatePickerView: UIView { } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } + /// UI 初始化 func setupSubviews() { - let pickerBackView = UIView() - pickerBackView.translatesAutoresizingMaskIntoConstraints = false - pickerBackView.backgroundColor = .white addSubview(pickerBackView) NSLayoutConstraint.activate([ @@ -81,29 +87,29 @@ public class BirthdayDatePickerView: UIView { pickerBackView.heightAnchor.constraint(equalToConstant: 229), ]) - pickerBackView.addSubview(cancelBtn) - pickerBackView.addSubview(sureBtn) + pickerBackView.addSubview(cancelButton) + pickerBackView.addSubview(sureButton) pickerBackView.addSubview(bottomLine) pickerBackView.addSubview(picker) NSLayoutConstraint.activate([ - cancelBtn.leftAnchor.constraint(equalTo: pickerBackView.leftAnchor, constant: 15), - cancelBtn.topAnchor.constraint(equalTo: pickerBackView.topAnchor, constant: 8), - cancelBtn.widthAnchor.constraint(equalToConstant: 45), - cancelBtn.heightAnchor.constraint(equalToConstant: 20), + cancelButton.leftAnchor.constraint(equalTo: pickerBackView.leftAnchor, constant: 15), + cancelButton.topAnchor.constraint(equalTo: pickerBackView.topAnchor, constant: 8), + cancelButton.widthAnchor.constraint(equalToConstant: 45), + cancelButton.heightAnchor.constraint(equalToConstant: 20), ]) NSLayoutConstraint.activate([ - sureBtn.rightAnchor.constraint(equalTo: pickerBackView.rightAnchor, constant: -15), - sureBtn.topAnchor.constraint(equalTo: pickerBackView.topAnchor, constant: 8), - sureBtn.widthAnchor.constraint(equalToConstant: 45), - sureBtn.heightAnchor.constraint(equalToConstant: 20), + sureButton.rightAnchor.constraint(equalTo: pickerBackView.rightAnchor, constant: -15), + sureButton.topAnchor.constraint(equalTo: pickerBackView.topAnchor, constant: 8), + sureButton.widthAnchor.constraint(equalToConstant: 45), + sureButton.heightAnchor.constraint(equalToConstant: 20), ]) NSLayoutConstraint.activate([ bottomLine.leftAnchor.constraint(equalTo: pickerBackView.leftAnchor), bottomLine.rightAnchor.constraint(equalTo: pickerBackView.rightAnchor), - bottomLine.topAnchor.constraint(equalTo: cancelBtn.bottomAnchor, constant: 8), + bottomLine.topAnchor.constraint(equalTo: cancelButton.bottomAnchor, constant: 8), bottomLine.heightAnchor.constraint(equalToConstant: 0.5), ]) diff --git a/app/Mine/View/MineTableViewCell.swift b/app/Mine/View/MineTableViewCell.swift index b3d712b7..076a997c 100644 --- a/app/Mine/View/MineTableViewCell.swift +++ b/app/Mine/View/MineTableViewCell.swift @@ -7,19 +7,41 @@ import NECommonUIKit import UIKit public class MineTableViewCell: UITableViewCell { -// override func awakeFromNib() { -// super.awakeFromNib() -// // Initialization code -// } -// -// override func setSelected(_ selected: Bool, animated: Bool) { -// super.setSelected(selected, animated: animated) -// -// // Configure the view for the selected state -// } + /// 头像 + public lazy var avatarImageView: UIImageView = { + let avatarView = UIImageView() + avatarView.translatesAutoresizingMaskIntoConstraints = false + return avatarView + }() + + /// 昵称 + public lazy var titleLabel: UILabel = { + let nameLabel = UILabel() + nameLabel.translatesAutoresizingMaskIntoConstraints = false + nameLabel.textColor = UIColor.ne_darkText + nameLabel.font = UIFont.systemFont(ofSize: 16.0) + nameLabel.text = NSLocalizedString("setting", comment: "") + nameLabel.accessibilityIdentifier = "id.titleLabel" + return nameLabel + }() + + /// 分割线 + private lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor(hexString: "0xDBE0E8") + return view + }() + + /// 箭头图片 + public lazy var arrowImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "arrow_right")) + imageView.translatesAutoresizingMaskIntoConstraints = false + return imageView + }() required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -29,21 +51,21 @@ public class MineTableViewCell: UITableViewCell { func setUpSubViews() { selectionStyle = .none - contentView.addSubview(avatarImage) + contentView.addSubview(avatarImageView) contentView.addSubview(titleLabel) contentView.addSubview(bottomLine) - contentView.addSubview(arrow) + contentView.addSubview(arrowImageView) NSLayoutConstraint.activate([ - avatarImage.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), - avatarImage.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - avatarImage.widthAnchor.constraint(equalToConstant: 20), - avatarImage.heightAnchor.constraint(equalToConstant: 20), + avatarImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: 20), + avatarImageView.heightAnchor.constraint(equalToConstant: 20), ]) NSLayoutConstraint.activate([ - titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 14), - titleLabel.centerYAnchor.constraint(equalTo: avatarImage.centerYAnchor), + titleLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: 14), + titleLabel.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor), ]) NSLayoutConstraint.activate([ @@ -54,47 +76,16 @@ public class MineTableViewCell: UITableViewCell { ]) NSLayoutConstraint.activate([ - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -25), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -25), // arrow.widthAnchor.constraint(equalToConstant: 15), - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) } func configCell(data: [String: String]) { titleLabel.text = data.keys.first if let imageName = data.values.first { - avatarImage.image = UIImage(named: imageName) + avatarImageView.image = UIImage(named: imageName) } } - - // MARK: lazy Method - - public lazy var avatarImage: UIImageView = { - let avatar = UIImageView() - avatar.translatesAutoresizingMaskIntoConstraints = false - return avatar - }() - - public lazy var titleLabel: UILabel = { - let name = UILabel() - name.translatesAutoresizingMaskIntoConstraints = false - name.textColor = UIColor.ne_darkText - name.font = UIFont.systemFont(ofSize: 16.0) - name.text = NSLocalizedString("setting", comment: "") - name.accessibilityIdentifier = "id.titleLabel" - return name - }() - - private lazy var bottomLine: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor(hexString: "0xDBE0E8") - return view - }() - - public lazy var arrow: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "arrow_right")) - imageView.translatesAutoresizingMaskIntoConstraints = false - return imageView - }() } diff --git a/app/Mine/View/NodeSelectCell.swift b/app/Mine/View/NodeSelectCell.swift index 62bc3b96..b3ae9875 100644 --- a/app/Mine/View/NodeSelectCell.swift +++ b/app/Mine/View/NodeSelectCell.swift @@ -7,6 +7,22 @@ import NETeamUIKit import UIKit class NodeSelectCell: CornerCell { + lazy var titleLabel: UILabel = { + let label = UILabel() + label.textColor = UIColor.ne_darkText + label.font = NEConstant.defaultTextFont(14.0) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + lazy var stateImageView: UIImageView = { + let imgView = UIImageView() + imgView.image = UIImage(named: "unselect") + imgView.highlightedImage = UIImage(named: "select") + imgView.translatesAutoresizingMaskIntoConstraints = false + return imgView + }() + override func awakeFromNib() { super.awakeFromNib() // Initialization code @@ -25,43 +41,27 @@ class NodeSelectCell: CornerCell { } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } public func configure(_ cellModel: SettingCellModel) { cornerType = cellModel.cornerType - stateImg.isHighlighted = cellModel.switchOpen ? true : false + stateImageView.isHighlighted = cellModel.switchOpen ? true : false titleLabel.text = cellModel.subTitle } func setupUI() { contentView.addSubview(titleLabel) - contentView.addSubview(stateImg) + contentView.addSubview(stateImageView) NSLayoutConstraint.activate([ - stateImg.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - stateImg.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 30), + stateImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + stateImageView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 30), ]) NSLayoutConstraint.activate([ titleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - titleLabel.leftAnchor.constraint(equalTo: stateImg.rightAnchor, constant: 10), + titleLabel.leftAnchor.constraint(equalTo: stateImageView.rightAnchor, constant: 10), ]) } - - lazy var titleLabel: UILabel = { - let label = UILabel() - label.textColor = UIColor.ne_darkText - label.font = NEConstant.defaultTextFont(14.0) - label.translatesAutoresizingMaskIntoConstraints = false - return label - }() - - lazy var stateImg: UIImageView = { - let img = UIImageView() - img.image = UIImage(named: "unselect") - img.highlightedImage = UIImage(named: "select") - img.translatesAutoresizingMaskIntoConstraints = false - return img - }() } diff --git a/app/Mine/View/StyleSelectionCell.swift b/app/Mine/View/StyleSelectionCell.swift index d14baed1..0b9dc8db 100644 --- a/app/Mine/View/StyleSelectionCell.swift +++ b/app/Mine/View/StyleSelectionCell.swift @@ -7,7 +7,7 @@ import UIKit open class StyleSelectionCell: UICollectionViewCell { var styleName = "default" var stylePreview = UIImageView() - var styleTitle = UILabel() + var styleTitleLabel = UILabel() var selectButton = UIButton() override public init(frame: CGRect) { @@ -17,7 +17,7 @@ open class StyleSelectionCell: UICollectionViewCell { } public required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func setupSubviews() { @@ -25,8 +25,9 @@ open class StyleSelectionCell: UICollectionViewCell { stylePreview.layer.cornerRadius = 8 addSubview(stylePreview) - styleTitle.translatesAutoresizingMaskIntoConstraints = false - addSubview(styleTitle) + styleTitleLabel.translatesAutoresizingMaskIntoConstraints = false + styleTitleLabel.accessibilityIdentifier = "id.styleTitle" + addSubview(styleTitleLabel) selectButton.translatesAutoresizingMaskIntoConstraints = false selectButton.setImage(UIImage(named: "unclicked"), for: .normal) @@ -42,13 +43,13 @@ open class StyleSelectionCell: UICollectionViewCell { ]) NSLayoutConstraint.activate([ - styleTitle.topAnchor.constraint(equalTo: stylePreview.bottomAnchor, constant: 16), - styleTitle.centerXAnchor.constraint(equalTo: centerXAnchor), - styleTitle.heightAnchor.constraint(equalToConstant: 18), + styleTitleLabel.topAnchor.constraint(equalTo: stylePreview.bottomAnchor, constant: 16), + styleTitleLabel.centerXAnchor.constraint(equalTo: centerXAnchor), + styleTitleLabel.heightAnchor.constraint(equalToConstant: 18), ]) NSLayoutConstraint.activate([ - selectButton.topAnchor.constraint(equalTo: styleTitle.bottomAnchor, constant: 16), + selectButton.topAnchor.constraint(equalTo: styleTitleLabel.bottomAnchor, constant: 16), selectButton.centerXAnchor.constraint(equalTo: centerXAnchor), selectButton.widthAnchor.constraint(equalToConstant: 22), selectButton.heightAnchor.constraint(equalToConstant: 22), @@ -58,7 +59,7 @@ open class StyleSelectionCell: UICollectionViewCell { func configData(model: StyleCellModel) { styleName = model.styleName stylePreview.image = UIImage(named: model.styleImageName) - styleTitle.text = model.styleTitle + styleTitleLabel.text = model.styleTitle selectButton.setImage(UIImage(named: model.selectedImageName), for: .selected) selectButton.isSelected = model.selected } diff --git a/app/Mine/View/Theme/CustomTeamArrowSettingCell.swift b/app/Mine/View/Theme/CustomTeamArrowSettingCell.swift index 064b08c6..e0002f02 100644 --- a/app/Mine/View/Theme/CustomTeamArrowSettingCell.swift +++ b/app/Mine/View/Theme/CustomTeamArrowSettingCell.swift @@ -28,10 +28,10 @@ class CustomTeamArrowSettingCell: TeamArrowSettingCell { titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -68), ]) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), ]) dividerLineLeftMargin?.constant = 20 dividerLineRightMargin?.constant = 0 diff --git a/app/Mine/View/Theme/CustomTeamSettingHeaderCell.swift b/app/Mine/View/Theme/CustomTeamSettingHeaderCell.swift index 1e716cd6..a68eb3c8 100644 --- a/app/Mine/View/Theme/CustomTeamSettingHeaderCell.swift +++ b/app/Mine/View/Theme/CustomTeamSettingHeaderCell.swift @@ -28,15 +28,15 @@ class CustomTeamSettingHeaderCell: TeamSettingHeaderCell { titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -68), ]) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), ]) contentView.addSubview(headerView) NSLayoutConstraint.activate([ - headerView.centerYAnchor.constraint(equalTo: arrow.centerYAnchor), + headerView.centerYAnchor.constraint(equalTo: arrowView.centerYAnchor), headerView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -48.0), headerView.widthAnchor.constraint(equalToConstant: 42.0), headerView.heightAnchor.constraint(equalToConstant: 42.0), diff --git a/app/Mine/View/Theme/CustomTeamSettingRightCustomCell.swift b/app/Mine/View/Theme/CustomTeamSettingRightCustomCell.swift index 051aba0b..faa6a857 100644 --- a/app/Mine/View/Theme/CustomTeamSettingRightCustomCell.swift +++ b/app/Mine/View/Theme/CustomTeamSettingRightCustomCell.swift @@ -23,7 +23,7 @@ class CustomTeamSettingRightCustomCell: TeamSettingRightCustomCell { contentView.addSubview(titleLabel) contentView.addSubview(subTitleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) NSLayoutConstraint.activate([ titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), @@ -34,14 +34,14 @@ class CustomTeamSettingRightCustomCell: TeamSettingRightCustomCell { titleWidthAnchor?.isActive = true NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - arrow.widthAnchor.constraint(equalToConstant: 7), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowView.widthAnchor.constraint(equalToConstant: 7), ]) NSLayoutConstraint.activate([ subTitleLabel.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: 10), - subTitleLabel.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -10), + subTitleLabel.rightAnchor.constraint(equalTo: arrowView.leftAnchor, constant: -10), subTitleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) diff --git a/app/Mine/View/Theme/CustomTeamSettingSubtitleCell.swift b/app/Mine/View/Theme/CustomTeamSettingSubtitleCell.swift index 98e3cfb1..0b2a22b1 100644 --- a/app/Mine/View/Theme/CustomTeamSettingSubtitleCell.swift +++ b/app/Mine/View/Theme/CustomTeamSettingSubtitleCell.swift @@ -23,7 +23,7 @@ class CustomTeamSettingSubtitleCell: TeamSettingSubtitleCell { contentView.addSubview(titleLabel) contentView.addSubview(subTitleLabel) - contentView.addSubview(arrow) + contentView.addSubview(arrowView) NSLayoutConstraint.activate([ titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), @@ -34,14 +34,14 @@ class CustomTeamSettingSubtitleCell: TeamSettingSubtitleCell { titleWidthAnchor?.isActive = true NSLayoutConstraint.activate([ - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - arrow.widthAnchor.constraint(equalToConstant: 7), + arrowView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowView.widthAnchor.constraint(equalToConstant: 7), ]) NSLayoutConstraint.activate([ subTitleLabel.leftAnchor.constraint(equalTo: titleLabel.rightAnchor, constant: 10), - subTitleLabel.rightAnchor.constraint(equalTo: arrow.leftAnchor, constant: -10), + subTitleLabel.rightAnchor.constraint(equalTo: arrowView.leftAnchor, constant: -10), subTitleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) diff --git a/app/Mine/View/Theme/CustomTeamSettingSwitchCell.swift b/app/Mine/View/Theme/CustomTeamSettingSwitchCell.swift index d383fc1e..ec79b6f7 100644 --- a/app/Mine/View/Theme/CustomTeamSettingSwitchCell.swift +++ b/app/Mine/View/Theme/CustomTeamSettingSwitchCell.swift @@ -33,7 +33,7 @@ class CustomTeamSettingSwitchCell: TeamSettingSwitchCell { tSwitch.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), tSwitch.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), ]) - tSwitch.onTintColor = UIColor(hexString: "#58BE6B") + tSwitch.onTintColor = .ne_funTheme tSwitch.addTarget(self, action: #selector(switchChange(_:)), for: .touchUpInside) diff --git a/app/Mine/View/VersionCell.swift b/app/Mine/View/VersionCell.swift index 9a799576..c62d5a6c 100644 --- a/app/Mine/View/VersionCell.swift +++ b/app/Mine/View/VersionCell.swift @@ -12,14 +12,49 @@ enum IntroduceCellType: Int { } class VersionCell: UITableViewCell { + /// 标题 + lazy var titleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor(hexString: "0x333333") + label.font = UIFont.systemFont(ofSize: 14) + return label + }() + + /// 子标题 + lazy var subTitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor(hexString: "0x333333") + label.font = UIFont.systemFont(ofSize: 14) + label.isHidden = true + label.accessibilityIdentifier = "id.version" + return label + }() + + /// 箭头图片 + public lazy var arrowImageView: UIImageView = { + let imageView = UIImageView(image: UIImage(named: "arrow_right")) + imageView.translatesAutoresizingMaskIntoConstraints = false + imageView.isHidden = true + return imageView + }() + + private lazy var bottomLine: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = UIColor(hexString: "0xDBE0E8") + return view + }() + public var cellType: IntroduceCellType? { didSet { if cellType == .version { - subTitle.isHidden = false - arrow.isHidden = true + subTitleLabel.isHidden = false + arrowImageView.isHidden = true } else { - subTitle.isHidden = true - arrow.isHidden = false + subTitleLabel.isHidden = true + arrowImageView.isHidden = false } } } @@ -43,8 +78,8 @@ class VersionCell: UITableViewCell { func setupSubviews() { contentView.addSubview(titleLabel) - contentView.addSubview(subTitle) - contentView.addSubview(arrow) + contentView.addSubview(subTitleLabel) + contentView.addSubview(arrowImageView) contentView.addSubview(bottomLine) NSLayoutConstraint.activate([ @@ -53,13 +88,13 @@ class VersionCell: UITableViewCell { ]) NSLayoutConstraint.activate([ - subTitle.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - subTitle.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + subTitleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + subTitleLabel.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) NSLayoutConstraint.activate([ - arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), - arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrowImageView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + arrowImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), ]) NSLayoutConstraint.activate([ @@ -71,43 +106,11 @@ class VersionCell: UITableViewCell { } required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") + super.init(coder: coder) } func configData(model: SettingCellModel) { titleLabel.text = model.cellName - subTitle.text = model.subTitle + subTitleLabel.text = model.subTitle } - - lazy var titleLabel: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = UIColor(hexString: "0x333333") - label.font = UIFont.systemFont(ofSize: 14) - return label - }() - - lazy var subTitle: UILabel = { - let label = UILabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.textColor = UIColor(hexString: "0x333333") - label.font = UIFont.systemFont(ofSize: 14) - label.isHidden = true - label.accessibilityIdentifier = "id.version" - return label - }() - - public lazy var arrow: UIImageView = { - let imageView = UIImageView(image: UIImage(named: "arrow_right")) - imageView.translatesAutoresizingMaskIntoConstraints = false - imageView.isHidden = true - return imageView - }() - - private lazy var bottomLine: UIView = { - let view = UIView() - view.translatesAutoresizingMaskIntoConstraints = false - view.backgroundColor = UIColor(hexString: "0xDBE0E8") - return view - }() } diff --git a/app/Mine/ViewModel/IntroduceViewModel.swift b/app/Mine/ViewModel/IntroduceViewModel.swift index c4109b15..afdccda6 100644 --- a/app/Mine/ViewModel/IntroduceViewModel.swift +++ b/app/Mine/ViewModel/IntroduceViewModel.swift @@ -5,6 +5,7 @@ import Foundation import NETeamUIKit +import NIMSDK @objcMembers public class IntroduceViewModel: NSObject { @@ -18,8 +19,13 @@ public class IntroduceViewModel: NSObject { versionItem.subTitle = "V\(version)" } + let imVersionItem = SettingCellModel() + imVersionItem.cellName = "IM 版本" + imVersionItem.subTitle = "\(NIMSDK.shared().sdkVersion())" + imVersionItem.type = SettingCellType.SettingSubtitleCell.rawValue + let introduceItem = SettingCellModel() introduceItem.cellName = NSLocalizedString("product_intro", comment: "") - sectionData.append(contentsOf: [versionItem, introduceItem]) + sectionData.append(contentsOf: [versionItem, imVersionItem, introduceItem]) } } diff --git a/app/Mine/ViewModel/MessageRemindViewModel.swift b/app/Mine/ViewModel/MessageRemindViewModel.swift index 169f66dc..e9d0ea9b 100644 --- a/app/Mine/ViewModel/MessageRemindViewModel.swift +++ b/app/Mine/ViewModel/MessageRemindViewModel.swift @@ -5,16 +5,16 @@ import Foundation import NETeamUIKit +import NIMSDK @objcMembers public class MessageRemindViewModel: NSObject { var sectionData = [SettingSectionModel]() - let repo = SettingRepo.shared + let settingRepo = SettingRepo.shared func getData() { sectionData.append(getFirstSection()) -// sectionData.append(getSecondSection()) sectionData.append(getThirdSection()) } @@ -26,9 +26,13 @@ public class MessageRemindViewModel: NSObject { let messageNotify = SettingCellModel() messageNotify.cellName = NSLocalizedString("new_message_remind", comment: "") messageNotify.type = SettingCellType.SettingSwitchCell.rawValue - messageNotify.switchOpen = repo.getPushEnable() + // TODO: 换V2 + messageNotify.switchOpen = settingRepo.getPushEnable() messageNotify.swichChange = { isOpen in - weakSelf?.repo.setPushEnable(isOpen) +// let config = V2NIMDndConfig() +// config.dndOn = isOpen +// weakSelf?.repo.setDndConfig(config: config) + weakSelf?.settingRepo.setPushEnable(isOpen) } model.cellModels.append(contentsOf: [ messageNotify, // 新消息通知 @@ -37,49 +41,22 @@ public class MessageRemindViewModel: NSObject { return model } - private func getSecondSection() -> SettingSectionModel { - let model = SettingSectionModel() - weak var weakSelf = self - let ringBellItem = SettingCellModel() - ringBellItem.cellName = NSLocalizedString("ring_mode", comment: "") - ringBellItem.type = SettingCellType.SettingSwitchCell.rawValue - ringBellItem.switchOpen = repo.getRingMode() - ringBellItem.swichChange = { isOpen in - weakSelf?.repo.setRingMode(isOpen) - } - - let vibrationItem = SettingCellModel() - vibrationItem.cellName = NSLocalizedString("vibration_mode", comment: "") - vibrationItem.type = SettingCellType.SettingSwitchCell.rawValue - vibrationItem.switchOpen = repo.getVibrateMode() - vibrationItem.swichChange = { isOpen in - weakSelf?.repo.setVibrateMode(isOpen) - } - model.cellModels.append(contentsOf: [ - ringBellItem, - vibrationItem, - ]) - model.setCornerType() - return model - } - private func getThirdSection() -> SettingSectionModel { let model = SettingSectionModel() weak var weakSelf = self -// let receiveItem = SettingCellModel() -// receiveItem.cellName = NSLocalizedString("syn_receive_push", comment: "") -// receiveItem.type = SettingCellType.SettingSwitchCell.rawValue -// receiveItem.switchOpen = repo.getPcWebPushEnable() -// receiveItem.swichChange = { isOpen in -// weakSelf?.repo.updatePcWebPushEnable(isOpen) -// } let messageDetailItem = SettingCellModel() messageDetailItem.cellName = NSLocalizedString("display_message_detail", comment: "") messageDetailItem.type = SettingCellType.SettingSwitchCell.rawValue - messageDetailItem.switchOpen = repo.getPushShowDetail() + // TODO: 换V2 + messageDetailItem.switchOpen = settingRepo.getPushDetailEnable() messageDetailItem.swichChange = { isOpen in - weakSelf?.repo.setPushShowDetail(isOpen) + weakSelf?.settingRepo.setPushShowDetail(isOpen) { error in + if let err = error { + print("设置失败: \(err)") + messageDetailItem.switchOpen = !isOpen + } + } } model.cellModels.append(contentsOf: [messageDetailItem]) diff --git a/app/Mine/ViewModel/MineSettingViewModel.swift b/app/Mine/ViewModel/MineSettingViewModel.swift index 9b38538d..3e578fb0 100644 --- a/app/Mine/ViewModel/MineSettingViewModel.swift +++ b/app/Mine/ViewModel/MineSettingViewModel.swift @@ -55,7 +55,7 @@ public class MineSettingViewModel: NSObject { #if DEBUG let configTest = SettingCellModel() - configTest.cellName = "配置测试页" + configTest.cellName = "全局配置" configTest.type = SettingCellType.SettingArrowCell.rawValue configTest.cellClick = { weakSelf?.delegate?.didClickConfigTest() @@ -74,10 +74,10 @@ public class MineSettingViewModel: NSObject { receiverModel.cellName = NSLocalizedString("receiver_mode", comment: "") receiverModel.type = SettingCellType.SettingSwitchCell.rawValue // receiverModel.switchOpen = CoreKitEngine.instance.repo.getHandSetMode() - receiverModel.switchOpen = IMKitClient.instance.getSettingRepo().getHandsetMode() + receiverModel.switchOpen = SettingRepo.shared.getHandsetMode() receiverModel.swichChange = { isOpen in - IMKitClient.instance.getSettingRepo().setHandsetMode(isOpen) + SettingRepo.shared.setHandsetMode(isOpen) } // //过滤通知 // let filterNotify = SettingCellModel() @@ -93,10 +93,10 @@ public class MineSettingViewModel: NSObject { // let deleteFriend = SettingCellModel() // deleteFriend.cellName = NSLocalizedString("delete_friend", comment: "") // deleteFriend.type = SettingCellType.SettingSwitchCell.rawValue -// deleteFriend.switchOpen = IMKitClient.instance.getSettingRepo().getDeleteFriendAlias() +// deleteFriend.switchOpen = SettingRepo.shared.getDeleteFriendAlias() // // deleteFriend.swichChange = { isOpen in -// IMKitClient.instance.getSettingRepo().setDeleteFriendAlias(isOpen) +// SettingRepo.shared.setDeleteFriendAlias(isOpen) // } // 消息已读未读功能 @@ -104,9 +104,9 @@ public class MineSettingViewModel: NSObject { hasRead.cellName = NSLocalizedString("message_read_function", comment: "") hasRead.type = SettingCellType.SettingSwitchCell.rawValue // hasRead.switchOpen = true - hasRead.switchOpen = IMKitClient.instance.getSettingRepo().getShowReadStatus() + hasRead.switchOpen = SettingRepo.shared.getShowReadStatus() hasRead.swichChange = { isOpen in - IMKitClient.instance.getSettingRepo().setShowReadStatus(isOpen) + SettingRepo.shared.setShowReadStatus(isOpen) } model.cellModels.append(contentsOf: [ receiverModel, // 听筒模式 diff --git a/app/Mine/ViewModel/NodeViewModel.swift b/app/Mine/ViewModel/NodeViewModel.swift index e8989c75..2bd67e4d 100644 --- a/app/Mine/ViewModel/NodeViewModel.swift +++ b/app/Mine/ViewModel/NodeViewModel.swift @@ -19,12 +19,12 @@ class NodeViewModel: NSObject { let home = SettingCellModel() home.subTitle = NSLocalizedString("domestic_node", comment: "") home.rowHeight = 44.0 - home.switchOpen = IMKitClient.instance.getSettingRepo().getNodeValue() == true ? true : false + home.switchOpen = SettingRepo.shared.getNodeValue() == true ? true : false // 海外节点配置 let overseas = SettingCellModel() overseas.subTitle = NSLocalizedString("overseas_node", comment: "") - overseas.switchOpen = IMKitClient.instance.getSettingRepo().getNodeValue() == true ? false : true + overseas.switchOpen = SettingRepo.shared.getNodeValue() == true ? false : true overseas.rowHeight = 44.0 model.cellModels.append(contentsOf: [ diff --git a/app/Mine/ViewModel/PersonInfoViewModel.swift b/app/Mine/ViewModel/PersonInfoViewModel.swift index 8a7e54c9..564d1f19 100644 --- a/app/Mine/ViewModel/PersonInfoViewModel.swift +++ b/app/Mine/ViewModel/PersonInfoViewModel.swift @@ -21,15 +21,33 @@ protocol PersonInfoViewModelDelegate: AnyObject { @objcMembers public class PersonInfoViewModel: NSObject { var sectionData = [SettingSectionModel]() - public let friendProvider = FriendProvider.shared - public let userProvider = UserInfoProvider.shared - private var userInfo: NEKitUser? + let contactRepo = ContactRepo.shared + + var userInfo: NEUserWithFriend? weak var delegate: PersonInfoViewModelDelegate? - func getData() { + func getData(_ completion: @escaping () -> Void) { + sectionData.removeAll() + + if let userFriend = NEFriendUserCache.shared.getFriendInfo(IMKitClient.instance.account()) { + userInfo = userFriend + sectionData.append(getFirstSection()) + sectionData.append(getSecondSection()) + completion() + } else { + ContactRepo.shared.getUserList(accountIds: [IMKitClient.instance.account()]) { [weak self] userFriend, error in + guard let self = self else { return } + self.userInfo = userFriend?.first + self.sectionData.append(self.getFirstSection()) + self.sectionData.append(self.getSecondSection()) + completion() + } + } + } + + func refreshData() { sectionData.removeAll() - userInfo = userProvider.getUserInfo(userId: IMKitClient.instance.imAccid()) sectionData.append(getFirstSection()) sectionData.append(getSecondSection()) } @@ -45,8 +63,9 @@ public class PersonInfoViewModel: NSObject { let headImageItem = SettingCellModel() headImageItem.type = SettingCellType.SettingHeaderCell.rawValue headImageItem.cellName = NSLocalizedString("headImage", comment: "") - headImageItem.headerUrl = userInfo?.userInfo?.avatarUrl + headImageItem.headerUrl = userInfo?.user?.avatar headImageItem.defaultHeadData = userInfo?.showName() + headImageItem.subTitle = userInfo?.user?.accountId headImageItem.rowHeight = 64.0 headImageItem.cellClick = { weakSelf?.delegate?.didClickHeadImage() @@ -66,11 +85,11 @@ public class PersonInfoViewModel: NSObject { let accountItem = SettingCellModel() accountItem.type = SettingCellType.SettingSubtitleCustomCell.rawValue accountItem.cellName = NSLocalizedString("account", comment: "") - accountItem.subTitle = mineInfo.userId + accountItem.subTitle = mineInfo.user?.accountId accountItem.rowHeight = 46.0 accountItem.rightCustomViewIcon = "copy_icon" accountItem.customViewClick = { - weakSelf?.delegate?.didCopyAccount(account: mineInfo.userId ?? "") + weakSelf?.delegate?.didCopyAccount(account: mineInfo.user?.accountId ?? "") } // 性别 @@ -78,10 +97,10 @@ public class PersonInfoViewModel: NSObject { sexItem.type = SettingCellType.SettingSubtitleCell.rawValue sexItem.cellName = NSLocalizedString("gender", comment: "") var sex = NSLocalizedString("unknown", comment: "") - switch mineInfo.userInfo?.gender { - case .male: + switch mineInfo.user?.gender { + case 1: sex = NSLocalizedString("male", comment: "") - case .female: + case 2: sex = NSLocalizedString("female", comment: "") default: sex = NSLocalizedString("unknown", comment: "") @@ -96,16 +115,17 @@ public class PersonInfoViewModel: NSObject { let birthdayItem = SettingCellModel() birthdayItem.type = SettingCellType.SettingSubtitleCell.rawValue birthdayItem.cellName = NSLocalizedString("birthday", comment: "") - birthdayItem.subTitle = mineInfo.userInfo?.birth + birthdayItem.subTitle = mineInfo.user?.birthday birthdayItem.rowHeight = 46.0 birthdayItem.cellClick = { - weakSelf?.delegate?.didClickBirthday(birth: mineInfo.userInfo?.birth ?? "") + weakSelf?.delegate?.didClickBirthday(birth: mineInfo.user?.birthday ?? "") } + // 手机 let telephoneItem = SettingCellModel() telephoneItem.type = SettingCellType.SettingSubtitleCell.rawValue telephoneItem.cellName = NSLocalizedString("phone", comment: "") - telephoneItem.subTitle = mineInfo.userInfo?.mobile + telephoneItem.subTitle = mineInfo.user?.mobile telephoneItem.rowHeight = 46.0 telephoneItem.cellClick = { weakSelf?.delegate?.didClickMobile(mobile: telephoneItem.subTitle ?? "") @@ -115,11 +135,12 @@ public class PersonInfoViewModel: NSObject { let emailItem = SettingCellModel() emailItem.type = SettingCellType.SettingSubtitleCell.rawValue emailItem.cellName = NSLocalizedString("email", comment: "") - emailItem.subTitle = mineInfo.userInfo?.email + emailItem.subTitle = mineInfo.user?.email emailItem.rowHeight = 46.0 emailItem.cellClick = { weakSelf?.delegate?.didClickEmail(email: emailItem.subTitle ?? "") } + model.cellModels.append(contentsOf: [ headImageItem, nickNameItem, @@ -142,7 +163,7 @@ public class PersonInfoViewModel: NSObject { let signItem = SettingCellModel() signItem.type = SettingCellType.SettingSubtitleCell.rawValue signItem.cellName = NSLocalizedString("individuality_sign", comment: "") - signItem.subTitle = mineInfo.userInfo?.sign + signItem.subTitle = mineInfo.user?.sign signItem.rowHeight = 46.0 signItem.titleWidth = 64 weak var weakSelf = self @@ -154,81 +175,86 @@ public class PersonInfoViewModel: NSObject { return model } - func updateAvatar(avatar: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.avatar.rawValue): avatar] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户头像 + /// - Parameter avatar: 头像地址 + /// - Parameter completion: 更新结果回调 + func updateSelfAvatar(_ avatar: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.avatar = avatar + contactRepo.updateSelfUserProfile(parameter) { error in + completion(error) } } - func updateSex(sex: NIMUserGender, _ completion: @escaping (NSError?) -> Void) { - let changeValue = - [NSNumber(value: NIMUserInfoUpdateTag.gender.rawValue): NSNumber(value: sex.rawValue)] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户性别 + /// - Parameter gender: 用户性别 + /// - Parameter completion: 更新结果回调 + func updateSelfSex(_ gender: V2NIMGender, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.gender = gender + contactRepo.updateSelfUserProfile(parameter) { error in + completion(error) } } - func updateBirthday(birthDay: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.birth.rawValue): birthDay] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户生日 + /// - Parameter birthDay: 生日 + /// - Parameter completion: 更新结果回调 + func updateSelfBirthday(_ birthDay: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.birthday = birthDay + contactRepo.updateSelfUserProfile(parameter) { error in + completion(error) } } - func updateNickName(name: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.nick.rawValue): name] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户昵称 + /// - Parameter nickName: 昵称 + /// - Parameter completion: 更新结果回调 + func updateSelfNickName(_ nickName: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.name = nickName + + // 如果昵称为空(不设置昵称),则使用账号作为昵称 + if nickName.isEmpty { + parameter.name = IMKitClient.instance.account() + } + + contactRepo.updateSelfUserProfile(parameter) { error in + completion(error) } } - func updateMobile(mobile: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.mobile.rawValue): mobile] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户电话号码 + /// - Parameter mobile: 电话号码 + /// - Parameter completion: 更新结果回调 + func updateSelfMobile(_ mobile: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.mobile = mobile + contactRepo.updateSelfUserProfile(parameter) { error in + completion(error) } } - func updateEmail(email: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.email.rawValue): email] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户的邮箱 + /// - Parameter email: 邮箱 + /// - Parameter completion: 完成回调 + func updateSelfEmail(_ email: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.email = email + contactRepo.updateSelfUserProfile(parameter) { error in + completion(error) } } - func updateSign(sign: String, _ completion: @escaping (NSError?) -> Void) { - let changeValue = [NSNumber(value: NIMUserInfoUpdateTag.sign.rawValue): sign] - userProvider.updateMyUserInfo(values: changeValue) { error in - if error == nil { - completion(nil) - } else { - completion(error) - } + /// 更新当前用户的签名 + /// - Parameter sign: 签名 + /// - Parameter completion: 完成回调 + func updateSelfSign(_ sign: String, _ completion: @escaping (NSError?) -> Void) { + let parameter = V2NIMUserUpdateParams() + parameter.sign = sign + contactRepo.updateSelfUserProfile(parameter) { error in + completion(error) } } }