IMChatViewController.swift 44 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079
  1. //
  2. // IMChatViewController.swift
  3. // O2Platform
  4. //
  5. // Created by FancyLou on 2020/6/8.
  6. // Copyright © 2020 zoneland. All rights reserved.
  7. //
  8. import UIKit
  9. import CocoaLumberjack
  10. import BSImagePicker
  11. import Photos
  12. import Alamofire
  13. import AlamofireImage
  14. import QuickLook
  15. import SnapKit
  16. class IMChatViewController: UIViewController {
  17. // MARK: - IBOutlet
  18. //消息列表
  19. @IBOutlet weak var tableView: UITableView!
  20. //消息输入框
  21. @IBOutlet weak var messageInputView: UITextField!
  22. //底部工具栏的高度约束
  23. @IBOutlet weak var bottomBarHeightConstraint: NSLayoutConstraint!
  24. //底部工具栏
  25. @IBOutlet weak var bottomBar: UIView!
  26. // 底部工具栏 底部约束 输入法弹出的时候使用
  27. @IBOutlet weak var bottomBarBottomConstraint: NSLayoutConstraint!
  28. private let emojiBarHeight = 196
  29. //表情窗口
  30. private lazy var emojiBar: IMChatEmojiBarView = {
  31. let view = Bundle.main.loadNibNamed("IMChatEmojiBarView", owner: self, options: nil)?.first as! IMChatEmojiBarView
  32. view.frame = CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: emojiBarHeight.toCGFloat)
  33. return view
  34. }()
  35. //语音录制按钮
  36. private lazy var audioBtnView: IMChatAudioView = {
  37. let view = Bundle.main.loadNibNamed("IMChatAudioView", owner: self, options: nil)?.first as! IMChatAudioView
  38. view.frame = CGRect(x: 0, y: 0, width: SCREEN_WIDTH, height: emojiBarHeight.toCGFloat)
  39. view.delegate = self
  40. return view
  41. }()
  42. //录音的时候显示的view
  43. private var voiceIconImage: UIImageView?
  44. private var voiceIocnTitleLable: UILabel?
  45. private var voiceImageSuperView: UIView?
  46. //预览文件
  47. private lazy var previewVC: CloudFilePreviewController = {
  48. return CloudFilePreviewController()
  49. }()
  50. private lazy var viewModel: IMViewModel = {
  51. return IMViewModel()
  52. }()
  53. // MARK: - properties
  54. var conversation: IMConversationInfo? = nil
  55. //private
  56. private var chatMessageList: [IMMessageInfo] = []
  57. private var page = 0
  58. private var isShowEmoji = false
  59. private var isShowAudioView = false
  60. private var bottomBarHeight = 64 //底部输入框 表情按钮 的高度
  61. private let bottomToolbarHeight = 46 //底部工具栏 麦克风 相册 相机等按钮的位置
  62. private var playingAudioMessageId: String? // 正在播放音频的消息id
  63. // 当前选中的消息对象 长按菜单需要使用
  64. private var currentSelectMsg: IMMessageInfo? = nil
  65. private var imConfig = IMConfig()
  66. deinit {
  67. AudioPlayerManager.shared.delegate = nil
  68. }
  69. // MARK: - functions
  70. override func viewDidLoad() {
  71. super.viewDidLoad()
  72. // 配置文件
  73. if let config = O2UserDefaults.shared.imConfig {
  74. imConfig = config
  75. } else {
  76. imConfig.enableClearMsg = false
  77. imConfig.enableRevokeMsg = false
  78. }
  79. self.tableView.delegate = self
  80. self.tableView.dataSource = self
  81. self.tableView.register(UINib(nibName: "IMChatMessageViewCell", bundle: nil), forCellReuseIdentifier: "IMChatMessageViewCell")
  82. self.tableView.register(UINib(nibName: "IMChatMessageSendViewCell", bundle: nil), forCellReuseIdentifier: "IMChatMessageSendViewCell")
  83. self.tableView.separatorStyle = .none
  84. // self.tableView.rowHeight = UITableView.automaticDimension
  85. // self.tableView.estimatedRowHeight = 144
  86. self.tableView.backgroundColor = UIColor(hex: "#f3f3f3")
  87. self.tableView.mj_header = MJRefreshNormalHeader(refreshingBlock: {
  88. self.loadMsgList()
  89. })
  90. // 输入框 delegate
  91. self.messageInputView.delegate = self
  92. // 播放audio delegate
  93. AudioPlayerManager.shared.delegate = self
  94. //底部安全距离 老机型没有
  95. self.bottomBarHeight = Int(iPhoneX ? 64 + IPHONEX_BOTTOM_SAFE_HEIGHT: 64) + self.bottomToolbarHeight
  96. self.bottomBarHeightConstraint.constant = self.bottomBarHeight.toCGFloat
  97. self.bottomBar.topBorder(width: 1, borderColor: base_gray_color.alpha(0.5))
  98. self.messageInputView.backgroundColor = base_gray_color
  99. //标题
  100. if self.conversation?.type == o2_im_conversation_type_single {
  101. if let c = self.conversation {
  102. var person = ""
  103. c.personList?.forEach({ (p) in
  104. if p != O2AuthSDK.shared.myInfo()?.distinguishedName {
  105. person = p
  106. }
  107. })
  108. if !person.isEmpty {
  109. self.title = person.split("@").first ?? ""
  110. }
  111. }
  112. } else {
  113. self.title = self.conversation?.title
  114. }
  115. //群会话 添加修改标题的按钮
  116. if self.conversation?.type == o2_im_conversation_type_group &&
  117. O2AuthSDK.shared.myInfo()?.distinguishedName == self.conversation?.adminPerson {
  118. navigationItem.rightBarButtonItem = UIBarButtonItem(title: "修改", style: .plain, target: self, action: #selector(clickUpdate))
  119. } else if self.conversation?.type == o2_im_conversation_type_single {
  120. if imConfig.enableClearMsg == true {
  121. navigationItem.rightBarButtonItem = UIBarButtonItem(title: "清除聊天记录", style: .plain, target: self, action: #selector(clearAllChatMsg))
  122. }
  123. }
  124. //获取聊天数据
  125. self.loadMsgList()
  126. //阅读
  127. self.viewModel.readConversation(conversationId: self.conversation?.id)
  128. }
  129. override func viewWillAppear(_ animated: Bool) {
  130. // websocket 消息监听
  131. NotificationCenter.default.addObserver(self, selector: #selector(receiveMessageFromWs(notice:)), name: OONotification.websocket.notificationName, object: nil)
  132. // 监听 键盘打开
  133. NotificationCenter.default.addObserver(self, selector: #selector(openKeyboard(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
  134. // 监听 键盘大小变化
  135. // NotificationCenter.default.addObserver(self, selector: #selector(openKeyboard(notification:)), name: UIResponder.keyboardDidChangeFrameNotification, object: nil)
  136. // 监听 键盘关闭
  137. NotificationCenter.default.addObserver(self, selector: #selector(closeKeyboard(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
  138. }
  139. override func viewWillDisappear(_ animated: Bool) {
  140. NotificationCenter.default.removeObserver(self)
  141. }
  142. // 打开键盘的时候
  143. @objc private func openKeyboard(notification: Notification) {
  144. if let userInfo = notification.userInfo {
  145. let v = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue
  146. let y = v?.cgRectValue.origin.y ?? 0
  147. DDLogDebug("当前键盘高度: \(y)")
  148. let duration = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber
  149. UIView.animate(withDuration: duration?.doubleValue ?? 0.3, animations: {
  150. self.bottomBarBottomConstraint.constant = y
  151. })
  152. }
  153. }
  154. // 关闭键盘
  155. @objc private func closeKeyboard(notification: Notification) {
  156. UIView.animate(withDuration: 0.3, animations: {
  157. self.bottomBarBottomConstraint.constant = 0
  158. })
  159. }
  160. @objc private func receiveMessageFromWs(notice: Notification) {
  161. DDLogDebug("接收到websocket im 消息")
  162. if let message = notice.object as? IMMessageInfo {
  163. if message.conversationId == self.conversation?.id {
  164. self.chatMessageList.append(message)
  165. self.scrollMessageToBottom()
  166. self.viewModel.readConversation(conversationId: self.conversation?.id)
  167. }
  168. }
  169. }
  170. @objc private func clickUpdate() {
  171. var arr = [
  172. UIAlertAction(title: "修改群名", style: .default, handler: { (action) in
  173. self.updateTitle()
  174. }),
  175. UIAlertAction(title: "修改成员", style: .default, handler: { (action) in
  176. self.updatePeople()
  177. })
  178. ]
  179. if imConfig.enableClearMsg == true {
  180. arr.append(UIAlertAction(title: "清除聊天记录", style: .default, handler: { (action) in
  181. self.clearAllChatMsg()
  182. }))
  183. }
  184. self.showSheetAction(title: "", message: "选择要修改的项", actions: arr)
  185. }
  186. @objc private func clearAllChatMsg() {
  187. self.showDefaultConfirm(title: "提示", message: "确定要清空聊天记录吗,清空后当前会话所有人都将看不到这些聊天记录?") { action in
  188. // 清空聊天记录
  189. if let id = self.conversation?.id {
  190. self.viewModel.clearAllChatMsg(conversationId: id).then { result in
  191. if result {
  192. self.showMessage(msg: "清空聊天记录成功!")
  193. self.chatMessageList.removeAll()
  194. self.tableView.reloadData()
  195. self.page = 0
  196. self.loadMsgList()
  197. } else {
  198. self.showError(title: "清空失败!")
  199. }
  200. }
  201. }
  202. }
  203. }
  204. private func updateTitle() {
  205. self.showPromptAlert(title: "", message: "修改群名", inputText: "") { (action, result) in
  206. if result.isEmpty {
  207. self.showError(title: "请输入群名")
  208. }else {
  209. self.showLoading()
  210. self.viewModel.updateConversationTitle(id: (self.conversation?.id!)!, title: result)
  211. .then { (c) in
  212. self.title = result
  213. self.conversation?.title = result
  214. self.showSuccess(title: "修改成功")
  215. }.catch { (err) in
  216. DDLogError(err.localizedDescription)
  217. self.showError(title: "修改失败")
  218. }
  219. }
  220. }
  221. }
  222. private func updatePeople() {
  223. //选择人员 反选已经存在的成员
  224. if let users = self.conversation?.personList {
  225. self.showContactPicker(modes: [.person], callback: { (result) in
  226. if let people = result.users {
  227. if people.count >= 3 {
  228. var peopleDNs: [String] = []
  229. var containMe = false
  230. people.forEach { (item) in
  231. peopleDNs.append(item.distinguishedName!)
  232. if O2AuthSDK.shared.myInfo()?.distinguishedName == item.distinguishedName {
  233. containMe = true
  234. }
  235. }
  236. if !containMe {
  237. peopleDNs.append((O2AuthSDK.shared.myInfo()?.distinguishedName)!)
  238. }
  239. self.showLoading()
  240. self.viewModel.updateConversationPeople(id: (self.conversation?.id!)!, users: peopleDNs)
  241. .then { (c) in
  242. self.conversation?.personList = peopleDNs
  243. self.showSuccess(title: "修改成功")
  244. }.catch { (err) in
  245. DDLogError(err.localizedDescription)
  246. self.showError(title: "修改失败")
  247. }
  248. }else {
  249. self.showError(title: "选择人数不足3人")
  250. }
  251. }else {
  252. self.showError(title: "请选择人员")
  253. }
  254. }, initUserPickedArray: users)
  255. }else {
  256. self.showError(title: "成员列表数据错误!")
  257. }
  258. }
  259. //获取消息
  260. private func loadMsgList() {
  261. if let c = self.conversation, let id = c.id {
  262. self.viewModel.myMsgPageList(page: self.page + 1, conversationId: id).then { (list) in
  263. if !list.isEmpty {
  264. self.page += 1
  265. self.chatMessageList.insert(contentsOf: list, at: 0)
  266. if self.page == 1 {
  267. self.scrollMessageToBottom()
  268. }else {
  269. DispatchQueue.main.async {
  270. self.tableView.reloadData()
  271. }
  272. }
  273. }
  274. if self.tableView.mj_header.isRefreshing(){
  275. self.tableView.mj_header.endRefreshing()
  276. }
  277. }.catch { (error) in
  278. DDLogError(error.localizedDescription)
  279. if self.tableView.mj_header.isRefreshing(){
  280. self.tableView.mj_header.endRefreshing()
  281. }
  282. }
  283. } else {
  284. self.showError(title: "参数错误!!!")
  285. }
  286. }
  287. //刷新tableview 滚动到底部
  288. private func scrollMessageToBottom() {
  289. DispatchQueue.main.async {
  290. self.tableView.reloadData()
  291. if self.chatMessageList.count > 0 {
  292. self.tableView.scrollToRow(at: IndexPath(row: self.chatMessageList.count - 1, section: 0), at: .bottom, animated: false)
  293. }
  294. }
  295. }
  296. //发送文本消息
  297. private func sendTextMessage() {
  298. guard let msg = self.messageInputView.text else {
  299. return
  300. }
  301. self.messageInputView.text = ""
  302. let body = IMMessageBodyInfo()
  303. body.type = o2_im_msg_type_text
  304. body.body = msg
  305. sendMessage(body: body)
  306. }
  307. //发送表情消息
  308. private func sendEmojiMessage(emoji: String) {
  309. let body = IMMessageBodyInfo()
  310. body.type = o2_im_msg_type_emoji
  311. body.body = emoji
  312. sendMessage(body: body)
  313. }
  314. //发送地图消息消息
  315. private func sendLocationMessage(loc: O2LocationData) {
  316. let body = IMMessageBodyInfo()
  317. body.type = o2_im_msg_type_location
  318. body.body = o2_im_msg_body_location
  319. body.address = loc.address
  320. body.addressDetail = loc.addressDetail
  321. body.longitude = loc.longitude
  322. body.latitude = loc.latitude
  323. sendMessage(body: body)
  324. }
  325. //发送消息到服务器
  326. private func sendMessage(body: IMMessageBodyInfo) {
  327. let message = IMMessageInfo()
  328. message.body = body.toJSONString()
  329. message.id = UUID().uuidString
  330. message.conversationId = self.conversation?.id
  331. message.createPerson = O2AuthSDK.shared.myInfo()?.distinguishedName
  332. message.createTime = Date().formatterDate(formatter: "yyyy-MM-dd HH:mm:ss")
  333. //添加到界面
  334. self.chatMessageList.append(message)
  335. self.scrollMessageToBottom()
  336. //发送消息到服务器
  337. self.viewModel.sendMsg(msg: message)
  338. .then { (result) in
  339. DDLogDebug("发送消息成功 \(result)")
  340. self.viewModel.readConversation(conversationId: self.conversation?.id)
  341. }.catch { (error) in
  342. DDLogError(error.localizedDescription)
  343. self.showError(title: "发送消息失败!")
  344. }
  345. }
  346. //选择照片
  347. private func chooseImage() {
  348. self.choosePhotoWithImagePicker { (fileName, newData) in
  349. let localFilePath = self.storageLocalImage(imageData: newData, fileName: fileName)
  350. let msgId = self.prepareForSendImageMsg(filePath: localFilePath)
  351. self.uploadFileAndSendMsg(messageId: msgId, data: newData, fileName: fileName, type: o2_im_msg_type_image)
  352. }
  353. }
  354. //临时存储本地
  355. private func storageLocalImage(imageData: Data, fileName: String) -> String {
  356. let fileTempPath = FileUtil.share.cacheDir().appendingPathComponent(fileName)
  357. do {
  358. try imageData.write(to: fileTempPath)
  359. return fileTempPath.path
  360. } catch {
  361. print(error.localizedDescription)
  362. return fileTempPath.path
  363. }
  364. }
  365. //发送消息前 先载入界面
  366. private func prepareForSendImageMsg(filePath: String) -> String {
  367. let body = IMMessageBodyInfo()
  368. body.type = o2_im_msg_type_image
  369. body.body = o2_im_msg_body_image
  370. body.fileTempPath = filePath
  371. let message = IMMessageInfo()
  372. let msgId = UUID().uuidString
  373. message.body = body.toJSONString()
  374. message.id = msgId
  375. message.conversationId = self.conversation?.id
  376. message.createPerson = O2AuthSDK.shared.myInfo()?.distinguishedName
  377. message.createTime = Date().formatterDate(formatter: "yyyy-MM-dd HH:mm:ss")
  378. //添加到界面
  379. self.chatMessageList.append(message)
  380. self.scrollMessageToBottom()
  381. return msgId
  382. }
  383. private func prepareForSendFileMsg(filePath: String) -> String {
  384. let body = IMMessageBodyInfo()
  385. body.type = o2_im_msg_type_file
  386. body.body = o2_im_msg_body_file
  387. body.fileTempPath = filePath
  388. let message = IMMessageInfo()
  389. let msgId = UUID().uuidString
  390. message.body = body.toJSONString()
  391. message.id = msgId
  392. message.conversationId = self.conversation?.id
  393. message.createPerson = O2AuthSDK.shared.myInfo()?.distinguishedName
  394. message.createTime = Date().formatterDate(formatter: "yyyy-MM-dd HH:mm:ss")
  395. //添加到界面
  396. self.chatMessageList.append(message)
  397. self.scrollMessageToBottom()
  398. return msgId
  399. }
  400. //发送消息前 先载入界面
  401. private func prepareForSendAudioMsg(tempMessage: IMMessageBodyInfo) -> String {
  402. let message = IMMessageInfo()
  403. let msgId = UUID().uuidString
  404. message.body = tempMessage.toJSONString()
  405. message.id = msgId
  406. message.conversationId = self.conversation?.id
  407. message.createPerson = O2AuthSDK.shared.myInfo()?.distinguishedName
  408. message.createTime = Date().formatterDate(formatter: "yyyy-MM-dd HH:mm:ss")
  409. //添加到界面
  410. self.chatMessageList.append(message)
  411. self.scrollMessageToBottom()
  412. return msgId
  413. }
  414. //上传图片 音频 文档 等文件到服务器并发送消息
  415. private func uploadFileAndSendMsg(messageId: String, data: Data, fileName: String, type: String) {
  416. guard let cId = self.conversation?.id else {
  417. return
  418. }
  419. self.viewModel.uploadFile(conversationId: cId, type: type, fileName: fileName, file: data).then { back in
  420. DDLogDebug("上传文件成功")
  421. guard let message = self.chatMessageList.first (where: { (info) -> Bool in
  422. return info.id == messageId
  423. }) else {
  424. DDLogDebug("没有找到对应的消息")
  425. return
  426. }
  427. let body = IMMessageBodyInfo.deserialize(from: message.body)
  428. body?.fileId = back.id
  429. body?.fileExtension = back.fileExtension
  430. body?.fileName = back.fileName
  431. body?.fileTempPath = nil
  432. message.body = body?.toJSONString()
  433. //发送消息到服务器
  434. self.viewModel.sendMsg(msg: message)
  435. .then { (result) in
  436. DDLogDebug("消息 发送成功 \(result)")
  437. self.viewModel.readConversation(conversationId: self.conversation?.id)
  438. }.catch { (error) in
  439. DDLogError(error.localizedDescription)
  440. self.showError(title: "发送消息失败!")
  441. }
  442. }.catch { err in
  443. self.showError(title: "上传错误,\(err.localizedDescription)")
  444. }
  445. }
  446. // 选择外部文件
  447. private func chooseFile() {
  448. let documentTypes = ["public.content",
  449. "public.text",
  450. "public.source-code",
  451. "public.image",
  452. "public.audiovisual-content",
  453. "com.adobe.pdf",
  454. "com.apple.keynote.key",
  455. "com.microsoft.word.doc",
  456. "com.microsoft.excel.xls",
  457. "com.microsoft.powerpoint.ppt"]
  458. let picker = UIDocumentPickerViewController(documentTypes: documentTypes, in: .open)
  459. picker.delegate = self
  460. self.present(picker, animated: true, completion: nil)
  461. }
  462. private func playAudioGif(id: String?) {
  463. self.playingAudioMessageId = id
  464. self.tableView.reloadData()
  465. }
  466. private func stopPlayAudioGif() {
  467. self.playingAudioMessageId = nil
  468. self.tableView.reloadData()
  469. }
  470. // 播放audio
  471. private func playAudio(url: URL) {
  472. do {
  473. let data = try Data(contentsOf: url)
  474. AudioPlayerManager.shared.managerAudioWithData(data, toplay: true)
  475. } catch {
  476. DDLogError(error.localizedDescription)
  477. }
  478. }
  479. // MARK: - IBAction
  480. //点击表情按钮
  481. @IBAction func clickEmojiBtn(_ sender: UIButton) {
  482. self.isShowEmoji.toggle()
  483. self.view.endEditing(true)
  484. if self.isShowEmoji {
  485. //audio view 先关闭
  486. self.isShowAudioView = false
  487. self.audioBtnView.removeFromSuperview()
  488. //开始添加emojiBar
  489. self.bottomBarHeightConstraint.constant = self.bottomBarHeight.toCGFloat + self.emojiBarHeight.toCGFloat
  490. self.emojiBar.delegate = self
  491. self.emojiBar.translatesAutoresizingMaskIntoConstraints = false
  492. self.bottomBar.addSubview(self.emojiBar)
  493. let top = NSLayoutConstraint(item: self.emojiBar, attribute: .top, relatedBy: .equal, toItem: self.emojiBar.superview!, attribute: .top, multiplier: 1, constant: CGFloat(self.bottomBarHeight))
  494. let width = NSLayoutConstraint(item: self.emojiBar, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: SCREEN_WIDTH)
  495. let height = NSLayoutConstraint(item: self.emojiBar, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: self.emojiBarHeight.toCGFloat)
  496. NSLayoutConstraint.activate([top, width, height])
  497. } else {
  498. self.bottomBarHeightConstraint.constant = self.bottomBarHeight.toCGFloat
  499. self.emojiBar.removeFromSuperview()
  500. }
  501. self.view.layoutIfNeeded()
  502. }
  503. @IBAction func micBtnClick(_ sender: UIButton) {
  504. DDLogDebug("点击了麦克风按钮")
  505. self.isShowAudioView.toggle()
  506. self.view.endEditing(true)
  507. if self.isShowAudioView {
  508. //emoji view 先关闭
  509. self.isShowEmoji = false
  510. self.emojiBar.removeFromSuperview()
  511. //开始添加emojiBar
  512. self.bottomBarHeightConstraint.constant = self.bottomBarHeight.toCGFloat + self.emojiBarHeight.toCGFloat
  513. self.audioBtnView.translatesAutoresizingMaskIntoConstraints = false
  514. self.bottomBar.addSubview(self.audioBtnView)
  515. let top = NSLayoutConstraint(item: self.audioBtnView, attribute: .top, relatedBy: .equal, toItem: self.audioBtnView.superview!, attribute: .top, multiplier: 1, constant: CGFloat(self.bottomBarHeight))
  516. let width = NSLayoutConstraint(item: self.audioBtnView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: SCREEN_WIDTH)
  517. let height = NSLayoutConstraint(item: self.audioBtnView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: self.emojiBarHeight.toCGFloat)
  518. NSLayoutConstraint.activate([top, width, height])
  519. } else {
  520. self.bottomBarHeightConstraint.constant = self.bottomBarHeight.toCGFloat
  521. self.audioBtnView.removeFromSuperview()
  522. }
  523. self.view.layoutIfNeeded()
  524. }
  525. @IBAction func imgBtnClick(_ sender: UIButton) {
  526. DDLogDebug("点击了图片按钮")
  527. self.chooseImage()
  528. }
  529. @IBAction func cameraBtnClick(_ sender: UIButton) {
  530. DDLogDebug("点击了相机按钮")
  531. self.takePhoto(delegate: self)
  532. }
  533. @IBAction func locationBtnClick(_ sender: UIButton) {
  534. DDLogDebug("点击了位置按钮")
  535. let vc = IMLocationChooseController.openChooseLocation { (data) in
  536. self.sendLocationMessage(loc: data)
  537. }
  538. self.navigationController?.pushViewController(vc, animated: false)
  539. }
  540. @IBAction func fileBtnClick(_ sender: UIButton) {
  541. DDLogDebug("点击了文件按钮")
  542. self.chooseFile()
  543. }
  544. // MARK: - UIMenuController 消息操作
  545. /// 长按事件
  546. @objc private func longpressEvent(gestureRecognizer: UILongPressGestureRecognizer) {
  547. if (gestureRecognizer.state == .began) {
  548. if let cell = gestureRecognizer.view {
  549. if cell.isKind(of: IMChatMessageSendViewCell.self), let myCell = cell as? IMChatMessageSendViewCell {
  550. myCell.becomeFirstResponder()
  551. self.showMenu(frame: myCell.frame, msg: myCell.msgInfo)
  552. }
  553. if cell.isKind(of: IMChatMessageViewCell.self), let myCell = cell as? IMChatMessageViewCell {
  554. myCell.becomeFirstResponder()
  555. self.showMenu(frame: myCell.frame, msg: myCell.msgInfo)
  556. }
  557. }
  558. }
  559. }
  560. /// 展现菜单
  561. private func showMenu(frame: CGRect, msg: IMMessageInfo?) {
  562. //定义菜单
  563. var menus:[UIMenuItem] = []
  564. if self.imConfig.enableRevokeMsg == true, let cp = msg?.createPerson {
  565. if cp == O2AuthSDK.shared.myInfo()?.distinguishedName {
  566. //发送者
  567. menus.append(UIMenuItem(title: "撤回", action: #selector(revokeMsg)))
  568. } else if self.conversation?.type == o2_im_conversation_type_group &&
  569. O2AuthSDK.shared.myInfo()?.distinguishedName == self.conversation?.adminPerson {
  570. // 群主
  571. menus.append(UIMenuItem(title: "撤回成员消息", action: #selector(revokeMsg)))
  572. }
  573. }
  574. // 文字消息 添加复制按钮
  575. if let body = msg?.body, let bodyInfo = IMMessageBodyInfo.deserialize(from: body) {
  576. if bodyInfo.type == o2_im_msg_type_text {
  577. menus.append(UIMenuItem(title: "复制", action: #selector(copyTextMsg)))
  578. }
  579. }
  580. if menus.count > 0 {
  581. self.currentSelectMsg = msg
  582. UIMenuController.shared.setTargetRect(frame, in: self.tableView)
  583. UIMenuController.shared.menuItems = menus
  584. UIMenuController.shared.update()
  585. UIMenuController.shared.setMenuVisible(true, animated: true)
  586. DDLogDebug("showMenu")
  587. }
  588. }
  589. /// 撤回菜单
  590. @objc private func revokeMsg() {
  591. DDLogDebug("点击了撤回消息")
  592. if let msg = self.currentSelectMsg, let id = msg.id {
  593. DDLogDebug("撤回消息,id: \(id)")
  594. self.viewModel.revokeChatMsg(msgId: id).then { result in
  595. if result {
  596. self.showMessage(msg: "撤回成功!")
  597. var newList: [IMMessageInfo] = []
  598. for item in self.chatMessageList {
  599. if item.id != id {
  600. newList.append(item)
  601. }
  602. }
  603. self.chatMessageList = newList
  604. self.tableView.reloadData()
  605. } else {
  606. self.showError(title: "撤回失败!")
  607. }
  608. }
  609. }
  610. }
  611. // 复制文字消息
  612. @objc private func copyTextMsg() {
  613. DDLogDebug("复制文字消息")
  614. if let msg = self.currentSelectMsg, let body = msg.body, let bodyInfo = IMMessageBodyInfo.deserialize(from: body) {
  615. UIPasteboard.general.string = bodyInfo.body
  616. self.showSuccess(title: "复制成功!")
  617. }
  618. }
  619. }
  620. // MARK: - 外部文件选择代理
  621. extension IMChatViewController: UIDocumentPickerDelegate {
  622. func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
  623. if urls.count > 0 {
  624. let file = urls[0]
  625. if file.startAccessingSecurityScopedResource() { //访问权限
  626. let fileName = file.lastPathComponent
  627. if let data = try? Data(contentsOf: file) {
  628. let fileext = file.pathExtension
  629. if O2.isImageExt(fileext) { // 图片消息
  630. let localFilePath = self.storageLocalImage(imageData: data, fileName: fileName)
  631. let msgId = self.prepareForSendImageMsg(filePath: localFilePath)
  632. self.uploadFileAndSendMsg(messageId: msgId, data: data, fileName: fileName, type: o2_im_msg_type_image)
  633. }else { // 文件消息
  634. let localFilePath = self.storageLocalImage(imageData: data, fileName: fileName)
  635. let msgId = self.prepareForSendFileMsg(filePath: localFilePath)
  636. self.uploadFileAndSendMsg(messageId: msgId, data: data, fileName: fileName, type: o2_im_msg_type_file)
  637. }
  638. } else {
  639. self.showError(title: "读取文件失败")
  640. }
  641. } else {
  642. self.showError(title: "没有获取文件的权限")
  643. }
  644. }
  645. }
  646. }
  647. // MARK: - 录音delegate
  648. extension IMChatViewController: IMChatAudioViewDelegate {
  649. private func audioRecordingGif() -> UIImage? {
  650. let url: URL? = Bundle.main.url(forResource: "listener08_anim", withExtension: "gif")
  651. guard let u = url else {
  652. return nil
  653. }
  654. guard let data = try? Data.init(contentsOf: u) else {
  655. return nil
  656. }
  657. return UIImage.sd_animatedGIF(with: data)
  658. }
  659. func showAudioRecordingView() {
  660. if self.voiceIconImage == nil {
  661. self.voiceImageSuperView = UIView()
  662. self.view.addSubview(self.voiceImageSuperView!)
  663. self.voiceImageSuperView?.backgroundColor = UIColor(displayP3Red: 0, green: 0, blue: 0, alpha: 0.6)
  664. self.voiceImageSuperView?.snp_makeConstraints { (make) in
  665. make.center.equalTo(self.view)
  666. make.size.equalTo(CGSize(width:140, height:140))
  667. }
  668. self.voiceIconImage = UIImageView()
  669. self.voiceImageSuperView?.addSubview(self.voiceIconImage!)
  670. self.voiceIconImage?.snp_makeConstraints { (make) in
  671. make.top.left.equalTo(self.voiceImageSuperView!).inset(UIEdgeInsets(top: 20, left: 35, bottom: 0, right: 0))
  672. make.size.equalTo(CGSize(width: 70, height: 70))
  673. }
  674. let voiceIconTitleLabel = UILabel()
  675. self.voiceIocnTitleLable = voiceIconTitleLabel
  676. self.voiceIconImage?.addSubview(voiceIconTitleLabel)
  677. voiceIconTitleLabel.textColor = UIColor.white
  678. voiceIconTitleLabel.font = .systemFont(ofSize: 12)
  679. voiceIconTitleLabel.text = "松开发送,上滑取消"
  680. voiceIconTitleLabel.snp_makeConstraints { (make) in
  681. make.bottom.equalTo(self.voiceImageSuperView!).offset(-15)
  682. make.centerX.equalTo(self.voiceImageSuperView!)
  683. }
  684. }
  685. self.voiceImageSuperView?.isHidden = false
  686. if let gifImage = self.audioRecordingGif() {
  687. self.voiceIconImage?.image = gifImage
  688. }else {
  689. self.voiceIconImage?.image = UIImage(named: "chat_audio_voice")
  690. }
  691. self.voiceIocnTitleLable?.text = "松开发送,上滑取消";
  692. }
  693. func hideAudioRecordingView() {
  694. self.voiceImageSuperView?.isHidden = true
  695. }
  696. func changeRecordingView2uplide() {
  697. self.voiceIocnTitleLable?.text = "松开手指,取消发送";
  698. self.voiceIconImage?.image = UIImage(named: "chat_audio_cancel")
  699. }
  700. func changeRecordingView2down() {
  701. if let gifImage = self.audioRecordingGif() {
  702. self.voiceIconImage?.image = gifImage
  703. }else {
  704. self.voiceIconImage?.image = UIImage(named: "chat_audio_voice")
  705. }
  706. self.voiceIocnTitleLable?.text = "松开发送,上滑取消";
  707. }
  708. func sendVoice(path: String, voice: Data, duration: String) {
  709. let msg = IMMessageBodyInfo()
  710. msg.fileTempPath = path
  711. msg.body = o2_im_msg_body_audio
  712. msg.type = o2_im_msg_type_audio
  713. msg.audioDuration = duration
  714. let msgId = self.prepareForSendAudioMsg(tempMessage: msg)
  715. let fileName = path.split("/").last ?? "MySound.ilbc"
  716. DDLogDebug("音频文件:\(fileName)")
  717. self.uploadFileAndSendMsg(messageId: msgId, data: voice, fileName: fileName, type: o2_im_msg_type_audio)
  718. }
  719. }
  720. // MARK: - 拍照delegate
  721. extension IMChatViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate {
  722. func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
  723. if let image = info[.editedImage] as? UIImage {
  724. let fileName = "\(UUID().uuidString).png"
  725. let newData = image.pngData()!
  726. let localFilePath = self.storageLocalImage(imageData: newData, fileName: fileName)
  727. let msgId = self.prepareForSendImageMsg(filePath: localFilePath)
  728. self.uploadFileAndSendMsg(messageId: msgId, data: newData, fileName: fileName, type: o2_im_msg_type_image)
  729. } else {
  730. DDLogError("没有选择到图片!")
  731. }
  732. picker.dismiss(animated: true, completion: nil)
  733. // var newData = data
  734. // //处理图片旋转的问题
  735. // if imageOrientation != UIImage.Orientation.up {
  736. // let newImage = UIImage(data: data)?.fixOrientation()
  737. // if newImage != nil {
  738. // newData = newImage!.pngData()!
  739. // }
  740. // }
  741. // var fileName = ""
  742. // if dict?["PHImageFileURLKey"] != nil {
  743. // let fileURL = dict?["PHImageFileURLKey"] as! URL
  744. // fileName = fileURL.lastPathComponent
  745. // } else {
  746. // fileName = "\(UUID().uuidString).png"
  747. // }
  748. }
  749. }
  750. // MARK: - audio 播放 delegate
  751. extension IMChatViewController: AudioPlayerManagerDelegate {
  752. func didAudioPlayerBeginPlay(_ AudioPlayer: AVAudioPlayer) {
  753. DDLogDebug("播放开始")
  754. }
  755. func didAudioPlayerStopPlay(_ AudioPlayer: AVAudioPlayer) {
  756. DDLogDebug("播放结束")
  757. self.stopPlayAudioGif()
  758. }
  759. func didAudioPlayerPausePlay(_ AudioPlayer: AVAudioPlayer) {
  760. DDLogDebug("播放暂停")
  761. }
  762. }
  763. // MARK: - 消息点击 delegate
  764. extension IMChatViewController: IMChatMessageDelegate {
  765. func openApplication(storyboard: String) {
  766. if storyboard == "mind" {
  767. // let flutterViewController = O2FlutterViewController()
  768. // flutterViewController.setInitialRoute("mindMap")
  769. // flutterViewController.modalPresentationStyle = .fullScreen
  770. // self.present(flutterViewController, animated: false, completion: nil)
  771. }else {
  772. let storyBoard = UIStoryboard(name: storyboard, bundle: nil)
  773. guard let destVC = storyBoard.instantiateInitialViewController() else {
  774. return
  775. }
  776. destVC.modalPresentationStyle = .fullScreen
  777. if destVC.isKind(of: ZLNavigationController.self) {
  778. self.show(destVC, sender: nil)
  779. }else{
  780. self.navigationController?.pushViewController(destVC, animated: true)
  781. }
  782. }
  783. }
  784. func openWork(workId: String) {
  785. self.openWorkPage(work: workId)
  786. // 已经支持 未结束和结束的工作打开
  787. // self.showLoading()
  788. // self.viewModel.isWorkCompleted(work: workId).always {
  789. // self.hideLoading()
  790. // }.then{ result in
  791. // if result {
  792. // self.showMessage(msg: "工作已经完成了!")
  793. // }else {
  794. // self.openWorkPage(work: workId)
  795. // }
  796. // }.catch {_ in
  797. // self.showMessage(msg: "工作已经完成了!")
  798. // }
  799. }
  800. private func openWorkPage(work: String) {
  801. let storyBoard = UIStoryboard(name: "task", bundle: nil)
  802. let destVC = storyBoard.instantiateViewController(withIdentifier: "todoTaskDetailVC") as! TodoTaskDetailViewController
  803. let json = """
  804. {"work":"\(work)", "workCompleted":"", "title":""}
  805. """
  806. let todo = TodoTask(JSONString: json)
  807. destVC.todoTask = todo
  808. destVC.backFlag = 3 //隐藏就行
  809. self.show(destVC, sender: nil)
  810. }
  811. func openLocatinMap(info: IMMessageBodyInfo) {
  812. IMShowLocationViewController.pushShowLocation(vc: self, latitude: info.latitude, longitude: info.longitude,
  813. address: info.address, addressDetail: info.addressDetail)
  814. }
  815. func openImageOrFileMessage(info: IMMessageBodyInfo) {
  816. if let id = info.fileId {
  817. self.showLoading()
  818. var ext = info.fileExtension ?? "png"
  819. if ext.isEmpty {
  820. ext = "png"
  821. }
  822. O2IMFileManager.shared
  823. .getFileLocalUrl(fileId: id, fileExtension: ext)
  824. .always {
  825. self.hideLoading()
  826. }.then { (path) in
  827. let currentURL = NSURL(fileURLWithPath: path.path)
  828. DDLogDebug(currentURL.description)
  829. DDLogDebug(path.path)
  830. if QLPreviewController.canPreview(currentURL) {
  831. self.previewVC.currentFileURLS.removeAll()
  832. self.previewVC.currentFileURLS.append(currentURL)
  833. self.previewVC.reloadData()
  834. self.pushVC(self.previewVC)
  835. } else {
  836. self.showError(title: "当前文件类型不支持预览!")
  837. }
  838. }
  839. .catch { (error) in
  840. DDLogError(error.localizedDescription)
  841. self.showError(title: "获取文件异常!")
  842. }
  843. } else if let temp = info.fileTempPath {
  844. let currentURL = NSURL(fileURLWithPath: temp)
  845. DDLogDebug(currentURL.description)
  846. DDLogDebug(temp)
  847. if QLPreviewController.canPreview(currentURL) {
  848. self.previewVC.currentFileURLS.removeAll()
  849. self.previewVC.currentFileURLS.append(currentURL)
  850. self.previewVC.reloadData()
  851. self.pushVC(self.previewVC)
  852. } else {
  853. self.showError(title: "当前文件类型不支持预览!")
  854. }
  855. }
  856. }
  857. func playAudio(info: IMMessageBodyInfo, id: String?) {
  858. if self.playingAudioMessageId != nil && self.playingAudioMessageId == id {
  859. DDLogError("正在播放中。。。。。")
  860. return
  861. }
  862. if let fileId = info.fileId {
  863. var ext = info.fileExtension ?? "mp3"
  864. if ext.isEmpty {
  865. ext = "mp3"
  866. }
  867. O2IMFileManager.shared.getFileLocalUrl(fileId: fileId, fileExtension: ext)
  868. .then { (url) in
  869. self.playAudio(url: url)
  870. }.catch { (e) in
  871. DDLogError(e.localizedDescription)
  872. }
  873. } else if let filePath = info.fileTempPath {
  874. self.playAudio(url: URL(fileURLWithPath: filePath))
  875. }
  876. self.playAudioGif(id: id)
  877. }
  878. }
  879. // MARK: - 表情点击 delegate
  880. extension IMChatViewController: IMChatEmojiBarClickDelegate {
  881. func clickEmoji(emoji: String) {
  882. DDLogDebug("发送表情消息 \(emoji)")
  883. self.sendEmojiMessage(emoji: emoji)
  884. }
  885. }
  886. // MARK: - tableview delegate
  887. extension IMChatViewController: UITableViewDelegate, UITableViewDataSource {
  888. func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
  889. return self.chatMessageList.count
  890. }
  891. func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
  892. let msg = self.chatMessageList[indexPath.row]
  893. let isPlaying = self.playingAudioMessageId == nil ? false : (self.playingAudioMessageId == msg.id)
  894. if msg.createPerson == O2AuthSDK.shared.myInfo()?.distinguishedName { //发送者
  895. if let cell = tableView.dequeueReusableCell(withIdentifier: "IMChatMessageSendViewCell", for: indexPath) as? IMChatMessageSendViewCell {
  896. cell.setContent(item: self.chatMessageList[indexPath.row], isPlayingAudio: isPlaying)
  897. cell.delegate = self
  898. let longpress = UILongPressGestureRecognizer()
  899. longpress.addTarget(self, action: #selector(longpressEvent))
  900. cell.addGestureRecognizer(longpress)
  901. return cell
  902. }
  903. } else {
  904. if let cell = tableView.dequeueReusableCell(withIdentifier: "IMChatMessageViewCell", for: indexPath) as? IMChatMessageViewCell {
  905. cell.setContent(item: self.chatMessageList[indexPath.row], isPlayingAudio: isPlaying)
  906. cell.delegate = self
  907. let longpress = UILongPressGestureRecognizer()
  908. longpress.addTarget(self, action: #selector(longpressEvent))
  909. cell.addGestureRecognizer(longpress)
  910. return cell
  911. }
  912. }
  913. return UITableViewCell()
  914. }
  915. func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
  916. let msg = self.chatMessageList[indexPath.row]
  917. return cellHeight(item: msg)
  918. }
  919. func cellHeight(item: IMMessageInfo) -> CGFloat {
  920. if let jsonBody = item.body, let body = IMMessageBodyInfo.deserialize(from: jsonBody){
  921. if body.type == o2_im_msg_type_emoji {
  922. // 上边距 69 + emoji高度 + 内边距 + 底部空白高度
  923. return 69 + 36 + 20 + 10
  924. } else if body.type == o2_im_msg_type_image {
  925. // 上边距 69 + 图片高度 + 内边距 + 底部空白高度
  926. return 69 + 192 + 20 + 10
  927. } else if o2_im_msg_type_audio == body.type {
  928. // 上边距 69 + audio高度 + 内边距 + 底部空白高度
  929. return 69 + IMAudioView.IMAudioView_height + 20 + 10
  930. } else if o2_im_msg_type_location == body.type {
  931. // 上边距 69 + 位置图高度 + 内边距 + 底部空白高度
  932. return 69 + IMLocationView.IMLocationViewHeight + 20 + 10
  933. } else if o2_im_msg_type_file == body.type {
  934. return 69 + IMFileView.IMFileView_height + 20 + 10
  935. } else if o2_im_msg_type_process == body.type {
  936. return 69 + IMProcessCardView.IMProcessCardView_height + 20 + 10
  937. } else {
  938. if let bodyText = body.body {
  939. let size = bodyText.getSizeWithMaxWidth(fontSize: 16, maxWidth: messageWidth)
  940. // 上边距 69 + 文字高度 + 内边距 + 底部空白高度
  941. return 69 + size.height + 28 + 10
  942. }
  943. }
  944. }
  945. return 132
  946. }
  947. func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
  948. tableView.deselectRow(at: indexPath, animated: false)
  949. }
  950. }
  951. // MARK: - textField delegate
  952. extension IMChatViewController: UITextFieldDelegate {
  953. func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
  954. DDLogDebug("准备开始输入......")
  955. closeOtherView()
  956. return true
  957. }
  958. private func closeOtherView() {
  959. self.isShowEmoji = false
  960. self.isShowAudioView = false
  961. self.bottomBarHeightConstraint.constant = self.bottomBarHeight.toCGFloat
  962. self.view.layoutIfNeeded()
  963. }
  964. func textFieldShouldReturn(_ textField: UITextField) -> Bool {
  965. DDLogDebug("回车。。。。")
  966. self.sendTextMessage()
  967. return true
  968. }
  969. }