TSChatViewController.swift 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. //
  2. // MIT License
  3. //
  4. // Copyright (c) 2017-2020 MessageKit
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in all
  14. // copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  22. // SOFTWARE.
  23. import InputBarAccessoryView
  24. import MessageKit
  25. import UIKit
  26. import MapKit
  27. // MARK: - TSChatViewController
  28. /// A base class for the example controllers
  29. class TSChatViewController: MessagesViewController, MessagesDataSource {
  30. // MARK: Internal
  31. var viewModel:TSAIChatVM = TSAIChatVM()
  32. // MARK: - Public properties
  33. lazy var messageList: [TSChatMessage] = []
  34. private(set) lazy var refreshControl: UIRefreshControl = {
  35. let control = UIRefreshControl()
  36. control.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged)
  37. return control
  38. }()
  39. // MARK: Private
  40. lazy var textMessageSizeCalculator = TSTextLayoutSizeCalculator(layout: self.messagesCollectionView.messagesCollectionViewFlowLayout)
  41. lazy var inputBarVC: TSChatInputBarVC = {
  42. let inputBarVC = TSChatInputBarVC()
  43. inputBarVC.sendComplete = { [weak self] components in
  44. guard let self = self else { return }
  45. inputSendMsg(components)
  46. }
  47. return inputBarVC
  48. }()
  49. let inputBarBgView:UIView = {
  50. let inputBarBgView = UIView()
  51. inputBarBgView.addShadow(shadowColor: "#111111".uiColor.cgColor, shadowOffset: CGSize(width: 0, height: -10), shadowRadius: 10, shadowOpacity: 1.0)
  52. return inputBarBgView
  53. }()
  54. let inputBarTopView:UIView = UIView()
  55. //免费次数
  56. lazy var freeText: UILabel = {
  57. let textLabel = UILabel.createLabel(
  58. text: "Remaining \(kPurchaseDefault.freeNum(type: .aichat)) free times",
  59. font: .font(size: 12),
  60. textColor: "#E83E3E".uiColor,
  61. textAlignment: .center,
  62. numberOfLines: 0
  63. )
  64. textLabel.isHidden = false
  65. return textLabel
  66. }()
  67. lazy var upgradeVipBg: UIView = {
  68. let upgradeVipBg = UIView()
  69. let imageView = UIImageView.createImageView(imageName: "vip_upgrade_bg",contentMode: .scaleToFill)
  70. upgradeVipBg.addSubview(imageView)
  71. imageView.snp.makeConstraints { make in
  72. make.edges.equalToSuperview()
  73. }
  74. let label = UILabel.createLabel(
  75. text:"Free usage limit reached. Upgrade for unlimited chats.".localized,
  76. font: .font(size: 14,weight: .bold),
  77. textColor: "#111111".uiColor,
  78. numberOfLines: 0
  79. )
  80. upgradeVipBg.addSubview(label)
  81. label.snp.makeConstraints { make in
  82. make.leading.equalTo(16)
  83. make.top.equalTo(8)
  84. make.bottom.equalTo(-8)
  85. make.trailing.equalTo(-95)
  86. }
  87. let upgradeBtn = UIButton.createButton(
  88. title: "Upgrade".localized,
  89. backgroundColor: "#111111".uiColor,
  90. font:.font(size: 12,weight: .medium),
  91. titleColor:.white,
  92. corner: 14) { [weak self] in
  93. guard let self = self else { return }
  94. TSPurchaseVC.show(target: self) { [weak self] in
  95. guard let self = self else { return }
  96. updateVipView()
  97. }
  98. }
  99. upgradeVipBg.addSubview(upgradeBtn)
  100. upgradeBtn.snp.makeConstraints { make in
  101. make.trailing.equalTo(-12)
  102. make.centerY.equalToSuperview()
  103. make.width.equalTo(70)
  104. make.height.equalTo(28)
  105. }
  106. return upgradeVipBg
  107. }()
  108. lazy var scrollToBottomButton: UIButton = {
  109. let backBottomBtn = UIButton.createButton(image: UIImage(named: "down_arrow_line")) { [weak self] in
  110. guard let self = self else { return }
  111. messagesCollectionView.scrollToLastItem(animated: false)
  112. }
  113. backBottomBtn.isHidden = true
  114. backBottomBtn.backgroundColor = .popupColor
  115. backBottomBtn.cornerRadius = 16.0
  116. return backBottomBtn
  117. }()
  118. override func viewDidLoad() {
  119. super.viewDidLoad()
  120. navigationItem.title = "MessageKit"
  121. configureMessageCollectionView()
  122. configureMessageInputBar()
  123. configureOtherUI()
  124. loadFirstMessages()
  125. if viewModel.uiStyle == .chat {
  126. // 注册通知监听,App死的时候,保存本次聊天记录到本地
  127. NotificationCenter.default.addObserver(self, selector: #selector(saveChatList), name: .kApplicationWillTerminate, object: nil)
  128. }
  129. }
  130. @objc func saveChatList() {
  131. messageList.remove(at: 0)
  132. viewModel.updateMessages(msgModels: messageList)
  133. }
  134. override func viewDidAppear(_ animated: Bool) {
  135. super.viewDidAppear(animated)
  136. }
  137. override func viewDidDisappear(_ animated: Bool) {
  138. super.viewDidDisappear(animated)
  139. }
  140. func loadFirstMessages() {
  141. //获取消息数量
  142. self.messageList = viewModel.getHistoryChatMessage()
  143. self.messagesCollectionView.reloadData()
  144. self.messagesCollectionView.scrollToLastItem(animated: false)
  145. }
  146. @objc
  147. func loadMoreMessages() {
  148. //获取更多消息数量
  149. }
  150. func configureMessageCollectionView() {
  151. view.backgroundColor = .clear
  152. //设置自定义FlowLayout,itemsize等,都在这里控制
  153. let flowLayout = CustomMessagesFlowLayout()
  154. flowLayout.sectionInset = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0)
  155. messagesCollectionView.collectionViewLayout = flowLayout
  156. messagesCollectionView.backgroundColor = .clear
  157. messagesCollectionView.register(TSTextMessageContentCell.self)
  158. messagesCollectionView.messagesLayoutDelegate = self
  159. messagesCollectionView.messagesDisplayDelegate = self
  160. messagesCollectionView.messagesDataSource = self
  161. messagesCollectionView.messageCellDelegate = self
  162. messagesCollectionView.clipsToBounds = true
  163. scrollsToLastItemOnKeyboardBeginsEditing = true // default false
  164. maintainPositionOnInputBarHeightChanged = true // default false
  165. showMessageTimestampOnSwipeLeft = false // default false
  166. // messagesCollectionView.refreshControl = refreshControl
  167. messagesCollectionView.reloadData()
  168. }
  169. func configureMessageInputBar() {
  170. inputBarBgView.addSubview(inputBarTopView)
  171. inputBarTopView.snp.makeConstraints { make in
  172. make.leading.equalTo(0)
  173. make.trailing.equalTo(0)
  174. make.top.equalTo(0)
  175. }
  176. if viewModel.uiStyle == .chat {
  177. inputBarBgView.addSubview(inputBarVC.view)
  178. inputBarVC.view.snp.makeConstraints { make in
  179. make.leading.equalTo(0)
  180. make.trailing.equalTo(0)
  181. make.top.equalTo(inputBarTopView.snp.bottom)
  182. make.bottom.equalTo(0)
  183. }
  184. }
  185. inputBarType = .custom(inputBarBgView)
  186. }
  187. func configureOtherUI() {
  188. view.addSubview(scrollToBottomButton)
  189. scrollToBottomButton.snp.makeConstraints { make in
  190. make.centerX.equalToSuperview()
  191. make.bottom.equalTo(inputContainerView.snp.top).offset(-14)
  192. make.width.height.equalTo(32)
  193. }
  194. self.scrollViewDidScroll(self.messagesCollectionView)
  195. updateVipView()
  196. }
  197. func updateVipView() {
  198. inputBarTopView.subviews.forEach { $0.removeFromSuperview()}
  199. if viewModel.uiStyle == .chat ,
  200. kPurchaseDefault.isVip == false
  201. {
  202. let freeNum = kPurchaseDefault.freeNum(type: .aichat)
  203. if freeNum > 0 {
  204. freeText.text = "Remaining \(freeNum) free times"
  205. inputBarTopView.addSubview(freeText)
  206. freeText.snp.makeConstraints { make in
  207. make.leading.equalTo(20)
  208. make.trailing.equalTo(-20)
  209. make.bottom.equalTo(-8)
  210. make.top.equalTo(8)
  211. }
  212. }else{
  213. inputBarTopView.addSubview(upgradeVipBg)
  214. upgradeVipBg.snp.makeConstraints { make in
  215. make.leading.equalTo(16)
  216. make.trailing.equalTo(-16)
  217. make.bottom.equalTo(-2)
  218. make.top.equalTo(14)
  219. }
  220. }
  221. }
  222. }
  223. // MARK: - Helpers
  224. var lastIndexPath:IndexPath{
  225. if messageList.count == 0 {
  226. return IndexPath(item: 0, section: 0)
  227. }
  228. return IndexPath(item: messageList.count - 1, section: 0)
  229. }
  230. func insertMessage(_ message: TSChatMessage) {
  231. messageList.append(message)
  232. messagesCollectionView.performBatchUpdates({
  233. messagesCollectionView.insertItems(at: [lastIndexPath])
  234. if messageList.count >= 2 {
  235. messagesCollectionView.reloadItems(at: [lastIndexPath])
  236. }
  237. }, completion: { [weak self] _ in
  238. if self?.isLastSectionVisible() == true {
  239. self?.messagesCollectionView.scrollToLastItem(animated: true)
  240. }
  241. })
  242. }
  243. func isLastSectionVisible() -> Bool {
  244. guard !messageList.isEmpty else { return false }
  245. return messagesCollectionView.indexPathsForVisibleItems.contains(lastIndexPath)
  246. }
  247. private let formatter: DateFormatter = {
  248. let formatter = DateFormatter()
  249. formatter.dateStyle = .medium
  250. return formatter
  251. }()
  252. }
  253. // MARK: MessagesDataSource
  254. extension TSChatViewController {
  255. var currentSender: SenderType {
  256. return viewModel.kUserSender
  257. }
  258. func numberOfSections(in _: MessagesCollectionView) -> Int {
  259. return 1
  260. }
  261. func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int{
  262. messageList.count
  263. }
  264. func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType {
  265. messageList[indexPath.item]
  266. }
  267. func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
  268. // if indexPath.item % 3 == 0 {
  269. // return NSAttributedString(
  270. // string: MessageKitDateFormatter.shared.string(from: message.sentDate),
  271. // attributes: [
  272. // NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
  273. // NSAttributedString.Key.foregroundColor: UIColor.darkGray,
  274. // ])
  275. // }
  276. return nil
  277. }
  278. func cellBottomLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? {
  279. NSAttributedString(
  280. string: "Read",
  281. attributes: [
  282. NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
  283. NSAttributedString.Key.foregroundColor: UIColor.darkGray,
  284. ])
  285. }
  286. func messageTopLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? {
  287. let name = message.sender.displayName
  288. return NSAttributedString(
  289. string: name,
  290. attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
  291. }
  292. func messageBottomLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? {
  293. let dateString = formatter.string(from: message.sentDate)
  294. return NSAttributedString(
  295. string: dateString,
  296. attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)])
  297. }
  298. func textCell(
  299. for message: MessageType,
  300. at indexPath: IndexPath,
  301. in messagesCollectionView: MessagesCollectionView)
  302. -> UICollectionViewCell?
  303. {
  304. let cell = messagesCollectionView.dequeueReusableCell(
  305. TSTextMessageContentCell.self,
  306. for: indexPath)
  307. cell.configure(
  308. with: message,
  309. at: indexPath,
  310. in: messagesCollectionView,
  311. dataSource: self,
  312. and: textMessageSizeCalculator)
  313. return cell
  314. }
  315. }
  316. // MARK: InputBarAccessoryViewDelegate
  317. extension TSChatViewController: InputBarAccessoryViewDelegate {
  318. // MARK: Internal
  319. @objc
  320. func inputBar(_: InputBarAccessoryView, didPressSendButtonWith _: String) {
  321. processInputBar(messageInputBar)
  322. }
  323. //聊天发送内容
  324. func processInputBar(_ inputBar: InputBarAccessoryView) {
  325. // Here we can parse for which substrings were autocompleted
  326. let attributedText = inputBar.inputTextView.attributedText!
  327. let range = NSRange(location: 0, length: attributedText.length)
  328. attributedText.enumerateAttribute(.autocompleted, in: range, options: []) { _, range, _ in
  329. let substring = attributedText.attributedSubstring(from: range)
  330. let context = substring.attribute(.autocompletedContext, at: 0, effectiveRange: nil)
  331. print("Autocompleted: `", substring, "` with context: ", context ?? "-")
  332. }
  333. let components = inputBar.inputTextView.components
  334. inputBar.inputTextView.text = String()
  335. inputBar.invalidatePlugins()
  336. inputBar.inputTextView.placeholder = "Aa"
  337. sendMessages(components)
  338. messagesCollectionView.scrollToLastItem(animated: true)
  339. }
  340. // MARK: Private
  341. private func sendMessages(_ data: [Any]) {
  342. let user = viewModel.kUserSender
  343. for component in data {
  344. if let str = component as? String {
  345. let message = TSChatMessage(text: str, user: user, messageId: UUID().uuidString, date: Date())
  346. insertMessage(message)
  347. //保存这条消息到本地数据库
  348. //发送消息后,进行AI 对话生成
  349. generativeAIChat(message: message)
  350. }
  351. }
  352. }
  353. func generativeAIChat(message:TSChatMessage) {
  354. var messageString = ""
  355. switch message.kind {
  356. case .text(let message):
  357. messageString = message
  358. default:
  359. break
  360. }
  361. if messageString.count == 0 {
  362. return
  363. }
  364. let message = TSChatMessage(attributedText: kMDAttributedString(text: ""), user: viewModel.kAIUser, messageId: UUID().uuidString, date: Date())
  365. message.sendState = .start
  366. insertMessage(message)
  367. inputBarVC.sendEnabled(enabled: false)
  368. viewModel.sendChatMessage(message: messageString) {[weak self] string in
  369. guard let self = self else { return }
  370. debugPrint("viewModel.AiMDString=\(viewModel.AiMDString)")
  371. message.kind = .attributedText(kMDAttributedString(text: viewModel.AiMDString))
  372. message.sendState = .progress(0.5)
  373. updataAIChatCellUI()
  374. } completion: {[weak self] data, error in
  375. guard let self = self else { return }
  376. if let netData = data {
  377. message.sendState = .success("netData")
  378. //保存这条消息到本地数据库
  379. //消耗一次 AI 次数
  380. kPurchaseDefault.useOnceForFree(type: .aichat)
  381. }else {
  382. message.kind = .attributedText(kMDAttributedString(text: kAIErrorString))
  383. message.sendState = .failed(kAIErrorString)
  384. //保存这条消息到本地数据库
  385. }
  386. updataAIChatCellUI()
  387. kExecuteOnMainThread {
  388. self.inputBarVC.sendEnabled(enabled: true)
  389. }
  390. }
  391. }
  392. func updataAIChatCellUI(){
  393. kExecuteOnMainThread {
  394. if self.messageList.count >= 2 {
  395. UIView.performWithoutAnimation {
  396. self.messagesCollectionView.reloadItems(at: [self.lastIndexPath])
  397. }
  398. }else{
  399. self.messagesCollectionView.reloadData()
  400. }
  401. //更新 Vip
  402. if kPurchaseDefault.isVip == false{
  403. self.updateVipView()
  404. }
  405. self.messagesCollectionView.scrollToLastItem(animated: false)
  406. }
  407. }
  408. }
  409. extension TSChatViewController{
  410. func inputSendMsg(_ data: [Any]) {
  411. //判断 vip
  412. if kJudgeVip(externalBool: kPurchaseDefault.freeNumAvailable(type: .aichat) == false, vc: self, closePageBlock: {[weak self] in
  413. guard let self = self else { return }
  414. }){ return }
  415. sendMessages(data)
  416. messagesCollectionView.scrollToLastItem(animated: true)
  417. }
  418. }
  419. extension TSChatViewController{
  420. // UICollectionViewDelegate 方法
  421. override func scrollViewDidScroll(_ scrollView: UIScrollView) {
  422. let offsetY = scrollView.contentOffset.y
  423. let contentHeight = scrollView.contentSize.height
  424. let frameHeight = scrollView.frame.size.height
  425. // 判断是否需要显示滚动到底部的按钮
  426. if offsetY > contentHeight - frameHeight - 400 {
  427. scrollToBottomButton.isHidden = true
  428. } else {
  429. scrollToBottomButton.isHidden = false
  430. }
  431. }
  432. }