Created
November 9, 2019 01:59
-
-
Save bguidolim/ba9339d5f13803809d8e24376e69d630 to your computer and use it in GitHub Desktop.
Link Preview implementation using MessageKit
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// ChatCollectionViewFlowLayout.swift | |
// Engage | |
// | |
// Created by Bruno Guidolim on 04.08.19. | |
// Copyright © 2019 COYO GmbH. All rights reserved. | |
// | |
import MessageKit | |
internal final class ChatCollectionViewFlowLayout: MessagesCollectionViewFlowLayout { | |
lazy var systemMessageCalculator: ChatSystemMessageCalculator = .init(layout: self) | |
lazy var linkMessageCalculator: LinkMessageSizeCalculator = .init(layout: self) | |
override init() { | |
super.init() | |
setMessageIncomingMessagePadding(UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 30)) | |
setMessageOutgoingMessagePadding(UIEdgeInsets(top: 0, left: 60, bottom: 0, right: 8)) | |
setMessageOutgoingAvatarSize(.zero) | |
textMessageSizeCalculator.messageLabelFont = UIFont.bodyFont | |
linkMessageCalculator.messageLabelFont = UIFont.bodyFont | |
let labelInsets: UIEdgeInsets = .init(top: 7, left: 12, bottom: 7, right: 12) | |
textMessageSizeCalculator.incomingMessageLabelInsets = labelInsets | |
textMessageSizeCalculator.outgoingMessageLabelInsets = labelInsets | |
linkMessageCalculator.incomingMessageLabelInsets = labelInsets | |
linkMessageCalculator.outgoingMessageLabelInsets = labelInsets | |
setMessageIncomingAvatarPosition(.init(vertical: .messageBottom)) | |
let outgoingBottomLabelAlignment: LabelAlignment = .init(textAlignment: .right, | |
textInsets: UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 19)) | |
setMessageOutgoingMessageBottomLabelAlignment(outgoingBottomLabelAlignment) | |
let incomingLabelAlignment: LabelAlignment = .init(textAlignment: .left, | |
textInsets: UIEdgeInsets(top: 0, left: 50, bottom: 0, right: 0)) | |
setMessageIncomingMessageBottomLabelAlignment(incomingLabelAlignment) | |
setMessageIncomingMessageTopLabelAlignment(incomingLabelAlignment) | |
minimumLineSpacing = 2 | |
sectionInset = UIEdgeInsets(top: 1, left: 16, bottom: 1, right: 16) | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
override func messageSizeCalculators() -> [MessageSizeCalculator] { | |
return super.messageSizeCalculators() + [systemMessageCalculator, linkMessageCalculator] | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// LinkMessageCell.swift | |
// Engage | |
// | |
// Created by Bruno Guidolim on 04.08.19. | |
// Copyright © 2019 COYO GmbH. All rights reserved. | |
// | |
import MessageKit | |
internal final class LinkMessageCell: TextMessageCell { | |
private let linkPreviewView: LinkPreviewView = .init() | |
private var linkURL: URL? | |
override func configure(with message: MessageType, | |
at indexPath: IndexPath, | |
and messagesCollectionView: MessagesCollectionView) { | |
guard let displayDelegate = messagesCollectionView.messagesDisplayDelegate else { | |
return | |
} | |
let textColor: UIColor = displayDelegate.textColor(for: message, at: indexPath, in: messagesCollectionView) | |
linkPreviewView.titleLabel.textColor = textColor | |
linkPreviewView.teaserLabel.textColor = textColor | |
linkPreviewView.domainLabel.textColor = textColor | |
guard case .custom(let object) = message.kind, | |
let customType = object as? ChatMessageCustomType, | |
let chatMessage = message as? ChatMessage else { | |
preconditionFailure("Was not possible to unwrap the custom type.") | |
} | |
switch customType { | |
case .link(let messageText, let linkURL, let linkPreview): | |
let newChatMessage: ChatMessage = .init(sender: chatMessage.sender, | |
messageId: chatMessage.messageId, | |
sentDate: chatMessage.sentDate, | |
kind: .text(messageText), | |
updatedID: chatMessage.updatedID) | |
super.configure(with: newChatMessage, at: indexPath, and: messagesCollectionView) | |
if linkPreviewView.superview == nil { | |
linkPreviewView.translatesAutoresizingMaskIntoConstraints = false | |
messageContainerView.addSubview(linkPreviewView) | |
linkPreviewView.leadingAnchor.constraint(equalTo: messageContainerView.leadingAnchor, | |
constant: messageLabel.textInsets.left).isActive = true | |
linkPreviewView.trailingAnchor.constraint(equalTo: messageContainerView.trailingAnchor, | |
constant: messageLabel.textInsets.right * -1).isActive = true | |
linkPreviewView.bottomAnchor.constraint(equalTo: messageContainerView.bottomAnchor, | |
constant: messageLabel.textInsets.bottom * -1).isActive = true | |
} | |
if let linkPreview: ChatLinkPreview = linkPreview, !linkPreview.teaser.isEmptyOrNil { | |
linkPreviewView.titleLabel.text = linkPreview.title | |
linkPreviewView.teaserLabel.text = linkPreview.teaser | |
linkPreviewView.domainLabel.text = linkPreview.domain?.lowercased() | |
// Images for link preview are always temporary on the server, so we try to keep this image forever in the cache | |
linkPreviewView.imageView.setImage(from: linkPreview.imageURL, | |
placeholder: UIImage(named: "link"), | |
options: [.diskCacheExpiration(.never), | |
.memoryCacheExpiration(.never)]) | |
self.linkURL = linkURL | |
} | |
default: | |
fatalError("Invalid type for this cell.") | |
} | |
} | |
override func prepareForReuse() { | |
super.prepareForReuse() | |
linkPreviewView.titleLabel.text = nil | |
linkPreviewView.teaserLabel.text = nil | |
linkPreviewView.domainLabel.text = nil | |
linkPreviewView.imageView.image = nil | |
linkURL = nil | |
} | |
override func handleTapGesture(_ gesture: UIGestureRecognizer) { | |
let touchLocation: CGPoint = convert(gesture.location(in: self), to: linkPreviewView) | |
if linkPreviewView.bounds.contains(touchLocation), let url: URL = linkURL { | |
delegate?.didSelectURL(url) | |
return | |
} | |
super.handleTapGesture(gesture) | |
} | |
} | |
fileprivate final class LinkPreviewView: UIView { | |
lazy var imageView: UIImageView = { | |
let imageView: UIImageView = .init() | |
imageView.clipsToBounds = true | |
imageView.contentMode = .scaleAspectFill | |
imageView.translatesAutoresizingMaskIntoConstraints = false | |
addSubview(imageView) | |
imageView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true | |
imageView.topAnchor.constraint(equalTo: topAnchor).isActive = true | |
imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: 1).isActive = true | |
imageView.widthAnchor.constraint(equalToConstant: LinkMessageSizeCalculator.ImageViewSize).isActive = true | |
imageView.bottomAnchor.constraint(lessThanOrEqualTo: bottomAnchor).isActive = true | |
return imageView | |
}() | |
lazy var titleLabel: UILabel = { | |
let label: UILabel = .init() | |
label.numberOfLines = 0 | |
label.font = .caption1SemiBoldFont | |
label.translatesAutoresizingMaskIntoConstraints = false | |
return label | |
}() | |
lazy var teaserLabel: UILabel = { | |
let label: UILabel = .init() | |
label.numberOfLines = 0 | |
label.font = .caption2Font | |
label.translatesAutoresizingMaskIntoConstraints = false | |
return label | |
}() | |
lazy var domainLabel: UILabel = { | |
let label: UILabel = .init() | |
label.numberOfLines = 0 | |
label.font = .caption2SemiBoldFont | |
label.translatesAutoresizingMaskIntoConstraints = false | |
return label | |
}() | |
private lazy var contentView: UIView = { | |
let view: UIView = .init(frame: .zero) | |
view.translatesAutoresizingMaskIntoConstraints = false | |
addSubview(view) | |
view.topAnchor.constraint(equalTo: topAnchor).isActive = true | |
view.leadingAnchor.constraint(equalTo: imageView.trailingAnchor, | |
constant: LinkMessageSizeCalculator.ImageViewMargin).isActive = true | |
view.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true | |
view.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true | |
return view | |
}() | |
init() { | |
super.init(frame: .zero) | |
contentView.addSubview(titleLabel) | |
contentView.addSubview(teaserLabel) | |
contentView.addSubview(domainLabel) | |
titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true | |
titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor).isActive = true | |
titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true | |
teaserLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true | |
teaserLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 3).isActive = true | |
teaserLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true | |
teaserLabel.setContentHuggingPriority(.init(249), for: .vertical) | |
domainLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor).isActive = true | |
domainLabel.topAnchor.constraint(equalTo: teaserLabel.bottomAnchor, constant: 3).isActive = true | |
domainLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor).isActive = true | |
domainLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor).isActive = true | |
} | |
required init?(coder aDecoder: NSCoder) { | |
fatalError("init(coder:) has not been implemented") | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// LinkMessageSizeCalculator.swift | |
// Engage | |
// | |
// Created by Bruno Guidolim on 04.08.19. | |
// Copyright © 2019 COYO GmbH. All rights reserved. | |
// | |
import MessageKit | |
internal final class LinkMessageSizeCalculator: TextMessageSizeCalculator { | |
private typealias Message = (message: String, linkURL: URL, linkPreview: ChatLinkPreview?) | |
static let ImageViewSize: CGFloat = 60 | |
static let ImageViewMargin: CGFloat = 8 | |
private var chatLinkPreview: ChatLinkPreview? | |
override func messageContainerMaxWidth(for message: MessageType) -> CGFloat { | |
return chatLinkPreview == nil ? | |
super.messageContainerMaxWidth(for: message) : | |
(layout?.collectionView?.bounds.width ?? 0.0) * 0.75 | |
} | |
override func messageContainerSize(for message: MessageType) -> CGSize { | |
let messageTuple: Message = unwrapMessage(message) | |
self.chatLinkPreview = messageTuple.linkPreview | |
guard let chatMessage = message as? ChatMessage else { return .zero } | |
let dummyMessage: ChatMessage = .init(sender: message.sender, | |
messageId: message.messageId, | |
sentDate: message.sentDate, | |
kind: .text(messageTuple.message), | |
updatedID: chatMessage.updatedID) | |
var containerSize: CGSize = super.messageContainerSize(for: dummyMessage) | |
guard let linkPreview = chatLinkPreview, !linkPreview.teaser.isEmptyOrNil else { | |
return containerSize | |
} | |
let labelInsets: UIEdgeInsets = messageLabelInsets(for: message) | |
let maxWidth: CGFloat = messageContainerMaxWidth(for: message) | |
containerSize.width = max(containerSize.width, maxWidth) | |
let minHeight: CGFloat = containerSize.height + LinkMessageSizeCalculator.ImageViewSize | |
let previewMaxWidth: CGFloat = containerSize.width - (LinkMessageSizeCalculator.ImageViewSize + LinkMessageSizeCalculator.ImageViewMargin + labelInsets.horizontal) | |
calculateContainerSize(with: NSAttributedString(string: linkPreview.title ?? "", attributes: [.font: UIFont.caption1SemiBoldFont]), | |
containerSize: &containerSize, | |
maxWidth: previewMaxWidth) | |
calculateContainerSize(with: NSAttributedString(string: linkPreview.teaser ?? "", attributes: [.font: UIFont.caption2Font]), | |
containerSize: &containerSize, | |
maxWidth: previewMaxWidth) | |
calculateContainerSize(with: NSAttributedString(string: linkPreview.domain ?? "", attributes: [.font: UIFont.caption2SemiBoldFont]), | |
containerSize: &containerSize, | |
maxWidth: previewMaxWidth) | |
containerSize.height = max(minHeight, containerSize.height) + labelInsets.vertical | |
return containerSize | |
} | |
private func calculateContainerSize(with attibutedString: NSAttributedString, containerSize: inout CGSize, maxWidth: CGFloat) { | |
if attibutedString.string.isEmpty { | |
return | |
} | |
let size: CGSize = attibutedString.labelSize(considering: maxWidth) | |
containerSize.height += size.height | |
} | |
private func messageLabelInsets(for message: MessageType) -> UIEdgeInsets { | |
let dataSource: MessagesDataSource = messagesLayout.messagesDataSource | |
let isFromCurrentSender: Bool = dataSource.isFromCurrentSender(message: message) | |
return isFromCurrentSender ? outgoingMessageLabelInsets : incomingMessageLabelInsets | |
} | |
private func unwrapMessage(_ message: MessageType) -> Message { | |
guard case .custom(let object) = message.kind, | |
let customType = object as? ChatMessageCustomType, | |
case let .link(attributes) = customType else { | |
preconditionFailure("Was not possible to unwrap the custom type.") | |
} | |
return (attributes.message, attributes.linkURL, attributes.linkPreview) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment