diff --git a/NEChatUIKit/NEChatUIKit.podspec b/NEChatUIKit/NEChatUIKit.podspec index 6dbe13c0..66d202af 100644 --- a/NEChatUIKit/NEChatUIKit.podspec +++ b/NEChatUIKit/NEChatUIKit.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |spec| # spec.name = 'NEChatUIKit' - spec.version = '9.2.10' + spec.version = '9.5.0' spec.summary = 'Chat Module of IM.' # This description is used to generate tags and improve search results. @@ -49,12 +49,13 @@ TODO: Add long description of the pod here. spec.resource = 'NEChatUIKit/Assets/**/*' spec.dependency 'NEChatKit' spec.dependency 'NECommonUIKit' - spec.dependency 'RSKPlaceholderTextView' spec.dependency 'MJRefresh' spec.dependency 'NIMSDK_LITE' + spec.dependency 'YXAlog' + spec.dependency 'UITextView+Placeholder' + spec.dependency 'SDWebImageWebPCoder' + spec.dependency 'SDWebImageSVGKitPlugin' + -# spec.dependency 'AMap2DMap' -# spec.dependency 'AMapSearch' -# spec.dependency 'AMapLocation' end diff --git a/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings b/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings index 1311f81b..c187fba1 100644 --- a/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings +++ b/NEChatUIKit/NEChatUIKit/Assets/en.lproj/Localizable.strings @@ -21,6 +21,7 @@ "select_from_album"="album"; "select_from_icloud"="icloud"; "editing"="typing"; +"downloading"="downloading..."; "read"="read"; "unread"="unread"; "network_unavailable"="No internet, Please check your setting"; @@ -33,32 +34,36 @@ "enter"="enter"; "unknown_system_message"="Unknown message"; "You"="you"; -"discussion_group"="temp group"; +"discussion_group"="group"; "group"="group"; +"group_chat"="group chat"; "dissolve"="dissolved"; "kick"="将 "; "out"="remove"; -"has_updated" = "updated"; +"has_updated" = "update"; +"to" = "to "; +"only_team_owner" = "Only the group owner"; +"everyone" = "Everyone"; -"team_name" = "name"; -"team_intro" = "Info"; +"team_name" = "Group Name"; +"team_intro" = "Group Introduction"; "team_anouncement" = "Notice"; -"team_avatar" = "Avatar"; +"team_avatar" = "Group Avatar"; "team_join_mode" = "join mode"; "team_be_invited_mode" = "invite mode"; -"team_be_invited_permission" = "invite permission"; +"team_be_invited_permission" = "Invite Permission"; "team_be_invited_author" = "是否需要被邀请者同意权限"; "team_update_info_permission" = "update info permission"; -"team_update_client_custom"="update custom permission"; +"team_update_client_custom"="Update Permission"; "team_custom_info" = "custom info"; "not_mute" = "unmute"; "mute" = "mute"; -"team_has_been_removed" = "This group was removed"; +"team_has_been_removed" = "This group is disbanded"; "team_has_quit" = "Group chat has exited"; "join"="joined"; "pass"="Passed"; -"leave"="leave"; +"leave"="quit"; "transfer"="Transferred the group leader identity to"; "added_manager"="add to be Group administrator"; "removed_manager"="Group administrator identity revoked"; @@ -67,8 +72,8 @@ "team_all_mute"="Mute all"; "team_all_no_mute"="Unmute"; -"pin_text_P2P"="pinned this message for both"; -"pin_text_team"="pinned this message for both all"; +"pin_text_P2P"="pinned this message"; +"pin_text_team"="pinned this message"; "session_set_top"="sticky to top"; "message_remind"="open notification"; @@ -146,3 +151,14 @@ "editable_time_expired"="Editable time has expired"; "message_not_found"="This message is gone"; + +"unkonw_pin_message"="unkonw message type"; +"pin_jump_detail"="jump to details"; +"cancel_pin_success"="Unpin"; +"no_pin_message"="No pin message"; + +"you_were_mentioned"="[You were mentioned]"; +// MARK: error toast +"failed_operation"="Failed Operation"; +"team_not_exist"="team not exist"; +"unpin_failed"="Unpin failed"; diff --git a/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings index a9292507..498907b4 100644 --- a/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEChatUIKit/NEChatUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -22,40 +22,39 @@ "select_from_album"="从相册选择"; "select_from_icloud"="从iCloud选择"; "editing"="正在输入中..."; +"downloading"="正在下载中..."; "read"="已读(0)"; "unread"="未读(0)"; "network_unavailable"="当前网络不可用,请检查你当网络设置。"; - - - - - - - //MARK: tips //MARK: team "invite"="邀请 "; "humans"="人"; -"enter"="进入了"; +"enter"="加入"; "unknown_system_message"="未知消息类型"; "You"="你 "; -"discussion_group"="讨论组"; +"discussion_group"="群"; "group"="群"; +"group_chat"="群聊"; "dissolve"="解散了"; "kick"="将 "; "out"="移除了 "; "has_updated" = "更新了"; +"to" = "为 "; +"only_team_owner" = "仅群主"; +"everyone" = "所有人"; -"team_name" = "名字"; +"team_name" = "名称"; "team_intro" = "介绍"; "team_anouncement" = "公告"; "team_avatar" = "头像"; "team_join_mode" = "加入方式"; +"team_permission" = "群权限"; "team_be_invited_mode" = "邀请方式"; -"team_be_invited_permission" = "邀请权限"; +"team_be_invited_permission" = "邀请他人权限"; "team_be_invited_author" = "是否需要被邀请者同意权限"; -"team_update_info_permission" = "更新群信息权限"; +"team_update_info_permission" = "群资料修改权限"; "team_update_client_custom"="更新客户端自定义字段的权限"; "team_custom_info" = "自定义扩展字段"; @@ -65,16 +64,16 @@ "team_has_quit" = "已退出群聊"; "join"="进入了"; "pass"="通过了"; -"leave"="离开了"; +"leave"="退出了"; "transfer"="转移了群主身份给"; "added_manager"="被添加为群管理员"; "removed_manager"="被撤销了群管理员身份"; "accept"="接受入群邀请来自"; "team_mute"="当前群主设置为禁言"; -"team_all_mute"="群全体禁言"; -"team_all_no_mute"="取消群全体禁言"; -"pin_text_P2P"="标记了这条信息,对话内容双方均可见"; -"pin_text_team"="标记了这条信息,所有群成员均可见"; +"team_all_mute"="群禁言已开启"; +"team_all_no_mute"="群禁言已关闭"; +"pin_text_P2P"="标记了这条信息"; +"pin_text_team"="标记了这条信息"; "session_set_top"="聊天置顶"; "message_remind"="开启消息提醒"; "open_soon"="暂未开放"; @@ -101,7 +100,7 @@ "no_permession"="暂无权限"; "msg_reply"="回复"; "msg_image"="图片"; -"msg_audio"="语音"; +"msg_audio"="语音消息"; "msg_video"="视频"; "msg_file"="文件"; "msg_location"="地理位置"; @@ -149,4 +148,15 @@ "call_busy"="忙线未接听"; "editable_time_expired"="已超过可编辑时间"; -"message_not_found"="该消息已移除"; +"message_not_found"="该消息已撤回或删除"; + +"unkonw_pin_message"="暂不支持消息类型"; +"pin_jump_detail"="跳转查看"; +"cancel_pin_success"="已取消标记"; +"no_pin_message"="暂无标记消息"; + +"you_were_mentioned"="[有人@我]"; +// MARK: error toast +"failed_operation"="操作失败"; +"team_not_exist"="群组不存在"; +"unpin_failed"="取消标记失败"; diff --git a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatBaseCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift similarity index 65% rename from NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatBaseCell.swift rename to NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift index fbf75b9b..8967e920 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/ChatBaseCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Base/BaseView/NEChatBaseCell.swift @@ -6,17 +6,19 @@ import UIKit @objcMembers -public class ChatBaseCell: UITableViewCell { - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { +open class NEChatBaseCell: UITableViewCell { + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } public func uploadProgress(_ progress: Float) { fatalError("override in sub class") } + + open func setModel(_ model: MessageContentModel) {} } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift index ebfc2e86..d6dbfd56 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/ChatViewController.swift @@ -15,11 +15,14 @@ import WebKit import NEChatKit import Photos +let leftCellTypeSufixx = "_left" +let rightCellTypeSufixx = "_right" + @objcMembers open class ChatViewController: ChatBaseViewController, UINavigationControllerDelegate, ChatInputViewDelegate, ChatViewModelDelegate, NIMMediaManagerDelegate, MessageOperationViewDelegate, UIGestureRecognizerDelegate, UITableViewDataSource, - UITableViewDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate, CLLocationManagerDelegate { + UITableViewDelegate, UIDocumentPickerDelegate, UIDocumentInteractionControllerDelegate, CLLocationManagerDelegate, UITextViewDelegate { private let tag = "ChatViewController" public var viewmodel: ChatViewModel public var inputViewTopConstraint: NSLayoutConstraint? @@ -27,6 +30,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel public var menuView: ChatInputView = .init() public var operationView: MessageOperationView? private var playingCell: ChatAudioCell? + private var playingModel: MessageAudioModel? private var atUsers = [NSRange]() private var timer: Timer? var replyView = ReplyView() @@ -35,6 +39,48 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel private var needMarkReadMsgs = [NIMMessage]() private var isCurrentPage = true var titleContent = "" + public var bottomExanpndHeight: CGFloat = 204 // 底部展开高度 + + public var registerCellDic = [ + getLeftCellType(MessageType.text.rawValue): ChatTextLeftCell.self, + getRitghtCellType(MessageType.text.rawValue): ChatTextRightCell.self, + getLeftCellType(MessageType.rtcCallRecord.rawValue): ChatCallRecordLeftCell.self, + getRitghtCellType(MessageType.rtcCallRecord.rawValue): ChatCallRecordRightCell.self, + getLeftCellType(MessageType.audio.rawValue): ChatAudioLeftCell.self, + getRitghtCellType(MessageType.audio.rawValue): ChatAudioRightCell.self, + getLeftCellType(MessageType.image.rawValue): ChatImageLeftCell.self, + getRitghtCellType(MessageType.image.rawValue): ChatImageRightCell.self, + getLeftCellType(MessageType.revoke.rawValue): ChatRevokeLeftCell.self, + getRitghtCellType(MessageType.revoke.rawValue): ChatRevokeRightCell.self, + getLeftCellType(MessageType.video.rawValue): ChatVideoLeftCell.self, + getRitghtCellType(MessageType.video.rawValue): ChatVideoRightCell.self, + getLeftCellType(MessageType.file.rawValue): ChatFileLeftCell.self, + getRitghtCellType(MessageType.file.rawValue): ChatFileRightCell.self, + getLeftCellType(MessageType.reply.rawValue): ChatReplyLeftCell.self, + getRitghtCellType(MessageType.reply.rawValue): ChatReplyRightCell.self, + getLeftCellType(MessageType.location.rawValue): ChatLocationLeftCell.self, + getRitghtCellType(MessageType.location.rawValue): ChatLocationRightCell.self, + "\(MessageType.time.rawValue)": ChatTimeTableViewCell.self, + ] + + public lazy var inputTopExtendView: UIView = { + let content = UIView() + content.translatesAutoresizingMaskIntoConstraints = false + content.backgroundColor = UIColor.clear + return content + }() + + public lazy var navigationBarBottomExtendView: UIView = { + let content = UIView() + content.translatesAutoresizingMaskIntoConstraints = false + content.backgroundColor = UIColor.clear + return content + }() + + public lazy var inputTopExtendHeight: CGFloat = 0 + public lazy var navigationBarBottomExtendHeight: CGFloat = 0 + public var inputTopExtendHeightConstant: NSLayoutConstraint? + public var navigationBarBottomExtendHeightConstant: NSLayoutConstraint? private lazy var manager = CLLocationManager() @@ -45,8 +91,6 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel NEKeyboardManager.shared.enableAutoToolbar = false NIMSDK.shared().mediaManager.add(self) NIMSDK.shared().mediaManager.setNeedProximityMonitor(viewmodel.getHandSetEnable()) - // 注册自定义消息的解析器 - // NIMCustomObject.registerCustomDecoder(CustomAttachmentDecoder()) } public required init?(coder: NSCoder) { @@ -62,7 +106,15 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel super.viewDidLoad() commonUI() addObseve() - loadData() + weak var weakSelf = self + viewmodel.fetchPinMessage { + weakSelf?.loadData() + } + } + + public 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)] + return true } override open func viewWillAppear(_ animated: Bool) { @@ -72,6 +124,9 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel isCurrentPage = true navigationController?.isNavigationBarHidden = false markNeedReadMsg() + getSessionInfo(session: viewmodel.session) + tableView.reloadData() + clearAtRemind() } override open func viewWillDisappear(_ animated: Bool) { @@ -83,6 +138,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel if NIMSDK.shared().mediaManager.isPlaying() { NIMSDK.shared().mediaManager.stopPlay() } + clearAtRemind() } override open func viewDidDisappear(_ animated: Bool) { @@ -174,7 +230,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return view }() - private lazy var tableView: UITableView = { + lazy var tableView: UITableView = { let tableView = UITableView(frame: .zero, style: .plain) tableView.translatesAutoresizingMaskIntoConstraints = false tableView.separatorStyle = .none @@ -251,7 +307,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // MARK: objc 方法 - func toSetting() { + public func toSetting() { if viewmodel.session.sessionType == .team { Router.shared.use( TeamSettingViewRouter, @@ -273,22 +329,38 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel view.addSubview(tableView) tableViewBottomConstraint = tableView.bottomAnchor.constraint( equalTo: view.bottomAnchor, - constant: -100 + constant: -100 - inputTopExtendHeight ) tableViewBottomConstraint?.isActive = true + view.addSubview(navigationBarBottomExtendView) + if #available(iOS 10, *) { + self.navigationBarBottomExtendHeightConstant = navigationBarBottomExtendView.heightAnchor.constraint(equalToConstant: navigationBarBottomExtendHeight) + self.navigationBarBottomExtendHeightConstant?.isActive = true + NSLayoutConstraint.activate([ + navigationBarBottomExtendView.topAnchor.constraint(equalTo: view.topAnchor, constant: kNavigationHeight + KStatusBarHeight), + navigationBarBottomExtendView.leftAnchor.constraint(equalTo: view.leftAnchor), + navigationBarBottomExtendView.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) NSLayoutConstraint.activate([ tableView.topAnchor.constraint( - equalTo: view.topAnchor, - constant: kNavigationHeight + KStatusBarHeight + equalTo: navigationBarBottomExtendView.bottomAnchor, + constant: 0 ), tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), ]) } else { + navigationBarBottomExtendHeightConstant = navigationBarBottomExtendView.heightAnchor.constraint(equalToConstant: navigationBarBottomExtendHeight) + navigationBarBottomExtendHeightConstant?.isActive = true + NSLayoutConstraint.activate([ + navigationBarBottomExtendView.topAnchor.constraint(equalTo: view.topAnchor, constant: 0), + navigationBarBottomExtendView.leftAnchor.constraint(equalTo: view.leftAnchor), + navigationBarBottomExtendView.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) NSLayoutConstraint.activate([ tableView.topAnchor.constraint( - equalTo: view.topAnchor, + equalTo: navigationBarBottomExtendView.bottomAnchor, constant: 0 ), tableView.leftAnchor.constraint(equalTo: view.leftAnchor), @@ -296,85 +368,24 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ]) } - tableView.register(ChatCallRecordLeftCell.self, forCellReuseIdentifier: "\(ChatCallRecordLeftCell.self)") - - tableView.register(ChatCallRecordRightCell.self, forCellReuseIdentifier: "\(ChatCallRecordRightCell.self)") - - tableView.register( - ChatTimeTableViewCell.self, - forCellReuseIdentifier: "\(ChatTimeTableViewCell.self)" - ) - tableView.register( ChatBaseLeftCell.self, forCellReuseIdentifier: "\(ChatBaseLeftCell.self)" ) + tableView.register( ChatBaseRightCell.self, forCellReuseIdentifier: "\(ChatBaseRightCell.self)" ) - tableView.register( - ChatTextRightCell.self, - forCellReuseIdentifier: "\(ChatTextRightCell.self)" - ) - tableView.register( - ChatTextLeftCell.self, - forCellReuseIdentifier: "\(ChatTextLeftCell.self)" - ) - tableView.register( - ChatAudioLeftCell.self, - forCellReuseIdentifier: "\(ChatAudioLeftCell.self)" - ) - tableView.register( - ChatAudioRightCell.self, - forCellReuseIdentifier: "\(ChatAudioRightCell.self)" - ) - tableView.register( - ChatImageLeftCell.self, - forCellReuseIdentifier: "\(ChatImageLeftCell.self)" - ) - tableView.register( - ChatImageRightCell.self, - forCellReuseIdentifier: "\(ChatImageRightCell.self)" - ) - tableView.register( - ChatRevokeLeftCell.self, - forCellReuseIdentifier: "\(ChatRevokeLeftCell.self)" - ) - tableView.register( - ChatRevokeRightCell.self, - forCellReuseIdentifier: "\(ChatRevokeRightCell.self)" - ) - - tableView.register( - ChatVideoLeftCell.self, - forCellReuseIdentifier: "\(ChatVideoLeftCell.self)" - ) - tableView.register( - ChatVideoRightCell.self, - forCellReuseIdentifier: "\(ChatVideoRightCell.self)" - ) - tableView.register( - ChatFileLeftCell.self, - forCellReuseIdentifier: "\(ChatFileLeftCell.self)" - ) - tableView.register( - ChatFileRightCell.self, - forCellReuseIdentifier: "\(ChatFileRightCell.self)" - ) + NEChatUIKitClient.instance.getRegisterCustomCell().forEach { (key: String, value: UITableViewCell.Type) in + registerCellDic[key] = value + } - tableView.register( - ChatReplyRightCell.self, - forCellReuseIdentifier: "\(ChatReplyRightCell.self)" - ) - tableView.register( - ChatReplyLeftCell.self, - forCellReuseIdentifier: "\(ChatReplyLeftCell.self)" - ) + registerCellDic.forEach { (key: String, value: UITableViewCell.Type) in + tableView.register(value, forCellReuseIdentifier: key) + } - tableView.register(ChatLocationLeftCell.self, forCellReuseIdentifier: "\(ChatLocationLeftCell.self)") - tableView.register(ChatLocationRightCell.self, forCellReuseIdentifier: "\(ChatLocationRightCell.self)") viewmodel.delegate = self menuView.backgroundColor = UIColor(hexString: "#EFF1F3") @@ -394,6 +405,15 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel ]) inputViewTopConstraint?.isActive = true + view.addSubview(inputTopExtendView) + inputTopExtendHeightConstant = inputTopExtendView.heightAnchor.constraint(equalToConstant: inputTopExtendHeight) + inputTopExtendHeightConstant?.isActive = true + NSLayoutConstraint.activate([ + inputTopExtendView.bottomAnchor.constraint(equalTo: menuView.topAnchor), + inputTopExtendView.leftAnchor.constraint(equalTo: view.leftAnchor), + inputTopExtendView.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) + weak var weakSelf = self NEChatDetectNetworkTool.shareInstance.netWorkReachability { status in if status == .notReachable, let networkView = weakSelf?.brokenNetworkView { @@ -573,40 +593,39 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - // offset:value which from self.view.bottom to inputView.bottom - var count = 0 - func layoutInputView(offset: CGFloat) { + public func layoutInputView(offset: CGFloat) { print("layoutInputView offset : ", offset) weak var weakSelf = self UIView.animate(withDuration: 0.1, animations: { weakSelf?.inputViewTopConstraint?.constant = -100 - offset - weakSelf?.tableViewBottomConstraint?.constant = -100 - offset + weakSelf?.tableViewBottomConstraint?.constant = -100 - offset - (weakSelf?.inputTopExtendHeight ?? 0) }) } // MARK: ChatInputViewDelegate - public func sendText(text: String?) { + public func sendText(text: String?, attribute: NSAttributedString?) { guard let content = text, content.count > 0 else { return } + let remoteExt = menuView.getRemoteExtension(attribute) + menuView.cleartAtCache() if viewmodel.isReplying, let msg = viewmodel.operationModel?.message { - viewmodel - .replyMessage(MessageUtils.textMessage(text: content), msg) { [weak self] error in - NELog.infoLog( - ModuleName + " " + (self?.tag ?? "ChatViewController"), - desc: "CALLBACK replyMessage " + (error?.localizedDescription ?? "no error") - ) - if error != nil { - self?.view.makeToast(error?.localizedDescription) - } else { - self?.viewmodel.isReplying = false - self?.replyView.removeFromSuperview() - } + viewmodel.replyMessageWithoutThread(message: MessageUtils.textMessage(text: content, remoteExt: remoteExt), target: msg) { [weak self] error in + NELog.infoLog( + ModuleName + " " + (self?.tag ?? "ChatViewController"), + desc: "CALLBACK replyMessage " + (error?.localizedDescription ?? "no error") + ) + if error != nil { + self?.view.makeToast(error?.localizedDescription) + } else { + self?.viewmodel.isReplying = false + self?.replyView.removeFromSuperview() } + } } else { - viewmodel.sendTextMessage(text: content) { [weak self] error in + viewmodel.sendTextMessage(text: content, remoteExt: remoteExt) { [weak self] error in NELog.infoLog( ModuleName + " " + (self?.tag ?? "ChatViewController"), desc: "CALLBACK sendTextMessage " + (error?.localizedDescription ?? "no error") @@ -743,19 +762,23 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } // 记录当前光标位置(removeSubrange后光标会直接跳到末尾) - var selRange = menuView.textField.selectedTextRange - if let oldCursor = menuView.textField.selectedTextRange { - if let newCursor = menuView.textField.position(from: oldCursor.start, offset: -rmRange.length) { - selRange = menuView.textField.textRange(from: newCursor, to: newCursor) + var selRange = menuView.textView.selectedTextRange + if let oldCursor = menuView.textView.selectedTextRange { + if let newCursor = menuView.textView.position(from: oldCursor.start, offset: -rmRange.length) { + selRange = menuView.textView.textRange(from: newCursor, to: newCursor) } } // 删除rmRange范围内的字符串("@xxx ") - let subRange = menuView.textField.text.utf16.index(menuView.textField.text.startIndex, offsetBy: rmRange.location) ... menuView.textField.text.utf16.index(menuView.textField.text.startIndex, offsetBy: rmRange.location + rmRange.length - 1) - menuView.textField.text.removeSubrange(subRange) + let subRange = menuView.textView.text.utf16.index(menuView.textView.text.startIndex, offsetBy: rmRange.location) ... menuView.textView.text.utf16.index(menuView.textView.text.startIndex, offsetBy: rmRange.location + rmRange.length - 1) + + let key = "\(rmRange.location)_\(rmRange.length - 1)" + menuView.atRangeCache.removeValue(forKey: key) + + menuView.textView.text.removeSubrange(subRange) // 重新设置光标到删除前的位置 - menuView.textField.selectedTextRange = selRange + menuView.textView.selectedTextRange = selRange } return false } @@ -785,16 +808,16 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel public func willSelectItem(button: UIButton, index: Int) { operationView?.removeFromSuperview() if index == 0 { - layoutInputView(offset: 204) + layoutInputView(offset: bottomExanpndHeight) scrollTableViewToBottom() } else if index == 1 { - layoutInputView(offset: 204) + layoutInputView(offset: bottomExanpndHeight) scrollTableViewToBottom() } else if index == 2 { goPhotoAlbumWithVideo(self) } else { // 更多 - layoutInputView(offset: 204) + layoutInputView(offset: bottomExanpndHeight) scrollTableViewToBottom() UIApplication.shared.keyWindow?.endEditing(true) } @@ -844,6 +867,12 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel present(imagePickerVC, animated: true) {} } + func clearAtRemind() { + let sessionId = viewmodel.session.sessionId + let param = ["sessionId": sessionId] + Router.shared.use("ClearAtMessageRemind", parameters: param, closure: nil) + } + func sendMediaMessage(didFinishPickingMediaWithInfo info: [UIImagePickerController .InfoKey: Any]) { var imageName = "IMG_0001" @@ -1059,12 +1088,14 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return indexPaths } - public func onDeleteMessage(_ message: NIMMessage, atIndexs: [IndexPath]) { + public func onDeleteMessage(_ message: NIMMessage, atIndexs: [IndexPath], reloadIndex: [IndexPath]) { if atIndexs.isEmpty { return } - // self.tableView.reloadData() tableViewDeleteIndexs(atIndexs) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in + self?.tableViewReloadIndexs(reloadIndex) + })) } public func updateDownloadProgress(_ message: NIMMessage, atIndex: IndexPath, progress: Float) { @@ -1101,7 +1132,6 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } public func tableViewReloadIndexs(_ indexs: [IndexPath]) { -// print("table view reload stack : ", Thread.callStackSymbols) weak var weakSelf = self if #available(iOS 11.0, *) { tableView.performBatchUpdates { @@ -1116,11 +1146,6 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel public func didReadedMessageIndexs() { tableView.reloadData() -// if let indexPaths = tableView.indexPathsForVisibleRows, indexPaths.count > 0 { -// tableView.beginUpdates() -// tableView.reloadRows(at: indexPaths, with: .none) -// tableView.endUpdates() -// } } public func tableViewUpdateDownload(_ index: IndexPath) { @@ -1129,6 +1154,10 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel tableView.endUpdates() } + public func didRefreshTable() { + tableView.reloadData() + } + // record audio public func startRecord() { let dur = 60.0 @@ -1167,8 +1196,8 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel opeView.removeFromSuperview() } else { - if menuView.textField.isFirstResponder { - menuView.textField.resignFirstResponder() + if menuView.textView.isFirstResponder { + menuView.textView.resignFirstResponder() } else { layoutInputView(offset: 0) } @@ -1177,12 +1206,16 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // MARK: audio play - private func startPlay(cell: ChatAudioCell?, audio: NIMAudioObject) { - if cell?.isPlaying == true { + private func startPlay(cell: ChatAudioCell?, model: MessageAudioModel?) { + guard let audio = model?.message?.messageObject as? NIMAudioObject else { + return + } + if NIMSDK.shared().mediaManager.isPlaying() { stopPlay() } else { stopPlay() playingCell = cell + playingModel = model playingCell?.startAnimation() if let url = audio.path { if viewmodel.getHandSetEnable() == true { @@ -1218,6 +1251,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel showToast(e.localizedDescription) // stop playingCell?.stopAnimation() + playingModel?.isPlaying = false } } @@ -1228,6 +1262,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } // stop playingCell?.stopAnimation() + playingModel?.isPlaying = false } public func stopPlayAudio(_ filePath: String, didCompletedWithError error: Error?) { @@ -1236,6 +1271,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel showToast(e.localizedDescription) } playingCell?.stopAnimation() + playingModel?.isPlaying = false } public func playAudio(_ filePath: String, progress value: Float) {} @@ -1248,6 +1284,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel print(#function) // stop play playingCell?.stopAnimation() + playingModel?.isPlaying = false } // record @@ -1308,7 +1345,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return } var indexs = [IndexPath]() - for (i, model) in viewmodel.messages.enumerated() { + for (i, _) in viewmodel.messages.enumerated() { if i >= oldRows { indexs.append(IndexPath(row: i, section: 0)) } @@ -1324,34 +1361,45 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - func addToAtUsers(addText: String, isReply: Bool = false) { - // @列表中添加一项 - let anIndex = menuView.textField.selectedRange.location - var range = NSRange(location: anIndex - 1, length: addText.utf16.count + 1) - if isReply { - range = NSRange(location: anIndex, length: addText.utf16.count) - } - atUsers.append(range) - - // 添加range后,range.location后面所有的atUser的location都发生了变化 - var atUsersTmp = [NSRange]() - for atUser in atUsers { - if range.location < atUser.location { - atUsersTmp.append(NSRange(location: atUser.location + range.length, length: atUser.length)) + func addToAtUsers(addText: String, isReply: Bool = false, accid: String, _ isLongPress: Bool = false) { + if let font = menuView.textView.font { + let mutaString = NSMutableAttributedString(attributedString: menuView.textView.attributedText) + let atString = NSAttributedString(string: addText, attributes: [NSAttributedString.Key.foregroundColor: UIColor.ne_blueText, NSAttributedString.Key.font: font]) + var selectRange = NSMakeRange(0, 0) + var location = 0 + if menuView.textView.isFirstResponder == true { + location = menuView.textView.selectedRange.location + selectRange = menuView.textView.selectedRange } else { - atUsersTmp.append(atUser) + location = menuView.textView.attributedText.length + selectRange = NSMakeRange(location, 0) } - } - if atUsersTmp.count > 0 { - atUsers = atUsersTmp - } - // range范围内添加字符串("@xxx ") - if let pos = menuView.textField.selectedTextRange { - // 用replace代替insert(start==end) - menuView.textField.replace(pos, withText: addText) - } else { - menuView.textField.text += addText + if isReply || isLongPress { + let temMutaString = NSMutableAttributedString(attributedString: atString) + let spaceStr = NSAttributedString(string: " ", attributes: [NSAttributedString.Key.font: font]) + temMutaString.append(spaceStr) + mutaString.insert(temMutaString, at: location) + + menuView.nickAccidDic[addText] = accid.count > 0 ? accid : "ait_all" + menuView.textView.attributedText = mutaString + menuView.textView.selectedRange = NSMakeRange(selectRange.location + temMutaString.length, 0) + return + } + + if menuView.textView.selectedRange.location > 0 { + mutaString.replaceCharacters(in: NSMakeRange(menuView.textView.selectedRange.location - 1, 1), with: "") + let temMutaString = NSMutableAttributedString(attributedString: atString) + let spaceStr = NSAttributedString(string: " ", attributes: [NSAttributedString.Key.font: font]) + temMutaString.append(spaceStr) + mutaString.insert(temMutaString, at: menuView.textView.selectedRange.location - 1) + selectRange = NSMakeRange(selectRange.location - 1, selectRange.length) + } + + menuView.nickAccidDic[addText] = accid.count > 0 ? accid : "ait_all" + + menuView.textView.attributedText = mutaString + menuView.textView.selectedRange = NSMakeRange(selectRange.location + addText.count + atRangeOffset, 0) } } @@ -1360,22 +1408,20 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel selectVC.modalPresentationStyle = .formSheet selectVC.selectedBlock = { [weak self] index, model in var addText = "" + var accid = "" if index == 0 { - addText += chatLocalizable("user_select_all") + " " + addText += chatLocalizable("user_select_all") } else { if let m = model { - var name = "" - if let nick = m.nimUser?.userInfo?.nickName { - name = nick - } else if let uid = m.nimUser?.userId { - name = uid + addText += m.showNameInTeam() + if let uid = m.nimUser?.userId { + accid = uid } - addText += name + " " } } - - self?.addToAtUsers(addText: addText) + addText = "@" + addText + "" + self?.addToAtUsers(addText: addText, accid: accid) } present(selectVC, animated: true, completion: nil) } @@ -1424,7 +1470,7 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } } - private func showReplyMessageView() { + private func showReplyMessageView(isReEdit: Bool = false) { viewmodel.isReplying = true view.addSubview(replyView) replyView.closeButton.addTarget(self, action: #selector(cancelReply), for: .touchUpInside) @@ -1436,39 +1482,53 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel replyView.heightAnchor.constraint(equalToConstant: 36), ]) if let message = viewmodel.operationModel?.message { - var text = chatLocalizable("msg_reply") - if let uid = message.from { - let user = viewmodel.getUserInfo(userId: uid) - let name = user?.userInfo?.nickName - if viewmodel.session.sessionType != .P2P, - !IMKitClient.instance.isMySelf(uid) { - addToAtUsers(addText: "@" + (name ?? uid) + " ", isReply: true) + if isReEdit { + 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 } - text += " " + (name ?? uid) - } - text += ": " - switch message.messageType { - case .text: - if let t = message.text { - text += t + } else { + var text = chatLocalizable("msg_reply") + if let uid = message.from { + var showName = viewmodel.getShowName(userId: uid, teamId: viewmodel.session.sessionId, false) + if viewmodel.session.sessionType != .P2P, + !IMKitClient.instance.isMySelf(uid) { + addToAtUsers(addText: "@" + showName + "", isReply: true, accid: uid) + } + let user = viewmodel.getUserInfo(userId: uid) + if let alias = user?.alias { + showName = alias + } + text += " " + showName + } + text += ": " + switch message.messageType { + case .text: + if let t = message.text { + text += t + } + case .image: + text += "[\(chatLocalizable("msg_image"))]" + case .audio: + text += "[\(chatLocalizable("msg_audio"))]" + case .video: + text += "[\(chatLocalizable("msg_video"))]" + case .file: + text += "[\(chatLocalizable("msg_file"))]" + case .location: + text += "[\(chatLocalizable("msg_location"))]" + case .custom: + text += "[\(chatLocalizable("msg_custom"))]" + default: + text += "" } - case .image: - text += "[\(chatLocalizable("msg_image"))]" - case .audio: - text += "[\(chatLocalizable("msg_audio"))]" - case .video: - text += "[\(chatLocalizable("msg_video"))]" - case .file: - text += "[\(chatLocalizable("msg_file"))]" - case .location: - text += "[\(chatLocalizable("msg_location"))]" - case .custom: - text += "[\(chatLocalizable("msg_custom"))]" - default: - text += "" - } - replyView.textLabel.text = text - menuView.textField.becomeFirstResponder() + replyView.textLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, + font: replyView.textLabel.font, + color: replyView.textLabel.textColor) + menuView.textView.becomeFirstResponder() + } } } @@ -1614,14 +1674,24 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } private func pinMessage() { - if let message = viewmodel.operationModel?.message { + 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"), desc: "CALLBACK pinMessage " + (error?.localizedDescription ?? "no error") ) - if error != nil { - self?.view.makeToast(error?.localizedDescription) + if let err = error as? NSError { + if err.code == 408 { + self?.view.makeToast(chatLocalizable("failed_operation")) + } else { + self?.view.makeToast(error?.localizedDescription) + } } else { // update UI if index >= 0 { @@ -1633,14 +1703,24 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel } private func removePinMessage() { - if let message = viewmodel.operationModel?.message { + 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"), desc: "CALLBACK removePinMessage " + (error?.localizedDescription ?? "no error") ) - if error != nil { - self?.view.makeToast(error?.localizedDescription) + if let err = error as? NSError { + if err.code == 404 { + return + } else if err.code == 408 { + self?.view.makeToast(chatLocalizable("failed_operation")) + } else { + self?.view.makeToast(error?.localizedDescription) + } } else { // update UI if index >= 0 { @@ -1665,28 +1745,20 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel var reuseId = "" if let isSend = model.message?.isOutgoingMsg, isSend { if model.replyedModel?.isReplay == true { - reuseId = "\(ChatReplyRightCell.self)" + reuseId = ChatViewController.getRitghtCellType(MessageType.reply.rawValue) } else { - switch model.type { - case .text: - reuseId = "\(ChatTextRightCell.self)" - case .image: - reuseId = "\(ChatImageRightCell.self)" - case .audio: - reuseId = "\(ChatAudioRightCell.self)" - case .video: - reuseId = "\(ChatVideoRightCell.self)" - case .time, .tip, .notification: - reuseId = "\(ChatTimeTableViewCell.self)" - case .revoke: - reuseId = "\(ChatRevokeRightCell.self)" - case .location: - reuseId = "\(ChatLocationRightCell.self)" - case .file: - reuseId = "\(ChatFileRightCell.self)" - case .rtcCallRecord: - reuseId = "\(ChatCallRecordRightCell.self)" - default: + let key = ChatViewController.getRitghtCellType(model.type.rawValue) + if model.type == .custom, let object = model.message?.messageObject as? NIMCustomObject, let custom = object.attachment as? NECustomAttachmentProtocol { + if registerCellDic["\(custom.customType)"] != nil { + reuseId = "\(custom.customType)" + } else { + reuseId = "\(ChatBaseRightCell.self)" + } + } else if model.type == .time || model.type == .notification || model.type == .tip { + reuseId = "\(MessageType.time.rawValue)" + } else if registerCellDic[key] != nil { + reuseId = key + } else { reuseId = "\(ChatBaseRightCell.self)" } } @@ -1702,34 +1774,39 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel if let m = model as? MessageContentModel { c.setModel(m) } + + if let audioCell = cell as? ChatAudioRightCell, + let m = model as? MessageAudioModel, + m.message?.messageId == playingModel?.message?.messageId { + if NIMSDK.shared().mediaManager.isPlaying() { + playingCell = audioCell + playingCell?.startAnimation() + } + } + return c + } else if let c = cell as? NEChatBaseCell, let m = model as? MessageContentModel { + c.setModel(m) + return cell } else { return ChatBaseRightCell() } } else { if model.replyedModel?.isReplay == true { - reuseId = "\(ChatReplyLeftCell.self)" + reuseId = ChatViewController.getLeftCellType(MessageType.reply.rawValue) } else { - switch model.type { - case .text: - reuseId = "\(ChatTextLeftCell.self)" - case .image: - reuseId = "\(ChatImageLeftCell.self)" - case .audio: - reuseId = "\(ChatAudioLeftCell.self)" - case .video: - reuseId = "\(ChatVideoLeftCell.self)" - case .file: - reuseId = "\(ChatFileLeftCell.self)" - case .time, .tip, .notification: - reuseId = "\(ChatTimeTableViewCell.self)" - case .revoke: - reuseId = "\(ChatRevokeLeftCell.self)" - case .location: - reuseId = "\(ChatLocationLeftCell.self)" - case .rtcCallRecord: - reuseId = "\(ChatCallRecordLeftCell.self)" - default: + let key = ChatViewController.getLeftCellType(model.type.rawValue) + if model.type == .custom, let object = model.message?.messageObject as? NIMCustomObject, let custom = object.attachment as? NECustomAttachmentProtocol { + if registerCellDic["\(custom.customType)"] != nil { + reuseId = "\(custom.customType)" + } else { + reuseId = "\(ChatBaseLeftCell.self)" + } + } else if model.type == .time || model.type == .notification || model.type == .tip { + reuseId = "\(MessageType.time.rawValue)" + } else if registerCellDic[key] != nil { + reuseId = key + } else { reuseId = "\(ChatBaseLeftCell.self)" } } @@ -1742,20 +1819,45 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel return c } else if let c = cell as? ChatBaseLeftCell { c.delegate = self + + // 更新好友昵称、头像 if let m = model as? MessageContentModel { + if let from = model.message?.from, + let user = viewmodel.newUserInfoDic[from] { + if let uid = user.userId, + viewmodel.session.sessionType == .team || + viewmodel.session.sessionType == .superTeam { + m.fullName = viewmodel.getShowName(userId: uid, teamId: viewmodel.session.sessionId) + m.shortName = viewmodel.getShortName(name: user.showName(false) ?? "", length: 2) + } + m.avatar = user.userInfo?.avatarUrl + } c.setModel(m) } + + if let audioCell = cell as? ChatAudioLeftCell, + let m = model as? MessageAudioModel, + m.message?.messageId == playingModel?.message?.messageId { + if NIMSDK.shared().mediaManager.isPlaying() { + playingCell = audioCell + playingCell?.startAnimation() + } + } + return c + } else if let c = cell as? NEChatBaseCell, let m = model as? MessageContentModel { + c.setModel(m) + return cell } else { return ChatBaseLeftCell() } } } - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { operationView?.removeFromSuperview() - if menuView.textField.isFirstResponder { - menuView.textField.resignFirstResponder() + if menuView.textView.isFirstResponder { + menuView.textView.resignFirstResponder() } else { layoutInputView(offset: 0) } @@ -1764,6 +1866,11 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { let m = viewmodel.messages[indexPath.row] + if m.type == .custom { + if let object = m.message?.messageObject as? NIMCustomObject, let custom = object.attachment as? NECustomAttachmentProtocol { + return custom.cellHeight + } + } return CGFloat(m.height) } @@ -1819,15 +1926,31 @@ open class ChatViewController: ChatBaseViewController, UINavigationControllerDel // MARK: ChatBaseCellDelegate extension ChatViewController: ChatBaseCellDelegate { + public func didLongPressAvatar(_ cell: UITableViewCell, _ model: MessageContentModel?) { + print("didLongPressAvatar") + if viewmodel.session.sessionType == .P2P { + return + } + var addText = "" + var accid = "" + + if let m = model, let from = m.message?.from { + accid = from + addText += viewmodel.getShowName(userId: from, teamId: viewmodel.session.sessionId, false) + } + + addText = "@" + addText + "" + + addToAtUsers(addText: addText, accid: accid, true) + } + public func didTapAvatarView(_ cell: UITableViewCell, _ model: MessageContentModel?) { didTapHeadPortrait(model: model) } - public func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) { + func didTapMessage(_ cell: UITableViewCell?, _ model: MessageContentModel?, _ replyIndex: Int? = nil) { if model?.type == .audio { - if let audio = model?.message?.messageObject as? NIMAudioObject { - startPlay(cell: cell as? ChatAudioCell, audio: audio) - } + startPlay(cell: cell as? ChatAudioCell, model: model as? MessageAudioModel) } else if model?.type == .image { if let imageObject = model?.message?.messageObject as? NIMImageObject { var imageUrl = "" @@ -1847,17 +1970,10 @@ extension ChatViewController: ChatBaseCellDelegate { showController.modalPresentationStyle = .overFullScreen present(showController, animated: false, completion: nil) } - -// if let url = imageObject.url { -// let showController = PhotoBrowserController(urls: viewmodel.getUrls(), url: url) -// showController.modalPresentationStyle = .overFullScreen -// self.present(showController, animated: false, completion: nil) -// } } } else if model?.type == .video, let object = model?.message?.messageObject as? NIMVideoObject { - print("video click") stopPlay() weak var weakSelf = self let videoPlayer = VideoPlayerViewController() @@ -1871,7 +1987,11 @@ extension ChatViewController: ChatBaseCellDelegate { } 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 left = cell as? ChatVideoLeftCell { left.setModel(videoModel) @@ -1887,39 +2007,20 @@ extension ChatViewController: ChatBaseCellDelegate { videoModel.state = .Success } videoModel.cell?.uploadProgress(progress) - - // SDK返回异常 -// let trueProgress = -progress / Float(object.fileLength) -// videoModel.progress = trueProgress -// if trueProgress >= 1.0 { -// videoModel.state = .Success -// } -// videoModel.cell?.uploadProgress(trueProgress) } _: { error in if let err = error as NSError? { weakSelf?.showToast(err.localizedDescription) } } } - } else if model?.type == .text { -// location at replied message - if model?.replyedModel != nil { - if model?.message?.repliedMessageId != nil { - var index = -1 - for (i, m) in viewmodel.messages.enumerated() { - if model?.message?.repliedMessageServerId == m.message?.serverID { - index = i - break - } - } - if index >= 0 { - tableView.scrollToRow( - at: IndexPath(row: index, section: 0), - at: .middle, - animated: true - ) - } - } + } else if replyIndex != nil, model?.type == .text { + print("message did tap: text") + if let text = model?.message?.text { + let textView = TextViewController(content: text) + textView.modalPresentationStyle = .fullScreen + DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: DispatchWorkItem(block: { [weak self] in + self?.navigationController?.present(textView, animated: false) + })) } } else if model?.type == .location { if let locationModel = model as? MessageLocationModel, let lat = locationModel.lat, let lng = locationModel.lng { @@ -1930,8 +2031,42 @@ extension ChatViewController: ChatBaseCellDelegate { navigationController?.pushViewController(mapDetail, animated: true) } } else if model?.type == .file, - let object = model?.message?.messageObject as? NIMFileObject { - if let path = object.path, FileManager.default.fileExists(atPath: path) == true { + 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 left = cell as? ChatFileLeftCell { + left.setModel(fileModel) + } else if let right = cell as? ChatFileRightCell { + right.setModel(fileModel) + } + + viewmodel.downLoad(urlString, path) { [weak self] progress in + NELog.infoLog(ModuleName + " " + (self?.tag ?? "ChatViewController"), desc: "@@# CALLBACK downLoad: \(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(newProgress) + + } _: { [weak self] error in + if let err = error as NSError? { + self?.showToast(err.localizedDescription) + } + } + } + } else { let url = URL(fileURLWithPath: path) print("@@# file:\(url) has download") interactionController.url = url @@ -1940,32 +2075,6 @@ extension ChatViewController: ChatBaseCellDelegate { else { interactionController.presentOptionsMenu(from: view.bounds, in: view, animated: true) } - } else if let urlString = object.url, let path = object.path, - let fileModel = model as? MessageFileModel { - fileModel.state = .Downalod - if let left = cell as? ChatFileLeftCell { - left.setModel(fileModel) - } else if let right = cell as? ChatFileRightCell { - right.setModel(fileModel) - } - - viewmodel.downLoad(urlString, path) { [weak self] progress in - NELog.infoLog(ModuleName + " " + (self?.tag ?? "ChatViewController"), desc: "@@# CALLBACK downLoad: \(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(newProgress) - - } _: { [weak self] error in - if let err = error as NSError? { - self?.showToast(err.localizedDescription) - } - } } } else if model?.type == .rtcCallRecord, let object = model?.message?.messageObject as? NIMRtcCallRecordObject { var param = [String: AnyObject]() @@ -1982,7 +2091,33 @@ extension ChatViewController: ChatBaseCellDelegate { } Router.shared.use(CallViewRouter, parameters: param) } else { - print(#function + "message did tap but type unknow") + print(#function + "message did tap, type:\(model?.type.rawValue)") + } + } + + public func didTapMessageView(_ cell: UITableViewCell, _ model: MessageContentModel?) { + 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 { + var index = -1 + var replyModel: MessageModel? + for (i, m) in viewmodel.messages.enumerated() { + if id == m.message?.messageId { + index = i + replyModel = m + break + } + } + + let replyCell = tableView.cellForRow(at: IndexPath(row: index, section: 0)) + if let replyContentModel = replyModel as? MessageContentModel { + didTapMessage(replyCell, replyContentModel, index) + } + } else { + didTapMessage(cell, model) } } @@ -2025,8 +2160,38 @@ extension ChatViewController: ChatBaseCellDelegate { tableView.reloadData() return } - menuView.textField.text = model?.message?.text - menuView.textField.becomeFirstResponder() + if model?.message?.remoteExt?[keyReplyMsgKey] != nil { + showReplyMessageView(isReEdit: true) + } + guard let text = message.text else { + return + } + let attributeStr = NSMutableAttributedString(string: text) + attributeStr.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)], range: NSMakeRange(0, text.utf16.count)) + if let remoteExt = message.remoteExt, let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { + dic.forEach { (key: String, value: AnyObject) in + if let contentDic = value as? [String: AnyObject] { + 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 var text = contentDic[atTextKey] as? String { + if text.last == " " { + text = String(text.prefix(text.count - 1)) + } + menuView.nickAccidDic[text] = key + } + + if attributeStr.length > model.end { + attributeStr.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_blueText, range: NSMakeRange(model.start, model.end - model.start)) + } + } + } + } + } + } + } + menuView.textView.attributedText = attributeStr + menuView.textView.becomeFirstResponder() } } @@ -2036,4 +2201,12 @@ extension ChatViewController: ChatBaseCellDelegate { navigationController?.pushViewController(readVC, animated: true) } } + + public static func getLeftCellType(_ type: Int) -> String { + "\(type)\(leftCellTypeSufixx)" + } + + public static func getRitghtCellType(_ type: Int) -> String { + "\(type)\(rightCellTypeSufixx)" + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/GroupChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/GroupChatViewController.swift index f965207a..f8ae7c81 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/GroupChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/GroupChatViewController.swift @@ -9,6 +9,9 @@ import NECoreIMKit @objcMembers open class GroupChatViewController: ChatViewController, TeamChatViewModelDelegate { + private var isLeaveTeamBySelf = false // 是否是主动退出群聊 + private var isdismissTeam = false // 群聊是否已解散 + public init(session: NIMSession, anchor: NIMMessage?) { // self.viewmodel = ChatViewModel(session: session) super.init(session: session) @@ -27,8 +30,21 @@ open class GroupChatViewController: ChatViewController, TeamChatViewModelDelegat fatalError("init(coder:) has not been implemented") } + override open func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // 被动解散群聊 + if isdismissTeam { + weak var weakSelf = self + showSingleAlert(message: chatLocalizable("team_has_been_removed")) { + weakSelf?.navigationController?.popViewController(animated: true) + } + } + } + override open func viewDidLoad() { super.viewDidLoad() + NotificationCenter.default.addObserver(self, selector: #selector(leaveTeamBySelf), name: NotificationName.leaveTeamBySelf, object: nil) } override open func getSessionInfo(session: NIMSession) { @@ -41,24 +57,33 @@ open class GroupChatViewController: ChatViewController, TeamChatViewModelDelegat // MARK: private method + func leaveTeamBySelf(noti: Notification) { + if let flag = noti.object as? Bool { + isLeaveTeamBySelf = flag + } + } + + private func getPlaceHolder(text: String) -> NSMutableAttributedString { + let attribute = NSMutableAttributedString(string: text) + let style = NSMutableParagraphStyle() + style.lineBreakMode = .byTruncatingTail + style.alignment = .left + attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.utf16.count)) + 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)) + return attribute + } + private func updateTeamInfo(team: NIMTeam) { title = team.getShowName() if team.inAllMuteMode(), team.owner != NIMSDK.shared().loginManager.currentAccount() { - menuView.textField.isEditable = false - menuView.textField.placeholder = chatLocalizable("team_mute") as NSString? + menuView.textView.isEditable = false + menuView.textView.attributedPlaceholder = getPlaceHolder(text: chatLocalizable("team_mute")) layoutInputView(offset: 0) menuView.stackView.isUserInteractionEnabled = false } else { - menuView.textField.isEditable = true - let text = "\(chatLocalizable("send_to"))\(team.getShowName())" as NSString? -// let attribute = NSMutableAttributedString(string: text) -// let style = NSMutableParagraphStyle() -// style.lineBreakMode = .byTruncatingTail -// style.alignment = .left -// attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.count)) -// attribute.addAttribute(.font, value: UIFont.systemFont(ofSize: 16), range: NSMakeRange(0, text.count)) -// attribute.addAttribute(.foregroundColor, value: UIColor.gray, range: NSMakeRange(0, text.count)) - menuView.textField.placeholder = text + menuView.textView.isEditable = true + menuView.textView.attributedPlaceholder = getPlaceHolder(text: "\(chatLocalizable("send_to"))\(team.getShowName())") menuView.stackView.isUserInteractionEnabled = true } } @@ -66,19 +91,24 @@ open class GroupChatViewController: ChatViewController, TeamChatViewModelDelegat // MARK: TeamChatViewModelDelegate public func onTeamRemoved(team: NIMTeam) { - navigationController?.popViewController(animated: true) - - /* 后续优化逻辑,暂时不做修改 - if team.clientCustomInfo?.contains(discussTeamKey) == true { - navigationController?.popViewController(animated: true) - return - } - if team.teamId == viewmodel.session.sessionId { - weak var weakSelf = self - showSingleAlert(message: chatLocalizable("team_has_been_removed")) { - weakSelf?.navigationController?.popViewController(animated: true) - } - } */ + // 退出讨论组 + if team.clientCustomInfo?.contains(discussTeamKey) == true { + navigationController?.popViewController(animated: true) + return + } + + // 离开群聊 + if team.teamId == viewmodel.session.sessionId { + if team.owner != NIMSDK.shared().loginManager.currentAccount() { // 退出群聊 + if isLeaveTeamBySelf { + navigationController?.popViewController(animated: true) + } else { + isdismissTeam = true + } + } else { // 主动解散 + navigationController?.popViewController(animated: true) + } + } } public func onTeamUpdate(team: NIMTeam) { @@ -87,4 +117,8 @@ open class GroupChatViewController: ChatViewController, TeamChatViewModelDelegat } updateTeamInfo(team: team) } + + public func onTeamMemberUpdate(team: NIMTeam) { + tableView.reloadData() + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift index 0129618b..4754bbc5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/NEDetailMapController.swift @@ -59,10 +59,7 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView func setupSubviews() { if let mapDelagate = NEChatKitClient.instance.delegate { weak var weakSelf = self -// mapDelagate.searchRoundPosition?(completion: { models, error in -// NELog.infoLog(self.className(), desc:"search around models : \(models)") -// weakSelf?.loadModels(models: models) -// }) + NELog.infoLog(className(), desc: "toSearchCurrentUserLocation setupSubviews call") toSearchCurrentUserLocation() if let mapView = mapDelagate.getMapView?() as? UIView { @@ -79,6 +76,7 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView }) if mapType == .detail, let map = mapView { + resetBtn.isSelected = true mapDelagate.setMapviewLocation?(lat: currentPoint.x, lng: currentPoint.y, mapview: map) } else { if let map = mapView { @@ -102,8 +100,10 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView } if mapType == .detail { + NEChatKitClient.instance.delegate?.setCustomAnnotation?(image: coreLoader.loadImage("location_point"), lat: currentPoint.x, lng: currentPoint.y) addDetailSubviews() } else { + NEChatKitClient.instance.delegate?.setCustomAnnotation?(image: nil, lat: 0, lng: 0) addSearchSubviews() } } @@ -386,10 +386,14 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView @objc func resetClick() { if let map = mapView { resetBtn.isSelected = false + if mapType == .detail, let map = mapView { + NEChatKitClient.instance.delegate?.setMapCenter?(mapview: map) + return + } + toSearchLocalWithMapView() NEChatKitClient.instance.delegate?.setMapCenter?(mapview: map) currentIndex = 0 tableView.reloadData() - toSearchCurrentUserLocation() } } @@ -401,6 +405,8 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView self.tableViewBottomConstraint?.constant = self.defaultTableHeight }) searchTextField.text = "" + NELog.infoLog(className(), desc: "toSearchCurrentUserLocation cancel earch call") + toSearchCurrentUserLocation() NEChatKitClient.instance.delegate?.setMapCenter?(mapview: mapView) } @@ -465,18 +471,31 @@ public class NEDetailMapController: ChatBaseViewController, NEMapGuideBottomView } } if searchText.count <= 0 { -// locations.removeAll() -// tableView.reloadData() if let map = mapView { NEChatKitClient.instance.delegate?.setMapCenter?(mapview: map) } - toSearchCurrentUserLocation() + toSearchLocalWithMapView() } } + func toSearchLocalWithMapView() { + guard let map = mapView else { + return + } + weak var weakSelf = self + NEChatKitClient.instance.delegate?.searchMapCenter?(mapview: map, completion: { models, error in + if let text = weakSelf?.searchTextField.text, text.count > 0 { + return + } + weakSelf?.loadModels(models: models) + }) + } + 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 ?? "")") if let text = weakSelf?.searchTextField.text, text.count > 0 { return } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/P2PChatViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/P2PChatViewController.swift index aa13d6a4..dd74862f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/P2PChatViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/P2PChatViewController.swift @@ -8,6 +8,11 @@ import NIMSDK @objcMembers open class P2PChatViewController: ChatViewController { + public init(session: NIMSession, anchor: NIMMessage?) { + super.init(session: session) + viewmodel = ChatViewModel(session: session, anchor: anchor) + } + override open func viewDidLoad() { super.viewDidLoad() @@ -19,17 +24,17 @@ open class P2PChatViewController: ChatViewController { let showName = user?.showName() ?? "" title = showName titleContent = showName - menuView.textField.placeholder = (chatLocalizable("send_to") + showName) as NSString? -// let text = "\(chatLocalizable("send_to"))\(showName))" -// menuView.textField.placeholder = text -// 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.count)) -// attribute.addAttribute(.foregroundColor, value: UIColor.gray, range: NSMakeRange(0, text.count)) -// attribute.addAttribute(.paragraphStyle, value: style, range: NSMakeRange(0, text.count)) -// menuView.textField.attributedPlaceholder = attribute +// menuView.textField.placeholder = chatLocalizable("send_to") + showName +// menuView.textField.placeholder = (chatLocalizable("send_to") + showName) as NSString? + 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)) + menuView.textView.attributedPlaceholder = attribute } /// 创建个人聊天页构造方法 diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/PinMessageViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/PinMessageViewController.swift new file mode 100644 index 00000000..8f7ad386 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/PinMessageViewController.swift @@ -0,0 +1,346 @@ +//// 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 + +let PinMessageDefaultType = 1000 +@objcMembers +public class PinMessageViewController: ChatBaseViewController, UITableViewDataSource, UITableViewDelegate, PinMessageViewModelDelegate, PinMessageCellDelegate { + let viewmodel = PinMessageViewModel() + + var session: NIMSession + + public init(session: NIMSession) { + self.session = session + super.init(nibName: nil, bundle: nil) + } + + public required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + var cellClassDic = [ + NIMMessageType.text.rawValue: PinMessageTextCell.self, + NIMMessageType.image.rawValue: PinMessageImageCell.self, + NIMMessageType.audio.rawValue: PinMessageAudioCell.self, + NIMMessageType.video.rawValue: PinMessageVideoCell.self, + NIMMessageType.location.rawValue: PinMessageLocationCell.self, + NIMMessageType.file.rawValue: PinMessageFileCell.self, + PinMessageDefaultType: PinMessageDefaultCell.self, + ] + + private 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 = .white + tableView.keyboardDismissMode = .onDrag + tableView.backgroundColor = UIColor(hexString: "#EFF1F3") + return tableView + }() + + private lazy var emptyView: NEEmptyDataView = { + let view = NEEmptyDataView( + imageName: "user_empty", + content: chatLocalizable("no_pin_message"), + frame: .zero + ) + view.translatesAutoresizingMaskIntoConstraints = false + view.isHidden = true + return view + }() + + override public func viewDidLoad() { + super.viewDidLoad() + + viewmodel.delegate = self + setupUI() + loadData() + } + + func loadData() { + weak var weakSelf = self + viewmodel.getPinitems(session: session) { error in + if let err = error as? NSError { + if weakSelf?.session.sessionType == .team, err.code == 414 { + weakSelf?.showToast(chatLocalizable("team_not_exist")) + } else { + weakSelf?.showToast(err.localizedDescription) + } + } else { + weakSelf?.emptyView.isHidden = (weakSelf?.viewmodel.items.count ?? 0) > 0 + weakSelf?.tableView.reloadData() + } + } + } + + func setupUI() { + title = chatLocalizable("operation_pin") + view.addSubview(tableView) + if #available(iOS 10, *) { + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint( + equalTo: view.topAnchor, + constant: kNavigationHeight + KStatusBarHeight + ), + tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.rightAnchor.constraint(equalTo: view.rightAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } else { + NSLayoutConstraint.activate([ + tableView.topAnchor.constraint( + equalTo: view.topAnchor, + constant: 0 + ), + tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.rightAnchor.constraint(equalTo: view.rightAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + view.addSubview(emptyView) + if #available(iOS 11.0, *) { + NSLayoutConstraint.activate([ + emptyView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + 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: view.topAnchor), + emptyView.leftAnchor.constraint(equalTo: view.leftAnchor), + emptyView.rightAnchor.constraint(equalTo: view.rightAnchor), + emptyView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + ]) + } + + cellClassDic.forEach { (key: Int, value: PinMessageBaseCell.Type) in + 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. + } + */ + + public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let item = viewmodel.items[indexPath.row] + if item.message.session?.sessionType == .P2P { + let session = item.message.session + Router.shared.use( + PushP2pChatVCRouter, + parameters: ["nav": navigationController as Any, "session": session as Any, + "anchor": item.message], + closure: nil + ) + } else if item.message.session?.sessionType == .team { + let session = item.message.session + Router.shared.use( + PushTeamChatVCRouter, + parameters: ["nav": navigationController as Any, "session": session as Any, + "anchor": item.message], + closure: nil + ) + } + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let model = viewmodel.items[indexPath.row] + let cell = tableView.dequeueReusableCell(withIdentifier: "\(model.getMessageType())", for: indexPath) as! PinMessageBaseCell + cell.delegate = self + cell.configure(model) + return cell + } + + public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + viewmodel.items.count + } + + public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + let model = viewmodel.items[indexPath.row] + return model.cellHeight() + } + + func showAction(item: PinMessageModel) { + var actions = [UIAlertAction]() + weak var weakSelf = self + /* + let jumpAction = UIAlertAction(title: chatLocalizable("pin_jump_detail"), style: .default) { _ in + if item.message.session?.sessionType == .P2P { + let session = item.message.session + Router.shared.use( + PushP2pChatVCRouter, + parameters: ["nav": weakSelf?.navigationController as Any, "session": session as Any, + "anchor": item.message], + closure: nil + ) + } else if item.message.session?.sessionType == .team { + let session = item.message.session + Router.shared.use( + PushTeamChatVCRouter, + parameters: ["nav": weakSelf?.navigationController as Any, "session": session as Any, + "anchor": item.message], + closure: nil + ) + } + } + actions.append(jumpAction) + */ + let cancelPinAction = UIAlertAction(title: chatLocalizable("operation_cancel_pin"), style: .default) { _ in + + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + weakSelf?.viewmodel.removePinMessage(item.message) { error, model in + if let err = error { +// weakSelf?.showToast(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")) + } + } + } + } + actions.append(cancelPinAction) + + if item.message.messageType == .text { + let copyAction = UIAlertAction(title: chatLocalizable("operation_copy"), style: .default) { _ in + let text = item.message.text + let pasteboard = UIPasteboard.general + pasteboard.string = text + weakSelf?.view.makeToast(chatLocalizable("copy_success"), duration: 2, position: .center) + } + actions.append(copyAction) + } + + if item.message.messageType != .audio { + let forwardAction = UIAlertAction(title: chatLocalizable("operation_forward"), style: .default) { _ in + if NEChatDetectNetworkTool.shareInstance.manager?.isReachable == false { + weakSelf?.showToast(commonLocalizable("network_error")) + return + } + weakSelf?.forwardMessage(item.message) + } + + actions.append(forwardAction) + } + + let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), style: .cancel) { _ in + } + actions.append(cancelAction) + + showActionSheet(actions) + } + + private func forwardMessage(_ message: NIMMessage) { + weak var weakSelf = self + let userAction = UIAlertAction(title: chatLocalizable("contact_user"), + style: .default) { action in + + Router.shared.register(ContactSelectedUsersRouter) { param in + print("user setting accids : ", param) + var items = [ForwardItem]() + + if let users = param["im_user"] as? [NIMUser] { + users.forEach { user in + let item = ForwardItem() + item.uid = user.userId + item.avatar = user.userInfo?.avatarUrl + item.name = user.userInfo?.nickName + items.append(item) + } + + let forwardAlert = ForwardAlertViewController() + forwardAlert.setItems(items) + if let senderName = message.senderName { + forwardAlert.context = senderName + } + weakSelf?.addChild(forwardAlert) + weakSelf?.view.addSubview(forwardAlert.view) + + forwardAlert.sureBlock = { + print("sure click ") + weakSelf?.viewmodel.forwardUserMessage(message, users) + } + } + } + var param = [String: Any]() + param["nav"] = weakSelf?.navigationController as Any + param["limit"] = 6 + if let session = weakSelf?.session, session.sessionType == .P2P { + var filters = Set() + filters.insert(session.sessionId) + param["filters"] = filters + } + Router.shared.use(ContactUserSelectRouter, parameters: param, closure: nil) + } + + let teamAction = UIAlertAction(title: chatLocalizable("team"), style: .default) { action in + + 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 = ForwardAlertViewController() + forwardAlert.setItems([item]) + if let senderName = message.senderName { + forwardAlert.context = senderName + } + forwardAlert.sureBlock = { + weakSelf?.viewmodel.forwardTeamMessage(message, team) + } + weakSelf?.addChild(forwardAlert) + weakSelf?.view.addSubview(forwardAlert.view) + } + } + + Router.shared.use( + ContactTeamListRouter, + parameters: ["nav": weakSelf?.navigationController as Any], + closure: nil + ) + } + + let cancelAction = UIAlertAction(title: chatLocalizable("cancel"), + style: .cancel) { action in + } + + showActionSheet([teamAction, userAction, cancelAction]) + } + + // MARK: PinMessageViewModelDelegate + + public func didNeedRefreshUI() { + loadData() + } + + // MARK: PinMessageCellDelegate + + func didClickMore(_ model: PinMessageModel?) { + print("did click more") + if let item = model { + showAction(item: item) + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift new file mode 100644 index 00000000..f89ad880 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/TextViewController.swift @@ -0,0 +1,80 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import Foundation + +class TextViewController: ChatBaseViewController { + let leftRightMargin: CGFloat = 24 + let textFont = UIFont.systemFont(ofSize: 24, weight: .regular) + lazy var textView: CopyableTextView = { + let textView = CopyableTextView() + textView.isEditable = false + textView.isSelectable = false + textView.translatesAutoresizingMaskIntoConstraints = false + textView.font = .systemFont(ofSize: 24, weight: .regular) + return textView + }() + + init(content: String) { + super.init(nibName: nil, bundle: nil) + textView.copyString = content + let att = NEEmotionTool.getAttWithStr(str: content, font: textFont) + textView.attributedText = att + let line = String.calculateMaxLines(width: kScreenWidth - 2 * leftRightMargin, + string: att.string, + font: textFont) + textView.textAlignment = line > 1 ? .justified : .center + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + navigationController?.isNavigationBarHidden = true + let tap = UITapGestureRecognizer(target: self, action: #selector(viewTap)) + textView.addGestureRecognizer(tap) + setupUI() + contentSizeToFit() + } + + @objc func viewTap() { + UIMenuController.shared.setMenuVisible(false, animated: true) + dismiss(animated: false) + } + + func setupUI() { + view.addSubview(textView) + if #available(iOS 11.0, *) { + NSLayoutConstraint.activate([ + textView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + textView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + textView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: leftRightMargin), + textView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -leftRightMargin), + ]) + } else { + NSLayoutConstraint.activate([ + textView.topAnchor.constraint(equalTo: view.topAnchor), + textView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + textView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: leftRightMargin), + textView.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -leftRightMargin), + ]) + } + } + + // textView 垂直居中 + func contentSizeToFit() { + guard !textView.text.isEmpty else { + return + } + let textSize = String.getTextRectSize(textView.text, font: textFont, size: CGSize(width: kScreenWidth - leftRightMargin * 2, height: CGFloat.greatestFiniteMagnitude)) + let textViewHeight = kScreenHeight - kNavigationHeight - KStatusBarHeight + if textSize.height <= textViewHeight { + let offsetY = (textViewHeight - textSize.height) / 2 + let offset = UIEdgeInsets(top: offsetY, left: 0, bottom: 0, right: 0) + textView.contentInset = offset + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/UserSettingViewController.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/UserSettingViewController.swift index ba03dc9e..95695d82 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/UserSettingViewController.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Controller/UserSettingViewController.swift @@ -58,6 +58,11 @@ public class UserSettingViewController: ChatBaseViewController, UserSettingViewM return table }() + public var cellClassDic = [ + UserSettingType.SwitchType.rawValue: UserSettingSwitchCell.self, + UserSettingType.SelectType.rawValue: UserSettingSelectCell.self, + ] + override public func viewDidLoad() { super.viewDidLoad() viewmodel.delegate = self @@ -78,10 +83,10 @@ public class UserSettingViewController: ChatBaseViewController, UserSettingViewM contentTable.topAnchor.constraint(equalTo: view.topAnchor), contentTable.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - contentTable.register( - UserSettingSwitchCell.self, - forCellReuseIdentifier: "\(UserSettingSwitchCell.self)" - ) + + cellClassDic.forEach { (key: Int, value: UserSettingBaseCell.Type) in + contentTable.register(value, forCellReuseIdentifier: "\(key)") + } } func headerView() -> UIView { @@ -113,7 +118,7 @@ public class UserSettingViewController: ChatBaseViewController, UserSettingViewM if let url = viewmodel.userInfo?.userInfo?.avatarUrl { userHeader.sd_setImage(with: URL(string: url), completed: nil) - } else if let name = viewmodel.userInfo?.showName() { + } else if let name = viewmodel.userInfo?.showName(false) { userHeader.setTitle(name) userHeader.backgroundColor = UIColor.colorWithString(string: viewmodel.userInfo?.userId) } @@ -227,7 +232,7 @@ public class UserSettingViewController: ChatBaseViewController, UserSettingViewM cellForRowAt indexPath: IndexPath) -> UITableViewCell { let model = viewmodel.cellDatas[indexPath.row] if let cell = tableView.dequeueReusableCell( - withIdentifier: "\(UserSettingSwitchCell.self)", + withIdentifier: "\(model.type)", for: indexPath ) as? UserSettingBaseCell { cell.configure(model) @@ -237,6 +242,12 @@ public class UserSettingViewController: ChatBaseViewController, UserSettingViewM } public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - // let model = viewmodel.cellDatas[indexPath.row] + if indexPath.row == 0 { + if let accid = userId { + let session = NIMSession(accid, type: .P2P) + let pin = PinMessageViewController(session: session) + navigationController?.pushViewController(pin, animated: true) + } + } } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift index a95a5571..0b1ec50f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Emoji/NEEmotionTool.swift @@ -25,7 +25,7 @@ public class NEEmotionTool: NSObject { .emoticonCatalog(catalogID: NIMKit_EmojiCatalog)?.emoticons let attStr = NSMutableAttributedString(string: str, attributes: [ NSAttributedString.Key.font: font, - .foregroundColor: NEKitChatConfig.shared.ui.messageColor, + .foregroundColor: NEKitChatConfig.shared.ui.messageTextColor, ]) if let regArr = regularArr, regArr.count > 0, let targetEmotions = emoticons { @@ -47,6 +47,12 @@ public class NEEmotionTool: NSObject { return attStr } + class func getAttWithStr(str: String, font: UIFont, color: UIColor, _ offset: CGPoint = CGPoint(x: 0, y: -3)) -> NSMutableAttributedString { + let att = getAttWithStr(str: str, font: font, offset) + att.addAttribute(.foregroundColor, value: color, range: NSRange(location: 0, length: att.length)) + return att + } + class func getAttWithEmotion(emotion: NIMInputEmoticon, font: UIFont, offset: CGPoint) -> NSAttributedString { let textAttachment = NEEmotionAttachment() diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift index d39b2b96..0c549807 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/MessageUtils.swift @@ -16,6 +16,16 @@ public class MessageUtils: NSObject { return message } + public 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 + } + return message + } + public class func imageMessage(image: UIImage) -> NIMMessage { imageMessage(imageObject: NIMImageObject(image: image)) } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift index d8f171dd..b6688817 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/NotificationMessageUtils.swift @@ -7,6 +7,12 @@ import Foundation import NIMSDK import NECoreIMKit import NECoreKit + +public enum TeamType { + case advanceTeam + case discussTeam +} + public class NotificationMessageUtils: NSObject { public class func textForNotification(message: NIMMessage) -> String { if message.messageType != .notification { @@ -29,6 +35,15 @@ public class NotificationMessageUtils: NSObject { return "" } + /// 是否是群通知 + public class func isDiscussSeniorTeamNoti(message: NIMMessage) -> Bool { + if let object = message.messageObject as? NIMNotificationObject, + let _ = object.content as? NIMTeamNotificationContent { + return true + } + return false + } + public class func isDiscussSeniorTeamUpdateCustomNoti(message: NIMMessage) -> Bool { if let object = message.messageObject as? NIMNotificationObject { guard let content = object.content as? NIMTeamNotificationContent else { @@ -37,7 +52,7 @@ public class NotificationMessageUtils: NSObject { // 转移讨论组的通知 if content.operationType == .transferOwner, - teamName(message: message) == chatLocalizable("discussion_group") { + teamType(message: message) == .discussTeam { return true } @@ -49,6 +64,7 @@ public class NotificationMessageUtils: NSObject { return false } + // 18:客户端自定义拓展字段, 19: 服务器自定义拓展字段 if tag == 18 || tag == 19 { return true } @@ -86,32 +102,15 @@ public class NotificationMessageUtils: NSObject { let teamName = teamName(message: message) var toNamestext = toNames.first ?? "" if toNames.count > 1 { - toNamestext = toNames.joined(separator: ",") + toNamestext = toNames.joined(separator: "、") } switch content.operationType { case .invite: - var str = fromName + chatLocalizable("invite") - if let first = toNames.first { - str += first - } - if toNames.count > 1 { - str = str + " " + String(toNames.count) + " " + chatLocalizable("humans") - } - str = str + chatLocalizable("enter") + teamName - text = str + text = fromName + chatLocalizable("invite") + toNamestext + chatLocalizable("enter") + chatLocalizable("group_chat") case .dismiss: - text = fromName + chatLocalizable("dissolve") + teamName + text = fromName + chatLocalizable("dissolve") + chatLocalizable("group_chat") case .kick: - var str = fromName + chatLocalizable("kick") - if let first = toNames.first { - str += first - } - if toNames.count > 1 { - str = str + " " + String(toNames.count) + " " + chatLocalizable("humans") - } - str = str + chatLocalizable("out") + teamName - text = str - + text = fromName + chatLocalizable("kick") + toNamestext + chatLocalizable("out") + chatLocalizable("group_chat") case .update: text = textOfUpdateTeam( fromName: fromName, @@ -119,10 +118,10 @@ public class NotificationMessageUtils: NSObject { content: content ) case .leave: - text = fromName + chatLocalizable("leave") + teamName + text = fromName + chatLocalizable("leave") + chatLocalizable("group_chat") case .applyPass: if fromName == toNamestext { - text = fromName + chatLocalizable("join") + teamName + text = fromName + chatLocalizable("join") + chatLocalizable("group_chat") } else { text = fromName + chatLocalizable("pass") + toNamestext } @@ -132,7 +131,7 @@ public class NotificationMessageUtils: NSObject { case .addManager: text = toFirstName + chatLocalizable("added_manager") case .removeManager: - text = toFirstName + chatLocalizable("removed_mamager") + text = toFirstName + chatLocalizable("removed_manager") case .acceptInvitation: text = fromName + chatLocalizable("accept") + toNamestext case .mute: @@ -154,6 +153,26 @@ public class NotificationMessageUtils: NSObject { return text } + // 获取展示的用户名字,p2p: 备注 > 昵称 > ID team: 备注 > 群昵称 > 昵称 > ID + public class func getShowName(userId: String, nimSession: NIMSession?) -> String { + let user = UserInfoProvider.shared.getUserInfo(userId: userId) + var fullName = userId + if let nickName = user?.userInfo?.nickName { + fullName = nickName + } + if let session = nimSession, session.sessionType == .team { + // team + let teamMember = TeamProvider.shared.teamMember(userId, session.sessionId) + if let teamNickname = teamMember?.nickname { + fullName = teamNickname + } + } + if let alias = user?.alias { + fullName = alias + } + return fullName + } + public class func fromName(message: NIMMessage) -> String { if let object = message.messageObject as? NIMNotificationObject { if let content = object.content as? NIMTeamNotificationContent { @@ -161,8 +180,7 @@ public class NotificationMessageUtils: NSObject { return chatLocalizable("You") } else { if let sourceId = content.sourceID { - let user = UserInfoProvider.shared.getUserInfo(userId: sourceId) - return user?.showName() ?? "" + return getShowName(userId: sourceId, nimSession: message.session) } } } @@ -182,27 +200,38 @@ public class NotificationMessageUtils: NSObject { toNames.append(chatLocalizable("You")) } else { toNames - .append(UserInfoProvider.shared.getUserInfo(userId: targetID)?.showName() ?? "") + .append(getShowName(userId: targetID, nimSession: message.session)) } } return toNames } public class func teamName(message: NIMMessage) -> String { + let teamtype = teamType(message: message) + switch teamtype { + case .advanceTeam: + return chatLocalizable("group") + case .discussTeam: + return chatLocalizable("discussion_group") + } + } + + public class func teamType(message: NIMMessage) -> TeamType { let team = TeamProvider.shared.teamInfo(teamId: message.session?.sessionId) if team?.type == .normalTeam || (team?.type == .advancedTeam && team?.nimTeam?.clientCustomInfo?.contains(discussTeamKey) == true) { - return chatLocalizable("discussion_group") + return .discussTeam } else { - return chatLocalizable("group") + return .advanceTeam } } - private class func textOfUpdateTeam(fromName: String, teamName: String, + private class func textOfUpdateTeam(fromName: String, + teamName: String, content: NIMTeamNotificationContent) -> String { var text = fromName + chatLocalizable("has_updated") + teamName if let attach = content.attachment as? NIMUpdateTeamInfoAttachment { - if let tag = attach.values?.keys.first?.intValue { - let string = getShowString(fromName, teamName, tag, attach.values?.values.first) + if let tag = attach.values { + let string = getShowString(fromName, teamName, tag) if string.count > 0 { text = string } @@ -218,50 +247,84 @@ public class NotificationMessageUtils: NSObject { return text } - private class func getShowString(_ fromName: String, _ teamName: String, _ tag: Int, _ muteState: String?) -> String { + private class func getShowString(_ fromName: String, + _ teamName: String, + _ tag: [NSNumber: String]) -> String { var text = "" - switch tag { - case 3: - text = fromName + chatLocalizable("has_updated") + teamName + " " + - chatLocalizable("team_name") - case 14: - text = fromName + chatLocalizable("has_updated") + teamName + " " + + + // 群名 + if let value = tag[3] { + text = fromName + " " + chatLocalizable("has_updated") + teamName + + chatLocalizable("team_name") + chatLocalizable("to") + "\"" + value + "\"" + } + + // 群简介 + if let _ = tag[14] { + text = fromName + " " + chatLocalizable("has_updated") + teamName + chatLocalizable("team_intro") - case 15: - text = fromName + chatLocalizable("has_updated") + teamName + " " + + } + + // 群公告 + if let _ = tag[15] { + text = fromName + " " + chatLocalizable("has_updated") + teamName + chatLocalizable("team_anouncement") - case 16: - text = fromName + chatLocalizable("has_updated") + teamName + " " + + } + + // 群验证方式 + if let _ = tag[16] { + text = fromName + " " + chatLocalizable("has_updated") + teamName + chatLocalizable("team_join_mode") - case 18: - text = fromName + chatLocalizable("has_updated") + " " + chatLocalizable("team_custom_info") - case 19: - text = fromName + chatLocalizable("has_updated") + " " + chatLocalizable("team_custom_info") - case 20: - text = fromName + chatLocalizable("has_updated") + teamName + " " + + } + + // 客户端自定义拓展字段 + 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 + chatLocalizable("team_avatar") - case 21: - text = fromName + chatLocalizable("has_updated") + " " + + } + + // 被邀请模式 + if let _ = tag[21] { + text = fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_be_invited_author") - case 22: - text = fromName + chatLocalizable("has_updated") + " " + - chatLocalizable("team_be_invited_permission") - case 23: - text = fromName + chatLocalizable("has_updated") + " " + - chatLocalizable("team_update_info_permission") - case 24: - text = fromName + chatLocalizable("has_updated") + " " + + } + + // 邀请权限,仅高级群有效 + 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 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 let _ = tag[24] { + text = fromName + " " + chatLocalizable("has_updated") + chatLocalizable("team_update_client_custom") - case 100: + } - if muteState == "1" || muteState == "3" { + // 群禁言模式 + if let value = tag[100] { + if value == "1" || value == "3" { text = chatLocalizable("team_all_mute") - } else if muteState == "0" { + } else if value == "0" { text = chatLocalizable("team_all_no_mute") } - default: - text = fromName + chatLocalizable("has_updated") + teamName } + return text } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift index bc62c91f..c1da4a28 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Helper/ReplyMessageUtil.swift @@ -30,8 +30,7 @@ public class ReplyMessageUtil: NSObject { case .custom: text += "[\(chatLocalizable("msg_custom"))]" case .location: - // text += chatLocalizable("msg_location") - text = "[\(chatLocalizable("msg_location"))]" + text += "[\(chatLocalizable("msg_location"))]" default: text += "[\(chatLocalizable("msg_unknown"))]" } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtCacheModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtCacheModel.swift new file mode 100644 index 00000000..e793f46c --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtCacheModel.swift @@ -0,0 +1,16 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +public class MessageAtCacheModel: NSObject { + public var atModel: MessageAtInfoModel + public var accid: String + public var text: String? + init(atModel: MessageAtInfoModel, accid: String) { + self.atModel = atModel + self.accid = accid + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtInfoModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtInfoModel.swift new file mode 100644 index 00000000..2b1c91d2 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageAtInfoModel.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 UIKit + +@objcMembers +public class MessageAtInfoModel: NSObject { + public var start = 0 + public var end = 0 + public var broken = false +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift index e5f11880..f2e609c3 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCallRecordModel.swift @@ -56,9 +56,9 @@ class MessageCallRecordModel: MessageContentModel { attributeStr?.insert(NSAttributedString(attachment: attachment), at: 0) } - attributeStr?.addAttribute(NSAttributedString.Key.font, value: NEKitChatConfig.shared.ui.messageFont, range: NSMakeRange(0, attributeStr?.length ?? 0)) + attributeStr?.addAttribute(NSAttributedString.Key.font, value: NEKitChatConfig.shared.ui.messageTextSize, range: NSMakeRange(0, attributeStr?.length ?? 0)) - attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: NEKitChatConfig.shared.ui.messageColor, range: NSMakeRange(0, attributeStr?.length ?? 0)) + attributeStr?.addAttribute(NSAttributedString.Key.foregroundColor, value: NEKitChatConfig.shared.ui.messageTextColor, range: NSMakeRange(0, attributeStr?.length ?? 0)) } let textSize = NEChatUITool.getSizeWithAtt( diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift index feb91336..2ca1bcec 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageContentModel.swift @@ -19,8 +19,8 @@ public class MessageContentModel: NSObject, MessageModel { public var message: NIMMessage? public var contentSize: CGSize public var height: Float - public var shortName: String? - public var fullName: String? + public var shortName: String? // 昵称 > uid + public var fullName: String? // 备注 >(群昵称)> 昵称 > uid public var avatar: String? public var replyText: String? public var fullNameHeight: Float = 0 diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift new file mode 100644 index 00000000..81111fab --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageCustomModel.swift @@ -0,0 +1,14 @@ +//// 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 + +@objc +public class MessageCustomModel: MessageContentModel { + required init(message: NIMMessage?) { + super.init(message: message) + type = .custom + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift index 3b1b35cb..2318fc83 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageFileModel.swift @@ -16,7 +16,7 @@ class MessageFileModel: MessageContentModel { public var progress: Float = 0 public var size: Float = 0 public var state = DownloadState.Success - public weak var cell: ChatBaseCell? + public weak var cell: NEChatBaseCell? required init(message: NIMMessage?) { super.init(message: message) diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift index d8222142..e13faeea 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTextModel.swift @@ -8,7 +8,8 @@ import NIMSDK @objcMembers class MessageTextModel: MessageContentModel { - public var attributeStr: NSAttributedString? + public var attributeStr: NSMutableAttributedString? + public var textHeight: CGFloat = 0 required init(message: NIMMessage?) { super.init(message: message) @@ -16,24 +17,34 @@ class MessageTextModel: MessageContentModel { attributeStr = NEEmotionTool.getAttWithStr( str: message?.text ?? "", - font: NEKitChatConfig.shared.ui.messageFont + font: NEKitChatConfig.shared.ui.messageTextSize ) + if let remoteExt = message?.remoteExt, let dic = remoteExt[yxAtMsg] as? [String: AnyObject] { + dic.forEach { (key: String, value: AnyObject) in + if let contentDic = value as? [String: AnyObject] { + 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)) + } + } + } + } + } + } + } + let textSize = NEChatUITool.getSizeWithAtt( att: attributeStr ?? NSAttributedString(string: ""), - font: DefaultTextFont(16), + font: NEKitChatConfig.shared.ui.messageTextSize, maxSize: CGSize(width: qChat_content_maxW, height: CGFloat.greatestFiniteMagnitude) ) - + textHeight = textSize.height var h = qChat_min_h -// if textSize.height > qChat_min_h { -// h = textSize.height + 32 -// } h = textSize.height + 24 - contentSize = CGSize(width: textSize.width + qChat_cell_margin * 2, height: h) - + contentSize = CGSize(width: textSize.width + qChat_margin * 2, height: h) height = Float(contentSize.height + qChat_margin) + fullNameHeight - -// print(">>text:\(message?.text) height:\(height)") } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift index 5b24c458..0ae12fc7 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageTipsModel.swift @@ -42,7 +42,8 @@ class MessageTipsModel: NSObject, MessageModel { } tipMessage = message tipTimeStamp = message?.timestamp - contentSize = CGSize(width: kScreenWidth, height: 35) - height = 35 + contentSize = CGSize(width: kScreenWidth - 64 * 2, height: kScreenHeight) + let size = String.getTextRectSize(text ?? "", font: DefaultTextFont(NEKitChatConfig.shared.ui.timeTextSize), size: contentSize) + height = Float(size.height) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift index 62199b0e..729e0c6b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/MessageVideoModel.swift @@ -17,7 +17,7 @@ class MessageVideoModel: MessageContentModel { public var imageUrl: String? public var state = DownloadState.Success public var progress: Float = 0 - public weak var cell: ChatBaseCell? + public weak var cell: NEChatBaseCell? required init(message: NIMMessage?) { super.init(message: message) type = .video diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NECustomAttachmentProtocol.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NECustomAttachmentProtocol.swift new file mode 100644 index 00000000..00738643 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/NECustomAttachmentProtocol.swift @@ -0,0 +1,11 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import Foundation + +@objc +public protocol NECustomAttachmentProtocol: NSObjectProtocol { + var customType: Int { get set } + var cellHeight: CGFloat { get set } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift new file mode 100644 index 00000000..67be26ee --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/PinMessageModel.swift @@ -0,0 +1,166 @@ +//// 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 NECoreIMKit +import NEChatKit + +public class PinMessageModel: NSObject { + var chatmodel: MessageModel? + var message: NIMMessage + var item: NIMMessagePinItem + var session: NIMSession + var repo = ChatRepo() + + init(message: NIMMessage, item: NIMMessagePinItem) { + self.message = message + session = item.session + self.item = item + super.init() + chatmodel = modelFromMessage(message: message) + } + + private func modelFromMessage(message: NIMMessage) -> MessageModel { + var model: MessageModel + switch message.messageType { + case .video: + model = MessageVideoModel(message: message) + case .text: + model = MessageTextModel(message: message) + case .image: + model = MessageImageModel(message: message) + case .audio: + model = MessageAudioModel(message: message) + case .notification: + model = MessageTipsModel(message: message) + case .file: + model = MessageFileModel(message: message) + case .tip: + model = MessageTipsModel(message: message) + case .location: + model = MessageLocationModel(message: message) + case .rtcCallRecord: + model = MessageCallRecordModel(message: message) + default: + // 未识别的消息类型,默认为文本消息类型,text为未知消息 + message.text = "未知消息" + model = MessageContentModel(message: message) + } + + if let uid = message.from { + let user = UserInfoProvider.shared.getUserInfo(userId: uid) + var fullName = uid + if let nickName = user?.userInfo?.nickName { + fullName = nickName + } + model.avatar = user?.userInfo?.avatarUrl + if session.sessionType == .team { + // team + let teamMember = TeamProvider.shared.teamMember(uid, session.sessionId) + if let teamNickname = teamMember?.nickname { + fullName = teamNickname + } + } + if let alias = user?.alias { + fullName = alias + } + model.fullName = fullName + model.shortName = fullName + .count > 2 ? String(fullName[fullName.index(fullName.endIndex, offsetBy: -2)...]) : + fullName + } + +// model.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 + } + + public func getMessageType() -> Int { + if message.messageType == .file || + message.messageType == .audio || + message.messageType == .text || + message.messageType == .image || + message.messageType == .video || + message.messageType == .location { + return message.messageType.rawValue + } + return PinMessageDefaultType + } + + public func cellHeight() -> CGFloat { + var height = chatmodel?.contentSize.height ?? 0 + if let textModel = chatmodel as? MessageTextModel { + height = textModel.textHeight + } + height += 100 + + if chatmodel?.type == .text, height > 162 { + height = 162 + } + + if chatmodel?.replyedModel?.isReplay == true { + height += 12 + } + + return height + } + + public 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 + } + + // 获取展示的用户名字,p2p: 备注 > 昵称 > ID team: 备注 > 群昵称 > 昵称 > ID + private func getShowName(userId: String, teamId: String?) -> String { + let user = getUserInfo(userId: userId) + var fullName = userId + if let nickName = user?.userInfo?.nickName { + fullName = nickName + } + if let tID = teamId, session.sessionType == .team { + // team + let teamMember = getTeamMember(userId: userId, teamId: tID) + if let teamNickname = teamMember?.nickname { + fullName = teamNickname + } + } + if let alias = user?.alias { + fullName = alias + } + return fullName + } + + public func getUserInfo(userId: String) -> User? { + repo.getUserInfo(userId: userId) + } + + public func getTeamMember(userId: String, teamId: String) -> NIMTeamMember? { + repo.getTeamMemberList(userId: userId, teamId: teamId) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/UserSettingCellModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/UserSettingCellModel.swift index ae473536..7c2be1eb 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/UserSettingCellModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/Model/UserSettingCellModel.swift @@ -5,6 +5,11 @@ import Foundation +enum UserSettingType: Int { + case SwitchType = 1 + case SelectType = 2 +} + @objcMembers public class UserSettingCellModel: NSObject { typealias SwitchChangeCompletion = (Bool) -> Void @@ -18,4 +23,5 @@ public class UserSettingCellModel: NSObject { // var headerUrl: String? var cellClick: CellClick? var switchOpen = false + var type = UserSettingType.SwitchType.rawValue } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatAudioLeftCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatAudioLeftCell.swift index 03acf188..da308c9d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatAudioLeftCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatAudioLeftCell.swift @@ -64,7 +64,7 @@ public class ChatAudioLeftCell: ChatBaseLeftCell, ChatAudioCell { } } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let m = model as? MessageAudioModel { timeLabel.text = "\(m.duration)" + "s" diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatAudioRightCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatAudioRightCell.swift index d0c1dfff..240dc6b4 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatAudioRightCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatAudioRightCell.swift @@ -78,7 +78,7 @@ public class ChatAudioRightCell: ChatBaseRightCell, ChatAudioCell { } } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let m = model as? MessageAudioModel { timeLabel.text = "\(m.duration)" + "s" diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatBaseLeftCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatBaseLeftCell.swift index 9a70b02f..0554ad4e 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatBaseLeftCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatBaseLeftCell.swift @@ -6,7 +6,7 @@ import UIKit import NIMSDK @objcMembers -public class ChatBaseLeftCell: ChatBaseCell { +public class ChatBaseLeftCell: NEChatBaseCell { public var avatarImage = UIImageView() public var nameLabel = UILabel() public var fullNameLabel = UILabel() @@ -54,8 +54,8 @@ public class ChatBaseLeftCell: ChatBaseCell { // name nameLabel.textAlignment = .center nameLabel.translatesAutoresizingMaskIntoConstraints = false - nameLabel.font = UIFont.systemFont(ofSize: 12) - nameLabel.textColor = .white + nameLabel.font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.userNickTextSize) + nameLabel.textColor = NEKitChatConfig.shared.ui.userNickColor contentView.addSubview(nameLabel) NSLayoutConstraint.activate([ nameLabel.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), @@ -78,7 +78,9 @@ public class ChatBaseLeftCell: ChatBaseCell { ]) // bubbleImage - if let image = NEKitChatConfig.shared.ui.leftBubbleBg { + if let bgColor = NEKitChatConfig.shared.ui.receiveMessageBg { + bubbleImage.backgroundColor = bgColor + } else if let image = NEKitChatConfig.shared.ui.leftBubbleBg { bubbleImage.image = image .resizableImage(withCapInsets: UIEdgeInsets(top: 35, left: 25, bottom: 10, right: 25)) } @@ -131,6 +133,7 @@ public class ChatBaseLeftCell: ChatBaseCell { pinLabel.textColor = UIColor.ne_greenText pinLabel.isHidden = true pinLabelH = pinLabel.heightAnchor.constraint(equalToConstant: 0) + pinLabel.lineBreakMode = .byTruncatingMiddle NSLayoutConstraint.activate([ pinLabel.topAnchor.constraint(equalTo: bubbleImage.bottomAnchor, constant: 4), @@ -146,6 +149,9 @@ public class ChatBaseLeftCell: ChatBaseCell { let tap = UITapGestureRecognizer(target: self, action: #selector(tapAvatar)) avatarImage.addGestureRecognizer(tap) + let avatarLongGesture = UILongPressGestureRecognizer(target: self, action: #selector(longPressAvatar)) + avatarImage.addGestureRecognizer(avatarLongGesture) + let messageTap = UITapGestureRecognizer(target: self, action: #selector(tapMessage)) bubbleImage.addGestureRecognizer(messageTap) @@ -176,6 +182,12 @@ public class ChatBaseLeftCell: ChatBaseCell { delegate?.didTapMessageView(self, contentModel) } + func longPressAvatar(longPress: UITapGestureRecognizer) { + if longPress.state == .began { + delegate?.didLongPressAvatar(self, contentModel) + } + } + func longPress(longPress: UILongPressGestureRecognizer) { print(#function) switch longPress.state { @@ -197,7 +209,7 @@ public class ChatBaseLeftCell: ChatBaseCell { // MARK: set data - func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { contentModel = model updatePinStatus(model) bubbleW?.constant = model.contentSize.width @@ -243,7 +255,7 @@ public class ChatBaseLeftCell: ChatBaseCell { pinLabel.isHidden = !model.isPined pinImage.isHidden = !model.isPined contentView.backgroundColor = model.isPined ? NEKitChatConfig.shared.ui - .chatPinColor : .white + .signalBgColor : .white if model.isPined { let pinText = model.message?.session?.sessionType == .P2P ? chatLocalizable("pin_text_P2P") : chatLocalizable("pin_text_team") if model.pinAccount == nil { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatBaseRightCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatBaseRightCell.swift index 42d3c03c..25c56ed8 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatBaseRightCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatBaseRightCell.swift @@ -15,10 +15,11 @@ public protocol ChatBaseCellDelegate: NSObjectProtocol { // reedit button event on revokecell func didTapReeditButton(_ cell: UITableViewCell, _ model: MessageContentModel?) func didTapReadView(_ cell: UITableViewCell, _ model: MessageContentModel?) + func didLongPressAvatar(_ cell: UITableViewCell, _ model: MessageContentModel?) } @objcMembers -public class ChatBaseRightCell: ChatBaseCell { +public class ChatBaseRightCell: NEChatBaseCell { public var pinImage = UIImageView() public var avatarImage = UIImageView() public var nameLabel = UILabel() @@ -67,8 +68,8 @@ public class ChatBaseRightCell: ChatBaseCell { // name nameLabel.textAlignment = .center nameLabel.translatesAutoresizingMaskIntoConstraints = false - nameLabel.font = UIFont.systemFont(ofSize: 12) - nameLabel.textColor = .white + nameLabel.font = UIFont.systemFont(ofSize: NEKitChatConfig.shared.ui.userNickTextSize) + nameLabel.textColor = NEKitChatConfig.shared.ui.userNickColor contentView.addSubview(nameLabel) NSLayoutConstraint.activate([ nameLabel.leftAnchor.constraint(equalTo: avatarImage.leftAnchor), @@ -78,11 +79,13 @@ public class ChatBaseRightCell: ChatBaseCell { ]) // bubbleImage - bubbleImage.translatesAutoresizingMaskIntoConstraints = false - if let image = NEKitChatConfig.shared.ui.rightBubbleBg { + if let bgColor = NEKitChatConfig.shared.ui.selfMessageBg { + bubbleImage.backgroundColor = bgColor + } else if let image = NEKitChatConfig.shared.ui.rightBubbleBg { bubbleImage.image = image .resizableImage(withCapInsets: UIEdgeInsets(top: 35, left: 25, bottom: 10, right: 25)) } + bubbleImage.translatesAutoresizingMaskIntoConstraints = false bubbleImage.isUserInteractionEnabled = true contentView.addSubview(bubbleImage) let top = NSLayoutConstraint( @@ -151,8 +154,11 @@ public class ChatBaseRightCell: ChatBaseCell { pinLabel.textColor = UIColor.ne_greenText pinLabel.font = UIFont.systemFont(ofSize: 12) pinLabel.textAlignment = .right + pinLabel.lineBreakMode = .byTruncatingMiddle + pinLabelW = pinLabel.widthAnchor.constraint(equalToConstant: 210) pinLabelH = pinLabel.heightAnchor.constraint(equalToConstant: 0) + NSLayoutConstraint.activate([ pinLabel.topAnchor.constraint(equalTo: bubbleImage.bottomAnchor, constant: 4), pinLabel.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: 0), @@ -243,7 +249,7 @@ public class ChatBaseRightCell: ChatBaseCell { // MARK: set data - func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { contentModel = model updatePinStatus(model) tapGesture?.isEnabled = true @@ -333,7 +339,7 @@ public class ChatBaseRightCell: ChatBaseCell { pinLabel.isHidden = !model.isPined pinImage.isHidden = !model.isPined contentView.backgroundColor = model.isPined ? NEKitChatConfig.shared.ui - .chatPinColor : .white + .signalBgColor : .white if model.isPined { let pinText = model.message?.session?.sessionType == .P2P ? chatLocalizable("pin_text_P2P") : chatLocalizable("pin_text_team") if model.pinAccount == nil { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatCallRecordLeftCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatCallRecordLeftCell.swift index 9171d6e2..45f19726 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatCallRecordLeftCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatCallRecordLeftCell.swift @@ -32,7 +32,7 @@ class ChatCallRecordLeftCell: ChatBaseLeftCell { ]) } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let m = model as? MessageCallRecordModel { contentLabel.attributedText = m.attributeStr diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatCallRecordRightCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatCallRecordRightCell.swift index 0d710927..2ce17ec5 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatCallRecordRightCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatCallRecordRightCell.swift @@ -34,7 +34,7 @@ class ChatCallRecordRightCell: ChatBaseRightCell { activityView.removeFromSuperview() } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let m = model as? MessageCallRecordModel { contentLabel.attributedText = m.attributeStr diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatFileLeftCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatFileLeftCell.swift index f02bfad6..5495898d 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatFileLeftCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatFileLeftCell.swift @@ -103,7 +103,7 @@ public class ChatFileLeftCell: ChatBaseLeftCell { ]) } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let fileObject = model.message?.messageObject as? NIMFileObject { if let fileModel = model as? MessageFileModel { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatFileRightCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatFileRightCell.swift index fe5aa042..f42c92cc 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatFileRightCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatFileRightCell.swift @@ -103,7 +103,7 @@ public class ChatFileRightCell: ChatBaseRightCell { ]) } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let fileObject = model.message?.messageObject as? NIMFileObject { if let fileModel = model as? MessageFileModel { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatImageLeftCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatImageLeftCell.swift index 990d1599..02e58761 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatImageLeftCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatImageLeftCell.swift @@ -39,7 +39,7 @@ public class ChatImageLeftCell: ChatBaseLeftCell { ]) } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let m = model as? MessageImageModel, let imageUrl = m.imageUrl { if imageUrl.hasPrefix("http") { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatImageRightCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatImageRightCell.swift index e98596fe..3df475ff 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatImageRightCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatImageRightCell.swift @@ -40,7 +40,7 @@ public class ChatImageRightCell: ChatBaseRightCell { ]) } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let m = model as? MessageImageModel, let imageUrl = m.imageUrl { if imageUrl.hasPrefix("http") { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatLocationLeftCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatLocationLeftCell.swift index 43038c2f..0dd892ff 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatLocationLeftCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatLocationLeftCell.swift @@ -114,7 +114,7 @@ class ChatLocationLeftCell: ChatBaseLeftCell { } } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let m = model as? MessageLocationModel { titleLabel.text = m.title diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatLocationRightCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatLocationRightCell.swift index 31f39d9a..3fee7ecf 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatLocationRightCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatLocationRightCell.swift @@ -112,7 +112,7 @@ class ChatLocationRightCell: ChatBaseRightCell { } } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let m = model as? MessageLocationModel { titleLabel.text = m.title diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatMapLeftCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatMapLeftCell.swift deleted file mode 100644 index e94484ba..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatMapLeftCell.swift +++ /dev/null @@ -1,35 +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 - -class ChatMapLeftCell: ChatBaseLeftCell { - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - commonUI() - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - func commonUI() { -// label.translatesAutoresizingMaskIntoConstraints = false -// label.textColor = UIColor.ne_greyText -// label.font = UIFont.systemFont(ofSize: 16.0) -// bubbleImage.addSubview(label) -// NSLayoutConstraint.activate([ -// label.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: 16), -// label.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: 0), -// label.heightAnchor.constraint(equalToConstant: qChat_min_h), -// label.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: -16), -// label.bottomAnchor.constraint(equalTo: bubbleImage.bottomAnchor, constant: 0), -// ]) - } - - override func setModel(_ model: MessageContentModel) { - super.setModel(model) - } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatMapRightCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatMapRightCell.swift deleted file mode 100644 index 5534ae3f..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatMapRightCell.swift +++ /dev/null @@ -1,19 +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 - -class ChatMapRightCell: ChatBaseRightCell { - 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 - } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatReplyLeftCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatReplyLeftCell.swift index f8d9d96c..62028219 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatReplyLeftCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatReplyLeftCell.swift @@ -24,10 +24,10 @@ public class ChatReplyLeftCell: ChatBaseLeftCell { replyLabel.translatesAutoresizingMaskIntoConstraints = false addSubview(replyLabel) NSLayoutConstraint.activate([ - replyLabel.leadingAnchor.constraint(equalTo: bubbleImage.leadingAnchor, constant: 8), - replyLabel.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: qChat_margin), + replyLabel.leadingAnchor.constraint(equalTo: bubbleImage.leadingAnchor, constant: qChat_margin), + replyLabel.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: qChat_margin - 1), replyLabel.heightAnchor.constraint(equalToConstant: 26.0), - replyLabel.trailingAnchor.constraint(equalTo: bubbleImage.trailingAnchor, constant: -8), + replyLabel.trailingAnchor.constraint(equalTo: bubbleImage.trailingAnchor, constant: -qChat_margin), ]) textView.translatesAutoresizingMaskIntoConstraints = false @@ -44,25 +44,38 @@ public class ChatReplyLeftCell: ChatBaseLeftCell { textView.backgroundColor = .clear bubbleImage.addSubview(textView) NSLayoutConstraint.activate([ - textView.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: 0), - textView.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: 8), - textView.topAnchor.constraint( - equalTo: replyLabel.bottomAnchor, - constant: -qChat_margin - ), - textView.bottomAnchor.constraint( - equalTo: bubbleImage.bottomAnchor, - constant: -qChat_margin - ), + textView.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: -qChat_margin), + textView.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: qChat_margin), + textView.topAnchor.constraint(equalTo: replyLabel.bottomAnchor, constant: -(qChat_margin - 1)), + textView.bottomAnchor.constraint(equalTo: bubbleImage.bottomAnchor, constant: -qChat_margin), ]) } - override func setModel(_ model: MessageContentModel) { - super.setModel(model) + func sizeWidthFromString(_ text: String, _ font: UIFont) -> Double { + // 根据内容计算size + let maxSize = CGSize(width: qChat_content_maxW, height: 0) + let attibutes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font] + let labelSize = NSString(string: text).boundingRect(with: maxSize, attributes: attibutes, context: nil) + return ceil(labelSize.width) + qChat_margin * 2 + } + + override open func setModel(_ model: MessageContentModel) { if let m = model as? MessageTextModel { -// textView.text = m.text textView.attributedText = m.attributeStr + if let text = textView.attributedText, + let font = textView.font { + model.contentSize.width = max(sizeWidthFromString(text.string, font), model.contentSize.width) + } } - replyLabel.text = model.replyText + + if let text = model.replyText, + let font = replyLabel.font { + replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, + font: replyLabel.font, + color: replyLabel.textColor) + model.contentSize.width = max(sizeWidthFromString(text, font), model.contentSize.width) + } + + super.setModel(model) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatReplyRightCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatReplyRightCell.swift index 34a2a249..bd443f24 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatReplyRightCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatReplyRightCell.swift @@ -24,10 +24,10 @@ public class ChatReplyRightCell: ChatBaseRightCell { replyLabel.translatesAutoresizingMaskIntoConstraints = false addSubview(replyLabel) NSLayoutConstraint.activate([ - replyLabel.leadingAnchor.constraint(equalTo: bubbleImage.leadingAnchor, constant: 8), - replyLabel.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: qChat_margin), + replyLabel.leadingAnchor.constraint(equalTo: bubbleImage.leadingAnchor, constant: qChat_margin), + replyLabel.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: qChat_margin - 1), replyLabel.heightAnchor.constraint(equalToConstant: 26.0), - replyLabel.trailingAnchor.constraint(equalTo: bubbleImage.trailingAnchor, constant: -8), + replyLabel.trailingAnchor.constraint(equalTo: bubbleImage.trailingAnchor, constant: -qChat_margin), ]) textView.translatesAutoresizingMaskIntoConstraints = false @@ -44,24 +44,38 @@ public class ChatReplyRightCell: ChatBaseRightCell { textView.backgroundColor = .clear bubbleImage.addSubview(textView) NSLayoutConstraint.activate([ - textView.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: 0), - textView.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: 8), - textView.topAnchor.constraint( - equalTo: replyLabel.bottomAnchor, - constant: -qChat_margin - ), - textView.bottomAnchor.constraint( - equalTo: bubbleImage.bottomAnchor, - constant: -qChat_margin - ), + textView.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: -qChat_margin), + textView.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: qChat_margin), + textView.topAnchor.constraint(equalTo: replyLabel.bottomAnchor, constant: -(qChat_margin - 1)), + textView.bottomAnchor.constraint(equalTo: bubbleImage.bottomAnchor, constant: -qChat_margin), ]) } - override func setModel(_ model: MessageContentModel) { - super.setModel(model) + func sizeWidthFromString(_ text: String, _ font: UIFont) -> Double { + // 根据内容计算size + let maxSize = CGSize(width: qChat_content_maxW, height: 0) + let attibutes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: font] + let labelSize = NSString(string: text).boundingRect(with: maxSize, attributes: attibutes, context: nil) + return ceil(labelSize.width) + qChat_margin * 2 + } + + override open func setModel(_ model: MessageContentModel) { if let m = model as? MessageTextModel { textView.attributedText = m.attributeStr + if let text = textView.attributedText, + let font = textView.font { + model.contentSize.width = max(sizeWidthFromString(text.string, font), model.contentSize.width) + } } - replyLabel.text = model.replyText + + if let text = model.replyText, + let font = replyLabel.font { + replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: text, + font: replyLabel.font, + color: replyLabel.textColor) + model.contentSize.width = max(sizeWidthFromString(text, font), model.contentSize.width) + } + + super.setModel(model) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatRevokeLeftCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatRevokeLeftCell.swift index 8c0744b7..f464dd94 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatRevokeLeftCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatRevokeLeftCell.swift @@ -31,7 +31,7 @@ public class ChatRevokeLeftCell: ChatBaseLeftCell { ]) } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) label.text = chatLocalizable("message_has_be_withdrawn") } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatRevokeRightCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatRevokeRightCell.swift index 61e2010a..8d598ca6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatRevokeRightCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatRevokeRightCell.swift @@ -5,7 +5,7 @@ import UIKit -// protocol ChatRevokeRightCellDelegate: ChatBaseCellDelegate { +// protocol ChatRevokeRightCellDelegate: NEChatBaseCellDelegate { // func onReeditMessage(_ cell: UITableViewCell, _ model: MessageContentModel?) // } @@ -16,7 +16,7 @@ public class ChatRevokeRightCell: ChatBaseRightCell { public var label = UILabel() public var reeditButton = UIButton(type: .custom) // public var reeditBlock: ReeditBlock? -// public override var delegate: ChatBaseCellDelegate? +// public override var delegate: NEChatBaseCellDelegate? override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonUI() @@ -56,7 +56,7 @@ public class ChatRevokeRightCell: ChatBaseRightCell { reeditButton.addTarget(self, action: #selector(reeditEvent), for: .touchUpInside) } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { if let time = model.message?.timestamp { let date = Date() let currentTime = date.timeIntervalSince1970 diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTeamMemberCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTeamMemberCell.swift index a9315cc1..20bc9419 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTeamMemberCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTeamMemberCell.swift @@ -66,7 +66,7 @@ public class ChatTeamMemberCell: UITableViewCell { headerView.setTitle("") } else { headerView.image = nil - headerView.setTitle(model.showNameInTeam()) + headerView.setTitle(model.showNickInTeam()) headerView.backgroundColor = UIColor.colorWithString(string: model.nimUser?.userId) } nameLabel.text = model.atNameInTeam() diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTextLeftCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTextLeftCell.swift index cf17888d..86db72ec 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTextLeftCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTextLeftCell.swift @@ -24,16 +24,17 @@ public class ChatTextLeftCell: ChatBaseLeftCell { contentLabel.isUserInteractionEnabled = false contentLabel.font = DefaultTextFont(16) contentLabel.backgroundColor = .clear + contentLabel.textAlignment = .justified bubbleImage.addSubview(contentLabel) NSLayoutConstraint.activate([ - contentLabel.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: 0), - contentLabel.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: 8), + contentLabel.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: -qChat_margin), + contentLabel.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: qChat_margin), contentLabel.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: 0), contentLabel.bottomAnchor.constraint(equalTo: bubbleImage.bottomAnchor, constant: 0), ]) } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let m = model as? MessageTextModel { contentLabel.attributedText = m.attributeStr diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTextRightCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTextRightCell.swift index 66e3e35d..d8e002d8 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTextRightCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTextRightCell.swift @@ -23,16 +23,17 @@ public class ChatTextRightCell: ChatBaseRightCell { contentLabel.isUserInteractionEnabled = false contentLabel.numberOfLines = 0 contentLabel.font = DefaultTextFont(16) + contentLabel.textAlignment = .justified bubbleImage.addSubview(contentLabel) NSLayoutConstraint.activate([ - contentLabel.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: 0), - contentLabel.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: 8), + contentLabel.rightAnchor.constraint(equalTo: bubbleImage.rightAnchor, constant: -qChat_margin), + contentLabel.leftAnchor.constraint(equalTo: bubbleImage.leftAnchor, constant: qChat_margin), contentLabel.topAnchor.constraint(equalTo: bubbleImage.topAnchor, constant: 0), contentLabel.bottomAnchor.constraint(equalTo: bubbleImage.bottomAnchor, constant: 0), ]) } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let m = model as? MessageTextModel { contentLabel.attributedText = m.attributeStr diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTimeTableViewCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTimeTableViewCell.swift index 622477a1..42679185 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTimeTableViewCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatTimeTableViewCell.swift @@ -7,16 +7,19 @@ import UIKit @objcMembers public class ChatTimeTableViewCell: UITableViewCell { + var timeLabelWidthAnchor: NSLayoutConstraint? + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) selectionStyle = .none + timeLabel.numberOfLines = 0 contentView.addSubview(timeLabel) NSLayoutConstraint.activate([ timeLabel.topAnchor.constraint(equalTo: contentView.topAnchor), - timeLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor), - timeLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor), - timeLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor), + timeLabel.centerXAnchor.constraint(equalTo: contentView.centerXAnchor), ]) + timeLabelWidthAnchor = timeLabel.widthAnchor.constraint(equalToConstant: kScreenWidth - 64 * 2) + timeLabelWidthAnchor?.isActive = true } required init?(coder: NSCoder) { @@ -25,12 +28,13 @@ public class ChatTimeTableViewCell: UITableViewCell { func setModel(_ model: MessageTipsModel) { timeLabel.text = model.text + timeLabelWidthAnchor?.constant = model.contentSize.width } private lazy var timeLabel: UILabel = { let label = UILabel() - label.font = DefaultTextFont(12) - label.textColor = NEKitChatConfig.shared.ui.timeColor + label.font = DefaultTextFont(NEKitChatConfig.shared.ui.timeTextSize) + label.textColor = NEKitChatConfig.shared.ui.timeTextColor label.textAlignment = .center label.translatesAutoresizingMaskIntoConstraints = false return label diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatVideoLeftCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatVideoLeftCell.swift index 3045fb28..e45cc282 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatVideoLeftCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatVideoLeftCell.swift @@ -79,7 +79,7 @@ public class ChatVideoLeftCell: ChatImageLeftCell { ]) } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let videoObject = model.message?.messageObject as? NIMVideoObject { if let path = videoObject.coverUrl { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatVideoRightCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatVideoRightCell.swift index 67f1c102..b5ea9e8a 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatVideoRightCell.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/ChatVideoRightCell.swift @@ -86,7 +86,7 @@ public class ChatVideoRightCell: ChatImageRightCell { ]) } - override func setModel(_ model: MessageContentModel) { + override open func setModel(_ model: MessageContentModel) { super.setModel(model) if let videoObject = model.message?.messageObject as? NIMVideoObject { if let path = videoObject.coverPath, FileManager.default.fileExists(atPath: path) { diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageAudioCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageAudioCell.swift new file mode 100644 index 00000000..89cc78b0 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageAudioCell.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 + +class PinMessageAudioCell: PinMessageBaseCell { + var audioImageView = UIImageView(image: UIImage.ne_imageNamed(name: "left_play_3")) + var audioTimeLabel = UILabel() + public var bubbleImage = UIImageView() + + 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 + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func setupUI() { + super.setupUI() + + if let image = NEKitChatConfig.shared.ui.leftBubbleBg { + bubbleImage.image = image + .resizableImage(withCapInsets: UIEdgeInsets(top: 35, left: 25, bottom: 10, right: 25)) + } + bubbleImage.translatesAutoresizingMaskIntoConstraints = false + bubbleImage.isUserInteractionEnabled = true + backView.addSubview(bubbleImage) + contentWidth = bubbleImage.widthAnchor.constraint(equalToConstant: 0) + contentHeight = bubbleImage.heightAnchor.constraint(equalToConstant: 0) + NSLayoutConstraint.activate([ + contentHeight!, + contentWidth!, + bubbleImage.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + bubbleImage.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), + ]) + + audioImageView.contentMode = .center + audioImageView.translatesAutoresizingMaskIntoConstraints = false + 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: 14) + audioTimeLabel.textColor = UIColor.ne_darkText + audioTimeLabel.textAlignment = .left + 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), + ]) + } + + override func configure(_ item: PinMessageModel) { + 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/PinMessageBaseCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageBaseCell.swift new file mode 100644 index 00000000..a56b7647 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageBaseCell.swift @@ -0,0 +1,157 @@ +//// 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 NECommonUIKit +import NECommonKit + +protocol PinMessageCellDelegate { + func didClickMore(_ model: PinMessageModel?) +} + +class PinMessageBaseCell: UITableViewCell { + public var contentWidth: NSLayoutConstraint? + + public var contentHeight: NSLayoutConstraint? + + public var pinModel: PinMessageModel? + + public var delegate: PinMessageCellDelegate? + + 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 + }() + + lazy var nameLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 12.0) + label.textColor = .ne_darkText + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + lazy var timeLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 12.0) + label.textColor = .ne_greyText + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + public let backView = UIView() + + public let line = UIView() + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + public func setupUI() { + contentView.backgroundColor = UIColor(hexString: "#EFF1F3") + backView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(backView) + backView.backgroundColor = UIColor.white + NSLayoutConstraint.activate([ + backView.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 20), + backView.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), + 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([ + 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") + let imageView = UIImageView() + imageView.image = image + imageView.translatesAutoresizingMaskIntoConstraints = false + backView.addSubview(imageView) + + let moreBtn = UIButton() + moreBtn.addTarget(self, action: #selector(moreClick), for: .touchUpInside) + moreBtn.translatesAutoresizingMaskIntoConstraints = false + backView.addSubview(moreBtn) + NSLayoutConstraint.activate([ + moreBtn.rightAnchor.constraint(equalTo: backView.rightAnchor), + moreBtn.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), + moreBtn.widthAnchor.constraint(equalToConstant: 50), + moreBtn.heightAnchor.constraint(equalToConstant: 40), + ]) + + NSLayoutConstraint.activate([ + imageView.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), + imageView.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: nameLabel.leftAnchor), + timeLabel.rightAnchor.constraint(equalTo: nameLabel.rightAnchor), + timeLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 2), + ]) + + 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.topAnchor.constraint(equalTo: headerView.bottomAnchor, constant: 12), + line.heightAnchor.constraint(equalToConstant: 1), + ]) + line.backgroundColor = .ne_greyLine + } + + public func configure(_ item: PinMessageModel) { + pinModel = item + headerView.configHeadData(headUrl: item.chatmodel?.avatar, name: item.chatmodel?.fullName ?? "") + nameLabel.text = item.chatmodel?.fullName + print("config time : ", item.message.timestamp) + timeLabel.text = String.stringFromDate(date: Date(timeIntervalSince1970: item.message.timestamp)) + + contentWidth?.constant = item.chatmodel?.contentSize.width ?? 0 + contentHeight?.constant = item.chatmodel?.contentSize.height ?? 0 + } + + @objc func moreClick() { + delegate?.didClickMore(pinModel) + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageDefaultCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageDefaultCell.swift new file mode 100644 index 00000000..57caa222 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageDefaultCell.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 + +class PinMessageDefaultCell: PinMessageTextCell { + 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 + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func setupUI() { + super.setupUI() + } + + override func configure(_ item: PinMessageModel) { + super.configure(item) + contentLabel.text = chatLocalizable("unkonw_pin_message") + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageFileCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageFileCell.swift new file mode 100644 index 00000000..805bf04b --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageFileCell.swift @@ -0,0 +1,167 @@ +//// 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 + +class PinMessageFileCell: PinMessageBaseCell { + public var bubbleImage = UIImageView() + + lazy var imgView: UIImageView = { + let view_img = UIImageView() + view_img.translatesAutoresizingMaskIntoConstraints = false + view_img.backgroundColor = .clear + return view_img + }() + + 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 + }() + + 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 + }() + + 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 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 + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func setupUI() { + super.setupUI() + + 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: 0) + contentHeight = bubbleImage.heightAnchor.constraint(equalToConstant: 0) + NSLayoutConstraint.activate([ + contentHeight!, + contentWidth!, + bubbleImage.leftAnchor.constraint(equalTo: backView.leftAnchor, constant: 16), + bubbleImage.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), + ]) + + backView.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), + ]) + + backView.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), + ]) + } + + override func configure(_ item: PinMessageModel) { + super.configure(item) + if let fileObject = item.message.messageObject as? NIMFileObject { + 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" + } + } + imgView.image = UIImage.ne_imageNamed(name: imageName) + titleLabel.text = fileObject.displayName ?? displayName + let size_B = Double(fileObject.fileLength) + 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 + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageImageCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageImageCell.swift new file mode 100644 index 00000000..95b7ee8f --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageImageCell.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 UIKit + +class PinMessageImageCell: PinMessageBaseCell { + public let contentImageView = UIImageView() + + 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 + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func setupUI() { + super.setupUI() + contentImageView.translatesAutoresizingMaskIntoConstraints = false + contentImageView.contentMode = .scaleAspectFill + contentImageView.clipsToBounds = true + contentImageView.addCustomCorner( + conrners: [.bottomLeft, .bottomRight, .topRight, .topLeft], + radius: 8, + backcolor: .white + ) + backView.addSubview(contentImageView) + NSLayoutConstraint.activate([ + contentImageView.leftAnchor.constraint(equalTo: line.leftAnchor, constant: 0), + contentImageView.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), + ]) + contentWidth = contentImageView.widthAnchor.constraint(equalToConstant: 0) + contentWidth?.isActive = true + contentHeight = contentImageView.heightAnchor.constraint(equalToConstant: 0) + contentHeight?.isActive = true + } + + override func configure(_ item: PinMessageModel) { + super.configure(item) + + if let m = item.chatmodel as? MessageImageModel, let imageUrl = m.imageUrl { + if imageUrl.hasPrefix("http") { + contentImageView.sd_setImage( + with: URL(string: imageUrl), + placeholderImage: nil, + options: .retryFailed, + progress: nil, + completed: nil + ) + } else { + contentImageView.image = UIImage(contentsOfFile: imageUrl) + } + + } else { + contentImageView.image = nil + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageLocationCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageLocationCell.swift new file mode 100644 index 00000000..56a3fe3a --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageLocationCell.swift @@ -0,0 +1,131 @@ +//// 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 NEChatKit + +class PinMessageLocationCell: PinMessageBaseCell { + private lazy var locationTitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor.ne_darkText + label.font = UIFont.systemFont(ofSize: 16.0) + return label + }() + + private lazy var subTitleLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = UIColor.ne_lightText + label.font = UIFont.systemFont(ofSize: 12.0) + return label + }() + + private 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 + return label + }() + + var mapView: UIView? + + 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 + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override 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: 0) + contentHeight = back.heightAnchor.constraint(equalToConstant: 0) + NSLayoutConstraint.activate([ + contentWidth!, + contentHeight!, + back.leftAnchor.constraint(equalTo: headerView.leftAnchor), + back.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), + ]) + + back.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), + ]) + + back.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), + ]) + } + } + + override func configure(_ item: PinMessageModel) { + 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) + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageTextCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageTextCell.swift new file mode 100644 index 00000000..ef4c8c7f --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageTextCell.swift @@ -0,0 +1,71 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +class PinMessageTextCell: PinMessageBaseCell { + lazy var contentLabel: UILabel = { + let label = UILabel() + label.font = UIFont.systemFont(ofSize: 14.0) + label.textColor = .ne_darkText + label.translatesAutoresizingMaskIntoConstraints = false + label.numberOfLines = 3 + return label + }() + + public let replyLabel = UILabel() + + 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 + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func setupUI() { + super.setupUI() + replyLabel.font = UIFont.systemFont(ofSize: 12) + replyLabel.textColor = UIColor(hexString: "#929299") + replyLabel.translatesAutoresizingMaskIntoConstraints = false + addSubview(replyLabel) + NSLayoutConstraint.activate([ + replyLabel.topAnchor.constraint(equalTo: line.bottomAnchor, constant: 12), + replyLabel.leftAnchor.constraint(equalTo: line.leftAnchor), + replyLabel.rightAnchor.constraint(equalTo: line.rightAnchor), + ]) + + backView.addSubview(contentLabel) + NSLayoutConstraint.activate([ + contentLabel.leftAnchor.constraint(equalTo: line.leftAnchor), + contentLabel.rightAnchor.constraint(equalTo: line.rightAnchor), + contentLabel.topAnchor.constraint(equalTo: replyLabel.bottomAnchor, constant: 1), + ]) + } + + override func configure(_ item: PinMessageModel) { + super.configure(item) + if let model = item.chatmodel as? MessageTextModel { + contentLabel.attributedText = model.attributeStr + if model.replyedModel?.isReplay == true { + replyLabel.attributedText = NEEmotionTool.getAttWithStr(str: model.replyText ?? "", + font: replyLabel.font, + color: replyLabel.textColor) + } else { + replyLabel.attributedText = nil + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageVideoCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageVideoCell.swift new file mode 100644 index 00000000..92e1ecd9 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/PinCell/PinMessageVideoCell.swift @@ -0,0 +1,99 @@ +//// 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 + +class PinMessageVideoCell: PinMessageImageCell { + lazy var stateView: VideoStateView = { + let state = VideoStateView() + state.translatesAutoresizingMaskIntoConstraints = false + state.backgroundColor = .clear + return state + }() + + lazy var videoTimeLabel: UILabel = { + let label = UILabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.textColor = .white + label.font = NEConstant.defaultTextFont(10.0) + label.textAlignment = .center + return label + }() + + lazy var timeView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(videoTimeLabel) + NSLayoutConstraint.activate([ + videoTimeLabel.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 4), + videoTimeLabel.topAnchor.constraint(equalTo: view.topAnchor, constant: 2), + videoTimeLabel.rightAnchor.constraint(equalTo: view.rightAnchor, constant: -4), + videoTimeLabel.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 init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + override func setupUI() { + super.setupUI() + contentImageView.addSubview(stateView) + contentImageView.addCustomCorner(conrners: [.topLeft], radius: 8, backcolor: .white) + NSLayoutConstraint.activate([ + stateView.centerXAnchor.constraint(equalTo: contentImageView.centerXAnchor), + stateView.centerYAnchor.constraint(equalTo: contentImageView.centerYAnchor), + stateView.heightAnchor.constraint(equalToConstant: 60), + stateView.widthAnchor.constraint(equalToConstant: 60), + ]) + + contentImageView.addSubview(timeView) + NSLayoutConstraint.activate([ + timeView.rightAnchor.constraint(equalTo: contentImageView.rightAnchor, constant: -7), + timeView.bottomAnchor.constraint(equalTo: contentImageView.bottomAnchor, constant: -7), + ]) + + stateView.isUserInteractionEnabled = false + } + + override func configure(_ item: PinMessageModel) { + 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 videoObject.duration > 0 { + timeView.isHidden = false + videoTimeLabel.text = Date.getFormatPlayTime(TimeInterval(videoObject.duration / 1000)) + } else { + timeView.isHidden = true + } + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserSettingSelectCell.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserSettingSelectCell.swift new file mode 100644 index 00000000..c37bb7c7 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/Cell/UserSettingSelectCell.swift @@ -0,0 +1,66 @@ +// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import UIKit + +@objcMembers +public class UserSettingSelectCell: UserSettingBaseCell { + public lazy var subTitleLabel: UILabel = { + let label = UILabel() + label.textColor = NEConstant.hexRGB(0x999999) + label.font = NEConstant.defaultTextFont(14.0) + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + override public func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override public func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + selectionStyle = .none + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + func setupUI() { + contentView.addSubview(titleLabel) + NSLayoutConstraint.activate([ + titleLabel.leftAnchor.constraint(equalTo: contentView.leftAnchor, constant: 36), + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 16), + titleLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -84), + ]) + + contentView.addSubview(subTitleLabel) + NSLayoutConstraint.activate([ + subTitleLabel.leftAnchor.constraint(equalTo: titleLabel.leftAnchor), + subTitleLabel.rightAnchor.constraint(equalTo: titleLabel.rightAnchor), + subTitleLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 6.0), + ]) + + contentView.addSubview(arrow) + NSLayoutConstraint.activate([ + arrow.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + arrow.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -36), + ]) + } + + override func configure(_ anyModel: Any) { + super.configure(anyModel) + if let model = anyModel as? UserSettingCellModel { + subTitleLabel.text = model.subTitle + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatInputView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatInputView.swift index 2139f66a..df75f8d6 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatInputView.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/ChatInputView.swift @@ -5,7 +5,8 @@ import UIKit import NECommonKit -import RSKPlaceholderTextView +import NECoreKit +import UITextView_Placeholder @objc public enum ChatMenuType: Int { case text = 0 @@ -15,9 +16,14 @@ import RSKPlaceholderTextView case addMore } +public let yxAtMsg = "yxAitMsg" +public let atRangeOffset = 1 +public let atSegmentsKey = "segments" +public let atTextKey = "text" + @objc public protocol ChatInputViewDelegate: NSObjectProtocol { - func sendText(text: String?) + func sendText(text: String?, attribute: NSAttributedString?) func willSelectItem(button: UIButton, index: Int) func didSelectMoreCell(cell: NEInputMoreCell) @@ -34,7 +40,7 @@ public protocol ChatInputViewDelegate: NSObjectProtocol { } @objcMembers -public class ChatInputView: UIView, ChatRecordViewDelegate, +open class ChatInputView: UIView, ChatRecordViewDelegate, InputEmoticonContainerViewDelegate, UITextViewDelegate, NEMoreViewDelagate { public weak var delegate: ChatInputViewDelegate? public var currentType: ChatMenuType = .text @@ -42,19 +48,27 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, public var contentHeight = 204.0 public var atCache: NIMInputAtCache? - var textField = RSKPlaceholderTextView() - var stackView = UIStackView() + public var atRangeCache = [String: MessageAtCacheModel]() + + public var nickAccidDic = [String: String]() + + public var textView = UITextView() // RSKPlaceholderTextView() + public var stackView = UIStackView() var contentView = UIView() public var contentSubView: UIView? private var greyView = UIView() private var recordView = ChatRecordView(frame: .zero) + private var textInput: UITextInput? + + public var textviewLeftConstraint: NSLayoutConstraint? + public var textviewRightConstraint: NSLayoutConstraint? override init(frame: CGRect) { super.init(frame: frame) commonUI() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -64,30 +78,30 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, func commonUI() { backgroundColor = UIColor(hexString: "#EFF1F3") - textField.layer.cornerRadius = 8 - textField.font = UIFont.systemFont(ofSize: 16) - textField.clipsToBounds = true - textField.translatesAutoresizingMaskIntoConstraints = false - textField.backgroundColor = .white - textField.returnKeyType = .send - textField.delegate = self - textField.allowsEditingTextAttributes = true - addSubview(textField) + textView.layer.cornerRadius = 8 + textView.font = UIFont.systemFont(ofSize: 16) + textView.clipsToBounds = true + textView.translatesAutoresizingMaskIntoConstraints = false + textView.backgroundColor = .white + textView.returnKeyType = .send + textView.delegate = self + textView.allowsEditingTextAttributes = true + textView.typingAttributes = [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)] + textView.linkTextAttributes = [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)] + textView.dataDetectorTypes = [] + addSubview(textView) + textviewLeftConstraint = textView.leftAnchor.constraint(equalTo: leftAnchor, constant: 7) + textviewRightConstraint = textView.rightAnchor.constraint(equalTo: rightAnchor, constant: -7) + NSLayoutConstraint.activate([ - textField.leftAnchor.constraint(equalTo: leftAnchor, constant: 7), - textField.topAnchor.constraint(equalTo: topAnchor, constant: 6), - textField.rightAnchor.constraint(equalTo: rightAnchor, constant: -7), - textField.heightAnchor.constraint(equalToConstant: 40), + textviewLeftConstraint!, + textviewRightConstraint!, + textView.topAnchor.constraint(equalTo: topAnchor, constant: 6), + textView.heightAnchor.constraint(equalToConstant: 40), ]) -// NotificationCenter.default.addObserver( -// textField, -// selector: #selector(textFieldChangeNoti), -// name: UITextView.textDidChangeNotification, -// object: nil -// ) + textInput = textView let imageNames = ["mic", "emoji", "photo", "add"] -// let imageNames = ["mic", "emoji", "photo", "chat_video", "add"] var items = [UIButton]() for i in 0 ... 3 { @@ -107,7 +121,7 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, stackView.leftAnchor.constraint(equalTo: leftAnchor), stackView.rightAnchor.constraint(equalTo: rightAnchor), stackView.heightAnchor.constraint(equalToConstant: 54), - stackView.topAnchor.constraint(equalTo: textField.bottomAnchor, constant: 0), + stackView.topAnchor.constraint(equalTo: textView.bottomAnchor, constant: 0), ]) greyView.translatesAutoresizingMaskIntoConstraints = false @@ -147,27 +161,27 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, contentView.addSubview(chatAddMoreView) } - func addRecordView() { + public func addRecordView() { if currentType != .audio { currentType = .audio - textField.resignFirstResponder() + textView.resignFirstResponder() contentSubView?.isHidden = true contentSubView = recordView contentSubView?.isHidden = false } } - func addEmojiView() { + public func addEmojiView() { if currentType != .emoji { currentType = .emoji - textField.resignFirstResponder() + textView.resignFirstResponder() contentSubView?.isHidden = true contentSubView = emojiView contentSubView?.isHidden = false } } - func addMoreActionView() { + public func addMoreActionView() { if currentType != .addMore { currentType = .addMore contentSubView?.isHidden = true @@ -176,70 +190,21 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, } } - // func doButtonDeleteText(){ - // let range = delRangeForLastComponent() - // if range.count == 1 { - // - // } - // print("\(textField.selectedTextRange?.start)") - // textField.deleteBackward() - // } - - // func delRangeForLastComponent() -> NSRange{ - // let text = textField.text as? NSString - // let selectedRange = self.textField.selectedRange - // if selectedRange.location == 0 { - // return NSRange.init(location: 0, length: 0) - // } - // - // let range:NSRange? - // let subRange = - // if selectedRange?.start >= 2 { - // let subStr = text?.substring(with: NSRange.init(location: selectedRange?.start - 2, - // length: 2)) - // isEmoji = sub - // } - // } - - // func rangeForPrefix(prefix:String,suffix:String) ->NSRange { - // let text = textField.text as? NSString - // let range = textField.selectedRange - // var selectedText:String? - // if range.length > 0 { - // selectedText = text?.substring(with: range) - // }else { - // selectedText = text as? String - // } - // let endLocaiton = range.location - // if endLocaiton <= 0{ - // return NSMakeRange(NSNotFound, 0) - // } - // let index = -1 - // - // if let selectStr = selectedText,selectStr.hasSuffix(suffix) { - // let p = 20 - // for index = endLocaiton in - // - // - // - // }else { - // return NSMakeRange(NSNotFound, 0) - // - // } - // - // - // - // } - // MARK: ===================== lazy method ===================== - public lazy var emojiView: InputEmoticonContainerView = { + 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.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true view.delegate = self - return view + 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 = { @@ -251,7 +216,7 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, }() public func textViewDidChange(_ textView: UITextView) { - delegate?.textFieldDidChange(textField) + delegate?.textFieldDidChange(textView) } public func textViewDidEndEditing(_ textView: UITextView) { @@ -267,29 +232,84 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, return true } - public func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool { - currentType = .text - return true + public func checkRemoveAtMessage(range: NSRange, attribute: NSAttributedString) -> NSRange? { + var temRange: NSRange? + let start = range.location +// let end = range.location + range.length + attribute.enumerateAttribute( + NSAttributedString.Key.foregroundColor, + in: NSMakeRange(0, attribute.length) + ) { value, findRange, stop in + guard let findColor = value as? UIColor else { + return + } + if isEqualToColor(findColor, UIColor.ne_blueText) == false { + return + } + if findRange.location <= start, start < findRange.location + findRange.length + atRangeOffset { + temRange = NSMakeRange(findRange.location, findRange.length + atRangeOffset) + stop.pointee = true + } + +// if (findRange.location <= start && start < findRange.location + findRange.length + atRangeOffset) || +// (findRange.location < end && end <= findRange.location + findRange.length + atRangeOffset) { +// temRange = NSMakeRange(findRange.location, findRange.length + atRangeOffset) +// stop.pointee = true +// } + } + return temRange } public func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool { + print("text view range : ", range) + print("select range : ", textView.selectedRange) + textView.typingAttributes = [NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16)] + if text == "\n" { - guard let text = getRealSendText(textField.attributedText)? - .trimmingCharacters(in: CharacterSet.whitespaces) else { + guard var realText = getRealSendText(textView.attributedText) else { return true } - delegate?.sendText(text: text) - textField.text = "" -// textView.resignFirstResponder() + if realText.trimmingCharacters(in: .whitespaces).isEmpty { + realText = "" + } + delegate?.sendText(text: realText, attribute: textView.attributedText) + textView.text = "" return false } - print("range:\(range) string:\(text)") + if textView.attributedText.length == 0, let pasteString = UIPasteboard.general.string, text.count > 0 { + if pasteString == text { + let muta = NSMutableAttributedString(string: text) + muta.addAttributes([NSAttributedString.Key.foregroundColor: UIColor.ne_darkText, NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0)], range: NSMakeRange(0, text.count)) + textView.attributedText = muta + textView.selectedRange = NSMakeRange(text.count - 1, 0) + return false + } + } + if text.count == 0 { - if let delegate = delegate { - return delegate.textDelete(range: range, text: text) +// let selectRange = textView.selectedRange + let temRange = checkRemoveAtMessage(range: range, attribute: textView.attributedText) + + if let findRange = temRange { + let mutableAttri = NSMutableAttributedString(attributedString: textView.attributedText) + if mutableAttri.length >= findRange.location + findRange.length { + mutableAttri.removeAttribute(NSAttributedString.Key.foregroundColor, range: findRange) + mutableAttri.removeAttribute(NSAttributedString.Key.font, range: findRange) + if range.length == 1 { + mutableAttri.replaceCharacters(in: findRange, with: "") + } + if mutableAttri.length <= 0 { + textView.attributedText = nil + } else { + textView.attributedText = mutableAttri + } + textView.selectedRange = NSMakeRange(findRange.location, 0) + } + return false } + return true } else { delegate?.textChanged(text: text) } @@ -297,6 +317,27 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, return true } + public func textViewDidChangeSelection(_ textView: UITextView) { + print("textViewDidChangeSelection") + let range = textView.selectedRange + if let findRange = checkRemoveAtMessage(range: range, attribute: textView.attributedText) { + textView.selectedRange = NSMakeRange(findRange.location + findRange.length, 0) + } + } + + @available(iOS 10.0, *) + public func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { + print("action : ", interaction) + + return true + } + +// @available(iOS 10.0, *) +// public func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { +// +// return true +// } + func buttonEvent(button: UIButton) { switch button.tag - 5 { case 0: @@ -315,11 +356,10 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, public func selectedEmoticon(emoticonID: String, emotCatalogID: String, description: String) { if emoticonID.isEmpty { // 删除键 - // doButtonDeleteText() - textField.deleteBackward() + textView.deleteBackward() print("delete ward") } else { - if let font = textField.font { + if let font = textView.font { let attribute = NEEmotionTool.getAttWithStr( str: description, font: font, @@ -327,7 +367,7 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, ) print("attribute : ", attribute) let mutaAttribute = NSMutableAttributedString() - if let origin = textField.attributedText { + if let origin = textView.attributedText { mutaAttribute.append(origin) } attribute.enumerateAttribute( @@ -344,19 +384,18 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, value: font, range: NSMakeRange(0, mutaAttribute.length) ) - textField.attributedText = mutaAttribute - textField.scrollRangeToVisible(NSMakeRange(textField.attributedText.length, 1)) -// [_textView scrollRangeToVisible:NSMakeRange(_textView.text.length, 1)]; + textView.attributedText = mutaAttribute + textView.scrollRangeToVisible(NSMakeRange(textView.attributedText.length, 1)) } } } public func didPressSend(sender: UIButton) { - guard let text = getRealSendText(textField.attributedText) else { + guard let text = getRealSendText(textView.attributedText) else { return } - delegate?.sendText(text: text) - textField.text = "" + delegate?.sendText(text: text, attribute: textView.attributedText) + textView.text = "" atCache?.clean() } @@ -391,9 +430,9 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, delegate?.endRecord(insideView: insideView) } - func textFieldChangeNoti() { - delegate?.textFieldDidChange(textField) - } +// func textFieldChangeNoti() { +// delegate?.textFieldDidChange(textField) +// } func getRealSendText(_ attribute: NSAttributedString) -> String? { let muta = NSMutableString() @@ -413,4 +452,116 @@ public class ChatInputView: UIView, ChatRecordViewDelegate, } return muta as String } + + public func getRemoteExtension(_ 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_blueText) == false { + return + } + if let range = Range(findRange, in: string) { + let text = string[range] + let model = MessageAtInfoModel() + print("range text : ", String(text)) + model.start = findRange.location + model.end = model.start + findRange.length + 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 + } + } + } + if atDic.count > 0 { + return [yxAtMsg: atDic] + } + return nil + } + + public 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 + } + } 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 + } + + public func cleartAtCache() { + nickAccidDic.removeAll() + } + + private func convertRangeToNSRange(range: UITextRange?) -> NSRange? { + if let start = range?.start, let end = range?.end { + let startIndex = textInput?.offset(from: textInput?.beginningOfDocument ?? start, to: start) ?? 0 + let endIndex = textInput?.offset(from: textInput?.beginningOfDocument ?? end, to: end) ?? 0 + return NSMakeRange(startIndex, endIndex - startIndex) + } + return nil + } + + private func isEqualToColor(_ color1: UIColor, _ color2: UIColor) -> Bool { + guard let components1 = color1.cgColor.components, + let components2 = color2.cgColor.components, + color1.cgColor.colorSpace == color2.cgColor.colorSpace, + color1.cgColor.numberOfComponents == 4, + color2.cgColor.numberOfComponents == 4 + else { + return false + } + + return components1[0] == components2[0] && // Red + components1[1] == components2[1] && // Green + components1[2] == components2[2] && // Blue + components1[3] == components2[3] // Alpha + } + + func missClickEmoj() { + print("click one px space") + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CopyableTextView.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CopyableTextView.swift new file mode 100644 index 00000000..c4bf9a71 --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/View/ChatView/CopyableTextView.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 UIKit +import NECommonUIKit +import MobileCoreServices + +class CopyableTextView: UITextView { + var copyString: String? + + override public var canBecomeFirstResponder: Bool { + true + } + + override init(frame: CGRect, textContainer: NSTextContainer?) { + super.init(frame: frame, textContainer: textContainer) + sharedInit() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + sharedInit() + } + + func sharedInit() { + isUserInteractionEnabled = true + addGestureRecognizer(UILongPressGestureRecognizer( + target: self, + action: #selector(showMenu(sender:)) + )) + } + + @objc + 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 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) + } + + @objc 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 func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + if action == #selector(copyText) { + return true + } + return false + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift index fd31137b..a44e8631 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/ChatViewModel.swift @@ -23,7 +23,7 @@ public protocol ChatViewModelDelegate: NSObjectProtocol { func send(_ message: NIMMessage, didCompleteWithError error: Error?) func send(_ message: NIMMessage, progress: Float) func didReadedMessageIndexs() - func onDeleteMessage(_ message: NIMMessage, atIndexs: [IndexPath]) + 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]) @@ -32,19 +32,21 @@ public protocol ChatViewModelDelegate: NSObjectProtocol { func remoteUserEndEditing() func didLeaveTeam() func didDismissTeam() + func didRefreshTable() } let revokeLocalMessage = "revoke_message_local" let revokeLocalMessageContent = "revoke_message_local_content" +let removePinMessageNoti = "remove_pin_message_noti" @objcMembers public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDelegate, - NIMConversationManagerDelegate, NIMSystemNotificationManagerDelegate, ChatExtendProviderDelegate { + NIMConversationManagerDelegate, NIMSystemNotificationManagerDelegate, ChatExtendProviderDelegate, NIMUserManagerDelegate { public var team: NIMTeam? public var session: NIMSession public var messages = [MessageModel]() public weak var delegate: ChatViewModelDelegate? - public var messageDic = [String: MessageModel]() + public var newUserInfoDic = [String: User]() // 上拉时间戳 private var newMsg: NIMMessage? // 下拉时间戳 @@ -69,10 +71,12 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel self.session = session anchor = nil super.init() + NIMSDK.shared().userManager.add(self) repo.addChatDelegate(delegate: self) repo.addSessionDelegate(delegate: self) repo.addSystemNotificationDelegate(delegate: self) repo.addChatExtendDelegate(delegate: self) + addObserver() } init(session: NIMSession, anchor: NIMMessage?) { @@ -83,10 +87,43 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel if anchor != nil { isHistoryChat = true } + NIMSDK.shared().userManager.add(self) repo.addChatDelegate(delegate: self) repo.addSessionDelegate(delegate: self) repo.addSystemNotificationDelegate(delegate: self) repo.addChatExtendDelegate(delegate: self) + addObserver() + } + + func addObserver() { + NotificationCenter.default.addObserver(self, selector: #selector(updateFriendInfo), name: NotificationName.updateFriendInfo, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(removePinNoti), name: Notification.Name(removePinMessageNoti), object: nil) + } + + func removePinNoti(_ noti: Notification) { + if let message = noti.object as? NIMMessage { + removeLocalPinMessage(message) + delegate?.didRefreshTable() + } + } + + public 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 + ) + } + + func updateFriendInfo(notification: Notification) { + if let user = notification.object as? User, + let uid = user.userId { + newUserInfoDic[uid] = user + } } public func sendTextMessage(text: String, _ completion: @escaping (Error?) -> Void) { @@ -190,7 +227,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel readMsg = messageArray.last weakSelf?.newMsg = messageArray.last } - for msg in datas { + for (i, msg) in datas.enumerated() { if let object = msg.messageObject as? NIMNotificationObject { if let content = object.content as? NIMTeamNotificationContent, content.operationType == .invite { if weakSelf?.filterInviteSet.contains(msg.messageId) == true { @@ -205,15 +242,23 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel weakSelf?.filterRevokeMessage([model]) if order == .desc { if weakSelf?.addTimeForHistoryMessage(msg) == true { - count = count + 1 + count += 1 } - count = count + 1 + count += 1 weakSelf?.messages.insert(model, at: 0) + + // 第一条消息默认显示时间 + if i == datas.count - 1 { + let model = MessageTipsModel(message: msg) + model.type = .time + model.text = String.stringFromDate(date: Date(timeIntervalSince1970: msg.timestamp)) + weakSelf?.messages.insert(model, at: 0) + } } else { if weakSelf?.addTimeMessage(msg) == true { - count = count + 1 + count += 1 } - count = count + 1 + count += 1 weakSelf?.messages.append(model) } } @@ -596,6 +641,15 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } + public func replyMessageWithoutThread(message: NIMMessage, + target: NIMMessage, + _ completion: @escaping (Error?) -> Void) { + NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId:" + message.messageId) + repo.replyMessageWithoutThread(message: message, session: session, target: target) { error in + completion(error) + } + } + public func revokeMessage(message: NIMMessage, _ completion: @escaping (Error?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId:" + message.messageId) repo.revokeMessage(message: message) { error in @@ -644,6 +698,15 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } + // MARK: NIMUserManagerDelegate + + public func onFriendChanged(_ user: NIMUser) { + if let uid = user.userId { + newUserInfoDic[uid] = User(user: user) + delegate?.didRefreshTable() + } + } + // MARK: NIMChatManagerDelegate // 收到消息 @@ -700,34 +763,32 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel public func willSend(_ message: NIMMessage) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId:" + message.messageId) print("\(#function)") + if message.session?.sessionId != session.sessionId { return } // 自定义消息发送之前的处理 - if message.messageType == .custom { - } else { - 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 newMsg == nil { + newMsg = message + } - if !isResend { - addTimeMessage(message) - let model = modelFromMessage(message: message) - filterRevokeMessage([model]) - messages.append(model) + var isResend = false + for (i, msg) in messages.enumerated() { + if message.messageId == msg.message?.messageId { + messages[i].message = message + isResend = true + break } + } - delegate?.willSend(message) + if !isResend { + addTimeMessage(message) + let model = modelFromMessage(message: message) + filterRevokeMessage([model]) + messages.append(model) } + + delegate?.willSend(message) } public func send(_ message: NIMMessage, progress: Float) { @@ -773,6 +834,9 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel var index = -1 for (i, model) in messages.enumerated() { if pinItem.messageServerID == model.message?.serverID { + if !messages[i].isPined { + return + } messages[i].isPined = false messages[i].pinAccount = nil messages[i].pinShowName = nil @@ -871,9 +935,13 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } switch model?.message?.messageType { case .location: + if let isPin = model?.isPined, isPin { + pinItem = OperationItem.removePinItem() + } items.append(contentsOf: [ OperationItem.replayItem(), OperationItem.forwardItem(), + pinItem, OperationItem.deleteItem(), ]) case .text: @@ -963,6 +1031,10 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel @discardableResult private func addTimeMessage(_ message: NIMMessage) -> Bool { NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + if NotificationMessageUtils.isDiscussSeniorTeamNoti(message: message) { + return false + } + let lastTs = messages.last?.message?.timestamp ?? 0.0 let curTs = message.timestamp let dur = curTs - lastTs @@ -975,6 +1047,10 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel private func addTimeForHistoryMessage(_ message: NIMMessage) -> Bool { NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + if NotificationMessageUtils.isDiscussSeniorTeamNoti(message: message) { + return false + } + let firstTs = messages.first?.message?.timestamp ?? 0.0 let curTs = message.timestamp let dur = firstTs - curTs @@ -1006,6 +1082,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel private func modelFromMessage(message: NIMMessage) -> MessageModel { NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) var model: MessageModel + print("message type : ", message.messageType.rawValue) switch message.messageType { case .video: model = MessageVideoModel(message: message) @@ -1027,8 +1104,9 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel // <#code#> // case .rtcCallRecord: // <#code#> -// case .custom: -// <#code#> + case .custom: + model = MessageCustomModel(message: message) + case .location: model = MessageLocationModel(message: message) case .rtcCallRecord: @@ -1041,8 +1119,10 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel if let uid = message.from { let user = getUserInfo(userId: uid) var fullName = uid + var shortName = uid if let nickName = user?.userInfo?.nickName { fullName = nickName + shortName = nickName } model.avatar = user?.userInfo?.avatarUrl if session.sessionType == .team { @@ -1056,11 +1136,9 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel fullName = alias } model.fullName = fullName - model.shortName = fullName - .count > 2 ? String(fullName[fullName.index(fullName.endIndex, offsetBy: -2)...]) : - fullName + model.shortName = getShortName(name: shortName, length: 2) } - model.replyedModel = getReplyMessage(message: message) + model.replyedModel = getReplyMessageWithoutThread(message: message) if let pin = repo.searchMessagePinHistory(message) { model.isPined = true model.pinAccount = pin.accountID @@ -1072,7 +1150,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel return model } - private func getReplyMessage(message: NIMMessage) -> MessageModel? { + public func getReplyMessage(message: NIMMessage) -> MessageModel? { NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) guard let id = message.repliedMessageId, id.count > 0 else { return nil @@ -1089,6 +1167,30 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel return model } + public 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 + } + + 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 + } + private func getUserInfo(_ userId: String, _ completion: @escaping (User?, NSError?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + userId) if let user = userInfo[userId] { @@ -1109,15 +1211,14 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel } } -// 获取展示的用户名字,p2p: 备注》昵称>ID team: 备注〉群昵称》 昵称〉 ID - private func getShowName(userId: String, teamId: String?) -> String { +// 获取展示的用户名字,p2p: 备注 > 昵称 > ID team: 备注 > 群昵称 > 昵称 > ID + func getShowName(userId: String, teamId: String?, _ showAlias: Bool = true) -> String { NELog.infoLog(ModuleName + " " + className, desc: #function + ", userId: " + userId) let user = getUserInfo(userId: userId) var fullName = userId if let nickName = user?.userInfo?.nickName { fullName = nickName } -// model.avatar = user?.userInfo?.thumbAvatarUrl if let tID = teamId, session.sessionType == .team { // team let teamMember = getTeamMember(userId: userId, teamId: tID) @@ -1125,14 +1226,14 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel fullName = teamNickname } } - if let alias = user?.alias { + if showAlias, let alias = user?.alias { fullName = alias } return fullName } // 全名后几位 - private func getShortName(name: String, length: Int) -> String { + func getShortName(name: String, length: Int) -> String { NELog.infoLog(ModuleName + " " + className, desc: #function + ", name: " + name) return name .count > length ? String(name[name.index(name.endIndex, offsetBy: -length)...]) : name @@ -1141,13 +1242,29 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel func deleteMessageUpdateUI(_ message: NIMMessage) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) var index = -1 + var replyIndex = [Int]() + var hasFind = false for (i, model) in messages.enumerated() { - if model.message?.serverID == message.serverID { - index = i - break + if hasFind { + 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, id == message.messageId { + messages[i].replyText = chatLocalizable("message_not_found") + replyIndex.append(i) + } + } else { + if model.message?.serverID == message.serverID { + index = i + hasFind = true + } } } + var indexs = [IndexPath]() + var reloadIndexs = [IndexPath]() if index >= 0 { // remove time tip let last = index - 1 @@ -1156,29 +1273,56 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel 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)) + } } } - delegate?.onDeleteMessage(message, atIndexs: indexs) + + delegate?.onDeleteMessage(message, atIndexs: indexs, reloadIndex: reloadIndexs) } func revokeMessageUpdateUI(_ message: NIMMessage) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) var index = -1 + var replyIndex = [Int]() + var hasFind = false for (i, model) in messages.enumerated() { - if model.message?.serverID == message.serverID { - index = i - break + if hasFind { + 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, id == message.messageId { + replyIndex.append(i) + } + } else { + if model.message?.serverID == message.serverID { + 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)) + } + delegate?.onRevokeMessage(message, atIndexs: indexs) } @@ -1240,7 +1384,13 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel users.forEach { user in if let uid = user.userId { let session = NIMSession(uid, type: .P2P) - weakSelf?.repo.makeForwardMessage(message, session) + // weakSelf?.repo.makeForwardMessage(message, session) + if let forwardMessage = weakSelf?.repo.makeForwardMessage(message) { + clearForwardAtMark(forwardMessage) + weakSelf?.repo.sendMessage(message: forwardMessage, session: session) { error in + NELog.infoLog("chat view model ", desc: "forward message : \(error?.localizedDescription ?? "")") + } + } } } } @@ -1249,7 +1399,13 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) if let tid = team.teamId { let session = NIMSession(tid, type: .team) - repo.makeForwardMessage(message, session) + // repo.makeForwardMessage(message, session) + if let forwardMessage = repo.makeForwardMessage(message) { + clearForwardAtMark(forwardMessage) + repo.sendMessage(message: forwardMessage, session: session) { error in + NELog.infoLog("chat view model ", desc: "forward message : \(error?.localizedDescription ?? "")") + } + } } } @@ -1257,24 +1413,32 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel _ completion: @escaping (Error?, NIMMessagePinItem?, Int) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) let item = NIMMessagePinItem(message: message) + guard let _ = NIMSDK.shared().conversationManager.messages(in: session, messageIds: [message.messageId]) else { + return + } + repo.addMessagePin(item) { [weak self] error, pinItem in - var index = -1 - if var messages = self?.messages { - for (i, model) in messages.enumerated() { - if message.messageId == model.message?.messageId { - messages[i].isPined = true - messages[i].pinAccount = NIMSDK.shared().loginManager.currentAccount() - messages[i].pinShowName = self?.getShowName( - userId: NIMSDK.shared().loginManager.currentAccount(), - teamId: message.session?.sessionId - ) - self?.messages = messages - index = i - break + if error != nil { + completion(error, nil, -1) + } 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 = self?.getShowName( + userId: NIMSDK.shared().loginManager.currentAccount(), + teamId: message.session?.sessionId + ) + self?.messages = messages + index = i + break + } } } + completion(nil, pinItem, index) } - completion(error, pinItem, index) } } @@ -1282,21 +1446,18 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel _ completion: @escaping (Error?, NIMMessagePinItem?, Int) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", messageId: " + message.messageId) + guard let _ = NIMSDK.shared().conversationManager.messages(in: session, messageIds: [message.messageId]) else { + return + } let item = NIMMessagePinItem(message: message) - repo.removeMessagePin(item) { [weak self] error, pinItem in - var index = -1 - if var messages = self?.messages { - for (i, model) in messages.enumerated() { - if message.messageId == model.message?.messageId { - messages[i].isPined = false - messages[i].pinAccount = nil - self?.messages = messages - index = i - break - } - } + 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) } - completion(error, pinItem, index) } } @@ -1353,6 +1514,7 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel messageNew.timestamp = message.timestamp messageNew.from = message.from messageNew.localExt = muta + messageNew.remoteExt = message.remoteExt let setting = NIMMessageSetting() setting.shouldBeCounted = false setting.isSessionUpdate = false @@ -1391,6 +1553,21 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel repo.refreshReceipts(receiptsMessages) } + @discardableResult + private func removeLocalPinMessage(_ message: NIMMessage) -> Int { + var index = -1 + + 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 + } + } + return index + } + // MARK: NIMConversationManagerDelegate // remote @@ -1417,4 +1594,18 @@ public class ChatViewModel: NSObject, ChatRepoMessageDelegate, NIMChatManagerDel deinit { print("deinit") } + + private func clearForwardAtMark(_ forwardMessage: NIMMessage) { + forwardMessage.remoteExt?.removeValue(forKey: yxAtMsg) + forwardMessage.remoteExt?.removeValue(forKey: keyReplyMsgKey) + if forwardMessage.remoteExt?.count ?? 0 <= 0 { + forwardMessage.remoteExt = nil + } + } + + func fetchPinMessage(_ completion: @escaping () -> Void) { + repo.fetchPinMessage(session.sessionId, session.sessionType) { error, items in + completion() + } + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.swift new file mode 100644 index 00000000..28afccdc --- /dev/null +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/PinMessageViewModel.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 UIKit +import NEChatKit +import NIMSDK + +@objc +public protocol PinMessageViewModelDelegate: NSObjectProtocol { + func didNeedRefreshUI() +} + +@objcMembers +public class PinMessageViewModel: NSObject, ChatExtendProviderDelegate, NIMChatManagerDelegate { + public let chatRepo = ChatRepo() + public var items = [PinMessageModel]() + public var delegate: PinMessageViewModelDelegate? + + override public init() { + super.init() + chatRepo.addChatDelegate(delegate: self) + chatRepo.addChatExtendDelegate(delegate: self) + } + + public func getPinitems(session: NIMSession, _ completion: @escaping (Error?) -> Void) { + weak var weakSelf = self + chatRepo.fetchPinMessage(session.sessionId, session.sessionType) { error, pinItems 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 + } + } else { + remoteMessages.append(item) + pinDic[item.messageServerID] = item + } + } + 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) + } + 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) + } + } + + } else { + completion(error) + } + } + } + + public func removePinMessage(_ message: NIMMessage, + _ completion: @escaping (Error?, NIMMessagePinItem?) + -> Void) { + NELog.infoLog("PinMessageViewModel", desc: #function + ", messageId: " + message.messageId) + let item = NIMMessagePinItem(message: message) + chatRepo.removeMessagePin(item) { error, pinItem in + completion(error, pinItem) + } + } + + public func forwardUserMessage(_ message: NIMMessage, _ users: [NIMUser]) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) + weak var weakSelf = self + users.forEach { user in + if let uid = user.userId { + let session = NIMSession(uid, type: .P2P) + if let forwardMessage = weakSelf?.chatRepo.makeForwardMessage(message) { + weakSelf?.clearForwardAtMark(forwardMessage) + weakSelf?.chatRepo.sendMessage(message: forwardMessage, session: session) { error in + } + } + } + } + } + + public func forwardTeamMessage(_ message: NIMMessage, _ team: NIMTeam) { + NELog.infoLog(ModuleName + " " + className(), desc: #function + ", messageId: " + message.messageId) + if let tid = team.teamId { + let session = NIMSession(tid, type: .team) + if let forwardMessage = chatRepo.makeForwardMessage(message) { + clearForwardAtMark(forwardMessage) + chatRepo.sendMessage(message: forwardMessage, session: session) { error in + NELog.infoLog("chat view model ", desc: "forward message : \(error?.localizedDescription ?? "")") + } + } + } + } + + // MARK: NIMChatManagerDelegate + + public func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { +// items = [PinMessageModel]() + delegate?.didNeedRefreshUI() + } + + // MARK: ChatExtendProviderDelegate + + public func onNotifyAddMessagePin(pinItem: NIMMessagePinItem) { +// items = [PinMessageModel]() + delegate?.didNeedRefreshUI() + } + + public func onNotifyRemoveMessagePin(pinItem: NIMMessagePinItem) { +// items = [PinMessageModel]() + delegate?.didNeedRefreshUI() + } + + private func clearForwardAtMark(_ forwardMessage: NIMMessage) { + forwardMessage.remoteExt?.removeValue(forKey: yxAtMsg) + forwardMessage.remoteExt?.removeValue(forKey: keyReplyMsgKey) + if forwardMessage.remoteExt?.count ?? 0 <= 0 { + forwardMessage.remoteExt = nil + } + } +} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift index f3c9de99..0a9cbd71 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/TeamChatViewModel.swift @@ -10,6 +10,7 @@ import NECoreIMKit public protocol TeamChatViewModelDelegate: ChatViewModelDelegate { func onTeamRemoved(team: NIMTeam) func onTeamUpdate(team: NIMTeam) + func onTeamMemberUpdate(team: NIMTeam) } @objcMembers @@ -73,4 +74,17 @@ public class TeamChatViewModel: ChatViewModel, NIMTeamManagerDelegate { } } } + + public func onTeamMemberUpdated(_ team: NIMTeam, withMembers memberIDs: [String]?) { + guard let membersIds = memberIDs else { + return + } + for memberId in membersIds { + let user = UserInfoProvider.shared.getUserInfo(userId: memberId) + newUserInfoDic[memberId] = user + } + if let delegate = delegate as? TeamChatViewModelDelegate { + delegate.onTeamMemberUpdate(team: team) + } + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift index d780ff58..2c3e217b 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Chat/ViewModel/UserSettingViewModel.swift @@ -31,9 +31,14 @@ public class UserSettingViewModel: NSObject { } userInfo = user weak var weakSelf = self + + let mark = UserSettingCellModel() + mark.cellName = chatLocalizable("operation_pin") + mark.type = UserSettingType.SelectType.rawValue + mark.cornerType = .topLeft.union(.topRight) + let remind = UserSettingCellModel() remind.cellName = chatLocalizable("message_remind") - remind.cornerType = .topLeft.union(.topRight) if let isNotiMsg = user.imUser?.notifyForNewMsg() { remind.switchOpen = isNotiMsg } @@ -113,6 +118,6 @@ public class UserSettingViewModel: NSObject { } } */ - cellDatas.append(contentsOf: [remind, setTop]) + cellDatas.append(contentsOf: [mark, remind, setTop]) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift b/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift index 82aa3051..bcb77912 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/ChatConfig/ChatUIConfig.swift @@ -14,17 +14,22 @@ import NECommonKit @objcMembers public class ChatUIConfig: NSObject { - /// 头像圆角大小 + /// UI 元素自定义 + + // 头像圆角大小 public var avatarCornerRadius = 4.0 - /// 头像类型 + // 头像类型 public var avatarType: NEChatAvatarType = .cycle - /// 设置聊天消息标记的背景色 - public var chatPinColor = UIColor.ne_yellowBackgroundColor + // 设置聊天消息标记的背景色 + public var signalBgColor = UIColor.ne_yellowBackgroundColor // 时间颜色 - public var timeColor = UIColor.ne_emptyTitleColor + public var timeTextColor = UIColor.ne_emptyTitleColor + + // 时间字体大小 + public var timeTextSize: Float = 12 // 右侧聊天背景气泡 public var rightBubbleBg = UIImage.ne_imageNamed(name: "chat_message_send") @@ -32,12 +37,53 @@ public class ChatUIConfig: NSObject { // 左侧聊天背景气泡 public var leftBubbleBg = UIImage.ne_imageNamed(name: "chat_message_receive") - /// 聊天字体大小(文本类型) - public var messageFont = UIFont.systemFont(ofSize: 16) + // 聊天字体大小(文本类型) + public var messageTextSize = UIFont.systemFont(ofSize: 16) + + // 聊天字体颜色(文本类型) + public var messageTextColor = UIColor.ne_darkText + + // 自己发送的消息体的背景色 + public var selfMessageBg: UIColor? + + // 接收到的消息体的背景色 + public var receiveMessageBg: UIColor? + + // 他人发送消息内容的背景资源ID + // 自己发送消息内容的背景资源ID + + // 不设置头像的用户所展示的文字头像中的文字颜色 + public var userNickColor: UIColor = .white + + // 不设置头像的用户所展示的文字头像中的文字字体大小 + public var userNickTextSize: CGFloat = 12 + + // 单聊中是否展示已读未读状态 + public var showP2pMessageStatus: Bool? + // 群聊中是否展示已读未读状态 + public var showTeamMessageStatus: Bool? + // 会话界面是否展示标题栏 + public var showTitleBar: Bool? + // 是否展示标题栏右侧图标按钮 + public var showTitleBarRightIcon: Bool? + // 设置标题栏右侧图标按钮展示图标 + public var titleBarRightRes: UIImage? + // 标题栏右侧图标的点击事件 + public var titleBarRightClick: (() -> Void)? + // 设置会话界面背景色 + public var chatViewBackground: UIColor = .white + + /// 界面布局自定义 + // 会话界面(即聊天界面)的标题视图 + // 会话界面的消息列表上方的小块视图 + // 会话界面的消息列表视图 + // 会话界面的消息列表下视图 + // 会话界面的底部输入框视图 + // 会话界面的消息列表 + // 会话界面的底部输入框视图 - /// 聊天字体颜色(文本类型) - public var messageColor = UIColor.ne_darkText + /// 用户可自定义参数 - /// 发送文件大小限制(单位:MB) + // 发送文件大小限制(单位:MB) public var fileSizeLimit: Double = 200 } diff --git a/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/ChatRouter.swift b/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/ChatRouter.swift index 7f7706c2..06e089ae 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/ChatRouter.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/ChatRouter/ChatRouter.swift @@ -6,10 +6,38 @@ import Foundation import NIMSDK import NECommonKit +import SDWebImage +import SDWebImageWebPCoder +import SDWebImageSVGKitPlugin @objcMembers public class ChatRouter: NSObject { public static func register() { + // pin + Router.shared.register(PushPinMessageVCRouter) { param in + let nav = param["nav"] as? UINavigationController + guard let session = param["session"] as? NIMSession else { + return + } + let pin = PinMessageViewController(session: session) + 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)") @@ -17,7 +45,15 @@ public class ChatRouter: NSObject { guard let session = param["session"] as? NIMSession else { return } - let p2pChatVC = P2PChatViewController(session: session) + let anchor = param["anchor"] as? NIMMessage + var p2pChatVC = P2PChatViewController(session: session, anchor: anchor) + for (i, vc) in (nav?.viewControllers ?? []).enumerated() { + if vc.isMember(of: P2PChatViewController.self) { + nav?.viewControllers[i] = p2pChatVC + nav?.popToViewController(p2pChatVC, animated: true) + return + } + } nav?.pushViewController(p2pChatVC, animated: true) } @@ -45,5 +81,9 @@ public class ChatRouter: NSObject { public static func setupInit() { NIMKitFileLocationHelper.setStaticAppkey(NIMSDK.shared().appKey()) NIMKitFileLocationHelper.setStaticUserId(NIMSDK.shared().loginManager.currentAccount()) + let webpCoder = SDImageWebPCoder() + SDImageCodersManager.shared.addCoder(webpCoder) + let svgCoder = SDImageSVGKCoder.shared + SDImageCodersManager.shared.addCoder(svgCoder) } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift index f40a3c5a..425d3108 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/ChatConstant.swift @@ -129,12 +129,14 @@ let HexRGBAlpha: ((Int, Float) -> UIColor) = { (rgbValue: Int, alpha: Float) -> // MARK: notificationkey enum NotificationName { + static let updateFriendInfo = Notification.Name("chat.updateFriendInfo") // 参数 serverId: string static let createServer = Notification.Name(rawValue: "qchat.createServer") // param channel: ChatChannel static let createChannel = Notification.Name(rawValue: "qchat.createChannel") static let updateChannel = Notification.Name(rawValue: "qchat.updateChannel") static let deleteChannel = Notification.Name(rawValue: "qchat.deleteChannel") + static let leaveTeamBySelf = Notification.Name(rawValue: "team.leaveTeamBySelf") // static let login = Notification.Name(rawValue:"qchat.login") static let logout = Notification.Name(rawValue: "qchat.logout") diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/IMCustomAttachment.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/IMCustomAttachment.swift deleted file mode 100644 index a61594eb..00000000 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/IMCustomAttachment.swift +++ /dev/null @@ -1,64 +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 -import NIMSDK - -public class CustomAttachment: NSObject, NIMCustomAttachment { - public var type = 0 - - public var goodsName = "name" - - public var goodsURL = "url" - - public func encode() -> String { - let info = ["goodsName": goodsName, "goodsURL": goodsURL, "type": type] as [String: Any] - - let jsonData = try? JSONSerialization.data(withJSONObject: info, options: .prettyPrinted) - var content = "" - if let data = jsonData { - content = String(data: data, encoding: .utf8) ?? "" - } - return content - } -} - -public class CustomAttachmentDecoder: NSObject, NIMCustomAttachmentCoding { - public func decodeAttachment(_ content: String?) -> NIMCustomAttachment? { - var attachment: NIMCustomAttachment? - let data = content?.data(using: .utf8) - guard let dataInfo = data else { - return attachment - } - - let infoDict = try? JSONSerialization.jsonObject( - with: dataInfo, - options: .mutableContainers - ) - let infoResult = infoDict as? [String: Any] - let type = infoResult?["type"] as? Int - - switch type { - case 0: - attachment = - decodeCustomMessage(info: infoDict as? [String: Any] ?? [String(): String()]) - default: - print("test") - } - - return attachment - } - - func decodeCustomMessage(info: [String: Any]) -> CustomAttachment { - let customAttachment = CustomAttachment() - customAttachment.goodsName = info["goodsName"] as? String ?? "" - customAttachment.goodsURL = info["goodsURL"] as? String ?? "" - if let type = info["type"] as? Int { - customAttachment.type = type - } - - return customAttachment - } -} diff --git a/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift b/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift index 6e98fc13..7177b0a9 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Common/NEChatUIKitClient.swift @@ -9,7 +9,7 @@ import NIMSDK @objcMembers public class NEChatUIKitClient: NSObject { public static let instance = NEChatUIKitClient() - + private var customRegisterDic = [String: UITableViewCell.Type]() public var moreAction = [NEMoreItemModel]() override init() { @@ -53,4 +53,15 @@ public class NEChatUIKitClient: NSObject { } return more } + + /// 新增聊天页针对自定义消息的cell扩展,以及现有cell样式覆盖 + public func regsiterCustomCell(_ registerDic: [String: UITableViewCell.Type]) { + registerDic.forEach { (key: String, value: UITableViewCell.Type) in + customRegisterDic[key] = value + } + } + + public func getRegisterCustomCell() -> [String: UITableViewCell.Type] { + customRegisterDic + } } diff --git a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift index 135b87de..531c772f 100644 --- a/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift +++ b/NEChatUIKit/NEChatUIKit/Classes/Extension/ChatStringExtension.swift @@ -15,6 +15,15 @@ extension String { return rect.size } + /// 计算 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 = getTextRectSize(string, font: font, size: maxSize) + let lines = Int(textSize.height / charSize) + return lines + } + static func stringFromDate(date: Date) -> String { let fmt = DateFormatter() if Calendar.current.isDateInToday(date) { diff --git a/NEContactUIKit/NEContactUIKit.podspec b/NEContactUIKit/NEContactUIKit.podspec index fbf614e0..2b829ef0 100644 --- a/NEContactUIKit/NEContactUIKit.podspec +++ b/NEContactUIKit/NEContactUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEContactUIKit' - s.version = '9.2.10' + s.version = '9.5.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NEContactUIKit/NEContactUIKit/Assets/NEKitContact.xcassets/refused.imageset/Contents.json b/NEContactUIKit/NEContactUIKit/Assets/NEKitContact.xcassets/refused.imageset/Contents.json new file mode 100644 index 00000000..b6772733 --- /dev/null +++ b/NEContactUIKit/NEContactUIKit/Assets/NEKitContact.xcassets/refused.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "refuse@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "refuse@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEContactUIKit/NEContactUIKit/Assets/NEKitContact.xcassets/refused.imageset/refuse@2x.png b/NEContactUIKit/NEContactUIKit/Assets/NEKitContact.xcassets/refused.imageset/refuse@2x.png new file mode 100644 index 00000000..afe72547 Binary files /dev/null and b/NEContactUIKit/NEContactUIKit/Assets/NEKitContact.xcassets/refused.imageset/refuse@2x.png differ diff --git a/NEContactUIKit/NEContactUIKit/Assets/NEKitContact.xcassets/refused.imageset/refuse@3x.png b/NEContactUIKit/NEContactUIKit/Assets/NEKitContact.xcassets/refused.imageset/refuse@3x.png new file mode 100644 index 00000000..0443ea4e Binary files /dev/null and b/NEContactUIKit/NEContactUIKit/Assets/NEKitContact.xcassets/refused.imageset/refuse@3x.png differ diff --git a/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings b/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings index b3b869a5..0014116b 100644 --- a/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings +++ b/NEContactUIKit/NEContactUIKit/Assets/en.lproj/Localizable.strings @@ -35,6 +35,7 @@ "select"="Select"; "select_contact"="Please select Contact"; "user_not_exist"="No contact"; +"team_not_exist"="No team"; "no_friend"="No Friend"; "chat"="Chat"; "message_remind"="Message notification"; @@ -42,3 +43,10 @@ "sure_delte_friend"="Wether to delete Contact?"; "input_userId"="Input user ID"; "space_not_support"="All Spaces are not supported"; + + +"let_us_chat"="Nice to meet you, let's chat"; + +// 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 aa63bba9..c99cdea1 100644 --- a/NEContactUIKit/NEContactUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEContactUIKit/NEContactUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -35,6 +35,7 @@ "select"="选择"; "select_contact"="请选择联系人"; "user_not_exist"="该用户不存在"; +"team_not_exist"="该群不存在"; "no_friend"="暂无好友"; "chat"="聊天"; "message_remind"="消息提醒"; @@ -42,3 +43,10 @@ "sure_delte_friend"="是否确定删除好友?"; "input_userId"="请输入账号"; "space_not_support"="不支持全空格"; + + +"let_us_chat"="我已经同意了你的申请,现在开始聊天吧~"; + +// error toast +"validate_processed"="该验证消息已在其他端处理"; +"failed_operation"="操作失败"; diff --git a/NEContactUIKit/NEContactUIKit/Classes/Base/ContactBaseViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Base/ContactBaseViewCell.swift index 6dd817fa..4420e381 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Base/ContactBaseViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Base/ContactBaseViewCell.swift @@ -23,6 +23,20 @@ open class ContactBaseViewCell: UITableViewCell { return avatar }() + public lazy var redAngleView: RedAngleLabel = { + let label = RedAngleLabel() + label.translatesAutoresizingMaskIntoConstraints = false + label.font = NEConstant.defaultTextFont(12) + label.textColor = .white + label.text = "99+" + label.backgroundColor = NEConstant.hexRGB(0xF24957) + label.textInsets = UIEdgeInsets(top: 3, left: 7, bottom: 3, right: 7) + label.layer.cornerRadius = 9 + label.clipsToBounds = true + label.isHidden = true + return label + }() + public lazy var nameLabel: UILabel = { let name = UILabel() name.translatesAutoresizingMaskIntoConstraints = false @@ -42,6 +56,15 @@ open class ContactBaseViewCell: UITableViewCell { return label }() + public lazy var optionLabel: UILabel = { + let label = UILabel() + label.textAlignment = .left + label.translatesAutoresizingMaskIntoConstraints = false + label.font = UIFont.systemFont(ofSize: 14.0) + label.textColor = UIColor(hexString: "333333") + return label + }() + var leftConstraint: NSLayoutConstraint? override public func awakeFromNib() { diff --git a/NEContactUIKit/NEContactUIKit/Classes/BlackList/Views/BlackListCell.swift b/NEContactUIKit/NEContactUIKit/Classes/BlackList/Views/BlackListCell.swift index 6c37199f..5fc127e1 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/BlackList/Views/BlackListCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/BlackList/Views/BlackListCell.swift @@ -75,7 +75,7 @@ class BlackListCell: TeamTableViewCell { nameLabel.text = "" avatarImage.sd_setImage(with: URL(string: imageUrl), completed: nil) } else { - nameLabel.text = user.shortName(count: 2) + nameLabel.text = user.shortName(showAlias: false, count: 2) avatarImage.image = nil } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift b/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift index 3c869f0b..f3f4530c 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Common/ContactConst.swift @@ -24,3 +24,9 @@ func localizable(_ key: String) -> String { } public let ModuleName = "NEContactUIKit" + +// MARK: notificationkey + +enum NotificationName { + static let updateFriendInfo = Notification.Name("chat.updateFriendInfo") +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift index 3aed26ad..03c876a3 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Model/ContactInfo.swift @@ -18,4 +18,5 @@ public class ContactInfo { public var router = ContactPersonRouter public var isSelected = false public var headerBackColor: UIColor? + public var localExtension: [String: Any]? } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/TeamListViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/TeamListViewController.swift index 854c56f6..e7cd416f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/TeamListViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewController/TeamListViewController.swift @@ -17,6 +17,10 @@ public class TeamListViewController: UIViewController, UITableViewDelegate, UITa super.viewDidLoad() commonUI() loadData() + weak var weakSelf = self + viewModel.refresh = { + weakSelf?.tableView.reloadData() + } } func commonUI() { diff --git a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift index c5e840d6..cf4a8b5a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Team/ViewModel/TeamListViewModel.swift @@ -8,14 +8,54 @@ import NECoreKit import NECoreIMKit @objcMembers -public class TeamListViewModel: NSObject { +public class TeamListViewModel: NSObject, NIMTeamManagerDelegate { var contactRepo = ContactRepo() + var refresh: () -> Void = {} public var teamList = [Team]() private let className = "TeamListViewModel" + override public init() { + super.init() + contactRepo.addTeamDelegate(delegate: self) + } + + deinit { + contactRepo.removeTeamDelegate(delegate: self) + } + func getTeamList() -> [Team]? { NELog.infoLog(ModuleName + " " + className, desc: #function) teamList = contactRepo.getTeamList() + teamList.sort(by: { team1, team2 in + (team1.createTime ?? 0) > (team2.createTime ?? 0) + }) return teamList } + + // MARK: NIMTeamManagerDelegate + + public func onTeamAdded(_ team: NIMTeam) { + teamList.insert(Team(teamInfo: team), at: 0) + refresh() + } + + public func onTeamUpdated(_ team: NIMTeam) { + for (i, t) in teamList.enumerated() { + if t.teamId == team.teamId { + teamList[i] = Team(teamInfo: team) + refresh() + break + } + } + } + + public func onTeamRemoved(_ team: NIMTeam) { + for (i, t) in teamList.enumerated() { + if t.teamId == team.teamId { + teamList.remove(at: i) + refresh() + break + } + } + } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/UserInfoHeaderView.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/UserInfoHeaderView.swift index 6f94033d..19e1ddf4 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/UserInfoHeaderView.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/UserInfoHeaderView.swift @@ -86,25 +86,20 @@ public class UserInfoHeaderView: UIView { guard let user = user else { return } - avatarImage.backgroundColor = UIColor.colorWithString(string: user.userId) + // avatar - if let imageUrl = user.userInfo?.avatarUrl { + if let imageUrl = user.userInfo?.avatarUrl, !imageUrl.isEmpty { avatarImage.sd_setImage(with: URL(string: imageUrl), completed: nil) nameLabel.isHidden = true + } else { + avatarImage.sd_setImage(with: nil) + avatarImage.backgroundColor = UIColor.colorWithString(string: user.userId) + nameLabel.text = user.shortName(showAlias: false, count: 2) + nameLabel.isHidden = false } // title - var showName = user.alias?.count ?? 0 > 0 ? user.alias : user.userInfo?.nickName - if showName == nil || showName?.count == 0 { - showName = user.userId - } - if let name = showName { - titleLabel.text = name - if avatarImage.image == nil { - nameLabel.text = name - .count > 2 ? String(name[name.index(name.endIndex, offsetBy: -2)...]) : name - } - } + titleLabel.text = user.showName() detailLabel.text = "\(localizable("account")):\(user.userId ?? "")" } diff --git a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/ViewController/ContactUserViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/ViewController/ContactUserViewController.swift index 716a89eb..29d48f6a 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/UserInfo/ViewController/ContactUserViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/UserInfo/ViewController/ContactUserViewController.swift @@ -56,6 +56,13 @@ public class ContactUserViewController: ContactBaseViewController, UITableViewDe } } } + + NIMSDK.shared().systemNotificationManager.add(self) + } + + override func backToPrevious() { + super.backToPrevious() + NotificationCenter.default.post(name: NotificationName.updateFriendInfo, object: user) } func commonUI() { @@ -256,6 +263,7 @@ public class ContactUserViewController: ContactBaseViewController, UITableViewDe remark.completion = { u in self.user = u self.headerView?.setData(user: u) + NotificationCenter.default.post(name: NotificationName.updateFriendInfo, object: u) } navigationController?.pushViewController(remark, animated: true) @@ -359,3 +367,13 @@ public class ContactUserViewController: ContactBaseViewController, UITableViewDe } } } + +extension ContactUserViewController: NIMSystemNotificationManagerDelegate { + public func onReceive(_ notification: NIMSystemNotification) { + if notification.type == .friendAdd, + let obj = notification.attachment as? NIMUserAddAttachment, + obj.operationType == .verify { + loadData() + } + } +} diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewController/ValidationMessageViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewController/ValidationMessageViewController.swift index 4b626437..fececd04 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewController/ValidationMessageViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewController/ValidationMessageViewController.swift @@ -22,17 +22,35 @@ public class ValidationMessageViewController: ContactBaseViewController { emptyView.setttingContent(content: localizable("no_validation_message")) // viewModel.getValidationMessage() setupUI() + loadData() + weak var weakSelf = self - viewModel.getValidationMessage { - NELog.infoLog(ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "✅ getValidationMessage SUCCESS") + viewModel.dataRefresh = { + weakSelf?.emptyView.isHidden = (weakSelf?.viewModel.datas.count ?? 0) > 0 weakSelf?.tableView.reloadData() } - viewModel.dataRefresh = { + NotificationCenter.default.addObserver(self, selector: #selector(appEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) + } + + // 返回上一级页面 + override func backToPrevious() { + super.backToPrevious() + viewModel.clearNotiUnreadCount() + } + + func appEnterBackground() { + viewModel.clearNotiUnreadCount() + loadData() + } + + 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() } - emptyView.isHidden = viewModel.datas.count > 0 } func setupUI() { @@ -77,13 +95,10 @@ public class ValidationMessageViewController: ContactBaseViewController { } func clearMessage() { - weak var weakSelf = self - showAlert(message: localizable("clear_all_validate_message")) { - weakSelf?.viewModel.clearAllNoti { - NELog.infoLog(ModuleName + " " + self.tag, desc: "✅ clearAllNoti SUCCESS") - weakSelf?.tableView.reloadData() - weakSelf?.emptyView.isHidden = false - } + viewModel.clearAllNoti { + NELog.infoLog(ModuleName + " " + self.tag, desc: "✅ clearAllNoti SUCCESS") + tableView.reloadData() + emptyView.isHidden = false } } } @@ -113,6 +128,27 @@ extension ValidationMessageViewController: UITableViewDelegate, UITableViewDataS } extension ValidationMessageViewController: SystemNotificationCellDelegate { + func changeValidationStatus(notifiModel: XNotification, notiStatus: IMHandleStatus) { + var notifiModels = [XNotification]() + if let msgList = notifiModel.msgList, + msgList.count > 0 { + for msg in msgList { + notifiModels.append(msg) + } + } + + 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!) + } + notifiModel.unReadCount = 0 + loadData() + } + func onAccept(_ notifiModel: XNotification) { weak var weakSelf = self guard let teamId = notifiModel.targetID, let invitorId = notifiModel.sourceID else { @@ -122,29 +158,37 @@ extension ValidationMessageViewController: SystemNotificationCellDelegate { if notifiModel.type == .teamInvite { viewModel.acceptInviteWithTeam(teamId, invitorId) { error in NELog.infoLog( - ModuleName + " " + self.tag, + ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "CALLBACK acceptInviteWithTeam " + (error?.localizedDescription ?? "no error") ) - if error != nil { - NELog.infoLog(ModuleName + " " + self.tag, desc: "❌CALLBACK acceptInviteWithTeam failed,error = \(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 == 803 { + weakSelf?.showToast(localizable("team_not_exist")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } } else { - notifiModel.handleStatus = .HandleTypeOk - notifiModel.imNotification?.handleStatus = 1 - weakSelf?.tableView.reloadData() + weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) } } } else if notifiModel.type == .addFriendRequest { viewModel.agreeRequest(invitorId) { error in NELog.infoLog( - ModuleName + " " + self.tag, + ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "CALLBACK agreeRequest " + (error?.localizedDescription ?? "no error") ) - if error != nil { + if let err = error { NELog.infoLog(ModuleName + " " + self.tag, desc: "❌CALLBACK agreeRequest failed,error = \(error!)") + weakSelf?.showToast(localizable("failed_operation")) } else { - notifiModel.handleStatus = .HandleTypeOk - notifiModel.imNotification?.handleStatus = 1 - weakSelf?.tableView.reloadData() + weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeOk) + + Router.shared.use(ChatAddFriendRouter, parameters: ["text": localizable("let_us_chat"), + "sessionId": invitorId, + "sessionType": NIMSessionType.P2P]) } } } @@ -159,29 +203,38 @@ extension ValidationMessageViewController: SystemNotificationCellDelegate { if notifiModel.type == .teamInvite { weakSelf?.viewModel.rejectInviteWithTeam(teamId, invitorId) { error in NELog.infoLog( - ModuleName + " " + self.tag, + ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "CALLBACK rejectInviteWithTeam " + (error?.localizedDescription ?? "no error") ) - if error != nil { - NELog.infoLog(ModuleName + " " + self.tag, desc: "❌CALLBACK rejectInviteWithTeam failed,error = \(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 == 803 { + weakSelf?.showToast(localizable("team_not_exist")) + } else { + weakSelf?.showToast(localizable("failed_operation")) + } } else { - notifiModel.handleStatus = .HandleTypeNo - notifiModel.imNotification?.handleStatus = 2 - weakSelf?.tableView.reloadData() + weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeNo) } } } else if notifiModel.type == .addFriendRequest { viewModel.refuseRequest(invitorId) { error in NELog.infoLog( - ModuleName + " " + self.tag, + ModuleName + " " + (weakSelf?.tag ?? "ValidationMessageViewController"), desc: "CALLBACK refuseRequest " + (error?.localizedDescription ?? "no error") ) - if error != nil { - NELog.infoLog(ModuleName + " " + self.tag, desc: "❌CALLBACK agreeRequest failed,error = \(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 { - notifiModel.handleStatus = .HandleTypeNo - notifiModel.imNotification?.handleStatus = 2 - weakSelf?.tableView.reloadData() + weakSelf?.changeValidationStatus(notifiModel: notifiModel, notiStatus: .HandleTypeNo) } } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift index 7247284f..ded62392 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/ViewModel/ValidationMessageViewModel.swift @@ -56,29 +56,83 @@ public class ValidationMessageViewModel: NSObject, ContactRepoSystemNotiDelegate // } // } + func isExist(xNoti: inout XNotification, list: inout [XNotification]) -> Bool { + for loopList in list { + if xNoti.isEqualTo(noti: loopList) { + if loopList.msgList == nil { + loopList.msgList = [xNoti] + } else { + loopList.msgList!.append(xNoti) + } + loopList.teamInfo = xNoti.teamInfo + loopList.userInfo = xNoti.userInfo + if let loopTime = loopList.timestamp, + let xNotiTime = xNoti.timestamp, + loopTime < xNotiTime { + loopList.timestamp = xNoti.timestamp + } + if !(xNoti.read ?? false) { + loopList.unReadCount += 1 + } + return true + } + } + if !(xNoti.read ?? false) { + xNoti.unReadCount += 1 + } + return false + } + public func onRecieveNotification(_ notification: XNotification) { NELog.infoLog(ModuleName + " " + className, desc: #function) -// if notification.type == .addFriendDirectly { -// datas.insert(notification, at: 0) -// } - datas.insert(notification, at: 0) - contactRepo.clearNotificationUnreadCount() + 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: () -> Void) { + func getValidationMessage(_ completin: @escaping () -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function) - let data = contactRepo.getNotificationList(limit: 500) - datas = data - if datas.count > 0 { + contactRepo.getNotificationList(limit: 500) { [weak self] xNotiList in + var data = [XNotification]() + let dateNow = Date().timeIntervalSince1970 + for xNoti in xNotiList { + var noti = xNoti + + // 过期事件:7天(10080s) + if noti.handleStatus == .HandleTypePending, + dateNow - (noti.timestamp ?? 0) > 10080 { + 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") + } completin() - } else { - NELog.warn(ModuleName + " " + className, desc: "⚠️NotificationList is empty") } } + func clearNotiUnreadCount() { + contactRepo.clearNotificationUnreadCount() + } + + func clearSingleNotifyUnreadCount(notification: NIMSystemNotification) { + contactRepo.clearSingleNotifyUnreadCount(notification: notification) + } + func clearAllNoti(_ completion: () -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function) contactRepo.clearNotification() diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/BaseValidationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/BaseValidationCell.swift index ed7c487a..6ece33fe 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/BaseValidationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/BaseValidationCell.swift @@ -34,10 +34,17 @@ public class BaseValidationCell: ContactBaseViewCell { func setupUI() { setupCommonCircleHeader() + contentView.addSubview(redAngleView) + NSLayoutConstraint.activate([ + redAngleView.centerXAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: -8), + redAngleView.centerYAnchor.constraint(equalTo: avatarImage.topAnchor, constant: 8), + redAngleView.heightAnchor.constraint(equalToConstant: 18), + ]) + addSubview(titleLabel) NSLayoutConstraint.activate([ titleLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 10), - titleLabel.centerYAnchor.constraint(equalTo: avatarImage.centerYAnchor), + titleLabel.topAnchor.constraint(equalTo: avatarImage.topAnchor), ]) titleLabelRightMargin = titleLabel.rightAnchor.constraint( equalTo: contentView.rightAnchor, @@ -45,6 +52,13 @@ public class BaseValidationCell: ContactBaseViewCell { ) titleLabelRightMargin?.isActive = true + addSubview(optionLabel) + NSLayoutConstraint.activate([ + optionLabel.leftAnchor.constraint(equalTo: avatarImage.rightAnchor, constant: 10), + optionLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor), + optionLabel.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -180), + ]) + let line = UIView() addSubview(line) line.translatesAutoresizingMaskIntoConstraints = false @@ -58,7 +72,7 @@ public class BaseValidationCell: ContactBaseViewCell { } public func confige(_ model: XNotification) { - var titleLabelContent = "" + var optionLabelContent = "" var nickName = "" var teamName = "" // 设置操作者名称 @@ -82,10 +96,10 @@ public class BaseValidationCell: ContactBaseViewCell { // 设置头像 if let headerUrl = model.userInfo?.userInfo?.avatarUrl, !headerUrl.isEmpty { avatarImage.sd_setImage(with: URL(string: headerUrl), completed: nil) - titleLabel.text = "" + nameLabel.text = "" } else if let teamUrl = model.teamInfo?.avatarUrl, !teamUrl.isEmpty { avatarImage.sd_setImage(with: URL(string: teamUrl), completed: nil) - titleLabel.text = "" + nameLabel.text = "" } else { // 无头像设置其name if !nickName.isEmpty { @@ -98,42 +112,54 @@ public class BaseValidationCell: ContactBaseViewCell { avatarImage.sd_setImage(with: URL(string: ""), completed: nil) } + // 设置未读状态(未读数角标+底色) + redAngleView.isHidden = true + contentView.backgroundColor = .white + if model.unReadCount > 0 { + contentView.backgroundColor = UIColor(hexString: "0xF3F5F7") + if model.unReadCount > 1 { + redAngleView.isHidden = false + redAngleView.text = model.unReadCount > 99 ? "99+" : "\(model.unReadCount)" + } + } + if let t = model.targetName { teamName = t } if let type = model.type { switch type { case .teamApply: - titleLabelContent = "\(nickName) 申请入群" + optionLabelContent = "申请加入群聊 \"\(teamName)\"" case .teamApplyReject: - titleLabelContent = "\(nickName) 拒绝入群" + optionLabelContent = "拒绝入群邀请 \"\(teamName)\"" case .teamInvite: - titleLabelContent = "\(nickName) 邀请你加入 \(teamName)" + optionLabelContent = "邀请您加入群聊 \"\(teamName)\"" case .teamInviteReject: - titleLabelContent = "\(nickName) 拒绝入群邀请" + optionLabelContent = "拒绝入群邀请 \"\(teamName)\"" case .superTeamApply: - titleLabelContent = "\(nickName) 申请加入超大群" + optionLabelContent = "申请加入超大群" case .superTeamApplyReject: - titleLabelContent = "\(nickName) 拒绝加入超大群" + optionLabelContent = "拒绝加入超大群" case .superTeamInvite: - titleLabelContent = "\(nickName) 邀请加入 \(teamName) 群" + optionLabelContent = "邀请您加入群聊 \"\(teamName)\"" case .superTeamInviteReject: - titleLabelContent = "\(nickName) 拒绝加入 \(teamName) 群" + optionLabelContent = "拒绝入群邀请 \"\(teamName)\"" case .addFriendDirectly: - titleLabelContent = "\(nickName) 添加你为好友" + optionLabelContent = "添加您为好友" case .addFriendRequest: - titleLabelContent = "\(nickName) 好友申请" + optionLabelContent = "好友申请" case .addFriendVerify: - titleLabelContent = "\(nickName) 通过好友申请" + optionLabelContent = "同意了你的好友请求" case .addFriendReject: - titleLabelContent = "\(nickName) 拒绝好友申请" + optionLabelContent = "拒绝了你的好友请求" @unknown default: - titleLabelContent = "\(nickName) 未知操作" + optionLabelContent = "未知操作" } } - titleLabel.text = titleLabelContent + titleLabel.text = nickName + optionLabel.text = optionLabelContent } } diff --git a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/SystemNotificationCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/SystemNotificationCell.swift index 61252d0f..af25da2d 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/SystemNotificationCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Validation/Views/SystemNotificationCell.swift @@ -36,12 +36,11 @@ public class SystemNotificationCell: BaseValidationCell { override func setupUI() { super.setupUI() - titleLabel.numberOfLines = 1 contentView.addSubview(agreeBtn) contentView.addSubview(rejectBtn) contentView.addSubview(resultImage) contentView.addSubview(resultLabel) - resultLabel.text = "asdasdsadsa" + resultLabel.text = "" NSLayoutConstraint.activate([ agreeBtn.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), agreeBtn.rightAnchor.constraint(equalTo: contentView.rightAnchor, constant: -20), @@ -87,10 +86,13 @@ public class SystemNotificationCell: BaseValidationCell { 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 = "" } diff --git a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift index ff40e3c7..fc6b092b 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/ViewModel/ContactViewModel.swift @@ -43,10 +43,10 @@ public class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { public func onRecieveNotification(_ notification: XNotification) {} - func loadData(_ filters: Set? = nil, completion: @escaping (NSError?) -> Void) { + func loadData(fetch: Bool = false, _ filters: Set? = nil, completion: @escaping (NSError?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function) weak var weakSelf = self - getContactList(filters) { contacts, error in + getContactList(fetch, filters) { contacts, error in if let users = contacts { NELog.infoLog("contact loadData", desc: "contact data:\(contacts)") weakSelf?.contacts = users @@ -59,11 +59,15 @@ public class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { } } - func getContactList(_ filters: Set? = nil, _ completion: @escaping ([ContactSection]?, NSError?) -> Void) { + func reLoadData(completion: @escaping (NSError?) -> Void) { + loadData(fetch: true, completion: completion) + } + + func getContactList(_ fetch: Bool = false, _ filters: Set? = nil, _ completion: @escaping ([ContactSection]?, NSError?) -> Void) { NELog.infoLog(ModuleName + " " + className, desc: #function + ", filters.count: \(filters?.count ?? 0)") var contactList: [ContactSection] = [] weak var weakSelf = self - contactRepo.getFriendList { [self] friends, error in + contactRepo.getFriendList(fetch) { friends, error in if var users = friends { NELog.infoLog("contact bar getFriendList", desc: "friend count:\(friends?.count)") weakSelf?.initalDict = [String: [ContactInfo]]() @@ -117,6 +121,10 @@ public class ContactViewModel: NSObject, ContactRepoSystemNotiDelegate { s1.user!.showName()! < s2.user!.showName()! } + guard let initalDict = weakSelf?.initalDict else { + return + } + for key in initalDict.keys { if var value = weakSelf?.initalDict[key] { value.sort { s1, s2 in diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/ContactTableViewCell.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/ContactTableViewCell.swift index ea7e106a..aac37ebe 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/ContactTableViewCell.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/ContactTableViewCell.swift @@ -9,7 +9,7 @@ import Foundation import NECoreKit @objcMembers -public class ContactTableViewCell: ContactBaseViewCell, ContactCellDataProtrol { +open class ContactTableViewCell: ContactBaseViewCell, ContactCellDataProtrol { public lazy var arrow: UIImageView = { let imageView = UIImageView(image: UIImage.ne_imageNamed(name: "arrowRight")) imageView.translatesAutoresizingMaskIntoConstraints = false @@ -17,27 +17,13 @@ public class ContactTableViewCell: ContactBaseViewCell, ContactCellDataProtrol { return imageView }() - lazy var redAngleView: RedAngleLabel = { - let label = RedAngleLabel() - label.translatesAutoresizingMaskIntoConstraints = false - label.font = UIFont.systemFont(ofSize: 12.0) - label.textColor = .white - label.text = "1" - label.backgroundColor = UIColor(hexString: "F24957") - label.textInsets = UIEdgeInsets(top: 3, left: 7, bottom: 3, right: 7) - label.layer.cornerRadius = 9 - label.clipsToBounds = true - label.isHidden = true - return label - }() - - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) commonUI() initSubviewsLayout() } - required init?(coder: NSCoder) { + public required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -83,7 +69,7 @@ public class ContactTableViewCell: ContactBaseViewCell, ContactCellDataProtrol { nameLabel.textColor = UIColor.white } - public func setModel(_ model: ContactInfo) { + open func setModel(_ model: ContactInfo) { guard let user = model.user else { return } @@ -92,7 +78,7 @@ public class ContactTableViewCell: ContactBaseViewCell, ContactCellDataProtrol { if model.contactCellType == 2 { // person titleLabel.text = user.showName() - nameLabel.text = user.shortName(count: 2) + nameLabel.text = user.shortName(showAlias: false, count: 2) if let imageUrl = user.userInfo?.avatarUrl, !imageUrl.isEmpty { NELog.infoLog("contact p2p cell configData", desc: "imageName:\(imageUrl)") @@ -102,6 +88,7 @@ public class ContactTableViewCell: ContactBaseViewCell, ContactCellDataProtrol { NELog.infoLog("contact p2p cell configData", desc: "imageName is nil") nameLabel.isHidden = false avatarImage.sd_setImage(with: nil) + avatarImage.backgroundColor = model.headerBackColor } arrow.isHidden = true diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/ContactsSelectedViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/ContactsSelectedViewController.swift index e0d6cd5b..f4b35993 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/ContactsSelectedViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/ContactsSelectedViewController.swift @@ -12,7 +12,7 @@ open class ContactsSelectedViewController: ContactBaseViewController { public var callBack: ContactsSelectCompletion? public var filterUsers: Set? - + var lastTitleIndex = 0 public var limit = 10 // max select count // 单聊中对方的userId @@ -252,6 +252,16 @@ extension ContactsSelectedViewController: UICollectionViewDelegate, UICollection viewModel.indexs } + public 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 + } + // MARK: Collection View DataSource And Delegate public func collectionView(_ collectionView: UICollectionView, diff --git a/NEContactUIKit/NEContactUIKit/Classes/Views/ContactsViewController.swift b/NEContactUIKit/NEContactUIKit/Classes/Views/ContactsViewController.swift index bff4da75..a167069f 100644 --- a/NEContactUIKit/NEContactUIKit/Classes/Views/ContactsViewController.swift +++ b/NEContactUIKit/NEContactUIKit/Classes/Views/ContactsViewController.swift @@ -7,18 +7,54 @@ import UIKit import NECoreIMKit import NECoreKit +public protocol ContactsViewControllerDelegate { + func onDataLoaded() +} + @objcMembers open class ContactsViewController: UIViewController, UITableViewDelegate, UITableViewDataSource, SystemMessageProviderDelegate, FriendProviderDelegate { + public var delegate: ContactsViewControllerDelegate? + + // custom ui cell public var customCells: [Int: ContactTableViewCell.Type] = [ ContactCellType.ContactPerson.rawValue: ContactTableViewCell.self, ContactCellType.ContactOthers.rawValue: ContactTableViewCell.self, - ] // custom ui cell + ] public var clickCallBacks = [Int: ConttactClickCallBack]() + var lastTitleIndex = 0 + + public var topViewHeight: CGFloat = 0 { + didSet { + topViewHeightAnchor?.constant = topViewHeight + topView.isHidden = topViewHeight <= 0 + } + } + + public var topViewHeightAnchor: NSLayoutConstraint? + public lazy var topView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + view.backgroundColor = .clear + 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.rowHeight = 52 + tableView.sectionHeaderHeight = 40 + tableView.sectionFooterHeight = 0 + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + return tableView + }() - var tableView = UITableView(frame: .zero, style: .grouped) - var viewModel = ContactViewModel(contactHeaders: [ + public var viewModel = ContactViewModel(contactHeaders: [ ContactHeadItem( name: localizable("validation_message"), imageName: "valid", @@ -38,8 +74,7 @@ open class ContactsViewController: UIViewController, UITableViewDelegate, UITabl color: UIColor(hexString: "#BE65D9") ), ]) - - public init() { + 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) @@ -59,78 +94,86 @@ open class ContactsViewController: UIViewController, UITableViewDelegate, UITabl // 添加UI addNavbarAction() commonUI() + } + + override open func viewWillAppear(_ animated: Bool) { // 刷新数据 - viewModel.loadData { error in + viewModel.reLoadData { [weak self] error in if error == nil { - weakSelf?.tableView.reloadData() + self?.delegate?.onDataLoaded() + self?.tableView.reloadData() } } } open func commonUI() { - tableView.separatorStyle = .none - tableView.delegate = self - tableView.dataSource = self - tableView.translatesAutoresizingMaskIntoConstraints = false - tableView.backgroundColor = UIColor.ne_backgroundColor + view.addSubview(topView) + NSLayoutConstraint.activate([ + topView.topAnchor.constraint(equalTo: view.topAnchor), + topView.leftAnchor.constraint(equalTo: view.leftAnchor), + topView.rightAnchor.constraint(equalTo: view.rightAnchor), + ]) + topViewHeightAnchor = topView.heightAnchor.constraint(equalToConstant: topViewHeight) + topViewHeightAnchor?.isActive = true view.addSubview(tableView) NSLayoutConstraint.activate([ - tableView.topAnchor.constraint(equalTo: view.topAnchor), tableView.leftAnchor.constraint(equalTo: view.leftAnchor), tableView.rightAnchor.constraint(equalTo: view.rightAnchor), + tableView.topAnchor.constraint(equalTo: topView.bottomAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - tableView.register( - ContactTableViewCell.self, - forCellReuseIdentifier: "\(ContactTableViewCell.self)" - ) + tableView.register( ContactSectionView.self, forHeaderFooterViewReuseIdentifier: "\(NSStringFromClass(ContactSectionView.self))" ) - tableView.rowHeight = 52 - tableView.sectionHeaderHeight = 40 - tableView.sectionFooterHeight = 0 - tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) + + customCells.forEach { (key: Int, value: ContactTableViewCell.Type) in + tableView.register(value, forCellReuseIdentifier: "\(key)") + } } open func loadData() { - viewModel.loadData { [self] error in - tableView.reloadData() + viewModel.loadData { [weak self] error in + if error == nil { + self?.delegate?.onDataLoaded() + self?.tableView.reloadData() + } } } // UITableViewDataSource - public func numberOfSections(in tableView: UITableView) -> Int { + open func numberOfSections(in tableView: UITableView) -> Int { viewModel.contacts.count } - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + 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 } - public func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { let info = viewModel.contacts[indexPath.section].contacts[indexPath.row] - let cell = tableView.dequeueReusableCell( - withIdentifier: "\(ContactTableViewCell.self)", - for: indexPath - ) as! ContactTableViewCell - 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 + var reusedId = "\(info.contactCellType)" + let cell = tableView.dequeueReusableCell(withIdentifier: reusedId, for: indexPath) + + if let c = cell as? ContactTableViewCell { + c.setModel(info) + if indexPath.section == 0, indexPath.row == 0, viewModel.unreadCount > 0 { + c.redAngleView.isHidden = false + c.redAngleView.text = viewModel.unreadCount > 99 ? "99+" : "\(viewModel.unreadCount)" + } else { + c.redAngleView.isHidden = true + } } return cell } - public func tableView(_ tableView: UITableView, - viewForHeaderInSection section: Int) -> UIView? { + open func tableView(_ tableView: UITableView, + viewForHeaderInSection section: Int) -> UIView? { let sectionView: ContactSectionView = tableView .dequeueReusableHeaderFooterView( withIdentifier: "\(NSStringFromClass(ContactSectionView.self))" @@ -139,29 +182,35 @@ open class ContactsViewController: UIViewController, UITableViewDelegate, UITabl return sectionView } - public func tableView(_ tableView: UITableView, - heightForHeaderInSection section: Int) -> CGFloat { + open func tableView(_ tableView: UITableView, + heightForHeaderInSection section: Int) -> CGFloat { if viewModel.contacts[section].initial.count > 0 { return 40 } return 0 } - public func sectionIndexTitles(for tableView: UITableView) -> [String]? { + open func sectionIndexTitles(for tableView: UITableView) -> [String]? { viewModel.indexs } - public func tableView(_ tableView: UITableView, sectionForSectionIndexTitle title: String, - at index: Int) -> Int { - index + 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 } - public func tableView(_ tableView: UITableView, - heightForRowAt indexPath: IndexPath) -> CGFloat { + open func tableView(_ tableView: UITableView, + heightForRowAt indexPath: IndexPath) -> CGFloat { 52 } - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let info = viewModel.contacts[indexPath.section].contacts[indexPath.row] if let callBack = clickCallBacks[info.contactCellType] { callBack(indexPath.row, indexPath.section) @@ -170,7 +219,6 @@ open class ContactsViewController: UIViewController, UITableViewDelegate, UITabl if info.contactCellType == ContactCellType.ContactOthers.rawValue { switch info.router { case ValidationMessageRouter: - viewModel.contactRepo.clearNotificationUnreadCount() let validationController = ValidationMessageViewController() validationController.hidesBottomBarWhenPushed = true navigationController?.pushViewController(validationController, animated: true) @@ -241,7 +289,7 @@ open class ContactsViewController: UIViewController, UITableViewDelegate, UITabl } extension ContactsViewController { - private func addNavbarAction() { + open func addNavbarAction() { edgesForExtendedLayout = [] let addItem = UIBarButtonItem( image: UIImage.ne_imageNamed(name: "add"), diff --git a/NEConversationUIKit/NEConversationUIKit.podspec b/NEConversationUIKit/NEConversationUIKit.podspec index 623dbfd0..30805f11 100644 --- a/NEConversationUIKit/NEConversationUIKit.podspec +++ b/NEConversationUIKit/NEConversationUIKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEConversationUIKit' - s.version = '9.2.10' + s.version = '9.5.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. diff --git a/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings b/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings index 8a06b04e..8f6c02ff 100644 --- a/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings +++ b/NEConversationUIKit/NEConversationUIKit/Assets/en.lproj/Localizable.strings @@ -33,6 +33,9 @@ "message_recalled"="message recalled"; +"you_were_mentioned"="[You were mentioned]"; + + diff --git a/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings index 8b4c288f..dd37bead 100644 --- a/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NEConversationUIKit/NEConversationUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -32,3 +32,5 @@ "appName"="云信IM"; "message_recalled"="此消息已撤回"; + +"you_were_mentioned"="[有人@我]"; diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/ConversationController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/ConversationController.swift index 90e41191..d1356088 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/ConversationController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/ConversationController.swift @@ -10,15 +10,15 @@ import NIMSDK @objcMembers open class ConversationController: UIViewController, NIMChatManagerDelegate { - let viewmodel = ConversationViewModel() - private var listCtrl = ConversationListViewController() + public let viewmodel = ConversationViewModel() + public var listCtrl = ConversationListViewController() override public func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) navigationController?.isNavigationBarHidden = true } - override public func viewDidLoad() { + override open func viewDidLoad() { super.viewDidLoad() setupSubviews() NIMSDK.shared().chatManager.add(self) @@ -51,7 +51,7 @@ open class ConversationController: UIViewController, NIMChatManagerDelegate { // MARK: lazyMethod - private lazy var navView: ConversationNavView = { + public lazy var navView: ConversationNavView = { let nav = ConversationNavView(frame: CGRect.zero) nav.translatesAutoresizingMaskIntoConstraints = false nav.backgroundColor = .white @@ -68,7 +68,7 @@ open class ConversationController: UIViewController, NIMChatManagerDelegate { } extension ConversationController: ConversationNavViewDelegate { - func searchAction() { + open func searchAction() { Router.shared.use( SearchContactPageRouter, parameters: ["nav": navigationController as Any], @@ -76,7 +76,7 @@ extension ConversationController: ConversationNavViewDelegate { ) } - func didClickAddBtn() { + open func didClickAddBtn() { print("add click") if children.contains(popListController) == false { diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/ConversationListViewController.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/ConversationListViewController.swift index 8f320252..71ea94ed 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/ConversationListViewController.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/Controller/ConversationListViewController.swift @@ -6,23 +6,26 @@ import UIKit import NIMSDK +public protocol ConversationListViewControllerDelegate { + func onDataLoaded() +} + @objcMembers open class ConversationListViewController: UIViewController { - private var viewModel = ConversationViewModel() + public var viewModel = ConversationViewModel() private let className = "ConversationListViewController" - private var tableViewTopConstraint: NSLayoutConstraint? - private lazy var emptyView: NEEmptyDataView = { - let view = NEEmptyDataView( - imageName: "user_empty", - content: localizable("session_empty"), - frame: CGRect.zero - ) - view.translatesAutoresizingMaskIntoConstraints = false - view.isHidden = true - return view + public var topViewHeight: CGFloat = 0 { + didSet { + topViewHeightAnchor?.constant = topViewHeight + topView.isHidden = topViewHeight <= 0 + } + } - }() + public var topViewHeightAnchor: NSLayoutConstraint? + public var delegate: ConversationListViewControllerDelegate? + + public var registerCellDic = [0: ConversationListCell.self] override open func viewDidLoad() { super.viewDidLoad() @@ -41,35 +44,52 @@ open class ConversationListViewController: UIViewController { if let infos = sessionInfos { weakSelf?.viewModel.stickTopInfos = infos weakSelf?.reloadTableView() + weakSelf?.delegate?.onDataLoaded() } } + } + + open func initialConfig() { + viewModel.delegate = self + weak var weakSelf = self NEChatDetectNetworkTool.shareInstance.netWorkReachability { status in if status == .notReachable { weakSelf?.brokenNetworkView.isHidden = false - weakSelf?.tableViewTopConstraint?.constant = 36 + weakSelf?.topView.isHidden = true + weakSelf?.topViewHeightAnchor?.constant = 36 } else { weakSelf?.brokenNetworkView.isHidden = true - weakSelf?.tableViewTopConstraint?.constant = 0 + weakSelf?.topView.isHidden = false + weakSelf?.topViewHeightAnchor?.constant = weakSelf?.topViewHeight ?? 0 } } } - open func initialConfig() { - viewModel.delegate = self - } - open func setupSubviews() { + view.addSubview(topView) view.addSubview(tableView) view.addSubview(emptyView) view.addSubview(brokenNetworkView) + NSLayoutConstraint.activate([ + topView.rightAnchor.constraint(equalTo: view.rightAnchor), + topView.leftAnchor.constraint(equalTo: view.leftAnchor), + topView.topAnchor.constraint(equalTo: view.topAnchor), + + ]) + topViewHeightAnchor = topView.heightAnchor.constraint(equalToConstant: topViewHeight) + topViewHeightAnchor?.isActive = true + NSLayoutConstraint.activate([ tableView.rightAnchor.constraint(equalTo: view.rightAnchor), tableView.leftAnchor.constraint(equalTo: view.leftAnchor), + tableView.topAnchor.constraint(equalTo: topView.bottomAnchor), tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor), ]) - tableViewTopConstraint = tableView.topAnchor.constraint(equalTo: view.topAnchor) - tableViewTopConstraint?.isActive = true + + registerCellDic.forEach { (key: Int, value: ConversationListCell.Type) in + tableView.register(value, forCellReuseIdentifier: "\(key)") + } NSLayoutConstraint.activate([ emptyView.topAnchor.constraint(equalTo: tableView.topAnchor, constant: 100), @@ -93,6 +113,7 @@ open class ConversationListViewController: UIViewController { if recentList.count > 0 { weakSelf?.emptyView.isHidden = true weakSelf?.reloadTableView() + weakSelf?.delegate?.onDataLoaded() } else { weakSelf?.emptyView.isHidden = false } @@ -110,28 +131,43 @@ open class ConversationListViewController: UIViewController { // MARK: lazy method - private lazy var tableView: UITableView = { + public lazy var topView: UIView = { + let view = UIView() + view.translatesAutoresizingMaskIntoConstraints = false + 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.register( - ConversationListCell.self, - forCellReuseIdentifier: "\(NSStringFromClass(ConversationListCell.self))" - ) tableView.rowHeight = 62 tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 0.1)) tableView.backgroundColor = .white return tableView }() - private lazy var brokenNetworkView: NEBrokenNetworkView = { + public lazy var brokenNetworkView: NEBrokenNetworkView = { let view = NEBrokenNetworkView(frame: CGRect(x: 0, y: 0, width: NEConstant.screenWidth, height: 36)) view.isHidden = true 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 + return view + + }() } // MARK: ====================== private method=========================== @@ -195,28 +231,28 @@ extension ConversationListViewController { } extension ConversationListViewController: UITableViewDelegate, UITableViewDataSource { - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + open func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { let count = viewModel.conversationListArray?.count ?? 0 NELog.infoLog(ModuleName + " " + "ConversationListViewController", desc: "numberOfRowsInSection count : \(count)") return count } - public func tableView(_ tableView: UITableView, - cellForRowAt indexPath: IndexPath) -> UITableViewCell { - let cell = tableView.dequeueReusableCell( - withIdentifier: "\(NSStringFromClass(ConversationListCell.self))", - for: indexPath - ) as! ConversationListCell - if let count = viewModel.conversationListArray?.count, count > indexPath.row { - let conversationModel = viewModel.conversationListArray?[indexPath.row] - cell.topStickInfos = viewModel.stickTopInfos - cell.configData(sessionModel: conversationModel) + open func tableView(_ tableView: UITableView, + cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let model = viewModel.conversationListArray?[indexPath.row] + var reusedId = "\(model?.customType ?? 0)" + let cell = tableView.dequeueReusableCell(withIdentifier: reusedId, for: indexPath) + + if let c = cell as? ConversationListCell { + c.topStickInfos = viewModel.stickTopInfos + c.configData(sessionModel: model) } + return cell } - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + open func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let conversationModel = viewModel.conversationListArray?[indexPath.row] guard let sid = conversationModel?.recentSession?.session?.sessionId else { @@ -228,8 +264,8 @@ extension ConversationListViewController: UITableViewDelegate, UITableViewDataSo onselectedTableRow(sessionType: sessionType, sessionId: sid, indexPath: indexPath) } - public func tableView(_ tableView: UITableView, - editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { + open func tableView(_ tableView: UITableView, + editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]? { weak var weakSelf = self var rowActions = [UITableViewRowAction]() @@ -240,44 +276,19 @@ extension ConversationListViewController: UITableViewDelegate, UITableViewDataSo } let deleteAction = UITableViewRowAction(style: .destructive, - title: localizable("delete")) { action, indexPath in - - weakSelf?.viewModel.deleteRecentSession(recentSession: recentSession) - weakSelf?.didDeleteConversationCell( - model: conversationModel ?? ConversationListModel(), - indexPath: indexPath - ) + title: NEKitConversationConfig.shared.ui.deleteBottonTitle) { action, indexPath in + weakSelf?.deleteActionHandler(action: action, indexPath: indexPath) } // 置顶和取消置顶 let isTop = viewModel.stickTopInfos[session] != nil let topAction = UITableViewRowAction(style: .destructive, - title: isTop ? localizable("cancel_stickTop") : - localizable("stickTop")) { action, indexPath in - if let recentSesstion = conversationModel?.recentSession { - weakSelf?.onTopRecentAtIndexPath( - rencent: recentSesstion, - indexPath: indexPath, - isTop: isTop - ) { error, sessionInfo in - if error == nil { - if isTop { - weakSelf?.didRemoveStickTopSession( - model: conversationModel ?? ConversationListModel(), - indexPath: indexPath - ) - } else { - weakSelf?.didAddStickTopSession( - model: conversationModel ?? ConversationListModel(), - indexPath: indexPath - ) - } - } - } - } + title: isTop ? NEKitConversationConfig.shared.ui.stickTopBottonCancelTitle : + NEKitConversationConfig.shared.ui.stickTopBottonTitle) { action, indexPath in + weakSelf?.topActionHandler(action: action, indexPath: indexPath, isTop: isTop) } - deleteAction.backgroundColor = NEConstant.hexRGB(0xA8ABB6) - topAction.backgroundColor = NEConstant.hexRGB(0x337EFF) + deleteAction.backgroundColor = NEKitConversationConfig.shared.ui.deleteBottonColor + topAction.backgroundColor = NEKitConversationConfig.shared.ui.stickTopBottonColor rowActions.append(deleteAction) rowActions.append(topAction) @@ -316,6 +327,42 @@ extension ConversationListViewController: UITableViewDelegate, UITableViewDataSo return actionConfig } */ + + open func deleteActionHandler(action: UITableViewRowAction, indexPath: IndexPath) { + let conversationModel = viewModel.conversationListArray?[indexPath.row] + if let recentSession = conversationModel?.recentSession { + viewModel.deleteRecentSession(recentSession: recentSession) + didDeleteConversationCell( + model: conversationModel ?? ConversationListModel(), + indexPath: indexPath + ) + } + } + + open func topActionHandler(action: UITableViewRowAction, indexPath: IndexPath, isTop: Bool) { + let conversationModel = viewModel.conversationListArray?[indexPath.row] + if let recentSession = conversationModel?.recentSession { + onTopRecentAtIndexPath( + rencent: recentSession, + indexPath: indexPath, + isTop: isTop + ) { [weak self] error, sessionInfo in + if error == nil { + if isTop { + self?.didRemoveStickTopSession( + model: conversationModel ?? ConversationListModel(), + indexPath: indexPath + ) + } else { + self?.didAddStickTopSession( + model: conversationModel ?? ConversationListModel(), + indexPath: indexPath + ) + } + } + } + } + } } // MARK: UI UIKit提供的重写方法 @@ -379,6 +426,10 @@ extension ConversationListViewController: ConversationViewModelDelegate { tableView.reloadRows(at: [indexPath], with: .none) } + public func reloadData() { + delegate?.onDataLoaded() + } + public func reloadTableView() { emptyView.isHidden = (viewModel.conversationListArray?.count ?? 0) > 0 viewModel.sortRecentSession() diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ConversationRouter/ConversationRouter.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ConversationRouter/ConversationRouter.swift index d91689e6..b8b8d639 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ConversationRouter/ConversationRouter.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ConversationRouter/ConversationRouter.swift @@ -19,5 +19,11 @@ public class ConversationRouter: NSObject { 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/Conversation/View/ConversationListCell.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/View/ConversationListCell.swift index 8dac7f95..d6d18514 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/View/ConversationListCell.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/View/ConversationListCell.swift @@ -22,7 +22,7 @@ open class ConversationListCell: UITableViewCell { // Configure the view for the selected state } - override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { super.init(style: style, reuseIdentifier: reuseIdentifier) setupSubviews() initSubviewsLayout() @@ -94,7 +94,7 @@ open class ConversationListCell: UITableViewCell { } } - func configData(sessionModel: ConversationListModel?) { + open func configData(sessionModel: ConversationListModel?) { guard let conversationModel = sessionModel else { return } if conversationModel.recentSession?.session?.sessionType == .P2P { @@ -103,7 +103,7 @@ open class ConversationListCell: UITableViewCell { headImge.setTitle("") headImge.sd_setImage(with: URL(string: imageName), completed: nil) } else { - headImge.setTitle(conversationModel.userInfo?.showName() ?? "") + headImge.setTitle(conversationModel.userInfo?.showName(false) ?? "") headImge.sd_setImage(with: nil, completed: nil) headImge.backgroundColor = UIColor .colorWithString(string: conversationModel.userInfo?.userId) @@ -139,7 +139,18 @@ open class ConversationListCell: UITableViewCell { // last message if let lastMessage = conversationModel.recentSession?.lastMessage { - subTitle.text = contentForRecentSession(message: lastMessage) + let text = contentForRecentSession(message: lastMessage) + let mutaAttri = NSMutableAttributedString(string: text) + if let sessionId = sessionModel?.recentSession?.session?.sessionId { + let isAtMessage = NEAtMessageManager.instance.isAtCurrentUser(sessionId: sessionId) + if isAtMessage == true { + let atStr = localizable("you_were_mentioned") + mutaAttri.insert(NSAttributedString(string: atStr), at: 0) + mutaAttri.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.ne_redText, range: NSMakeRange(0, atStr.count)) + mutaAttri.addAttribute(NSAttributedString.Key.font, value: NEKitConversationConfig.shared.ui.subTitleFont, range: NSMakeRange(0, mutaAttri.length)) + } + } + subTitle.attributedText = mutaAttri // contentForRecentSession(message: lastMessage) } // unRead message count @@ -208,14 +219,14 @@ open class ConversationListCell: UITableViewCell { } } - func contentForRecentSession(message: NIMMessage) -> String { + open func contentForRecentSession(message: NIMMessage) -> String { let text = NEMessageUtil.messageContent(message: message) return text } // MARK: lazy Method - lazy var headImge: NEUserHeaderView = { + public lazy var headImge: NEUserHeaderView = { let headView = NEUserHeaderView(frame: .zero) headView.titleLabel.textColor = .white headView.titleLabel.font = NEConstant.defaultTextFont(14) @@ -225,7 +236,7 @@ open class ConversationListCell: UITableViewCell { return headView }() - private lazy var redAngleView: RedAngleLabel = { + public lazy var redAngleView: RedAngleLabel = { let label = RedAngleLabel() label.translatesAutoresizingMaskIntoConstraints = false label.font = NEConstant.defaultTextFont(12) @@ -239,7 +250,7 @@ open class ConversationListCell: UITableViewCell { return label }() - private lazy var title: UILabel = { + public lazy var title: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = NEKitConversationConfig.shared.ui.titleColor @@ -248,7 +259,7 @@ open class ConversationListCell: UITableViewCell { return label }() - private lazy var subTitle: UILabel = { + public lazy var subTitle: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = NEKitConversationConfig.shared.ui.subTitleColor @@ -256,7 +267,7 @@ open class ConversationListCell: UITableViewCell { return label }() - private lazy var timeLabel: UILabel = { + public lazy var timeLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false label.textColor = NEKitConversationConfig.shared.ui.timeColor @@ -265,7 +276,7 @@ open class ConversationListCell: UITableViewCell { return label }() - private lazy var notifyMsg: UIImageView = { + public lazy var notifyMsg: UIImageView = { let notify = UIImageView() notify.translatesAutoresizingMaskIntoConstraints = false notify.image = UIImage.ne_imageNamed(name: "noNeed_notify") diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/View/ConversationNavView.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/View/ConversationNavView.swift index 52696b99..61565fc1 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/View/ConversationNavView.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/View/ConversationNavView.swift @@ -6,14 +6,14 @@ import UIKit import NECoreKit -protocol ConversationNavViewDelegate: AnyObject { +public protocol ConversationNavViewDelegate: AnyObject { func didClickAddBtn() func searchAction() } @objcMembers public class ConversationNavView: UIView { - weak var delegate: ConversationNavViewDelegate? + public weak var delegate: ConversationNavViewDelegate? override init(frame: CGRect) { super.init(frame: frame) @@ -80,7 +80,7 @@ public class ConversationNavView: UIView { // MARK: lazy method - private lazy var brandBtn: UIButton = { + public lazy var brandBtn: UIButton = { let button = UIButton() button.setTitle(localizable("appName"), for: .normal) button.setImage(UIImage.ne_imageNamed(name: "brand_yunxin"), for: .normal) @@ -91,7 +91,7 @@ public class ConversationNavView: UIView { return button }() - private lazy var searchBtn: UIButton = { + public lazy var searchBtn: UIButton = { let button = UIButton() button.setImage(UIImage.ne_imageNamed(name: "chat_search"), for: .normal) button.translatesAutoresizingMaskIntoConstraints = false @@ -99,7 +99,7 @@ public class ConversationNavView: UIView { return button }() - private lazy var addBtn: ExpandButton = { + public lazy var addBtn: ExpandButton = { let button = ExpandButton() button.setImage(UIImage.ne_imageNamed(name: "chat_add"), for: .normal) button.translatesAutoresizingMaskIntoConstraints = false @@ -107,7 +107,7 @@ public class ConversationNavView: UIView { return button }() - private lazy var bottomLine: UIView = { + public lazy var bottomLine: UIView = { let view = UIView() view.translatesAutoresizingMaskIntoConstraints = false view.backgroundColor = UIColor(hexString: "0xDBE0E8") diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift index 109a0a05..61c9f5dd 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Conversation/ViewModel/ConversationViewModel.swift @@ -12,6 +12,7 @@ let revokeLocalMessageContent = "revoke_message_local_content" public protocol ConversationViewModelDelegate: NSObjectProtocol { func didAddRecentSession() func didUpdateRecentSession(index: Int) + func reloadData() func reloadTableView() } @@ -32,9 +33,16 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, super.init() repo.delegate = self repo.addSessionDelegate(delegate: self) + repo.chatProvider.addDelegate(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) + } + + func atMessageChange() { + NELog.infoLog(className(), desc: "atMessageChange") + delegate?.reloadTableView() } public func fetchServerSessions(option: NIMFetchServerSessionOption, @@ -140,6 +148,7 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, NIMSDK.shared().userManager.remove(self) repo.removeSessionDelegate(delegate: self) repo.removeTeamDelegate(delegate: self) + NotificationCenter.default.removeObserver(self) } // MARK: ======================== private method ============================== @@ -207,6 +216,23 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, } } + // MARK: ==================== NIMChatManagerDelegate ========================== + + public 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) + } + } + } + } + } + } + // MARK: ==================== ConversationRepoDelegate ========================== public func onNotifyAddStickTopSession(_ newInfo: NIMStickTopSessionInfo) { @@ -221,6 +247,20 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, delegate?.reloadTableView() } + public func onNotifySyncStickTopSessions(_ response: NIMSyncStickTopSessionResponse) { + loadStickTopSessionInfos { [weak self] error, sessionInfos in + if error != nil { + if let infos = self?.repo.getStickTopInfos() { + self?.stickTopInfos = infos + } + } else if let infos = sessionInfos { + self?.stickTopInfos = infos + } + self?.delegate?.reloadTableView() + self?.delegate?.reloadData() + } + } + public func didServerSessionUpdated(_ recentSession: NIMRecentSession?) {} // MARK: ====================NIMConversationManagerDelegate===================== @@ -298,6 +338,11 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, ModuleName + " " + className, desc: #function + "recentSession, didUpdate sessionId:" + (recentSession.session?.sessionId ?? "nil") ) + if let sessionId = recentSession.session?.sessionId { + if NEAtMessageManager.instance.isAtCurrentUser(sessionId: sessionId) == true { + NEAtMessageManager.instance.clearAtRecord(sessionId) + } + } if let object = recentSession.lastMessage?.messageObject as? NIMNotificationObject, object.notificationType == .team { if let content = object.content as? NIMTeamNotificationContent { @@ -347,8 +392,19 @@ public class ConversationViewModel: NSObject, ConversationRepoDelegate, } 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 : \(recentSession.session?.sessionId ?? "")" + ) + conversationListArray?.remove(at: i) break } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift b/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift index 0752a67d..ef933b89 100644 --- a/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift +++ b/NEConversationUIKit/NEConversationUIKit/Classes/ConversationConfig/ConversationUIConfig.swift @@ -46,4 +46,16 @@ public class ConversationUIConfig: NSObject { /// 时间字体大小 public var timeFont = UIFont.systemFont(ofSize: 12) + + /// 会话列表 cell 左划置顶按钮文案内容 + public var stickTopBottonTitle = localizable("stickTop") + /// 会话列表 cell 左划取消置顶按钮文案内容 + public var stickTopBottonCancelTitle = localizable("cancel_stickTop") + /// 会话列表 cell 左划置顶按钮文案颜色 + public var stickTopBottonColor = NEConstant.hexRGB(0x337EFF) + + /// 会话列表 cell 左划删除按钮文案内容 + public var deleteBottonTitle = localizable("delete") + /// 会话列表 cell 左划删除按钮文案颜色 + public var deleteBottonColor = NEConstant.hexRGB(0xA8ABB6) } diff --git a/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift b/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift new file mode 100644 index 00000000..cc56de0b --- /dev/null +++ b/NEConversationUIKit/NEConversationUIKit/Classes/Manager/NEAtMessageManager.swift @@ -0,0 +1,349 @@ +//// Copyright (c) 2022 NetEase, Inc. All rights reserved. +// Use of this source code is governed by a MIT license that can be +// found in the LICENSE file. + +import Foundation +import NIMSDK + +public let atAllKey = "ait_all" +public let yxAitMsg = "yxAitMsg" +public let AtMessageChangeNoti = "at_message_change_noti" + +@objcMembers +public class AtMessageModel: NSObject { + public var messageId: String? + public var messageTime: NSNumber? +} + +@objcMembers +public class AtMEMessageRecord: NSObject { + public var atMessages = [String: NSNumber]() + public var lastTime: NSNumber? + public var isRead = false +} + +@objcMembers +public class NEAtMessageManager: NSObject, NIMChatManagerDelegate, NIMLoginManagerDelegate { + public static let instance = NEAtMessageManager() + private let workQueue = DispatchQueue(label: "AtMessageWorkQueue") + private let lock = NSLock() + private var atMessageDic = [String: AtMEMessageRecord]() + private var currentAccid = "" + + override public init() { + super.init() + NIMSDK.shared().chatManager.add(self) + NIMSDK.shared().loginManager.add(self) + } + + deinit { + NIMSDK.shared().chatManager.remove(self) + NIMSDK.shared().loginManager.remove(self) + } + + public func onLogin(_ step: NIMLoginStep) { + if step == .loginOK { + NELog.infoLog(className(), desc: "login ok") + currentAccid = NIMSDK.shared().loginManager.currentAccount() + 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() + } + } + + public func onRecvRevokeMessageNotification(_ notification: NIMRevokeMessageNotification) { + guard let msg = notification.message else { + return + } + removeRevokeAtMessage(messages: [msg]) + } + + private func getMessageDic() -> [String: AtMEMessageRecord] { + lock.lock() + let result = atMessageDic + lock.unlock() + return result + } + + private func setMessageDic(_ dic: [String: AtMEMessageRecord]) { + lock.lock() + atMessageDic = dic + lock.unlock() + } + + public func isAtCurrentUser(sessionId: String) -> Bool { + let dic = getMessageDic() + NELog.infoLog(className(), desc: "session id : \(sessionId)") + NELog.infoLog(className(), desc: "dic : \(dic)") + if let model = dic[sessionId], model.isRead == false { + return true + } + return false + } + + public func clearAtRecord(_ sessionId: String) { + weak var weakSelf = self + workQueue.async { + guard let dic = weakSelf?.getMessageDic() else { + return + } + if let model = dic[sessionId] { + model.isRead = true + model.atMessages.removeAll() + weakSelf?.setMessageDic(dic) + weakSelf?.writeCacheToDocument(dictionary: dic) + } + } + } + + public func filterAtMessage(messages: [NIMMessage]) { + NELog.infoLog(className(), desc: "at manager filterAtMessage : \(messages.count)") + weak var weakSelf = self + workQueue.async { + if let result = weakSelf?.filterAtMessageInWorkqueue(messages: messages), result == true { + weakSelf?.atMessageChangeNoti() + if let dic = weakSelf?.getMessageDic() { + weakSelf?.writeCacheToDocument(dictionary: dic) + } + } + } + } + + public func removeRevokeAtMessage(messages: [NIMMessage]) { + weak var weakSelf = self + workQueue.async { + weakSelf?.removeRevokeAtMessageInWorkqueue(messages: messages) + } + } + + public func startFilterRoamingMessagesTask() { + weak var weakSelf = self + workQueue.async { + weakSelf?.startFilterRoamingMessagesTaskInWorkqueue() + } + } + + 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() + 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 { + weakSelf?.addAtRecord(message: message, record: &temDic) + isExistAtMessage = true + return + } + if dic[currentAccid] != nil { + weakSelf?.addAtRecord(message: message, record: &temDic) + isExistAtMessage = true + return + } + } + } + setMessageDic(temDic) + return isExistAtMessage + } + + private func startFilterRoamingMessagesTaskInWorkqueue() { + let sessions = NIMSDK.shared().conversationManager.allRecentSessions() + NELog.infoLog(className(), desc: "startFilterRoamingMessagesTaskInWorkqueue session count : \(sessions?.count ?? 0)") + var temDic = getMessageDic() + weak var weakSelf = self + var isExistAtMessage = false + print("recent session filter at message") + sessions?.forEach { recentSession in + if recentSession.unreadCount <= 0 { + 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 + } + } + } + } + } + if isExistAtMessage == true { + writeCacheToDocument(dictionary: temDic) + setMessageDic(temDic) + atMessageChangeNoti() + } + } + + 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)") + do { + var jsonObject = [String: Any]() + dictionary.forEach { (key: String, value: AtMEMessageRecord) in + if let jsonValue = value.yx_modelToJSONObject() { + jsonObject[key] = jsonValue + } + } + + let jsonData = try JSONSerialization.data(withJSONObject: jsonObject, options: []) + try jsonData.write(to: filePath) + } catch { + NELog.infoLog(className(), desc: "write cache error : \(error.localizedDescription)") + } + } + } + + private func loadCacheFromDocument() { + NELog.infoLog(className(), desc: "loadCacheFromDocument") + if let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first { + weak var weakSelf = self + let documentDir = documentsDirectory.appendingPathComponent("NEIMUIKit/") + 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)") + } + } + 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)") + } 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 + if let model = AtMEMessageRecord.yx_model(with: value) { + temdDic?[key] = model + if let dic = jsonObject[key], let isRead = dic[#keyPath(AtMEMessageRecord.isRead)] as? Bool { + model.isRead = isRead + if let atMessagesJsonObject = dic[#keyPath(AtMEMessageRecord.atMessages)] { + if let atMessages = NSDictionary.yx_modelDictionary(with: NSDictionary.self, json: atMessagesJsonObject) as? [String: NSNumber] { + model.atMessages = atMessages + } + } + } + } + } + if let tem = temdDic { + weakSelf?.setMessageDic(tem) + } + } + } catch { + NELog.infoLog(className(), desc: "convert to message data to json object error : \(error.localizedDescription)") + } + } + } + } + + private func removeRecord(message: NIMMessage, 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 atMeRecord.atMessages.count <= 0 { + atMeRecord.isRead = true + didRemove = true + } + } + } + return didRemove + } + + private func addAtRecord(message: NIMMessage, record: inout [String: AtMEMessageRecord]) { + if let atMeRecord = record[message.session?.sessionId ?? ""] { + let lastTime = atMeRecord.lastTime?.doubleValue ?? 0 + if lastTime < message.timestamp { + 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) + atMeRecord.isRead = false + if let sessionId = message.session?.sessionId { + record[sessionId] = 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) + atMeRecord.isRead = false + if let sessionId = message.session?.sessionId { + record[sessionId] = atMeRecord + } + } + } + + private func atMessageChangeNoti(_ isCurrentThread: Bool = false) { + if isCurrentThread == false { + DispatchQueue.main.async { + NotificationCenter.default.post(name: Notification.Name(AtMessageChangeNoti), object: nil) + } + } else { + NotificationCenter.default.post(name: Notification.Name(AtMessageChangeNoti), object: nil) + } + } + + public func onRecvMessages(_ messages: [NIMMessage]) { + filterAtMessage(messages: messages) + } +} diff --git a/NEMapKit/NEMapKit.podspec b/NEMapKit/NEMapKit.podspec index 9e922ab0..72191f52 100644 --- a/NEMapKit/NEMapKit.podspec +++ b/NEMapKit/NEMapKit.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'NEMapKit' - s.version = '9.3.0' + s.version = '9.5.0' s.summary = 'Netease XKit' # This description is used to generate tags and improve search results. @@ -31,6 +31,7 @@ TODO: Add long description of the pod here. s.ios.deployment_target = '9.0' s.source_files = 'NEMapKit/Classes/**/*' +# s.resource = 'NEMapKit/Assets/**/*' s.dependency 'AMap2DMap' s.dependency 'AMapSearch' diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/Contents.json b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/Contents.json new file mode 100644 index 00000000..4585be34 --- /dev/null +++ b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "location_point@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "location_point@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/location_point@2x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/location_point@2x.png new file mode 100644 index 00000000..5057458d Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/location_point@2x.png differ diff --git a/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/location_point@3x.png b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/location_point@3x.png new file mode 100644 index 00000000..6159f924 Binary files /dev/null and b/NEMapKit/NEMapKit/Assets/NEMapKit.xcassets/ne_location.imageset/location_point@3x.png differ diff --git a/NEMapKit/NEMapKit/Classes/NEMapClient.m b/NEMapKit/NEMapKit/Classes/NEMapClient.m index 01a8b56d..f4c06c41 100644 --- a/NEMapKit/NEMapKit/Classes/NEMapClient.m +++ b/NEMapKit/NEMapKit/Classes/NEMapClient.m @@ -33,6 +33,8 @@ @interface NEMapClient () *_Nonnull, + NSError *_Nullable))completion { + if ([mapview isKindOfClass:[MAMapView class]]) { + self.searchRoundBlock = completion; + MAMapView *map = (MAMapView *)mapview; + [self searchRoundPositionWithLat:map.userLocation.location.coordinate.latitude + lng:map.userLocation.location.coordinate.longitude]; + } +} + - (void)dealloc { [[NEChatKitClient instance] removeMapDelegate:self]; } @@ -241,6 +252,7 @@ - (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 @@ -276,4 +288,15 @@ - (void)mapView:(MAMapView *)mapView mapWillMoveByUser:(BOOL)wasUserAction { } } +- (MAAnnotationView *)mapView:(MAMapView *)mapView viewForAnnotation:(id)annotation { + if (nil != self.annoationImage) { + MAAnnotationView *annotationView = [[MAAnnotationView alloc] initWithAnnotation:annotation + reuseIdentifier:nil]; + annotationView.image = self.annoationImage; + self.annoationImage = nil; + return annotationView; + } + return nil; +} + @end diff --git a/NEMapKit/NEMapKit/Classes/NEMapService.h b/NEMapKit/NEMapKit/Classes/NEMapService.h deleted file mode 100644 index 240e73ca..00000000 --- a/NEMapKit/NEMapKit/Classes/NEMapService.h +++ /dev/null @@ -1,13 +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 - -NS_ASSUME_NONNULL_BEGIN - -@interface NEMapService : NSObject - -@end - -NS_ASSUME_NONNULL_END diff --git a/NEMapKit/NEMapKit/Classes/NEMapService.m b/NEMapKit/NEMapKit/Classes/NEMapService.m deleted file mode 100644 index f99f4a98..00000000 --- a/NEMapKit/NEMapKit/Classes/NEMapService.m +++ /dev/null @@ -1,119 +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 "NEMapService.h" -#import -#import -#import -#import -#import - -@interface NEMapService () - -@property(nonatomic, strong) MAMapView *mapView; - -@property(nonatomic, strong) AMapLocationManager *locationManager; - -@end - -@implementation NEMapService -+ (instancetype)shared { - static id instance = nil; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - instance = [[[self class] alloc] init]; - }); - return instance; -} - -- (instancetype)init { - self = [super init]; - if (self) { - // [self configLocationManager]; - } - return self; -} - -- (void)setupMapClient { - // [[NEChatKitClient instance] addMapDelegate:self]; - // [[NEChatKitClient instance] addMapServiceDelegate:self]; -} - -- (void)setupMapSdkConfig { - // 初始化高德SDK - [[AMapServices sharedServices] setApiKey:@"46a3a36bb9d26934a26c6ce2b04aab6f"]; - [AMapServices sharedServices].enableHTTPS = YES; -} - -- (void)setupMapControllerWithMapType:(NSInteger)mapType { - // 隐藏指南针 - _mapView.showsCompass = NO; - // 隐藏比例尺 - _mapView.showsScale = NO; - _mapView.zoomLevel = 15; - _mapView.showsUserLocation = YES; - _mapView.userTrackingMode = MAUserTrackingModeFollow; - - if (mapType == NEMapTypeDetail) { - MAUserLocationRepresentation *r = [[MAUserLocationRepresentation alloc] init]; - r.showsAccuracyRing = YES; - [self.mapView updateUserLocationRepresentation:r]; - - } else { - } - - // 设置大头针 - MAPointAnnotation *pointAnnotation = [[MAPointAnnotation alloc] init]; - pointAnnotation.coordinate = _mapView.centerCoordinate; - [_mapView addAnnotation:pointAnnotation]; -} - -- (id)getMapView { - MAMapView *mapView = [[MAMapView alloc] - initWithFrame:CGRectMake(0, 0, NEConstant.screenWidth, NEConstant.screenHeight)]; - self.mapView = mapView; - return mapView; -} - -- (void)searchPositionWithKey:(NSString *)key - completion:(void (^)(NSArray *_Nonnull, - NSError *_Nullable))completion { - NSLog(@"search key : %@", key); -} - -- (void)dealloc { - // [[NEChatKitClient instance] removeMapServiceDelegate:self]; -} - -#pragma mark - location - -//- (void)configLocationManager{ -// self.locationManager = [[AMapLocationManager alloc] init]; -// [self.locationManager setDelegate:self]; -// [self.locationManager setPausesLocationUpdatesAutomatically:NO]; -// [self.locationManager setAllowsBackgroundLocationUpdates:YES]; -//} -// -//- (void)startUpdatingLocation{ -// [self.locationManager startUpdatingLocation]; -//} -// -//- (void)stopSerialLocation{ -// //停止定位 -// [self.locationManager stopUpdatingLocation]; -//} -// -//- (void)amapLocationManager:(AMapLocationManager *)manager didFailWithError:(NSError *)error{ -// //定位错误 -// NSLog(@"%s, amapLocationManager = %@, error = %@", __func__, [manager class], error); -//} -// -//- (void)amapLocationManager:(AMapLocationManager *)manager didUpdateLocation:(CLLocation -//*)location{ -// //定位结果 -// NSLog(@"location:{lat:%f; lon:%f; accuracy:%f}", location.coordinate.latitude, -// location.coordinate.longitude, location.horizontalAccuracy); -//} - -@end diff --git a/NERtcCallUIKit/NERtcCallUIKit.podspec b/NERtcCallUIKit/NERtcCallUIKit.podspec index 419b1958..dfe54ae6 100644 --- a/NERtcCallUIKit/NERtcCallUIKit.podspec +++ b/NERtcCallUIKit/NERtcCallUIKit.podspec @@ -34,11 +34,16 @@ TODO: Add long description of the pod here. s.resource = 'NERtcCallUIKit/Assets/**/*' s.dependency 'NERtcCallKit' - s.dependency 'NERtcSDK' s.dependency 'AFNetworking' s.dependency 'SDWebImage' s.dependency 'Toast' s.dependency 'NECoreKit' s.dependency 'NECommonKit' + # s.resource_bundles = { + # 'NERtcCallUIKit' => ['NERtcCallUIKit/Assets/*.png'] + # } + # s.public_header_files = 'Pod/Classes/**/*.h' + # s.frameworks = 'UIKit', 'MapKit' + # s.dependency 'AFNetworking', '~> 2.3' end diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m index 4dd9cb47..efa98ad7 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/Controller/NECallViewController.m @@ -127,15 +127,18 @@ - (void)setupSDK { channelName:self.callParam.channelName completion:^(NSError *_Nullable error) { NSLog(@"call error code : %@", error); - - if (weakSelf.callType == NERtcCallTypeVideo) { + __strong typeof(self) strongSelf = weakSelf; + if (strongSelf.callType == NERtcCallTypeVideo) { if ([[SettingManager shareInstance] isGlobalInit] == YES) { + __weak typeof(self) weakSelf2 = strongSelf; dispatch_async(dispatch_get_main_queue(), ^{ + __strong typeof(self) strongSelf2 = weakSelf2; [[NERtcCallKit sharedInstance] - setupLocalView:weakSelf.videoCallingController.bigVideoView.videoView]; + setupLocalView:strongSelf2.videoCallingController.bigVideoView.videoView]; }); } - self.videoCallingController.bigVideoView.userID = weakSelf.callParam.currentUserAccid; + strongSelf.videoCallingController.bigVideoView.userID = + strongSelf.callParam.currentUserAccid; } if (error) { @@ -143,7 +146,7 @@ - (void)setupSDK { if (error.code == 10202 || error.code == 10201) { return; } else { - [weakSelf onCallEnd]; + [strongSelf onCallEnd]; } [UIApplication.sharedApplication.keyWindow makeToast:error.localizedDescription]; } @@ -328,15 +331,17 @@ - (void)updateUIonStatus:(NERtcCallStatus)status { sd_setImageWithURL:[NSURL URLWithString:self.callParam.remoteAvatar] completed:^(UIImage *_Nullable image, NSError *_Nullable error, SDImageCacheType cacheType, NSURL *_Nullable imageURL) { + __strong typeof(self) strongSelf = weakSelf; if (image == nil) { image = [UIImage imageNamed:@"avator" - inBundle:self.bundle + inBundle:strongSelf.bundle compatibleWithTraitCollection:nil]; } - if (weakSelf.isCaller == false && weakSelf.callType == NERtcCallTypeVideo) { - [weakSelf.blurImage setHidden:NO]; + if (strongSelf.isCaller == false && + strongSelf.callType == NERtcCallTypeVideo) { + [strongSelf.blurImage setHidden:NO]; } - weakSelf.blurImage.image = image; + strongSelf.blurImage.image = image; }]; } break; @@ -412,11 +417,14 @@ - (void)cancelEvent:(UIButton *)button { [[NERtcCallKit sharedInstance] cancel:^(NSError *_Nullable error) { NSLog(@"cancel error %@", error); button.enabled = YES; + __strong typeof(weakSelf) strongSelf = weakSelf; if (error.code == 20016) { - [UIApplication.sharedApplication.keyWindow - makeToast:[self localizableWithKey:@"cancel_failed"]]; + if ([UIApplication.sharedApplication respondsToSelector:@selector(keyWindow)]) { + [UIApplication.sharedApplication.keyWindow + makeToast:[strongSelf localizableWithKey:@"cancel_failed"]]; + } } else { - [weakSelf destroy]; + [strongSelf destroy]; } }]; } @@ -428,16 +436,18 @@ - (void)rejectEvent:(UIButton *)button { __weak typeof(self) weakSelf = self; if ([[SettingManager shareInstance] rejectBusyCode] == YES) { - [[NERtcCallKit sharedInstance] rejectWithReason:TerminalCodeBusy - withCompletion:^(NSError *_Nullable error) { - weakSelf.calledController.acceptBtn.userInteractionEnabled = - YES; - [weakSelf destroy]; - }]; + [[NERtcCallKit sharedInstance] + rejectWithReason:TerminalCodeBusy + withCompletion:^(NSError *_Nullable error) { + __strong typeof(self) strongSelf = weakSelf; + strongSelf.calledController.acceptBtn.userInteractionEnabled = YES; + [strongSelf destroy]; + }]; } else { [[NERtcCallKit sharedInstance] reject:^(NSError *_Nullable error) { - weakSelf.calledController.acceptBtn.userInteractionEnabled = YES; - [weakSelf destroy]; + __strong typeof(self) strongSelf = weakSelf; + strongSelf.calledController.acceptBtn.userInteractionEnabled = YES; + [strongSelf destroy]; }]; } } @@ -454,27 +464,30 @@ - (void)acceptEvent:(UIButton *)button { [[NERtcCallKit sharedInstance] acceptWithToken:[[SettingManager shareInstance] customToken] withCompletion:^(NSError *_Nullable error) { - weakSelf.calledController.rejectBtn.userInteractionEnabled = YES; - weakSelf.calledController.acceptBtn.userInteractionEnabled = YES; + __strong typeof(self) strongSelf = weakSelf; + strongSelf.calledController.rejectBtn.userInteractionEnabled = YES; + strongSelf.calledController.acceptBtn.userInteractionEnabled = YES; if (error) { if (error.code != 10420) { [UIApplication.sharedApplication.keyWindow - makeToast:[NSString stringWithFormat:@"%@ %@", - [self localizableWithKey:@"accept_failed"], - error.localizedDescription]]; + makeToast:[NSString + stringWithFormat:@"%@ %@", + [strongSelf localizableWithKey:@"accept_failed"], + error.localizedDescription]]; } - + __weak typeof(self) weakSelf2 = strongSelf; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [weakSelf destroy]; + __strong typeof(self) strongSelf2 = weakSelf2; + [strongSelf2 destroy]; }); } else { [[NERtcCallKit sharedInstance] memberOfAccid:@"" completion:^(NIMSignalingMemberInfo *_Nullable info){ }]; - [weakSelf updateUIonStatus:NERtcCallStatusInCall]; - [weakSelf startTimer]; + [strongSelf updateUIonStatus:NERtcCallStatusInCall]; + [strongSelf startTimer]; } }]; } @@ -549,22 +562,24 @@ - (void)operationSwitchClick:(UIButton *)btn { switchCallType:type withState:NERtcSwitchStateInvite completion:^(NSError *_Nullable error) { + __strong typeof(self) strongSelf = weakSelf; // weakSelf.mediaSwitchBtn.enabled = YES; btn.enabled = YES; if (error == nil) { NSLog(@"切换成功 : %lu", type); NSLog(@"switch : %d", btn.selected); if (type == NERtcCallTypeVideo && [SettingManager.shareInstance isVideoConfirm]) { - [weakSelf showBannerView]; + [strongSelf showBannerView]; } else if (type == NERtcCallTypeAudio && [SettingManager.shareInstance isAudioConfirm]) { - [weakSelf showBannerView]; + [strongSelf showBannerView]; } } else { - [weakSelf.view - makeToast:[NSString stringWithFormat:@"%@: %@", - [self localizableWithKey:@"switch_error"], - error]]; + [strongSelf.view + makeToast:[NSString + stringWithFormat:@"%@: %@", + [strongSelf localizableWithKey:@"switch_error"], + error]]; } }]; } @@ -595,19 +610,21 @@ - (void)mediaClick:(UIButton *)btn { switchCallType:type withState:NERtcSwitchStateInvite completion:^(NSError *_Nullable error) { - weakSelf.mediaSwitchBtn.maskBtn.enabled = YES; + __strong typeof(self) strongSelf = weakSelf; + strongSelf.mediaSwitchBtn.maskBtn.enabled = YES; if (error == nil) { if (type == NERtcCallTypeVideo && [SettingManager.shareInstance isVideoConfirm]) { - [weakSelf showBannerView]; + [strongSelf showBannerView]; } else if (type == NERtcCallTypeAudio && [SettingManager.shareInstance isAudioConfirm]) { - [weakSelf showBannerView]; + [strongSelf showBannerView]; } } else { - [weakSelf.view - makeToast:[NSString stringWithFormat:@"%@ : %@", - [self localizableWithKey:@"switch_error"], - error]]; + [strongSelf.view + makeToast:[NSString + stringWithFormat:@"%@ : %@", + [strongSelf localizableWithKey:@"switch_error"], + error]]; } }]; } @@ -826,15 +843,16 @@ - (void)setUrl:(NSString *)url withPlaceholder:(NSString *)holder { sd_setImageWithURL:[NSURL URLWithString:url] completed:^(UIImage *_Nullable image, NSError *_Nullable error, SDImageCacheType cacheType, NSURL *_Nullable imageURL) { + __strong typeof(self) strongSelf = weakSelf; if (image == nil) { image = [UIImage imageNamed:holder - inBundle:self.bundle + inBundle:strongSelf.bundle compatibleWithTraitCollection:nil]; } - if (weakSelf.isCaller == false && weakSelf.callType == NERtcCallTypeVideo) { - [weakSelf.blurImage setHidden:NO]; + if (strongSelf.isCaller == false && strongSelf.callType == NERtcCallTypeVideo) { + [strongSelf.blurImage setHidden:NO]; } - weakSelf.blurImage.image = image; + strongSelf.blurImage.image = image; }]; } diff --git a/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m b/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m index 8edb6e21..8d459a99 100644 --- a/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m +++ b/NERtcCallUIKit/NERtcCallUIKit/Classes/NERtcCallUIKit.m @@ -217,9 +217,10 @@ - (void)didDismiss:(NSNotification *)noti { __weak typeof(self) weakSelf = self; [nav dismissViewControllerAnimated:YES completion:^{ - NSLog(@"self window %@", weakSelf.keywindow); - [weakSelf.keywindow resignKeyWindow]; - weakSelf.keywindow = nil; + __strong typeof(self) strongSelf = weakSelf; + NSLog(@"self window %@", strongSelf.keywindow); + [strongSelf.keywindow resignKeyWindow]; + strongSelf.keywindow = nil; }]; } diff --git a/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings b/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings index 7fecc69e..d7f568bd 100644 --- a/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings +++ b/NETeamUIKit/NETeamUIKit/Assets/en.lproj/Localizable.strings @@ -4,7 +4,6 @@ // found in the LICENSE file. "mark"="pin"; -"history"="History"; "message_remind"="Open message remind"; "session_set_top"="Sticky on Top"; "team_nick"="My Alias in Group"; @@ -32,7 +31,7 @@ "modify_headImage"="Modify Avatar"; "save"="Save"; "historical_record"="History"; -"search"="Search"; +"search"="Search History"; "no_search_results"="No History"; "discuss_info"="Temp Group Info"; "group_info"="Group Info"; @@ -43,9 +42,12 @@ "quit_team_chat"="Wether to leave Group"; "quit_discuss_chat"="Wether to leave Temp Group"; "cancel"="cancel"; -"remind"="remind"; +"remind"="tip"; "discuss_avatar"="Temp Group Avator"; "discuss_name"="Temp Group Name"; "discuss_intro"="Temp Group Introduce"; "invite_has_send"="Invitation sent"; + +// error toast "space_not_support"="All Spaces are not supported"; +"team_not_exist"="team not exist"; diff --git a/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings b/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings index 5834a476..6301cf68 100644 --- a/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings +++ b/NETeamUIKit/NETeamUIKit/Assets/zh-Hans.lproj/Localizable.strings @@ -4,7 +4,6 @@ // found in the LICENSE file. "mark"="标记"; -"history"="历史记录"; "message_remind"="开启消息提醒"; "session_set_top"="聊天置顶"; "team_nick"="我在群里的昵称"; @@ -24,7 +23,7 @@ "default_icon"="选择默认图标"; "senior_team"="高级群"; "normal_team"="讨论组"; -"create_senior_team_noti"="成功创建高级群"; +"create_senior_team_noti"="成功创建了群聊"; "team_all"="所有人"; "team_owner"="群主"; @@ -32,7 +31,7 @@ "modify_headImage"="修改头像"; "save"="保存"; "historical_record"="历史记录"; -"search"="搜索"; +"search"="搜索聊天内容"; "no_search_results"="暂无聊天记录"; "discuss_info"="讨论组信息"; "group_info"="群信息"; @@ -48,4 +47,7 @@ "discuss_name"="讨论组名称"; "discuss_intro"="讨论组介绍"; "invite_has_send"="邀请已发送"; + +// error toast "space_not_support"="不支持全空格"; +"team_not_exist"="群组不存在"; diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamHistoryMessageController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamHistoryMessageController.swift index ab1db21d..2697d836 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamHistoryMessageController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamHistoryMessageController.swift @@ -119,6 +119,7 @@ public class TeamHistoryMessageController: NEBaseViewController, UITextFieldDele func searchTextFieldChange(textfield: SearchTextField) { if textfield.text?.count == 0 { viewmodel.searchResultInfos?.removeAll() + emptyView.isHidden = true tableView.reloadData() } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamMembersController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamMembersController.swift index b426bfae..48f7d03a 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamMembersController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamMembersController.swift @@ -10,6 +10,8 @@ import NECoreKit @objcMembers public class TeamMembersController: NEBaseViewController, UITableViewDelegate, UITableViewDataSource { + var viewmodel: TeamSettingViewModel? + var datas: [TeamMemberInfoModel]? var ownerId: String? @@ -49,6 +51,30 @@ public class TeamMembersController: NEBaseViewController, UITableViewDelegate, return table }() + init(viewmodel: TeamSettingViewModel? = nil) { + super.init(nibName: nil, bundle: nil) + self.viewmodel = viewmodel + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewWillAppear(_ animated: Bool) { + viewmodel?.getTeamInfo(viewmodel?.teamInfoModel?.team?.teamId ?? "") { [weak self] error in + if let err = error as? NSError { + if err.code == 803 || err.code == 1 { + self?.showToast(localizable("team_not_exist")) + } else { + self?.showToast(err.localizedDescription) + } + } else { + self?.datas = self?.viewmodel?.teamInfoModel?.users + self?.contentTable.reloadData() + } + } + } + override public func viewDidLoad() { super.viewDidLoad() @@ -213,11 +239,17 @@ public class TeamMembersController: NEBaseViewController, UITableViewDelegate, closure: nil ) } else { - Router.shared.use( - ContactUserInfoPageRouter, - parameters: ["nav": navigationController as Any, "nim_user": user], - closure: nil - ) + if let uid = user.userId { + UserInfoProvider.shared.fetchUserInfo([uid]) { [weak self] error, users in + if let u = users?.first { + Router.shared.use( + ContactUserInfoPageRouter, + parameters: ["nav": self?.navigationController as Any, "nim_user": u], + closure: nil + ) + } + } + } } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamNameViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamNameViewController.swift index dd81c52e..cc8cd9f6 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamNameViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamNameViewController.swift @@ -178,6 +178,11 @@ public class TeamNameViewController: NEBaseViewController, UITextFieldDelegate { } func saveName() { + guard let tid = team?.teamId else { + showToast(localizable("team_not_exist")) + return + } + if let text = textField.text, !text.isEmpty { let trimText = text.trimmingCharacters(in: .whitespaces) @@ -191,7 +196,7 @@ public class TeamNameViewController: NEBaseViewController, UITextFieldDelegate { weak var weakSelf = self textField.resignFirstResponder() - if type == .TeamName, let tid = team?.teamId { + if type == .TeamName { let n = textField.text ?? "" view.makeToastActivity(.center) repo.updateTeamName(tid, n) { error in @@ -203,7 +208,7 @@ public class TeamNameViewController: NEBaseViewController, UITextFieldDelegate { weakSelf?.navigationController?.popViewController(animated: true) } } - } else if type == .NickName, let tid = team?.teamId, let uid = teamMember?.userId { + } else if type == .NickName, let uid = teamMember?.userId { let n = textField.text ?? "" view.makeToastActivity(.center) repo.updateMemberNick(tid, uid, n) { error in diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamSettingViewController.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamSettingViewController.swift index 9d177b4d..754d2c64 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamSettingViewController.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/TeamSettingViewController.swift @@ -430,7 +430,6 @@ public class TeamSettingViewController: NEBaseViewController, UICollectionViewDe weakSelf?.view.hideToastActivity() if let err = error { weakSelf?.showToast(err.localizedDescription) - return } weakSelf?.navigationController?.popViewController(animated: true) } @@ -446,7 +445,7 @@ public class TeamSettingViewController: NEBaseViewController, UICollectionViewDe } func toMemberList() { - let memberController = TeamMembersController() + let memberController = TeamMembersController(viewmodel: viewmodel) memberController.datas = viewmodel.teamInfoModel?.users if teamSettingType == .Senior { memberController.isSenior = true @@ -494,11 +493,17 @@ public class TeamSettingViewController: NEBaseViewController, UICollectionViewDe closure: nil ) } else { - Router.shared.use( - ContactUserInfoPageRouter, - parameters: ["nav": navigationController as Any, "user": nimUser], - closure: nil - ) + if let uid = nimUser.userId { + UserInfoProvider.shared.fetchUserInfo([uid]) { [weak self] error, users in + if let u = users?.first { + Router.shared.use( + ContactUserInfoPageRouter, + parameters: ["nav": self?.navigationController as Any, "user": u], + closure: nil + ) + } + } + } } } } @@ -626,9 +631,8 @@ extension TeamSettingViewController { weakSelf?.view.hideToastActivity() if let err = error { weakSelf?.showToast(err.localizedDescription) - } else { - weakSelf?.navigationController?.popViewController(animated: true) } + weakSelf?.navigationController?.popViewController(animated: true) } } } @@ -641,6 +645,9 @@ extension TeamSettingViewController { func leaveTeam() { if let tid = teamId { + // 需要先于 SDK 回调进行通知 + NotificationCenter.default.post(name: NotificationName.leaveTeamBySelf, object: true) + view.makeToastActivity(.center) viewmodel.quitTeam(tid) { [weak self] error in NELog.infoLog( @@ -648,9 +655,15 @@ extension TeamSettingViewController { desc: "CALLBACK quitTeam " + (error?.localizedDescription ?? "no error") ) self?.view.hideToastActivity() - if let err = error { + if let err = error as? NSError { + // 退出群聊失败则需要重置通知 + NotificationCenter.default.post(name: NotificationName.leaveTeamBySelf, object: false) + if err.code == 803 { + self?.navigationController?.popViewController(animated: true) + } self?.showToast(err.localizedDescription) } else { + // 会话列表中移除该群聊 let session = NIMSession(tid, type: .team) if let stickInfo = self?.viewmodel.getTopSessionInfo(session) { self?.viewmodel.removeStickTop(params: stickInfo) { err, _ in @@ -668,6 +681,13 @@ extension TeamSettingViewController { } extension TeamSettingViewController: TeamSettingViewModelDelegate { + 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) + } + } + func didError(_ error: Error) { showToast(error.localizedDescription) } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamMemberCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamMemberCell.swift index ba0b9ddf..374c7989 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamMemberCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamMemberCell.swift @@ -95,9 +95,9 @@ public class TeamMemberCell: UITableViewCell { headerView.setTitle("") } else { headerView.image = nil - headerView.setTitle(model.showNameInTeam()) + headerView.setTitle(model.showNickInTeam()) headerView.backgroundColor = UIColor.colorWithString(string: model.nimUser?.userId) } - nameLabel.text = model.showNameInTeam() + nameLabel.text = model.atNameInTeam() } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamUserCell.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamUserCell.swift index b17ed516..e19a306a 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamUserCell.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/View/TeamUserCell.swift @@ -14,7 +14,7 @@ import NETeamKit public class TeamUserCell: UICollectionViewCell { var user: TeamMemberInfoModel? { didSet { - if let name = user?.showNameInTeam() { + if let name = user?.showNickInTeam() { userHeader.setTitle(name) } if let url = user?.nimUser?.userInfo?.avatarUrl { diff --git a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift index 9dcfb20c..e237bdb4 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/Setting/ViewModel/TeamSettingViewModel.swift @@ -15,6 +15,7 @@ protocol TeamSettingViewModelDelegate: NSObjectProtocol { func didClickHistoryMessage() func didNeedRefreshUI() func didError(_ error: Error) + func didClickMark() } @objcMembers @@ -33,8 +34,6 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { private let className = "TeamSettingViewModel" - private var usersDic = [String: TeamMemberInfoModel]() - override public init() { super.init() NIMSDK.shared().teamManager.add(self) @@ -73,17 +72,19 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { NELog.infoLog(ModuleName + " " + className, desc: #function) let model = SettingSectionModel() - /* - let mark = SettingCellModel() - mark.cellName = localizable("mark") - mark.type = SettingCellType.SettingArrowCell.rawValue - mark.cornerType = .topLeft.union(.topRight) - */ weak var weakSelf = self + + let mark = SettingCellModel() + mark.cellName = localizable("mark") + mark.type = SettingCellType.SettingArrowCell.rawValue + mark.cornerType = .topLeft.union(.topRight) + mark.cellClick = { + weakSelf?.delegate?.didClickMark() + } + let history = SettingCellModel() - history.cellName = localizable("history") + history.cellName = localizable("historical_record") history.type = SettingCellType.SettingArrowCell.rawValue - history.cornerType = .topLeft.union(.topRight) history.cellClick = { weakSelf?.delegate?.didClickHistoryMessage() } @@ -161,7 +162,7 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { } } - model.cellModels.append(contentsOf: [history, remind, setTop]) + model.cellModels.append(contentsOf: [mark, history, remind, setTop]) return model } @@ -308,11 +309,6 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { } print("get team info team: ", teamInfo?.team as Any) print("get team info error: ", error as Any) - teamInfo?.users.forEach { model in - if let id = model.nimUser?.userId as? String { - weakSelf?.usersDic[id] = model - } - } if error == nil { weakSelf?.getData() weakSelf?.getCurrentMember(IMKitEngine.instance.imAccid, teamId) @@ -432,9 +428,11 @@ public class TeamSettingViewModel: NSObject, NIMTeamManagerDelegate { if let accids = memberIDs { weak var weakSelf = self accids.forEach { accid in - if let model = weakSelf?.usersDic[accid] { - if let index = weakSelf?.teamInfoModel?.users.firstIndex(of: model) { - weakSelf?.teamInfoModel?.users.remove(at: index) + if let users = teamInfoModel?.users { + for (i, m) in users.enumerated() { + if m.nimUser?.userId == accid { + teamInfoModel?.users.remove(at: i) + } } } } diff --git a/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift b/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift index 66ba7259..63b196b1 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/TeamConstant.swift @@ -15,3 +15,10 @@ func localizable(_ key: String) -> String { } public let ModuleName = "NETeamUIKit" + +// 创建群/邀请入群 人数限制 +public var peopleNumberLimit: UInt = 200 + +enum NotificationName { + static let leaveTeamBySelf = Notification.Name(rawValue: "team.leaveTeamBySelf") +} diff --git a/NETeamUIKit/NETeamUIKit/Classes/TeamRouter.swift b/NETeamUIKit/NETeamUIKit/Classes/TeamRouter.swift index 7103bd9a..7c3fcf07 100644 --- a/NETeamUIKit/NETeamUIKit/Classes/TeamRouter.swift +++ b/NETeamUIKit/NETeamUIKit/Classes/TeamRouter.swift @@ -46,6 +46,7 @@ public class TeamRouter: NSObject { option.beInviteMode = .noAuth option.updateInfoMode = .all option.updateClientCustomMode = .all + option.maxMemberCountLimitation = peopleNumberLimit repo.createAdvanceTeam(accids, option) { error, teamid, failedIds in var result = [String: Any]() @@ -56,6 +57,12 @@ public class TeamRouter: NSObject { result["code"] = 0 result["msg"] = "ok" result["teamId"] = teamid + repo.sendCreateAdavanceNoti( + teamid ?? "", + localizable("create_senior_team_noti") + ) { error in + print("send noti message : ", error as Any) + } if let tid = teamid { repo.updateTeamCustomInfo(discussTeamKey, tid) { error in if let err = error { @@ -84,6 +91,8 @@ public class TeamRouter: NSObject { option.type = .advanced option.avatarUrl = iconUrl option.name = name + option.beInviteMode = .noAuth + option.maxMemberCountLimitation = peopleNumberLimit repo.createAdvanceTeam(accids, option) { error, teamid, failedIds in var result = [String: Any]() diff --git a/app/Main/AppDelegate.swift b/app/Main/AppDelegate.swift index 83b7336f..cfab6d18 100644 --- a/app/Main/AppDelegate.swift +++ b/app/Main/AppDelegate.swift @@ -42,6 +42,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD NEKeyboardManager.shared.enable = true NEKeyboardManager.shared.shouldResignOnTouchOutside = true + // 登录IM之前先初始化 @ 消息监听mananger + let _ = NEAtMessageManager.instance + weak var weakSelf = self IMKitClient.instance.loginIM("<#accid#>", "<#token#>") { error in if let err = error {