/* 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 UIKit import MessageKit import InputBarAccessoryView import Kingfisher final class AutocompleteExampleViewController: ChatViewController { lazy var joinChatButton: UIButton = { let button = UIButton() button.layer.cornerRadius = 16 button.backgroundColor = .primaryColor button.setTitle("JOIN CHAT", for: .normal) button.setTitleColor(.white, for: .normal) button.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .highlighted) button.addTarget(self, action: #selector(joinChat), for: .touchUpInside) return button }() /// The object that manages autocomplete, from InputBarAccessoryView lazy var autocompleteManager: AutocompleteManager = { [unowned self] in let manager = AutocompleteManager(for: self.messageInputBar.inputTextView) manager.delegate = self manager.dataSource = self return manager }() var hastagAutocompletes: [AutocompleteCompletion] = { var array: [AutocompleteCompletion] = [] for _ in 1...100 { array.append(AutocompleteCompletion(text: Lorem.word(), context: nil)) } return array }() // Completions loaded async that get appeneded to local cached completions var asyncCompletions: [AutocompleteCompletion] = [] override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) MockSocket.shared.connect(with: [SampleData.shared.nathan, SampleData.shared.wu]) .onTypingStatus { [weak self] in self?.setTypingIndicatorViewHidden(false) }.onNewMessage { [weak self] message in self?.setTypingIndicatorViewHidden(true, performUpdates: { self?.insertMessage(message) }) } } override func viewDidLoad() { super.viewDidLoad() messageInputBar.inputTextView.keyboardType = .twitter // Configure AutocompleteManager autocompleteManager.register(prefix: "@", with: [.font: UIFont.preferredFont(forTextStyle: .body), .foregroundColor: UIColor.primaryColor, .backgroundColor: UIColor.primaryColor.withAlphaComponent(0.3)]) autocompleteManager.register(prefix: "#") autocompleteManager.maxSpaceCountDuringCompletion = 1 // Allow for autocompletes with a space // Set plugins messageInputBar.inputPlugins = [autocompleteManager] } override func configureMessageCollectionView() { super.configureMessageCollectionView() let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8) layout?.setMessageOutgoingCellBottomLabelAlignment(.init(textAlignment: .right, textInsets: .zero)) layout?.setMessageOutgoingAvatarSize(.zero) layout?.setMessageOutgoingMessageTopLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 12))) layout?.setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment(textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 12))) messagesCollectionView.messagesLayoutDelegate = self messagesCollectionView.messagesDisplayDelegate = self additionalBottomInset = 30 } override func configureMessageInputBar() { super.configureMessageInputBar() messageInputBar.layer.shadowColor = UIColor.black.cgColor messageInputBar.layer.shadowRadius = 4 messageInputBar.layer.shadowOpacity = 0.3 messageInputBar.layer.shadowOffset = CGSize(width: 0, height: 0) messageInputBar.separatorLine.isHidden = true messageInputBar.setRightStackViewWidthConstant(to: 0, animated: false) messageInputBar.setMiddleContentView(joinChatButton, animated: false) } private func configureMessageInputBarForChat() { messageInputBar.setMiddleContentView(messageInputBar.inputTextView, animated: false) messageInputBar.setRightStackViewWidthConstant(to: 52, animated: false) let bottomItems = [makeButton(named: "ic_at"), makeButton(named: "ic_hashtag"), .flexibleSpace] messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false) messageInputBar.sendButton.activityViewColor = .white messageInputBar.sendButton.backgroundColor = .primaryColor messageInputBar.sendButton.layer.cornerRadius = 10 messageInputBar.sendButton.setTitleColor(.white, for: .normal) messageInputBar.sendButton.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .highlighted) messageInputBar.sendButton.setTitleColor(UIColor(white: 1, alpha: 0.3), for: .disabled) messageInputBar.sendButton .onSelected { item in item.transform = CGAffineTransform(scaleX: 1.05, y: 1.05) }.onDeselected { item in item.transform = .identity } } @objc func joinChat() { configureMessageInputBarForChat() } // MARK: - Helpers func isTimeLabelVisible(at indexPath: IndexPath) -> Bool { return indexPath.section % 3 == 0 && !isPreviousMessageSameSender(at: indexPath) } func isPreviousMessageSameSender(at indexPath: IndexPath) -> Bool { guard indexPath.section - 1 >= 0 else { return false } return messageList[indexPath.section].user == messageList[indexPath.section - 1].user } func isNextMessageSameSender(at indexPath: IndexPath) -> Bool { guard indexPath.section + 1 < messageList.count else { return false } return messageList[indexPath.section].user == messageList[indexPath.section + 1].user } func setTypingIndicatorViewHidden(_ isHidden: Bool, performUpdates updates: (() -> Void)? = nil) { setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in if success, self?.isLastSectionVisible() == true { self?.messagesCollectionView.scrollToLastItem(animated: true) } } } private func makeButton(named: String) -> InputBarButtonItem { return InputBarButtonItem() .configure { $0.spacing = .fixed(10) $0.image = UIImage(named: named)?.withRenderingMode(.alwaysTemplate) $0.setSize(CGSize(width: 25, height: 25), animated: false) $0.tintColor = UIColor(white: 0.8, alpha: 1) }.onSelected { $0.tintColor = .primaryColor }.onDeselected { $0.tintColor = UIColor(white: 0.8, alpha: 1) }.onTouchUpInside { _ in print("Item Tapped") } } // MARK: - MessagesDataSource override func cellTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { if isTimeLabelVisible(at: indexPath) { return NSAttributedString(string: MessageKitDateFormatter.shared.string(from: message.sentDate), attributes: [NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: 10), NSAttributedString.Key.foregroundColor: UIColor.darkGray]) } return nil } override func messageTopLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { if !isPreviousMessageSameSender(at: indexPath) { let name = message.sender.displayName return NSAttributedString(string: name, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) } return nil } override func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { if !isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message) { return NSAttributedString(string: "Delivered", attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption1)]) } return nil } // Async autocomplete requires the manager to reload func inputBar(_ inputBar: InputBarAccessoryView, textViewTextDidChangeTo text: String) { guard autocompleteManager.currentSession != nil, autocompleteManager.currentSession?.prefix == "#" else { return } // Load some data asyncronously for the given session.prefix DispatchQueue.global(qos: .default).async { // fake background loading task var array: [AutocompleteCompletion] = [] for _ in 1...10 { array.append(AutocompleteCompletion(text: Lorem.word())) } sleep(1) DispatchQueue.main.async { [weak self] in self?.asyncCompletions = array self?.autocompleteManager.reloadData() } } } } extension AutocompleteExampleViewController: AutocompleteManagerDelegate, AutocompleteManagerDataSource { // MARK: - AutocompleteManagerDataSource func autocompleteManager(_ manager: AutocompleteManager, autocompleteSourceFor prefix: String) -> [AutocompleteCompletion] { if prefix == "@" { return SampleData.shared.senders .map { user in return AutocompleteCompletion(text: user.displayName, context: ["id": user.senderId]) } } else if prefix == "#" { return hastagAutocompletes + asyncCompletions } return [] } func autocompleteManager(_ manager: AutocompleteManager, tableView: UITableView, cellForRowAt indexPath: IndexPath, for session: AutocompleteSession) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(withIdentifier: AutocompleteCell.reuseIdentifier, for: indexPath) as? AutocompleteCell else { fatalError("Oops, some unknown error occurred") } let users = SampleData.shared.senders let id = session.completion?.context?["id"] as? String let user = users.filter { return $0.senderId == id }.first if let sender = user { cell.imageView?.image = SampleData.shared.getAvatarFor(sender: sender).image } cell.imageViewEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8) cell.imageView?.layer.cornerRadius = 14 cell.imageView?.layer.borderColor = UIColor.primaryColor.cgColor cell.imageView?.layer.borderWidth = 1 cell.imageView?.clipsToBounds = true cell.textLabel?.attributedText = manager.attributedText(matching: session, fontSize: 15) return cell } // MARK: - AutocompleteManagerDelegate func autocompleteManager(_ manager: AutocompleteManager, shouldBecomeVisible: Bool) { setAutocompleteManager(active: shouldBecomeVisible) } // Optional func autocompleteManager(_ manager: AutocompleteManager, shouldRegister prefix: String, at range: NSRange) -> Bool { return true } // Optional func autocompleteManager(_ manager: AutocompleteManager, shouldUnregister prefix: String) -> Bool { return true } // Optional func autocompleteManager(_ manager: AutocompleteManager, shouldComplete prefix: String, with text: String) -> Bool { return true } // MARK: - AutocompleteManagerDelegate Helper func setAutocompleteManager(active: Bool) { let topStackView = messageInputBar.topStackView if active && !topStackView.arrangedSubviews.contains(autocompleteManager.tableView) { topStackView.insertArrangedSubview(autocompleteManager.tableView, at: topStackView.arrangedSubviews.count) topStackView.layoutIfNeeded() } else if !active && topStackView.arrangedSubviews.contains(autocompleteManager.tableView) { topStackView.removeArrangedSubview(autocompleteManager.tableView) topStackView.layoutIfNeeded() } messageInputBar.invalidateIntrinsicContentSize() } } // MARK: - MessagesDisplayDelegate extension AutocompleteExampleViewController: MessagesDisplayDelegate { // MARK: - Text Messages func textColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { return isFromCurrentSender(message: message) ? .white : .darkText } func detectorAttributes(for detector: DetectorType, and message: MessageType, at indexPath: IndexPath) -> [NSAttributedString.Key: Any] { switch detector { case .hashtag, .mention: if isFromCurrentSender(message: message) { return [.foregroundColor: UIColor.white] } else { return [.foregroundColor: UIColor.primaryColor] } default: return MessageLabel.defaultAttributes } } func enabledDetectors(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> [DetectorType] { return [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag] } // MARK: - All Messages func backgroundColor(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UIColor { return isFromCurrentSender(message: message) ? .primaryColor : UIColor(red: 230/255, green: 230/255, blue: 230/255, alpha: 1) } func messageStyle(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> MessageStyle { return .bubble } func configureAvatarView(_ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { let avatar = SampleData.shared.getAvatarFor(sender: message.sender) avatarView.set(avatar: avatar) avatarView.isHidden = isNextMessageSameSender(at: indexPath) avatarView.layer.borderWidth = 2 avatarView.layer.borderColor = UIColor.primaryColor.cgColor } func configureAccessoryView(_ accessoryView: UIView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { // Cells are reused, so only add a button here once. For real use you would need to // ensure any subviews are removed if not needed accessoryView.subviews.forEach { $0.removeFromSuperview() } let button = UIButton(type: .infoLight) button.tintColor = .primaryColor accessoryView.addSubview(button) button.frame = accessoryView.bounds button.isUserInteractionEnabled = false // respond to accessoryView tap through `MessageCellDelegate` accessoryView.layer.cornerRadius = accessoryView.frame.height / 2 accessoryView.backgroundColor = UIColor.primaryColor.withAlphaComponent(0.3) } func configureMediaMessageImageView(_ imageView: UIImageView, for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) { if case MessageKind.photo(let media) = message.kind, let imageURL = media.url { imageView.kf.setImage(with: imageURL) } else { imageView.kf.cancelDownloadTask() } } } // MARK: - MessagesLayoutDelegate extension AutocompleteExampleViewController: MessagesLayoutDelegate { func cellTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { if isTimeLabelVisible(at: indexPath) { return 18 } return 0 } func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { if isFromCurrentSender(message: message) { return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0 } else { return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0 } } func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> CGFloat { return (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0 } }