diff --git a/CHANGELOG.md b/CHANGELOG.md index aef80f1f..d1ce5887 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The changelog for `MessageKit`. Also see the [releases](https://github.com/Messa - Fixes missing insets for link preview messages [#1447](https://github.com/MessageKit/MessageKit/pull/1447) by [@bguidolim](https://github.com/bguidolim) ### Added +- Show message time by swiping left over the chat controller. [#1444](https://github.com/MessageKit/MessageKit/pull/1444) by [@amirpirzad](https://github.com/amirpirzad) ### Changed diff --git a/Example/Sources/View Controllers/AdvancedExampleViewController.swift b/Example/Sources/View Controllers/AdvancedExampleViewController.swift index 98081c1c..e4fa529e 100644 --- a/Example/Sources/View Controllers/AdvancedExampleViewController.swift +++ b/Example/Sources/View Controllers/AdvancedExampleViewController.swift @@ -278,7 +278,6 @@ final class AdvancedExampleViewController: ChatViewController { } return nil } - } // MARK: - MessagesDisplayDelegate diff --git a/Example/Sources/View Controllers/ChatViewController.swift b/Example/Sources/View Controllers/ChatViewController.swift index 27f6d267..0d98d341 100644 --- a/Example/Sources/View Controllers/ChatViewController.swift +++ b/Example/Sources/View Controllers/ChatViewController.swift @@ -112,6 +112,8 @@ class ChatViewController: MessagesViewController, MessagesDataSource { scrollsToBottomOnKeyboardBeginsEditing = true // default false maintainPositionOnKeyboardFrameChanged = true // default false + + showMessageTimestampOnSwipeLeft = true // default false messagesCollectionView.refreshControl = refreshControl } diff --git a/Example/Sources/Views/SwiftUI/MessagesView.swift b/Example/Sources/Views/SwiftUI/MessagesView.swift index 4247ce2f..401cfc00 100644 --- a/Example/Sources/Views/SwiftUI/MessagesView.swift +++ b/Example/Sources/Views/SwiftUI/MessagesView.swift @@ -34,6 +34,7 @@ struct MessagesView: UIViewControllerRepresentable { messagesVC.messageInputBar.delegate = context.coordinator messagesVC.scrollsToBottomOnKeyboardBeginsEditing = true // default false messagesVC.maintainPositionOnKeyboardFrameChanged = true // default false + messagesVC.showMessageTimestampOnSwipeLeft = true // default false return messagesVC } @@ -95,6 +96,14 @@ extension MessagesView.Coordinator: MessagesDataSource { let dateString = formatter.string(from: message.sentDate) return NSAttributedString(string: dateString, attributes: [NSAttributedString.Key.font: UIFont.preferredFont(forTextStyle: .caption2)]) } + + func messageTimestampLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + let sentDate = message.sentDate + let sentDateString = MessageKitDateFormatter.shared.string(from: sentDate) + let timeLabelFont: UIFont = .boldSystemFont(ofSize: 10) + let timeLabelColor: UIColor = .systemGray + return NSAttributedString(string: sentDateString, attributes: [NSAttributedString.Key.font: timeLabelFont, NSAttributedString.Key.foregroundColor: timeLabelColor]) + } } @available(iOS 13.0, *) diff --git a/Sources/Controllers/MessagesViewController.swift b/Sources/Controllers/MessagesViewController.swift index 61c1f87d..63175407 100644 --- a/Sources/Controllers/MessagesViewController.swift +++ b/Sources/Controllers/MessagesViewController.swift @@ -28,7 +28,7 @@ import InputBarAccessoryView /// A subclass of `UIViewController` with a `MessagesCollectionView` object /// that is used to display conversation interfaces. open class MessagesViewController: UIViewController, -UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { +UICollectionViewDelegateFlowLayout, UICollectionViewDataSource, UIGestureRecognizerDelegate { /// The `MessagesCollectionView` managed by the messages view controller object. open var messagesCollectionView = MessagesCollectionView() @@ -56,6 +56,22 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { /// The default value of this property is `false`. open var maintainPositionOnKeyboardFrameChanged: Bool = false + /// Display the date of message by swiping left. + /// The default value of this property is `false`. + open var showMessageTimestampOnSwipeLeft: Bool = false { + didSet { + messagesCollectionView.showMessageTimestampOnSwipeLeft = showMessageTimestampOnSwipeLeft + if showMessageTimestampOnSwipeLeft { + addPanGesture() + } else { + removePanGesture() + } + } + } + + /// Pan gesture for display the date of message by swiping left. + private var panGesture: UIPanGestureRecognizer? + open override var canBecomeFirstResponder: Bool { return true } @@ -148,6 +164,53 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { // MARK: - Methods [Private] + /// Display time of message by swiping the cell + private func addPanGesture() { + panGesture = UIPanGestureRecognizer(target: self, action: #selector(handlePanGesture(_:))) + guard let panGesture = panGesture else { + return + } + panGesture.delegate = self + messagesCollectionView.addGestureRecognizer(panGesture) + messagesCollectionView.clipsToBounds = false + } + + private func removePanGesture() { + guard let panGesture = panGesture else { + return + } + panGesture.delegate = nil + self.panGesture = nil + messagesCollectionView.removeGestureRecognizer(panGesture) + messagesCollectionView.clipsToBounds = true + } + + @objc + private func handlePanGesture(_ gesture: UIPanGestureRecognizer) { + guard let parentView = gesture.view else { + return + } + + switch gesture.state { + case .began, .changed: + messagesCollectionView.showsVerticalScrollIndicator = false + let translation = gesture.translation(in: view) + let minX = -(view.frame.size.width * 0.35) + let maxX: CGFloat = 0 + var offsetValue = translation.x + offsetValue = max(offsetValue, minX) + offsetValue = min(offsetValue, maxX) + parentView.frame.origin.x = offsetValue + case .ended: + messagesCollectionView.showsVerticalScrollIndicator = true + UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.7, initialSpringVelocity: 0.8, options: .curveEaseOut, animations: { + parentView.frame.origin.x = 0 + }, completion: nil) + default: + break + } + } + private func setupDefaults() { extendedLayoutIncludesOpaqueBars = true view.backgroundColor = .collectionViewBackground @@ -420,4 +483,15 @@ UICollectionViewDelegateFlowLayout, UICollectionViewDataSource { @objc private func clearMemoryCache() { MessageStyle.bubbleImageCache.removeAllObjects() } + + // MARK: - UIGestureRecognizerDelegate + + /// check pan gesture direction + public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + guard let panGesture = gestureRecognizer as? UIPanGestureRecognizer else { + return false + } + let velocity = panGesture.velocity(in: messagesCollectionView) + return abs(velocity.x) > abs(velocity.y) + } } diff --git a/Sources/Layout/MessageSizeCalculator.swift b/Sources/Layout/MessageSizeCalculator.swift index 6fd768d3..dad69411 100644 --- a/Sources/Layout/MessageSizeCalculator.swift +++ b/Sources/Layout/MessageSizeCalculator.swift @@ -81,6 +81,7 @@ open class MessageSizeCalculator: CellSizeCalculator { attributes.cellTopLabelSize = cellTopLabelSize(for: message, at: indexPath) attributes.cellTopLabelAlignment = cellTopLabelAlignment(for: message) attributes.cellBottomLabelSize = cellBottomLabelSize(for: message, at: indexPath) + attributes.messageTimeLabelSize = messageTimeLabelSize(for: message, at: indexPath) attributes.cellBottomLabelAlignment = cellBottomLabelAlignment(for: message) attributes.messageTopLabelSize = messageTopLabelSize(for: message, at: indexPath) attributes.messageTopLabelAlignment = messageTopLabelAlignment(for: message) @@ -197,7 +198,18 @@ open class MessageSizeCalculator: CellSizeCalculator { let isFromCurrentSender = dataSource.isFromCurrentSender(message: message) return isFromCurrentSender ? outgoingMessageTopLabelAlignment : incomingMessageTopLabelAlignment } - + + // MARK: - Message time label + + open func messageTimeLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { + let dataSource = messagesLayout.messagesDataSource + guard let attributedText = dataSource.messageTimestampLabelAttributedText(for: message, at: indexPath) else { + return .zero + } + let size = attributedText.size() + return CGSize(width: size.width, height: size.height) + } + // MARK: - Bottom cell Label open func cellBottomLabelSize(for message: MessageType, at indexPath: IndexPath) -> CGSize { diff --git a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift index 9bf2270a..e182a63c 100644 --- a/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift +++ b/Sources/Layout/MessagesCollectionViewLayoutAttributes.swift @@ -50,6 +50,8 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu public var messageBottomLabelAlignment = LabelAlignment(textAlignment: .center, textInsets: .zero) public var messageBottomLabelSize: CGSize = .zero + public var messageTimeLabelSize: CGSize = .zero + public var accessoryViewSize: CGSize = .zero public var accessoryViewPadding: HorizontalEdgeInsets = .zero public var accessoryViewPosition: AccessoryPosition = .messageCenter @@ -74,6 +76,7 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu copy.cellTopLabelSize = cellTopLabelSize copy.cellBottomLabelAlignment = cellBottomLabelAlignment copy.cellBottomLabelSize = cellBottomLabelSize + copy.messageTimeLabelSize = messageTimeLabelSize copy.messageTopLabelAlignment = messageTopLabelAlignment copy.messageTopLabelSize = messageTopLabelSize copy.messageBottomLabelAlignment = messageBottomLabelAlignment @@ -100,6 +103,7 @@ open class MessagesCollectionViewLayoutAttributes: UICollectionViewLayoutAttribu && attributes.cellTopLabelSize == cellTopLabelSize && attributes.cellBottomLabelAlignment == cellBottomLabelAlignment && attributes.cellBottomLabelSize == cellBottomLabelSize + && attributes.messageTimeLabelSize == messageTimeLabelSize && attributes.messageTopLabelAlignment == messageTopLabelAlignment && attributes.messageTopLabelSize == messageTopLabelSize && attributes.messageBottomLabelAlignment == messageBottomLabelAlignment diff --git a/Sources/Protocols/MessagesDataSource.swift b/Sources/Protocols/MessagesDataSource.swift index 9db208f7..f81ba4fa 100644 --- a/Sources/Protocols/MessagesDataSource.swift +++ b/Sources/Protocols/MessagesDataSource.swift @@ -102,6 +102,18 @@ public protocol MessagesDataSource: AnyObject { /// /// The default value returned by this method is `nil`. func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? + + /// The attributed text to be used for cell's timestamp label. + /// The timestamp label is shown when showMessageTimestampOnSwipeLeft is enabled by swiping left over the chat controller. + /// + /// - Parameters: + /// - message: The `MessageType` that will be displayed by this cell. + /// - indexPath: The `IndexPath` of the cell. + /// - messagesCollectionView: The `MessagesCollectionView` in which this cell will be displayed. + /// + /// The default value returned by this method is `nil`. + func messageTimestampLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? + /// Custom collectionView cell for message with `custom` message type. /// @@ -148,7 +160,20 @@ public extension MessagesDataSource { func messageBottomLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { return nil } - + + func messageTimestampLabelAttributedText(for message: MessageType, at indexPath: IndexPath) -> NSAttributedString? { + let sentDate = message.sentDate + let sentDateString = MessageKitDateFormatter.shared.string(from: sentDate) + let timeLabelFont: UIFont = .boldSystemFont(ofSize: 10) + let timeLabelColor: UIColor + if #available(iOS 13, *) { + timeLabelColor = .systemGray + } else { + timeLabelColor = .darkGray + } + return NSAttributedString(string: sentDateString, attributes: [NSAttributedString.Key.font: timeLabelFont, NSAttributedString.Key.foregroundColor: timeLabelColor]) + } + func customCell(for message: MessageType, at indexPath: IndexPath, in messagesCollectionView: MessagesCollectionView) -> UICollectionViewCell { fatalError(MessageKitError.customDataUnresolvedCell) } diff --git a/Sources/Views/Cells/MessageContentCell.swift b/Sources/Views/Cells/MessageContentCell.swift index e857558b..64861335 100644 --- a/Sources/Views/Cells/MessageContentCell.swift +++ b/Sources/Views/Cells/MessageContentCell.swift @@ -69,6 +69,9 @@ open class MessageContentCell: MessageCollectionViewCell { return label }() + /// The time label of the messageBubble. + open var messageTimestampLabel: InsetLabel = InsetLabel() + // Should only add customized subviews - don't change accessoryView itself. open var accessoryView: UIView = UIView() @@ -95,6 +98,7 @@ open class MessageContentCell: MessageCollectionViewCell { contentView.addSubview(cellBottomLabel) contentView.addSubview(messageContainerView) contentView.addSubview(avatarView) + contentView.addSubview(messageTimestampLabel) } open override func prepareForReuse() { @@ -103,6 +107,7 @@ open class MessageContentCell: MessageCollectionViewCell { cellBottomLabel.text = nil messageTopLabel.text = nil messageBottomLabel.text = nil + messageTimestampLabel.attributedText = nil } // MARK: - Configuration @@ -118,6 +123,7 @@ open class MessageContentCell: MessageCollectionViewCell { layoutMessageTopLabel(with: attributes) layoutAvatarView(with: attributes) layoutAccessoryView(with: attributes) + layoutTimeLabelView(with: attributes) } /// Used to configure the cell. @@ -150,11 +156,13 @@ open class MessageContentCell: MessageCollectionViewCell { let bottomCellLabelText = dataSource.cellBottomLabelAttributedText(for: message, at: indexPath) let topMessageLabelText = dataSource.messageTopLabelAttributedText(for: message, at: indexPath) let bottomMessageLabelText = dataSource.messageBottomLabelAttributedText(for: message, at: indexPath) - + let messageTimestampLabelText = dataSource.messageTimestampLabelAttributedText(for: message, at: indexPath) cellTopLabel.attributedText = topCellLabelText cellBottomLabel.attributedText = bottomCellLabelText messageTopLabel.attributedText = topMessageLabelText messageBottomLabel.attributedText = bottomMessageLabelText + messageTimestampLabel.attributedText = messageTimestampLabelText + messageTimestampLabel.isHidden = !messagesCollectionView.showMessageTimestampOnSwipeLeft } /// Handle tap gesture on contentView and its subviews. @@ -344,4 +352,13 @@ open class MessageContentCell: MessageCollectionViewCell { accessoryView.frame = CGRect(origin: origin, size: attributes.accessoryViewSize) } + + /// Positions the message bubble's time label. + /// - attributes: The `MessagesCollectionViewLayoutAttributes` for the cell. + open func layoutTimeLabelView(with attributes: MessagesCollectionViewLayoutAttributes) { + let paddingLeft: CGFloat = 10 + let origin = CGPoint(x: contentView.frame.size.width + paddingLeft, y: contentView.frame.size.height * 0.5) + let size = CGSize(width: attributes.messageTimeLabelSize.width, height: attributes.messageTimeLabelSize.height) + messageTimestampLabel.frame = CGRect(origin: origin, size: size) + } } diff --git a/Sources/Views/MessagesCollectionView.swift b/Sources/Views/MessagesCollectionView.swift index 740cb960..df9e3590 100644 --- a/Sources/Views/MessagesCollectionView.swift +++ b/Sources/Views/MessagesCollectionView.swift @@ -41,6 +41,10 @@ open class MessagesCollectionView: UICollectionView { return messagesCollectionViewFlowLayout.isTypingIndicatorViewHidden } + /// Display the date of message by swiping left. + /// The default value of this property is `false`. + internal var showMessageTimestampOnSwipeLeft: Bool = false + private var indexPathForLastItem: IndexPath? { let lastSection = numberOfSections - 1 guard lastSection >= 0, numberOfItems(inSection: lastSection) > 0 else { return nil }