// 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 Kingfisher import MapKit import MessageKit import UIKit // MARK: - AdvancedExampleViewController final class AdvancedExampleViewController: ChatViewController { // MARK: Public // MARK: - UICollectionViewDataSource public override func collectionView( _ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell { guard let messagesDataSource = messagesCollectionView.messagesDataSource else { fatalError("Ouch. nil data source for messages") } // Very important to check this when overriding `cellForItemAt` // Super method will handle returning the typing indicator cell guard !isSectionReservedForTypingIndicator(indexPath.section) else { return super.collectionView(collectionView, cellForItemAt: indexPath) } let message = messagesDataSource.messageForItem(at: indexPath, in: messagesCollectionView) if case .custom = message.kind { let cell = messagesCollectionView.dequeueReusableCell(CustomCell.self, for: indexPath) cell.configure(with: message, at: indexPath, and: messagesCollectionView) return cell } return super.collectionView(collectionView, cellForItemAt: indexPath) } // MARK: Internal let outgoingAvatarOverlap: CGFloat = 17.5 override func viewDidLoad() { messagesCollectionView = MessagesCollectionView(frame: .zero, collectionViewLayout: CustomMessagesFlowLayout()) messagesCollectionView.register(CustomCell.self) super.viewDidLoad() updateTitleView(title: "MessageKit", subtitle: "2 Online") } 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 loadFirstMessages() { DispatchQueue.global(qos: .userInitiated).async { let count = UserDefaults.standard.mockMessagesCount() SampleData.shared.getAdvancedMessages(count: count) { messages in DispatchQueue.main.async { self.messageList = messages self.messagesCollectionView.reloadData() self.messagesCollectionView.scrollToLastItem() } } } } override func loadMoreMessages() { DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 1) { SampleData.shared.getAdvancedMessages(count: 20) { messages in DispatchQueue.main.async { self.messageList.insert(contentsOf: messages, at: 0) self.messagesCollectionView.reloadDataAndKeepOffset() self.refreshControl.endRefreshing() } } } } override func configureMessageCollectionView() { super.configureMessageCollectionView() let layout = messagesCollectionView.collectionViewLayout as? MessagesCollectionViewFlowLayout layout?.sectionInset = UIEdgeInsets(top: 1, left: 8, bottom: 1, right: 8) // Hide the outgoing avatar and adjust the label alignment to line up with the messages layout?.setMessageOutgoingAvatarSize(.zero) layout? .setMessageOutgoingMessageTopLabelAlignment(LabelAlignment( textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8))) layout? .setMessageOutgoingMessageBottomLabelAlignment(LabelAlignment( textAlignment: .right, textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 8))) // Set outgoing avatar to overlap with the message bubble layout? .setMessageIncomingMessageTopLabelAlignment(LabelAlignment( textAlignment: .left, textInsets: UIEdgeInsets(top: 0, left: 18, bottom: outgoingAvatarOverlap, right: 0))) layout?.setMessageIncomingAvatarSize(CGSize(width: 30, height: 30)) layout? .setMessageIncomingMessagePadding(UIEdgeInsets( top: -outgoingAvatarOverlap, left: -18, bottom: outgoingAvatarOverlap, right: 18)) layout?.setMessageIncomingAccessoryViewSize(CGSize(width: 30, height: 30)) layout?.setMessageIncomingAccessoryViewPadding(HorizontalEdgeInsets(left: 8, right: 0)) layout?.setMessageIncomingAccessoryViewPosition(.messageBottom) layout?.setMessageOutgoingAccessoryViewSize(CGSize(width: 30, height: 30)) layout?.setMessageOutgoingAccessoryViewPadding(HorizontalEdgeInsets(left: 0, right: 8)) messagesCollectionView.messagesLayoutDelegate = self messagesCollectionView.messagesDisplayDelegate = self } override func configureMessageInputBar() { // super.configureMessageInputBar() messageInputBar = CameraInputBarAccessoryView() messageInputBar.delegate = self messageInputBar.inputTextView.tintColor = .primaryColor messageInputBar.sendButton.setTitleColor(.primaryColor, for: .normal) messageInputBar.sendButton.setTitleColor( UIColor.primaryColor.withAlphaComponent(0.3), for: .highlighted) messageInputBar.isTranslucent = true messageInputBar.separatorLine.isHidden = true messageInputBar.inputTextView.tintColor = .primaryColor messageInputBar.inputTextView.backgroundColor = UIColor(red: 245 / 255, green: 245 / 255, blue: 245 / 255, alpha: 1) messageInputBar.inputTextView.placeholderTextColor = UIColor(red: 0.6, green: 0.6, blue: 0.6, alpha: 1) messageInputBar.inputTextView.textContainerInset = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 36) messageInputBar.inputTextView.placeholderLabelInsets = UIEdgeInsets(top: 8, left: 20, bottom: 8, right: 36) messageInputBar.inputTextView.layer.borderColor = UIColor(red: 200 / 255, green: 200 / 255, blue: 200 / 255, alpha: 1).cgColor messageInputBar.inputTextView.layer.borderWidth = 1.0 messageInputBar.inputTextView.layer.cornerRadius = 16.0 messageInputBar.inputTextView.layer.masksToBounds = true messageInputBar.inputTextView.scrollIndicatorInsets = UIEdgeInsets(top: 8, left: 0, bottom: 8, right: 0) configureInputBarItems() inputBarType = .custom(messageInputBar) } // MARK: - Helpers func isTimeLabelVisible(at indexPath: IndexPath) -> Bool { 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) { updateTitleView(title: "MessageKit", subtitle: isHidden ? "2 Online" : "Typing...") setTypingIndicatorViewHidden(isHidden, animated: true, whilePerforming: updates) { [weak self] success in if success, self?.isLastSectionVisible() == true { self?.messagesCollectionView.scrollToLastItem(animated: true) } } } // 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 } // MARK: Private private func configureInputBarItems() { messageInputBar.setRightStackViewWidthConstant(to: 36, animated: false) messageInputBar.sendButton.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1) messageInputBar.sendButton.contentEdgeInsets = UIEdgeInsets(top: 2, left: 2, bottom: 2, right: 2) messageInputBar.sendButton.setSize(CGSize(width: 36, height: 36), animated: false) messageInputBar.sendButton.image = #imageLiteral(resourceName: "ic_up") messageInputBar.sendButton.title = nil messageInputBar.sendButton.imageView?.layer.cornerRadius = 16 let charCountButton = InputBarButtonItem() .configure { $0.title = "0/140" $0.contentHorizontalAlignment = .right $0.setTitleColor(UIColor(white: 0.6, alpha: 1), for: .normal) $0.titleLabel?.font = UIFont.systemFont(ofSize: 10, weight: .bold) $0.setSize(CGSize(width: 50, height: 25), animated: false) }.onTextViewDidChange { item, textView in item.title = "\(textView.text.count)/140" let isOverLimit = textView.text.count > 140 item.inputBarAccessoryView? .shouldManageSendButtonEnabledState = !isOverLimit // Disable automated management when over limit if isOverLimit { item.inputBarAccessoryView?.sendButton.isEnabled = false } let color = isOverLimit ? .red : UIColor(white: 0.6, alpha: 1) item.setTitleColor(color, for: .normal) } let bottomItems = [.flexibleSpace, charCountButton] configureInputBarPadding() messageInputBar.setStackViewItems(bottomItems, forStack: .bottom, animated: false) // This just adds some more flare messageInputBar.sendButton .onEnabled { item in UIView.animate(withDuration: 0.3, animations: { item.imageView?.backgroundColor = .primaryColor }) }.onDisabled { item in UIView.animate(withDuration: 0.3, animations: { item.imageView?.backgroundColor = UIColor(white: 0.85, alpha: 1) }) } } /// The input bar will autosize based on the contained text, but we can add padding to adjust the height or width if necessary /// See the InputBar diagram here to visualize how each of these would take effect: /// https://raw.githubusercontent.com/MessageKit/MessageKit/master/Assets/InputBarAccessoryViewLayout.png private func configureInputBarPadding() { // Entire InputBar padding messageInputBar.padding.bottom = 8 // or MiddleContentView padding messageInputBar.middleContentViewPadding.right = -38 // or InputTextView padding messageInputBar.inputTextView.textContainerInset.bottom = 8 } private func makeButton(named: String) -> InputBarButtonItem { 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 { print("Item Tapped") let actionSheet = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let action = UIAlertAction(title: "Cancel", style: .cancel, handler: nil) actionSheet.addAction(action) if let popoverPresentationController = actionSheet.popoverPresentationController { popoverPresentationController.sourceView = $0 popoverPresentationController.sourceRect = $0.frame } self.navigationController?.present(actionSheet, animated: true, completion: nil) } } } // MARK: MessagesDisplayDelegate extension AdvancedExampleViewController: MessagesDisplayDelegate { // MARK: - Text Messages func textColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor { isFromCurrentSender(message: message) ? .white : .darkText } func detectorAttributes( for detector: DetectorType, and message: MessageType, at _: 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 _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> [DetectorType] { [.url, .address, .phoneNumber, .date, .transitInformation, .mention, .hashtag] } // MARK: - All Messages func backgroundColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor { 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) -> MessageStyle { var corners: UIRectCorner = [] if isFromCurrentSender(message: message) { corners.formUnion(.topLeft) corners.formUnion(.bottomLeft) if !isPreviousMessageSameSender(at: indexPath) { corners.formUnion(.topRight) } if !isNextMessageSameSender(at: indexPath) { corners.formUnion(.bottomRight) } } else { corners.formUnion(.topRight) corners.formUnion(.bottomRight) if !isPreviousMessageSameSender(at: indexPath) { corners.formUnion(.topLeft) } if !isNextMessageSameSender(at: indexPath) { corners.formUnion(.bottomLeft) } } return .custom { view in let radius: CGFloat = 16 let path = UIBezierPath( roundedRect: view.bounds, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius)) let mask = CAShapeLayer() mask.path = path.cgPath view.layer.mask = mask } } func configureAvatarView( _ avatarView: AvatarView, for message: MessageType, at indexPath: IndexPath, in _: 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 _: MessageType, at _: IndexPath, in _: 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() } accessoryView.backgroundColor = .clear let shouldShow = Int.random(in: 0 ... 10) == 0 guard shouldShow else { return } 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, in _: MessagesCollectionView) { if case MessageKind.photo(let media) = message.kind, let imageURL = media.url { imageView.kf.setImage(with: imageURL) } else { imageView.kf.cancelDownloadTask() } } // MARK: - Location Messages func annotationViewForLocation(message _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> MKAnnotationView? { let annotationView = MKAnnotationView(annotation: nil, reuseIdentifier: nil) let pinImage = #imageLiteral(resourceName: "ic_map_marker") annotationView.image = pinImage annotationView.centerOffset = CGPoint(x: 0, y: -pinImage.size.height / 2) return annotationView } func animationBlockForLocation( message _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> ((UIImageView) -> Void)? { { view in view.layer.transform = CATransform3DMakeScale(2, 2, 2) UIView.animate( withDuration: 0.6, delay: 0, usingSpringWithDamping: 0.9, initialSpringVelocity: 0, options: [], animations: { view.layer.transform = CATransform3DIdentity }, completion: nil) } } func snapshotOptionsForLocation( message _: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> LocationMessageSnapshotOptions { LocationMessageSnapshotOptions( showsBuildings: true, showsPointsOfInterest: true, span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)) } // MARK: - Audio Messages func audioTintColor(for message: MessageType, at _: IndexPath, in _: MessagesCollectionView) -> UIColor { isFromCurrentSender(message: message) ? .white : .primaryColor } func configureAudioCell(_ cell: AudioMessageCell, message: MessageType) { audioController .configureAudioCell( cell, message: message) // this is needed especially when the cell is reconfigure while is playing sound } } // MARK: MessagesLayoutDelegate extension AdvancedExampleViewController: MessagesLayoutDelegate { func cellTopLabelHeight(for _: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { if isTimeLabelVisible(at: indexPath) { return 18 } return 0 } func messageTopLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { if isFromCurrentSender(message: message) { return !isPreviousMessageSameSender(at: indexPath) ? 20 : 0 } else { return !isPreviousMessageSameSender(at: indexPath) ? (20 + outgoingAvatarOverlap) : 0 } } func messageBottomLabelHeight(for message: MessageType, at indexPath: IndexPath, in _: MessagesCollectionView) -> CGFloat { (!isNextMessageSameSender(at: indexPath) && isFromCurrentSender(message: message)) ? 16 : 0 } } // MARK: CameraInputBarAccessoryViewDelegate extension AdvancedExampleViewController: CameraInputBarAccessoryViewDelegate { func inputBar(_ inputBar: InputBarAccessoryView, didPressSendButtonWith attachments: [AttachmentManager.Attachment]) { for item in attachments { if case .image(let image) = item { self.sendImageMessage(photo: image) } } inputBar.invalidatePlugins() } func sendImageMessage(photo: UIImage) { let photoMessage = MockMessage(image: photo, user: currentSender as! MockUser, messageId: UUID().uuidString, date: Date()) insertMessage(photoMessage) } }