Swiftgram/TelegramUI/ChatMessageAttachedContentNode.swift
2019-06-11 18:22:53 +02:00

1025 lines
64 KiB
Swift

import Foundation
import UIKit
import Postbox
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import Postbox
private let titleFont: UIFont = Font.semibold(15.0)
private let textFont: UIFont = Font.regular(15.0)
private let textBoldFont: UIFont = Font.semibold(15.0)
private let textItalicFont: UIFont = Font.italic(15.0)
private let textFixedFont: UIFont = Font.regular(15.0)
private let buttonFont: UIFont = Font.semibold(13.0)
enum ChatMessageAttachedContentActionIcon {
case instant
}
struct ChatMessageAttachedContentNodeMediaFlags: OptionSet {
var rawValue: Int32
init(rawValue: Int32) {
self.rawValue = rawValue
}
init() {
self.rawValue = 0
}
static let preferMediaInline = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 0)
static let preferMediaBeforeText = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 1)
static let preferMediaAspectFilled = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 2)
}
final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode {
private let textNode: TextNode
private let iconNode: ASImageNode
private let highlightedTextNode: TextNode
private let backgroundNode: ASImageNode
private var regularImage: UIImage?
private var highlightedImage: UIImage?
private var regularIconImage: UIImage?
private var highlightedIconImage: UIImage?
var pressed: (() -> Void)?
override init() {
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.highlightedTextNode = TextNode()
self.highlightedTextNode.isUserInteractionEnabled = false
self.backgroundNode = ASImageNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.displayWithoutProcessing = true
self.backgroundNode.displaysAsynchronously = false
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.textNode)
self.addSubnode(self.highlightedTextNode)
self.highlightedTextNode.isHidden = true
self.highligthedChanged = { [weak self] highlighted in
if let strongSelf = self {
if highlighted {
strongSelf.backgroundNode.image = strongSelf.highlightedImage
strongSelf.iconNode.image = strongSelf.highlightedIconImage
strongSelf.textNode.isHidden = true
strongSelf.highlightedTextNode.isHidden = false
} else {
UIView.transition(with: strongSelf.view, duration: 0.2, options: [.transitionCrossDissolve], animations: {
strongSelf.backgroundNode.image = strongSelf.regularImage
strongSelf.iconNode.image = strongSelf.regularIconImage
strongSelf.textNode.isHidden = false
strongSelf.highlightedTextNode.isHidden = true
}, completion: nil)
}
}
}
self.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
}
@objc func buttonPressed() {
self.pressed?()
}
static func asyncLayout(_ current: ChatMessageAttachedContentButtonNode?) -> (_ width: CGFloat, _ regularImage: UIImage, _ highlightedImage: UIImage, _ iconImage: UIImage?, _ highlightedIconImage: UIImage?, _ title: String, _ titleColor: UIColor, _ highlightedTitleColor: UIColor) -> (CGFloat, (CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode)) {
let previousRegularImage = current?.regularImage
let previousHighlightedImage = current?.highlightedImage
let previousRegularIconImage = current?.regularIconImage
let previousHighlightedIconImage = current?.highlightedIconImage
let maybeMakeTextLayout = (current?.textNode).flatMap(TextNode.asyncLayout)
let maybeMakeHighlightedTextLayout = (current?.highlightedTextNode).flatMap(TextNode.asyncLayout)
return { width, regularImage, highlightedImage, iconImage, highlightedIconImage, title, titleColor, highlightedTitleColor in
let targetNode: ChatMessageAttachedContentButtonNode
if let current = current {
targetNode = current
} else {
targetNode = ChatMessageAttachedContentButtonNode()
}
let makeTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)
if let maybeMakeTextLayout = maybeMakeTextLayout {
makeTextLayout = maybeMakeTextLayout
} else {
makeTextLayout = TextNode.asyncLayout(targetNode.textNode)
}
let makeHighlightedTextLayout: (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)
if let maybeMakeHighlightedTextLayout = maybeMakeHighlightedTextLayout {
makeHighlightedTextLayout = maybeMakeHighlightedTextLayout
} else {
makeHighlightedTextLayout = TextNode.asyncLayout(targetNode.highlightedTextNode)
}
var updatedRegularImage: UIImage?
if regularImage !== previousRegularImage {
updatedRegularImage = regularImage
}
var updatedHighlightedImage: UIImage?
if highlightedImage !== previousHighlightedImage {
updatedHighlightedImage = highlightedImage
}
var updatedRegularIconImage: UIImage?
if iconImage !== previousRegularIconImage {
updatedRegularIconImage = iconImage
}
var updatedHighlightedIconImage: UIImage?
if highlightedIconImage !== previousHighlightedIconImage {
updatedHighlightedIconImage = highlightedIconImage
}
var iconWidth: CGFloat = 0.0
if let iconImage = iconImage {
iconWidth = iconImage.size.width + 5.0
}
let labelInset: CGFloat = 8.0
let (textSize, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: titleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0 - iconWidth), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets()))
let (_, highlightedTextApply) = makeHighlightedTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: title, font: buttonFont, textColor: highlightedTitleColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: max(1.0, width - labelInset * 2.0), height: CGFloat.greatestFiniteMagnitude), alignment: .left, cutout: nil, insets: UIEdgeInsets()))
return (textSize.size.width + labelInset * 2.0, { refinedWidth in
return (CGSize(width: refinedWidth, height: 33.0), {
targetNode.accessibilityLabel = title
if let updatedRegularImage = updatedRegularImage {
targetNode.regularImage = updatedRegularImage
if !targetNode.textNode.isHidden {
targetNode.backgroundNode.image = updatedRegularImage
}
}
if let updatedHighlightedImage = updatedHighlightedImage {
targetNode.highlightedImage = updatedHighlightedImage
if targetNode.textNode.isHidden {
targetNode.backgroundNode.image = updatedHighlightedImage
}
}
if let updatedRegularIconImage = updatedRegularIconImage {
targetNode.regularIconImage = updatedRegularIconImage
if !targetNode.textNode.isHidden {
targetNode.iconNode.image = updatedRegularIconImage
}
}
if let updatedHighlightedIconImage = updatedHighlightedIconImage {
targetNode.highlightedIconImage = updatedHighlightedIconImage
if targetNode.iconNode.isHidden {
targetNode.iconNode.image = updatedHighlightedIconImage
}
}
let _ = textApply()
let _ = highlightedTextApply()
targetNode.backgroundNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: refinedWidth, height: 33.0))
var textFrame = CGRect(origin: CGPoint(x: floor((refinedWidth - textSize.size.width) / 2.0), y: floor((34.0 - textSize.size.height) / 2.0)), size: textSize.size)
if let image = targetNode.iconNode.image {
textFrame.origin.x += floor(image.size.width / 2.0)
targetNode.iconNode.frame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 5.0, y: textFrame.minY + 2.0), size: image.size)
if targetNode.iconNode.supernode == nil {
targetNode.addSubnode(targetNode.iconNode)
}
} else if targetNode.iconNode.supernode != nil {
targetNode.iconNode.removeFromSupernode()
}
targetNode.textNode.frame = textFrame
targetNode.highlightedTextNode.frame = targetNode.textNode.frame
return targetNode
})
})
}
}
}
final class ChatMessageAttachedContentNode: ASDisplayNode {
private let lineNode: ASImageNode
private let textNode: TextNode
private let inlineImageNode: TransformImageNode
private var contentImageNode: ChatMessageInteractiveMediaNode?
private var contentInstantVideoNode: ChatMessageInteractiveInstantVideoNode?
private var contentFileNode: ChatMessageInteractiveFileNode?
private var buttonNode: ChatMessageAttachedContentButtonNode?
private let statusNode: ChatMessageDateAndStatusNode
private var additionalImageBadgeNode: ChatMessageInteractiveMediaBadge?
private var linkHighlightingNode: LinkHighlightingNode?
private var context: AccountContext?
private var message: Message?
private var media: Media?
private var theme: ChatPresentationThemeData?
var openMedia: ((InteractiveMediaNodeActivateContent) -> Void)?
var activateAction: (() -> Void)?
var visibility: ListViewItemNodeVisibility = .none {
didSet {
self.contentImageNode?.visibility = self.visibility
self.contentInstantVideoNode?.visibility = self.visibility
}
}
override init() {
self.lineNode = ASImageNode()
self.lineNode.isLayerBacked = true
self.lineNode.displaysAsynchronously = false
self.lineNode.displayWithoutProcessing = true
self.textNode = TextNode()
self.textNode.isUserInteractionEnabled = false
self.textNode.displaysAsynchronously = true
self.textNode.contentsScale = UIScreenScale
self.textNode.contentMode = .topLeft
self.inlineImageNode = TransformImageNode()
self.inlineImageNode.contentAnimations = [.subsequentUpdates]
self.inlineImageNode.isLayerBacked = !smartInvertColorsEnabled()
self.inlineImageNode.displaysAsynchronously = false
self.statusNode = ChatMessageDateAndStatusNode()
super.init()
self.addSubnode(self.lineNode)
self.addSubnode(self.textNode)
self.addSubnode(self.statusNode)
}
func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ constrainedSize: CGSize) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool) -> Void))) {
let textAsyncLayout = TextNode.asyncLayout(self.textNode)
let currentImage = self.media as? TelegramMediaImage
let imageLayout = self.inlineImageNode.asyncLayout()
let statusLayout = self.statusNode.asyncLayout()
let contentImageLayout = ChatMessageInteractiveMediaNode.asyncLayout(self.contentImageNode)
let contentFileLayout = ChatMessageInteractiveFileNode.asyncLayout(self.contentFileNode)
let contentInstantVideoLayout = ChatMessageInteractiveInstantVideoNode.asyncLayout(self.contentInstantVideoNode)
let makeButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.buttonNode)
let currentAdditionalImageBadgeNode = self.additionalImageBadgeNode
return { presentationData, automaticDownloadSettings, associatedData, context, controllerInteraction, message, messageRead, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, constrainedSize in
let incoming = message.effectivelyIncoming(context.account.peerId)
var horizontalInsets = UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0)
if displayLine {
horizontalInsets.left += 10.0
}
var preferMediaBeforeText = false
var preferMediaAspectFilled = false
if let (_, flags) = mediaAndFlags {
preferMediaBeforeText = flags.contains(.preferMediaBeforeText)
preferMediaAspectFilled = flags.contains(.preferMediaAspectFilled)
}
var contentMode: InteractiveMediaNodeContentMode = preferMediaAspectFilled ? .aspectFill : .aspectFit
var edited = false
var sentViaBot = false
var viewCount: Int?
for attribute in message.attributes {
if let _ = attribute as? EditedMessageAttribute {
edited = true
} else if let attribute = attribute as? ViewCountMessageAttribute {
viewCount = attribute.count
} else if let _ = attribute as? InlineBotMessageAttribute {
sentViaBot = true
}
}
if let author = message.author as? TelegramUser, author.botInfo != nil || author.flags.contains(.isSupport) {
sentViaBot = true
}
let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings)
var webpageGalleryMediaCount: Int?
for media in message.media {
if let media = media as? TelegramMediaWebpage {
if case let .Loaded(content) = media.content, let instantPage = content.instantPage, let image = content.image {
switch instantPageType(of: content) {
case .album:
let count = instantPageGalleryMedia(webpageId: media.webpageId, page: instantPage, galleryMedia: image).count
if count > 1 {
webpageGalleryMediaCount = count
}
default:
break
}
}
}
}
var textString: NSAttributedString?
var inlineImageDimensions: CGSize?
var inlineImageSize: CGSize?
var updateInlineImageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>?
var textCutout = TextNodeCutout()
var initialWidth: CGFloat = CGFloat.greatestFiniteMagnitude
var refineContentImageLayout: ((CGSize, Bool, Bool, ImageCorners) -> (CGFloat, (CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode)))?
var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode)))?
var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ContainedViewLayoutTransition) -> ChatMessageInteractiveInstantVideoNode)?
let string = NSMutableAttributedString()
var notEmpty = false
let bubbleTheme = presentationData.theme.theme.chat.bubble
if let title = title, !title.isEmpty {
string.append(NSAttributedString(string: title, font: titleFont, textColor: incoming ? bubbleTheme.incomingAccentTextColor : bubbleTheme.outgoingAccentTextColor))
notEmpty = true
}
if let subtitle = subtitle, subtitle.length > 0 {
if notEmpty {
string.append(NSAttributedString(string: "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor))
}
let updatedSubtitle = NSMutableAttributedString()
updatedSubtitle.append(subtitle)
updatedSubtitle.addAttribute(.foregroundColor, value: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, range: NSMakeRange(0, subtitle.length))
string.append(updatedSubtitle)
notEmpty = true
}
if let text = text, !text.isEmpty {
if notEmpty {
string.append(NSAttributedString(string: "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor))
}
if let entities = entities {
string.append(stringWithAppliedEntities(text, entities: entities, baseColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor, linkColor: incoming ? bubbleTheme.incomingLinkTextColor : bubbleTheme.outgoingLinkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, fixedFont: textFixedFont))
} else {
string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: incoming ? bubbleTheme.incomingPrimaryTextColor : bubbleTheme.outgoingPrimaryTextColor))
}
notEmpty = true
}
textString = string
if string.length > 1000 {
textString = string.attributedSubstring(from: NSMakeRange(0, 1000))
}
var automaticPlayback = false
if let (media, flags) = mediaAndFlags {
if let file = media as? TelegramMediaFile {
if file.isInstantVideo {
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
let (videoLayout, apply) = contentInstantVideoLayout(ChatMessageBubbleContentItem(context: context, controllerInteraction: controllerInteraction, message: message, read: messageRead, presentationData: presentationData, associatedData: associatedData), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, CGSize(width: 212.0, height: 212.0), .bubble, automaticDownload)
initialWidth = videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight
contentInstantVideoSizeAndApply = (videoLayout, apply)
} else if file.isVideo {
var automaticDownload: InteractiveMediaNodeAutodownloadMode = .none
if shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file) {
automaticDownload = .full
} else if shouldPredownloadMedia(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, media: file) {
automaticDownload = .prefetch
}
if file.isAnimated {
automaticPlayback = automaticDownloadSettings.autoplayGifs
} else if file.isVideo && automaticDownloadSettings.autoplayVideos {
var willDownloadOrLocal = false
if case .full = automaticDownload {
willDownloadOrLocal = true
} else {
willDownloadOrLocal = context.account.postbox.mediaBox.completedResourcePath(file.resource) != nil
}
if willDownloadOrLocal {
automaticPlayback = true
contentMode = .aspectFill
}
}
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, file, automaticDownload, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
refineContentImageLayout = refineLayout
} else if file.isSticker, let _ = file.dimensions {
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, file, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
refineContentImageLayout = refineLayout
} else {
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: file)
let statusType: ChatMessageDateAndStatusType
if message.effectivelyIncoming(context.account.peerId) {
statusType = .BubbleIncoming
} else {
if message.flags.contains(.Failed) {
statusType = .BubbleOutgoing(.Failed)
} else if message.flags.isSending && !message.isSentOrAcknowledged {
statusType = .BubbleOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sent(read: messageRead))
}
}
let (_, refineLayout) = contentFileLayout(context, presentationData, message, file, automaticDownload, message.effectivelyIncoming(context.account.peerId), false, statusType, CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height))
refineContentFileLayout = refineLayout
}
} else if let image = media as? TelegramMediaImage {
if !flags.contains(.preferMediaInline) {
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image)
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
refineContentImageLayout = refineLayout
} else if let dimensions = largestImageRepresentation(image.representations)?.dimensions {
inlineImageDimensions = dimensions
if image != currentImage {
updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, photoReference: .message(message: MessageReference(message), media: image))
}
}
} else if let image = media as? TelegramMediaWebFile {
let automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: image)
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, image, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
refineContentImageLayout = refineLayout
} else if let wallpaper = media as? WallpaperPreviewMedia {
let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData.theme.theme, presentationData.strings, presentationData.dateTimeFormat, message, wallpaper, .full, associatedData.automaticDownloadPeerType, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode)
initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right
refineContentImageLayout = refineLayout
}
}
if let _ = inlineImageDimensions {
inlineImageSize = CGSize(width: 54.0, height: 54.0)
if let inlineImageSize = inlineImageSize {
textCutout.topRight = CGSize(width: inlineImageSize.width + 10.0, height: inlineImageSize.height + 10.0)
}
}
return (initialWidth, { constrainedSize, position in
var insets = UIEdgeInsets(top: 0.0, left: horizontalInsets.left, bottom: 5.0, right: horizontalInsets.right)
switch position {
case .linear(.None, _):
insets.top += 8.0
default:
break
}
var statusInText = false
var statusSizeAndApply: (CGSize, (Bool) -> Void)?
let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom)
var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent?
switch position {
case .linear(_, .None):
let imageMode = !((refineContentImageLayout == nil && refineContentFileLayout == nil && contentInstantVideoSizeAndApply == nil) || preferMediaBeforeText)
statusInText = !imageMode
var skipStandardStatus = false
if let count = webpageGalleryMediaCount {
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.bubble.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.bubble.mediaDateAndStatusTextColor, text: NSAttributedString(string: "1 \(presentationData.strings.Common_of) \(count)"))
skipStandardStatus = imageMode
} else if let mediaBadge = mediaBadge {
additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.bubble.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.bubble.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge))
}
if !skipStandardStatus {
let statusType: ChatMessageDateAndStatusType
if message.effectivelyIncoming(context.account.peerId) {
if imageMode {
statusType = .ImageIncoming
} else {
statusType = .BubbleIncoming
}
} else {
if message.flags.contains(.Failed) {
if imageMode {
statusType = .ImageOutgoing(.Failed)
} else {
statusType = .BubbleOutgoing(.Failed)
}
} else if message.flags.isSending && !message.isSentOrAcknowledged {
if imageMode {
statusType = .ImageOutgoing(.Sending)
} else {
statusType = .BubbleOutgoing(.Sending)
}
} else {
if imageMode {
statusType = .ImageOutgoing(.Sent(read: messageRead))
} else {
statusType = .BubbleOutgoing(.Sent(read: messageRead))
}
}
}
statusSizeAndApply = statusLayout(presentationData, edited && !sentViaBot, viewCount, dateText, statusType, textConstrainedSize)
}
default:
break
}
var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge?
if let _ = additionalImageBadgeContent {
updatedAdditionalImageBadge = currentAdditionalImageBadgeNode ?? ChatMessageInteractiveMediaBadge()
}
var upatedTextCutout = textCutout
if statusInText, let (statusSize, _) = statusSizeAndApply {
upatedTextCutout.bottomRight = statusSize
}
let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets()))
var textFrame = CGRect(origin: CGPoint(), size: textLayout.size)
var statusFrame: CGRect?
if statusInText, let (statusSize, _) = statusSizeAndApply {
var frame = CGRect(origin: CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY - statusSize.height), size: statusSize)
/*let trailingLineWidth = textLayout.trailingLineWidth
if textLayout.size.width - trailingLineWidth >= statusSize.width {
frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY - statusSize.height)
} else if trailingLineWidth + statusSize.width < textConstrainedSize.width {
frame.origin = CGPoint(x: textFrame.minX + trailingLineWidth, y: textFrame.maxY - statusSize.height)
} else {
frame.origin = CGPoint(x: textFrame.maxX - statusSize.width, y: textFrame.maxY)
}*/
if let inlineImageSize = inlineImageSize {
if frame.origin.y < inlineImageSize.height + 4.0 {
frame.origin.y = inlineImageSize.height + 4.0
}
}
frame = frame.offsetBy(dx: insets.left, dy: insets.top)
statusFrame = frame
}
textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top)
let lineImage = incoming ? PresentationResourcesChat.chatBubbleVerticalLineIncomingImage(presentationData.theme.theme) : PresentationResourcesChat.chatBubbleVerticalLineOutgoingImage(presentationData.theme.theme)
var boundingSize = textFrame.size
var lineHeight = textFrame.size.height
if let statusFrame = statusFrame {
boundingSize = textFrame.union(statusFrame).size
if let _ = actionTitle {
lineHeight = boundingSize.height
}
}
if let inlineImageSize = inlineImageSize {
if boundingSize.height < inlineImageSize.height {
boundingSize.height = inlineImageSize.height
}
if lineHeight < inlineImageSize.height {
lineHeight = inlineImageSize.height
}
}
var finalizeContentImageLayout: ((CGFloat) -> (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode))?
if let refineContentImageLayout = refineContentImageLayout {
let (refinedWidth, finalizeImageLayout) = refineContentImageLayout(textConstrainedSize, automaticPlayback, true, ImageCorners(radius: 4.0))
finalizeContentImageLayout = finalizeImageLayout
boundingSize.width = max(boundingSize.width, refinedWidth)
}
var finalizeContentFileLayout: ((CGFloat) -> (CGSize, (Bool) -> ChatMessageInteractiveFileNode))?
if let refineContentFileLayout = refineContentFileLayout {
let (refinedWidth, finalizeFileLayout) = refineContentFileLayout(textConstrainedSize)
finalizeContentFileLayout = finalizeFileLayout
boundingSize.width = max(boundingSize.width, refinedWidth)
}
if let (videoLayout, _) = contentInstantVideoSizeAndApply {
boundingSize.width = max(boundingSize.width, videoLayout.contentSize.width + videoLayout.overflowLeft + videoLayout.overflowRight)
}
lineHeight += insets.top + insets.bottom
var imageApply: (() -> Void)?
if let inlineImageSize = inlineImageSize, let inlineImageDimensions = inlineImageDimensions {
let imageCorners = ImageCorners(topLeft: .Corner(4.0), topRight: .Corner(4.0), bottomLeft: .Corner(4.0), bottomRight: .Corner(4.0))
let arguments = TransformImageArguments(corners: imageCorners, imageSize: inlineImageDimensions.aspectFilled(inlineImageSize), boundingSize: inlineImageSize, intrinsicInsets: UIEdgeInsets(), emptyColor: incoming ? presentationData.theme.theme.chat.bubble.incomingMediaPlaceholderColor : presentationData.theme.theme.chat.bubble.outgoingMediaPlaceholderColor)
imageApply = imageLayout(arguments)
}
var continueActionButtonLayout: ((CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode))?
if let actionTitle = actionTitle {
let buttonImage: UIImage
let buttonHighlightedImage: UIImage
var buttonIconImage: UIImage?
var buttonHighlightedIconImage: UIImage?
let titleColor: UIColor
let titleHighlightedColor: UIColor
if incoming {
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(presentationData.theme.theme)!
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(presentationData.theme.theme)!
if let actionIcon = actionIcon, case .instant = actionIcon {
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)!
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
}
titleColor = presentationData.theme.theme.chat.bubble.incomingAccentTextColor
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColor.fill
} else {
buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(presentationData.theme.theme)!
buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(presentationData.theme.theme)!
if let actionIcon = actionIcon, case .instant = actionIcon {
buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)!
buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)!
}
titleColor = presentationData.theme.theme.chat.bubble.outgoingAccentTextColor
let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty)
titleHighlightedColor = bubbleColor.fill
}
let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, buttonIconImage, buttonHighlightedIconImage, actionTitle, titleColor, titleHighlightedColor)
boundingSize.width = max(buttonWidth, boundingSize.width)
continueActionButtonLayout = continueLayout
}
boundingSize.width += insets.left + insets.right
boundingSize.height += insets.top + insets.bottom
return (boundingSize.width, { boundingWidth in
var adjustedBoundingSize = boundingSize
var adjustedLineHeight = lineHeight
var imageFrame: CGRect?
if let inlineImageSize = inlineImageSize {
imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right, y: 0.0), size: inlineImageSize)
}
var contentImageSizeAndApply: (CGSize, (ContainedViewLayoutTransition, Bool) -> ChatMessageInteractiveMediaNode)?
if let finalizeContentImageLayout = finalizeContentImageLayout {
let (size, apply) = finalizeContentImageLayout(boundingWidth - insets.left - insets.right)
contentImageSizeAndApply = (size, apply)
var imageHeightAddition = size.height
if textFrame.size.height > CGFloat.ulpOfOne {
imageHeightAddition += 2.0
}
adjustedBoundingSize.height += imageHeightAddition + 5.0
adjustedLineHeight += imageHeightAddition + 4.0
if !statusInText, let (statusSize, _) = statusSizeAndApply {
statusFrame = CGRect(origin: CGPoint(), size: statusSize)
}
}
var contentFileSizeAndApply: (CGSize, (Bool) -> ChatMessageInteractiveFileNode)?
if let finalizeContentFileLayout = finalizeContentFileLayout {
let (size, apply) = finalizeContentFileLayout(boundingWidth - insets.left - insets.right)
contentFileSizeAndApply = (size, apply)
var imageHeightAddition = size.height + 6.0
if textFrame.size.height > CGFloat.ulpOfOne {
imageHeightAddition += 6.0
}
adjustedBoundingSize.height += imageHeightAddition + 5.0
adjustedLineHeight += imageHeightAddition + 4.0
}
if let (videoLayout, _) = contentInstantVideoSizeAndApply {
let imageHeightAddition = videoLayout.contentSize.height + 6.0
if textFrame.size.height > CGFloat.ulpOfOne {
//imageHeightAddition += 2.0
}
adjustedBoundingSize.height += imageHeightAddition// + 5.0
adjustedLineHeight += imageHeightAddition// + 4.0
}
var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))?
if let continueActionButtonLayout = continueActionButtonLayout {
let (size, apply) = continueActionButtonLayout(boundingWidth - 13.0 - insets.right)
actionButtonSizeAndApply = (size, apply)
adjustedBoundingSize.width = max(adjustedBoundingSize.width, insets.left + size.width + insets.right)
adjustedBoundingSize.height += 7.0 + size.height
}
var adjustedStatusFrame: CGRect?
if statusInText, let statusFrame = statusFrame {
adjustedStatusFrame = CGRect(origin: CGPoint(x: boundingWidth - statusFrame.size.width - insets.right, y: statusFrame.origin.y), size: statusFrame.size)
}
adjustedBoundingSize.width = max(boundingWidth, adjustedBoundingSize.width)
return (adjustedBoundingSize, { [weak self] animation, synchronousLoads in
if let strongSelf = self {
strongSelf.context = context
strongSelf.message = message
strongSelf.media = mediaAndFlags?.0
strongSelf.theme = presentationData.theme
var hasAnimation = true
var transition: ContainedViewLayoutTransition = .immediate
switch animation {
case .None:
hasAnimation = false
case let .System(duration):
hasAnimation = true
transition = .animated(duration: duration, curve: .easeInOut)
}
strongSelf.lineNode.image = lineImage
strongSelf.lineNode.frame = CGRect(origin: CGPoint(x: 13.0, y: insets.top), size: CGSize(width: 2.0, height: adjustedLineHeight - insets.top - insets.bottom - 2.0))
strongSelf.lineNode.isHidden = !displayLine
let _ = textApply()
if let imageFrame = imageFrame {
if let updateImageSignal = updateInlineImageSignal {
strongSelf.inlineImageNode.setSignal(updateImageSignal)
}
strongSelf.inlineImageNode.frame = imageFrame
if strongSelf.inlineImageNode.supernode == nil {
strongSelf.addSubnode(strongSelf.inlineImageNode)
}
if let imageApply = imageApply {
imageApply()
}
} else if strongSelf.inlineImageNode.supernode != nil {
strongSelf.inlineImageNode.removeFromSupernode()
}
var contentMediaHeight: CGFloat?
if let (contentImageSize, contentImageApply) = contentImageSizeAndApply {
contentMediaHeight = contentImageSize.height
let contentImageNode = contentImageApply(transition, synchronousLoads)
if strongSelf.contentImageNode !== contentImageNode {
strongSelf.contentImageNode = contentImageNode
strongSelf.addSubnode(contentImageNode)
contentImageNode.activateLocalContent = { [weak strongSelf] mode in
if let strongSelf = strongSelf {
strongSelf.openMedia?(mode)
}
}
contentImageNode.visibility = strongSelf.visibility
}
let _ = contentImageApply(transition, synchronousLoads)
let contentImageFrame: CGRect
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize)
} else {
contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: contentImageSize)
}
contentImageNode.frame = contentImageFrame
if !statusInText, let statusFrame = statusFrame {
adjustedStatusFrame = CGRect(origin: CGPoint(x: contentImageFrame.width - statusFrame.size.width - 2.0, y: contentImageFrame.height - statusFrame.size.height - 2.0), size: statusFrame.size)
}
} else if let contentImageNode = strongSelf.contentImageNode {
contentImageNode.visibility = .none
contentImageNode.removeFromSupernode()
strongSelf.contentImageNode = nil
}
if let updatedAdditionalImageBadge = updatedAdditionalImageBadge, let contentImageNode = strongSelf.contentImageNode, let contentImageSize = contentImageSizeAndApply?.0 {
if strongSelf.additionalImageBadgeNode != updatedAdditionalImageBadge {
strongSelf.additionalImageBadgeNode?.removeFromSupernode()
}
strongSelf.additionalImageBadgeNode = updatedAdditionalImageBadge
contentImageNode.addSubnode(updatedAdditionalImageBadge)
if mediaBadge != nil {
updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, animated: false)
updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: 2.0, y: 2.0), size: CGSize(width: 0.0, height: 0.0))
} else {
updatedAdditionalImageBadge.update(theme: presentationData.theme.theme, content: additionalImageBadgeContent, mediaDownloadState: nil, alignment: .right, animated: false)
updatedAdditionalImageBadge.frame = CGRect(origin: CGPoint(x: contentImageSize.width - 6.0, y: contentImageSize.height - 18.0 - 6.0), size: CGSize(width: 0.0, height: 0.0))
}
} else if let additionalImageBadgeNode = strongSelf.additionalImageBadgeNode {
strongSelf.additionalImageBadgeNode = nil
additionalImageBadgeNode.removeFromSupernode()
}
if let (contentFileSize, contentFileApply) = contentFileSizeAndApply {
contentMediaHeight = contentFileSize.height
let contentFileNode = contentFileApply(synchronousLoads)
if strongSelf.contentFileNode !== contentFileNode {
strongSelf.contentFileNode = contentFileNode
strongSelf.addSubnode(contentFileNode)
contentFileNode.activateLocalContent = { [weak strongSelf] in
if let strongSelf = strongSelf {
strongSelf.openMedia?(.default)
}
}
}
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentFileSize)
} else {
contentFileNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 8.0 : 0.0)), size: contentFileSize)
}
} else if let contentFileNode = strongSelf.contentFileNode {
contentFileNode.removeFromSupernode()
strongSelf.contentFileNode = nil
}
if let (videoLayout, apply) = contentInstantVideoSizeAndApply {
contentMediaHeight = videoLayout.contentSize.height
let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), transition)
if strongSelf.contentInstantVideoNode !== contentInstantVideoNode {
strongSelf.contentInstantVideoNode = contentInstantVideoNode
strongSelf.addSubnode(contentInstantVideoNode)
}
if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: videoLayout.contentSize)
} else {
contentInstantVideoNode.frame = CGRect(origin: CGPoint(x: insets.left, y: textFrame.maxY + (textFrame.size.height > CGFloat.ulpOfOne ? 4.0 : 0.0)), size: videoLayout.contentSize)
}
} else if let contentInstantVideoNode = strongSelf.contentInstantVideoNode {
contentInstantVideoNode.removeFromSupernode()
strongSelf.contentInstantVideoNode = nil
}
var textVerticalOffset: CGFloat = 0.0
if let contentMediaHeight = contentMediaHeight, let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
textVerticalOffset = contentMediaHeight + 7.0
}
strongSelf.textNode.frame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset)
if let (size, apply) = actionButtonSizeAndApply {
let buttonNode = apply()
if buttonNode !== strongSelf.buttonNode {
strongSelf.buttonNode?.removeFromSupernode()
strongSelf.buttonNode = buttonNode
strongSelf.addSubnode(buttonNode)
buttonNode.pressed = {
if let strongSelf = self {
strongSelf.activateAction?()
}
}
}
buttonNode.frame = CGRect(origin: CGPoint(x: 13.0, y: adjustedLineHeight - insets.top - insets.bottom - 2.0 + 6.0), size: size)
} else if let buttonNode = strongSelf.buttonNode {
buttonNode.removeFromSupernode()
strongSelf.buttonNode = nil
}
if let (_, statusApply) = statusSizeAndApply, let adjustedStatusFrame = adjustedStatusFrame {
strongSelf.statusNode.frame = adjustedStatusFrame.offsetBy(dx: 0.0, dy: textVerticalOffset)
if statusInText {
if strongSelf.statusNode.supernode != strongSelf {
strongSelf.addSubnode(strongSelf.statusNode)
}
} else if let contentImageNode = strongSelf.contentImageNode {
if strongSelf.statusNode.supernode != contentImageNode {
contentImageNode.addSubnode(strongSelf.statusNode)
}
}
statusApply(hasAnimation)
} else if strongSelf.statusNode.supernode != nil {
strongSelf.statusNode.removeFromSupernode()
}
}
})
})
})
}
}
func updateHiddenMedia(_ media: [Media]?) -> Bool {
if let currentMedia = self.media {
if let media = media {
var found = false
for m in media {
if currentMedia.isEqual(to: m) {
found = true
break
}
}
if let contentImageNode = self.contentImageNode {
contentImageNode.isHidden = found
contentImageNode.updateIsHidden(found)
return found
}
} else if let contentImageNode = self.contentImageNode {
contentImageNode.isHidden = false
contentImageNode.updateIsHidden(false)
}
}
return false
}
func transitionNode(media: Media) -> (ASDisplayNode, () -> (UIView?, UIView?))? {
if let contentImageNode = self.contentImageNode, let image = self.media as? TelegramMediaImage, image.isEqual(to: media) {
return (contentImageNode, { [weak contentImageNode] in
return (contentImageNode?.view.snapshotContentTree(unhide: true), nil)
})
} else if let contentImageNode = self.contentImageNode, let file = self.media as? TelegramMediaFile, file.isEqual(to: media) {
return (contentImageNode, { [weak contentImageNode] in
return (contentImageNode?.view.snapshotContentTree(unhide: true), nil)
})
}
return nil
}
func hasActionAtPoint(_ point: CGPoint) -> Bool {
if let buttonNode = self.buttonNode, buttonNode.frame.contains(point) {
return true
}
return false
}
func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture) -> ChatMessageBubbleContentTapAction {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
if let url = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.URL)] as? String {
var concealed = true
if let attributeText = self.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) {
concealed = !doesUrlMatchText(url: url, text: attributeText)
}
return .url(url: url, concealed: concealed)
} else if let peerMention = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention {
return .peerMention(peerMention.peerId, peerMention.mention)
} else if let peerName = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.PeerTextMention)] as? String {
return .textMention(peerName)
} else if let botCommand = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.BotCommand)] as? String {
return .botCommand(botCommand)
} else if let hashtag = attributes[NSAttributedStringKey(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag {
return .hashtag(hashtag.peerName, hashtag.hashtag)
} else {
return .none
}
} else {
return .none
}
}
func updateTouchesAtPoint(_ point: CGPoint?) {
if let context = self.context, let message = self.message, let theme = self.theme {
var rects: [CGRect]?
if let point = point {
let textNodeFrame = self.textNode.frame
if let (index, attributes) = self.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) {
let possibleNames: [String] = [
TelegramTextAttributes.URL,
TelegramTextAttributes.PeerMention,
TelegramTextAttributes.PeerTextMention,
TelegramTextAttributes.BotCommand,
TelegramTextAttributes.Hashtag
]
for name in possibleNames {
if let _ = attributes[NSAttributedStringKey(rawValue: name)] {
rects = self.textNode.attributeRects(name: name, at: index)
break
}
}
}
}
if let rects = rects {
let linkHighlightingNode: LinkHighlightingNode
if let current = self.linkHighlightingNode {
linkHighlightingNode = current
} else {
linkHighlightingNode = LinkHighlightingNode(color: message.effectivelyIncoming(context.account.peerId) ? theme.theme.chat.bubble.incomingLinkHighlightColor : theme.theme.chat.bubble.outgoingLinkHighlightColor)
self.linkHighlightingNode = linkHighlightingNode
self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode)
}
linkHighlightingNode.frame = self.textNode.frame
linkHighlightingNode.updateRects(rects)
} else if let linkHighlightingNode = self.linkHighlightingNode {
self.linkHighlightingNode = nil
linkHighlightingNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak linkHighlightingNode] _ in
linkHighlightingNode?.removeFromSupernode()
})
}
}
}
func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? {
return self.contentImageNode?.playMediaWithSound()
}
}