123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524 |
- //
- // MIT License
- //
- // Copyright (c) 2017-2020 MessageKit
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in all
- // copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
- // SOFTWARE.
- import InputBarAccessoryView
- import MessageKit
- import UIKit
- import MapKit
- // MARK: - TSChatViewController
- /// A base class for the example controllers
- class TSChatViewController: MessagesViewController, MessagesDataSource {
- // MARK: Internal
- var viewModel:TSAIChatVM = TSAIChatVM()
-
-
- // MARK: - Public properties
- lazy var messageList: [TSChatMessage] = []
-
- private(set) lazy var refreshControl: UIRefreshControl = {
- let control = UIRefreshControl()
- control.addTarget(self, action: #selector(loadMoreMessages), for: .valueChanged)
- return control
- }()
-
- // MARK: Private
- lazy var textMessageSizeCalculator = TSTextLayoutSizeCalculator(layout: self.messagesCollectionView.messagesCollectionViewFlowLayout)
-
- lazy var inputBarVC: TSChatInputBarVC = {
- let inputBarVC = TSChatInputBarVC()
- inputBarVC.sendComplete = { [weak self] components in
- guard let self = self else { return }
- inputSendMsg(components)
- }
- return inputBarVC
- }()
-
-
- let inputBarBgView:UIView = {
- let inputBarBgView = UIView()
- inputBarBgView.addShadow(shadowColor: "#111111".uiColor.cgColor, shadowOffset: CGSize(width: 0, height: -10), shadowRadius: 10, shadowOpacity: 1.0)
- return inputBarBgView
- }()
- let inputBarTopView:UIView = UIView()
-
- //免费次数
- lazy var freeText: UILabel = {
- let textLabel = UILabel.createLabel(
- text: "Remaining \(kPurchaseDefault.freeNum(type: .aichat)) free times",
- font: .font(size: 12),
- textColor: "#E83E3E".uiColor,
- textAlignment: .center,
- numberOfLines: 0
- )
- textLabel.isHidden = false
- return textLabel
- }()
-
-
- lazy var upgradeVipBg: UIView = {
- let upgradeVipBg = UIView()
-
- let imageView = UIImageView.createImageView(imageName: "vip_upgrade_bg",contentMode: .scaleToFill)
- upgradeVipBg.addSubview(imageView)
- imageView.snp.makeConstraints { make in
- make.edges.equalToSuperview()
- }
-
- let label = UILabel.createLabel(
- text:"Free usage limit reached. Upgrade for unlimited chats.".localized,
- font: .font(size: 14,weight: .bold),
- textColor: "#111111".uiColor,
- numberOfLines: 0
- )
- upgradeVipBg.addSubview(label)
- label.snp.makeConstraints { make in
- make.leading.equalTo(16)
- make.top.equalTo(8)
- make.bottom.equalTo(-8)
- make.trailing.equalTo(-95)
- }
-
- let upgradeBtn = UIButton.createButton(
- title: "Upgrade".localized,
- backgroundColor: "#111111".uiColor,
- font:.font(size: 12,weight: .medium),
- titleColor:.white,
- corner: 14) { [weak self] in
- guard let self = self else { return }
- TSPurchaseVC.show(target: self) { [weak self] in
- guard let self = self else { return }
- updateVipView()
- }
- }
- upgradeVipBg.addSubview(upgradeBtn)
- upgradeBtn.snp.makeConstraints { make in
- make.trailing.equalTo(-12)
- make.centerY.equalToSuperview()
- make.width.equalTo(70)
- make.height.equalTo(28)
- }
- return upgradeVipBg
- }()
-
-
-
- lazy var scrollToBottomButton: UIButton = {
- let backBottomBtn = UIButton.createButton(image: UIImage(named: "down_arrow_line")) { [weak self] in
- guard let self = self else { return }
- messagesCollectionView.scrollToLastItem(animated: false)
- }
- backBottomBtn.isHidden = true
- backBottomBtn.backgroundColor = .popupColor
- backBottomBtn.cornerRadius = 16.0
- return backBottomBtn
- }()
-
-
-
-
- override func viewDidLoad() {
- super.viewDidLoad()
- navigationItem.title = "MessageKit"
- configureMessageCollectionView()
- configureMessageInputBar()
- configureOtherUI()
- loadFirstMessages()
-
-
- if viewModel.uiStyle == .chat {
- // 注册通知监听,App死的时候,保存本次聊天记录到本地
- NotificationCenter.default.addObserver(self, selector: #selector(saveChatList), name: .kApplicationWillTerminate, object: nil)
- }
- }
-
- @objc func saveChatList() {
- messageList.remove(at: 0)
- viewModel.updateMessages(msgModels: messageList)
- }
-
- override func viewDidAppear(_ animated: Bool) {
- super.viewDidAppear(animated)
- }
-
- override func viewDidDisappear(_ animated: Bool) {
- super.viewDidDisappear(animated)
- }
-
- func loadFirstMessages() {
- //获取消息数量
- self.messageList = viewModel.getHistoryChatMessage()
- self.messagesCollectionView.reloadData()
- self.messagesCollectionView.scrollToLastItem(animated: false)
- }
-
- @objc
- func loadMoreMessages() {
- //获取更多消息数量
-
- }
-
- func configureMessageCollectionView() {
- view.backgroundColor = .clear
- //设置自定义FlowLayout,itemsize等,都在这里控制
- let flowLayout = CustomMessagesFlowLayout()
- flowLayout.sectionInset = UIEdgeInsets(top: 4, left: 0, bottom: 4, right: 0)
-
- messagesCollectionView.collectionViewLayout = flowLayout
- messagesCollectionView.backgroundColor = .clear
- messagesCollectionView.register(TSTextMessageContentCell.self)
- messagesCollectionView.messagesLayoutDelegate = self
- messagesCollectionView.messagesDisplayDelegate = self
- messagesCollectionView.messagesDataSource = self
- messagesCollectionView.messageCellDelegate = self
- messagesCollectionView.clipsToBounds = true
- scrollsToLastItemOnKeyboardBeginsEditing = true // default false
- maintainPositionOnInputBarHeightChanged = true // default false
- showMessageTimestampOnSwipeLeft = false // default false
- // messagesCollectionView.refreshControl = refreshControl
- messagesCollectionView.reloadData()
- }
-
- func configureMessageInputBar() {
-
- inputBarBgView.addSubview(inputBarTopView)
- inputBarTopView.snp.makeConstraints { make in
- make.leading.equalTo(0)
- make.trailing.equalTo(0)
- make.top.equalTo(0)
- }
-
- if viewModel.uiStyle == .chat {
- inputBarBgView.addSubview(inputBarVC.view)
- inputBarVC.view.snp.makeConstraints { make in
- make.leading.equalTo(0)
- make.trailing.equalTo(0)
- make.top.equalTo(inputBarTopView.snp.bottom)
- make.bottom.equalTo(0)
- }
- }
- inputBarType = .custom(inputBarBgView)
- }
-
-
- func configureOtherUI() {
- view.addSubview(scrollToBottomButton)
- scrollToBottomButton.snp.makeConstraints { make in
- make.centerX.equalToSuperview()
- make.bottom.equalTo(inputContainerView.snp.top).offset(-14)
- make.width.height.equalTo(32)
- }
- self.scrollViewDidScroll(self.messagesCollectionView)
-
- updateVipView()
- }
-
- func updateVipView() {
- inputBarTopView.subviews.forEach { $0.removeFromSuperview()}
-
- if viewModel.uiStyle == .chat ,
- kPurchaseDefault.isVip == false
- {
- let freeNum = kPurchaseDefault.freeNum(type: .aichat)
- if freeNum > 0 {
- freeText.text = "Remaining \(freeNum) free times"
- inputBarTopView.addSubview(freeText)
- freeText.snp.makeConstraints { make in
- make.leading.equalTo(20)
- make.trailing.equalTo(-20)
- make.bottom.equalTo(-8)
- make.top.equalTo(8)
- }
- }else{
- inputBarTopView.addSubview(upgradeVipBg)
- upgradeVipBg.snp.makeConstraints { make in
- make.leading.equalTo(16)
- make.trailing.equalTo(-16)
- make.bottom.equalTo(-2)
- make.top.equalTo(14)
- }
- }
- }
- }
-
-
-
- // MARK: - Helpers
- var lastIndexPath:IndexPath{
- if messageList.count == 0 {
- return IndexPath(item: 0, section: 0)
- }
- return IndexPath(item: messageList.count - 1, section: 0)
- }
-
- func insertMessage(_ message: TSChatMessage) {
- messageList.append(message)
- messagesCollectionView.performBatchUpdates({
- messagesCollectionView.insertItems(at: [lastIndexPath])
- if messageList.count >= 2 {
- messagesCollectionView.reloadItems(at: [lastIndexPath])
- }
- }, completion: { [weak self] _ in
- if self?.isLastSectionVisible() == true {
- self?.messagesCollectionView.scrollToLastItem(animated: true)
- }
- })
- }
-
- func isLastSectionVisible() -> Bool {
- guard !messageList.isEmpty else { return false }
- return messagesCollectionView.indexPathsForVisibleItems.contains(lastIndexPath)
- }
-
- private let formatter: DateFormatter = {
- let formatter = DateFormatter()
- formatter.dateStyle = .medium
- return formatter
- }()
- }
- // MARK: MessagesDataSource
- extension TSChatViewController {
-
- var currentSender: SenderType {
- return viewModel.kUserSender
- }
-
- func numberOfSections(in _: MessagesCollectionView) -> Int {
- return 1
- }
-
- func numberOfItems(inSection section: Int, in messagesCollectionView: MessagesCollectionView) -> Int{
- messageList.count
- }
-
- func messageForItem(at indexPath: IndexPath, in _: MessagesCollectionView) -> MessageType {
- messageList[indexPath.item]
- }
-
- func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? {
- // if indexPath.item % 3 == 0 {
- // return NSAttributedString(
- // string: MessageKitDateFormatter.shared.string(from: message.sentDate),
- // attributes: [
- // NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
- // NSAttributedString.Key.foregroundColor: UIColor.darkGray,
- // ])
- // }
- return nil
- }
-
- func cellBottomLabelAttributedText(for _: MessageType, at _: IndexPath) -> NSAttributedString? {
- NSAttributedString(
- string: "Read",
- attributes: [
- NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10),
- NSAttributedString.Key.foregroundColor: UIColor.darkGray,
- ])
- }
-
- func messageTopLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? {
- let name = message.sender.displayName
- return NSAttributedString(
- string: name,
- attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)])
- }
-
- func messageBottomLabelAttributedText(for message: MessageType, at _: IndexPath) -> NSAttributedString? {
- let dateString = formatter.string(from: message.sentDate)
- return NSAttributedString(
- string: dateString,
- attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)])
- }
-
- func textCell(
- for message: MessageType,
- at indexPath: IndexPath,
- in messagesCollectionView: MessagesCollectionView)
- -> UICollectionViewCell?
- {
- let cell = messagesCollectionView.dequeueReusableCell(
- TSTextMessageContentCell.self,
- for: indexPath)
- cell.configure(
- with: message,
- at: indexPath,
- in: messagesCollectionView,
- dataSource: self,
- and: textMessageSizeCalculator)
-
- return cell
- }
- }
- // MARK: InputBarAccessoryViewDelegate
- extension TSChatViewController: InputBarAccessoryViewDelegate {
- // MARK: Internal
-
- @objc
- func inputBar(_: InputBarAccessoryView, didPressSendButtonWith _: String) {
- processInputBar(messageInputBar)
- }
-
- //聊天发送内容
- func processInputBar(_ inputBar: InputBarAccessoryView) {
- // Here we can parse for which substrings were autocompleted
- let attributedText = inputBar.inputTextView.attributedText!
- let range = NSRange(location: 0, length: attributedText.length)
- attributedText.enumerateAttribute(.autocompleted, in: range, options: []) { _, range, _ in
-
- let substring = attributedText.attributedSubstring(from: range)
- let context = substring.attribute(.autocompletedContext, at: 0, effectiveRange: nil)
- print("Autocompleted: `", substring, "` with context: ", context ?? "-")
- }
-
- let components = inputBar.inputTextView.components
- inputBar.inputTextView.text = String()
- inputBar.invalidatePlugins()
-
- inputBar.inputTextView.placeholder = "Aa"
- sendMessages(components)
- messagesCollectionView.scrollToLastItem(animated: true)
- }
-
- // MARK: Private
-
- private func sendMessages(_ data: [Any]) {
- let user = viewModel.kUserSender
- for component in data {
- if let str = component as? String {
- let message = TSChatMessage(text: str, user: user, messageId: UUID().uuidString, date: Date())
- insertMessage(message)
- //保存这条消息到本地数据库
- //发送消息后,进行AI 对话生成
- generativeAIChat(message: message)
- }
- }
- }
-
- func generativeAIChat(message:TSChatMessage) {
- var messageString = ""
- switch message.kind {
- case .text(let message):
- messageString = message
- default:
- break
- }
-
- if messageString.count == 0 {
- return
- }
-
- let message = TSChatMessage(attributedText: kMDAttributedString(text: ""), user: viewModel.kAIUser, messageId: UUID().uuidString, date: Date())
- message.sendState = .start
- insertMessage(message)
-
- inputBarVC.sendEnabled(enabled: false)
- viewModel.sendChatMessage(message: messageString) {[weak self] string in
- guard let self = self else { return }
- debugPrint("viewModel.AiMDString=\(viewModel.AiMDString)")
- message.kind = .attributedText(kMDAttributedString(text: viewModel.AiMDString))
- message.sendState = .progress(0.5)
- updataAIChatCellUI()
-
- } completion: {[weak self] data, error in
- guard let self = self else { return }
- if let netData = data {
- message.sendState = .success("netData")
- //保存这条消息到本地数据库
- //消耗一次 AI 次数
- kPurchaseDefault.useOnceForFree(type: .aichat)
-
- }else {
- message.kind = .attributedText(kMDAttributedString(text: kAIErrorString))
- message.sendState = .failed(kAIErrorString)
- //保存这条消息到本地数据库
- }
- updataAIChatCellUI()
-
- kExecuteOnMainThread {
- self.inputBarVC.sendEnabled(enabled: true)
- }
- }
- }
-
- func updataAIChatCellUI(){
- kExecuteOnMainThread {
- if self.messageList.count >= 2 {
- UIView.performWithoutAnimation {
- self.messagesCollectionView.reloadItems(at: [self.lastIndexPath])
- }
- }else{
- self.messagesCollectionView.reloadData()
- }
- //更新 Vip
- if kPurchaseDefault.isVip == false{
- self.updateVipView()
- }
-
- self.messagesCollectionView.scrollToLastItem(animated: false)
-
- }
- }
-
- }
- extension TSChatViewController{
-
- func inputSendMsg(_ data: [Any]) {
-
- //判断 vip
- if kJudgeVip(externalBool: kPurchaseDefault.freeNumAvailable(type: .aichat) == false, vc: self, closePageBlock: {[weak self] in
- guard let self = self else { return }
- }){ return }
-
- sendMessages(data)
- messagesCollectionView.scrollToLastItem(animated: true)
- }
-
-
- }
- extension TSChatViewController{
-
- // UICollectionViewDelegate 方法
- override func scrollViewDidScroll(_ scrollView: UIScrollView) {
- let offsetY = scrollView.contentOffset.y
- let contentHeight = scrollView.contentSize.height
- let frameHeight = scrollView.frame.size.height
-
- // 判断是否需要显示滚动到底部的按钮
- if offsetY > contentHeight - frameHeight - 400 {
- scrollToBottomButton.isHidden = true
- } else {
- scrollToBottomButton.isHidden = false
- }
- }
-
-
- }
|