import Foundation import UIKit import Postbox import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore import TelegramPresentationData import TelegramUIPreferences import TextFormat import AccountContext import UrlEscaping import PhotoResources import WebsiteType import ChatMessageInteractiveMediaBadge import GalleryData import TextNodeWithEntities import AnimationCache import MultiAnimationRenderer import ChatControllerInteraction import ShimmerEffect import ChatMessageDateAndStatusNode import ChatHistoryEntry import ChatMessageItemCommon import ChatMessageBubbleContentNode import ChatMessageInteractiveInstantVideoNode import ChatMessageInteractiveFileNode import ChatMessageInteractiveMediaNode import WallpaperPreviewMedia import ChatMessageAttachedContentButtonNode public enum ChatMessageAttachedContentActionIcon { case instant case link } public struct ChatMessageAttachedContentNodeMediaFlags: OptionSet { public var rawValue: Int32 public init(rawValue: Int32) { self.rawValue = rawValue } public init() { self.rawValue = 0 } public static let preferMediaInline = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 0) public static let preferMediaBeforeText = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 1) public static let preferMediaAspectFilled = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 2) public static let titleBeforeMedia = ChatMessageAttachedContentNodeMediaFlags(rawValue: 1 << 3) } public final class ChatMessageAttachedContentNode: ASDisplayNode { private var backgroundView: UIImageView? private let topTitleNode: TextNode private let textNode: TextNodeWithEntities private let inlineImageNode: TransformImageNode private var contentImageNode: ChatMessageInteractiveMediaNode? private var contentInstantVideoNode: ChatMessageInteractiveInstantVideoNode? private var contentFileNode: ChatMessageInteractiveFileNode? private var buttonNode: ChatMessageAttachedContentButtonNode? public 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? public var openMedia: ((InteractiveMediaNodeActivateContent) -> Void)? public var activateAction: (() -> Void)? public var requestUpdateLayout: (() -> Void)? public var visibility: ListViewItemNodeVisibility = .none { didSet { if oldValue != self.visibility { self.contentImageNode?.visibility = self.visibility != .none self.contentInstantVideoNode?.visibility = self.visibility != .none switch self.visibility { case .none: self.textNode.visibilityRect = nil case let .visible(_, subRect): var subRect = subRect subRect.origin.x = 0.0 subRect.size.width = 10000.0 self.textNode.visibilityRect = subRect } } } } override public init() { self.topTitleNode = TextNode() self.topTitleNode.isUserInteractionEnabled = false self.topTitleNode.displaysAsynchronously = false self.topTitleNode.contentsScale = UIScreenScale self.topTitleNode.contentMode = .topLeft self.textNode = TextNodeWithEntities() self.textNode.textNode.isUserInteractionEnabled = false self.textNode.textNode.displaysAsynchronously = false self.textNode.textNode.contentsScale = UIScreenScale self.textNode.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.topTitleNode) self.addSubnode(self.textNode.textNode) self.addSubnode(self.statusNode) } public func asyncLayout() -> (_ presentationData: ChatPresentationData, _ automaticDownloadSettings: MediaAutoDownloadSettings, _ associatedData: ChatMessageItemAssociatedData, _ attributes: ChatMessageEntryAttributes, _ context: AccountContext, _ controllerInteraction: ChatControllerInteraction, _ message: Message, _ messageRead: Bool, _ chatLocation: ChatLocation, _ title: String?, _ subtitle: NSAttributedString?, _ text: String?, _ entities: [MessageTextEntity]?, _ media: (Media, ChatMessageAttachedContentNodeMediaFlags)?, _ mediaBadge: String?, _ actionIcon: ChatMessageAttachedContentActionIcon?, _ actionTitle: String?, _ displayLine: Bool, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ constrainedSize: CGSize, _ animationCache: AnimationCache, _ animationRenderer: MultiAnimationRenderer) -> (CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let topTitleAsyncLayout = TextNode.asyncLayout(self.topTitleNode) let textAsyncLayout = TextNodeWithEntities.asyncLayout(self.textNode) let currentImage = self.media as? TelegramMediaImage let currentMediaIsInline = self.inlineImageNode.supernode != nil 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, attributes, context, controllerInteraction, message, messageRead, chatLocation, title, subtitle, text, entities, mediaAndFlags, mediaBadge, actionIcon, actionTitle, displayLine, layoutConstants, preparePosition, constrainedSize, animationCache, animationRenderer in let isPreview = presentationData.isPreview let fontSize: CGFloat if message.adAttribute != nil { fontSize = floor(presentationData.fontSize.baseDisplaySize) } else { fontSize = floor(presentationData.fontSize.baseDisplaySize * 15.0 / 17.0) } let titleFont = Font.semibold(fontSize) let textFont = Font.regular(fontSize) let textBoldFont = Font.semibold(fontSize) let textItalicFont = Font.italic(fontSize) let textBoldItalicFont = Font.semiboldItalic(fontSize) let textFixedFont = Font.regular(fontSize) let textBlockQuoteFont = Font.regular(fontSize) var incoming = message.effectivelyIncoming(context.account.peerId) if let subject = associatedData.subject, case let .messageOptions(_, _, info) = subject, case .forward = info { incoming = false } var horizontalInsets = UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 10.0) if displayLine { horizontalInsets.left += 12.0 horizontalInsets.right += 12.0 } var titleBeforeMedia = false var preferMediaBeforeText = false var preferMediaAspectFilled = false if let (_, flags) = mediaAndFlags { preferMediaBeforeText = flags.contains(.preferMediaBeforeText) preferMediaAspectFilled = flags.contains(.preferMediaAspectFilled) titleBeforeMedia = flags.contains(.titleBeforeMedia) } var contentMode: InteractiveMediaNodeContentMode = preferMediaAspectFilled ? .aspectFill : .aspectFit var edited = false if attributes.updatingMedia != nil { edited = true } var viewCount: Int? var dateReplies = 0 var dateReactionsAndPeers = mergedMessageReactionsAndPeers(accountPeer: associatedData.accountPeer, message: message) if message.isRestricted(platform: "ios", contentSettings: context.currentContentSettings.with { $0 }) { dateReactionsAndPeers = ([], []) } for attribute in message.attributes { if let attribute = attribute as? EditedMessageAttribute { edited = !attribute.isHidden } else if let attribute = attribute as? ViewCountMessageAttribute { viewCount = attribute.count } else if let attribute = attribute as? ReplyThreadMessageAttribute, case .peer = chatLocation { if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .group = channel.info { dateReplies = Int(attribute.count) } } } let dateText = stringForMessageTimestampStatus(accountPeerId: context.account.peerId, message: message, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, strings: presentationData.strings, associatedData: associatedData) 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, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)))? var refineContentFileLayout: ((CGSize) -> (CGFloat, (CGFloat) -> (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> ChatMessageInteractiveFileNode)))? var contentInstantVideoSizeAndApply: (ChatMessageInstantVideoItemLayoutResult, (ChatMessageInstantVideoItemLayoutData, ListViewItemUpdateAnimation) -> ChatMessageInteractiveInstantVideoNode)? let topTitleString = NSMutableAttributedString() let string = NSMutableAttributedString() var notEmpty = false let messageTheme = incoming ? presentationData.theme.theme.chat.message.incoming : presentationData.theme.theme.chat.message.outgoing if let title = title, !title.isEmpty { if titleBeforeMedia { topTitleString.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor)) } else { string.append(NSAttributedString(string: title, font: titleFont, textColor: messageTheme.accentTextColor)) notEmpty = true } } if let subtitle = subtitle, subtitle.length > 0 { if notEmpty { string.append(NSAttributedString(string: "\n", font: textFont, textColor: messageTheme.primaryTextColor)) } let updatedSubtitle = NSMutableAttributedString() updatedSubtitle.append(subtitle) updatedSubtitle.addAttribute(.foregroundColor, value: messageTheme.primaryTextColor, range: NSMakeRange(0, subtitle.length)) updatedSubtitle.addAttribute(.font, value: titleFont, 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: messageTheme.primaryTextColor)) } if let entities = entities { string.append(stringWithAppliedEntities(text, entities: entities, baseColor: messageTheme.primaryTextColor, linkColor: messageTheme.linkTextColor, baseFont: textFont, linkFont: textFont, boldFont: textBoldFont, italicFont: textItalicFont, boldItalicFont: textBoldItalicFont, fixedFont: textFixedFont, blockQuoteFont: textBlockQuoteFont, message: nil, adjustQuoteFontSize: true)) } else { string.append(NSAttributedString(string: text + "\n", font: textFont, textColor: messageTheme.primaryTextColor)) } notEmpty = true } textString = string if string.length > 1000 { textString = string.attributedSubstring(from: NSMakeRange(0, 1000)) } var isReplyThread = false if case .replyThread = chatLocation { isReplyThread = true } var skipStandardStatus = false var isImage = false var isFile = false var automaticPlayback = false var textStatusType: ChatMessageDateAndStatusType? var imageStatusType: ChatMessageDateAndStatusType? var additionalImageBadgeContent: ChatMessageInteractiveMediaBadgeContent? if let (media, flags) = mediaAndFlags { if let file = media as? TelegramMediaFile { if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { isImage = true } else if file.isInstantVideo { isImage = true } else if file.isVideo { isImage = true } else if file.isSticker || file.isAnimatedSticker { isImage = true } else { isFile = true } } else if let _ = media as? TelegramMediaImage { if !flags.contains(.preferMediaInline) { isImage = true } } else if let _ = media as? TelegramMediaWebFile { isImage = true } else if let _ = media as? WallpaperPreviewMedia { isImage = true } else if let _ = media as? TelegramMediaStory { isImage = true } } if preferMediaBeforeText, let textString, textString.length != 0 { isImage = false } var statusInText = !isImage if let textString { if textString.length == 0 { statusInText = false } } else { statusInText = false } switch preparePosition { case .linear(_, .None), .linear(_, .Neighbour(true, _, _)): if let count = webpageGalleryMediaCount { additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: presentationData.strings.Items_NOfM("1", "\(count)").string), iconName: nil) skipStandardStatus = isImage } else if let mediaBadge = mediaBadge { additionalImageBadgeContent = .text(inset: 0.0, backgroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusFillColor, foregroundColor: presentationData.theme.theme.chat.message.mediaDateAndStatusTextColor, text: NSAttributedString(string: mediaBadge), iconName: nil) } else { skipStandardStatus = isFile } if !skipStandardStatus { if incoming { if isImage { imageStatusType = .ImageIncoming } else { textStatusType = .BubbleIncoming } } else { if message.flags.contains(.Failed) { if isImage { imageStatusType = .ImageOutgoing(.Failed) } else { textStatusType = .BubbleOutgoing(.Failed) } } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { if isImage { imageStatusType = .ImageOutgoing(.Sending) } else { textStatusType = .BubbleOutgoing(.Sending) } } else { if isImage { imageStatusType = .ImageOutgoing(.Sent(read: messageRead)) } else { textStatusType = .BubbleOutgoing(.Sent(read: messageRead)) } } } } default: break } let imageDateAndStatus = imageStatusType.flatMap { statusType -> ChatMessageDateAndStatus in ChatMessageDateAndStatus( type: statusType, edited: edited, viewCount: viewCount, dateReactions: dateReactionsAndPeers.reactions, dateReactionPeers: dateReactionsAndPeers.peers, dateReplies: dateReplies, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, dateText: dateText ) } if let (media, flags) = mediaAndFlags { if let file = media as? TelegramMediaFile { if file.mimeType == "application/x-tgtheme-ios", let size = file.size, size < 16 * 1024 { let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if file.isInstantVideo { let displaySize = CGSize(width: 212.0, height: 212.0) 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, topMessage: message, read: messageRead, chatLocation: chatLocation, presentationData: presentationData, associatedData: associatedData, attributes: attributes, isItemPinned: message.tags.contains(.pinned) && !isReplyThread, isItemEdited: false), constrainedSize.width - horizontalInsets.left - horizontalInsets.right, displaySize, displaySize, 0.0, .bubble, automaticDownload, 0.0) 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 = context.sharedContext.energyUsageSettings.autoplayGif } else if file.isVideo && context.sharedContext.energyUsageSettings.autoplayVideo { 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, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if file.isSticker || file.isAnimatedSticker { 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, presentationData.dateTimeFormat, message, associatedData, attributes, file, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) 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 incoming { statusType = .BubbleIncoming } else { if message.flags.contains(.Failed) { statusType = .BubbleOutgoing(.Failed) } else if (message.flags.isSending && !message.isSentOrAcknowledged) || attributes.updatingMedia != nil { statusType = .BubbleOutgoing(.Sending) } else { statusType = .BubbleOutgoing(.Sent(read: messageRead)) } } let (_, refineLayout) = contentFileLayout(ChatMessageInteractiveFileNode.Arguments( context: context, presentationData: presentationData, message: message, topMessage: message, associatedData: associatedData, chatLocation: chatLocation, attributes: attributes, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, forcedIsEdited: false, file: file, automaticDownload: automaticDownload, incoming: incoming, isRecentActions: false, forcedResourceStatus: associatedData.forcedResourceStatus, dateAndStatusType: statusType, displayReactions: false, messageSelection: nil, layoutConstants: layoutConstants, constrainedSize: CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height), controllerInteraction: controllerInteraction )) 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, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let dimensions = largestImageRepresentation(image.representations)?.dimensions { inlineImageDimensions = dimensions.cgSize if image != currentImage || !currentMediaIsInline { updateInlineImageSignal = chatWebpageSnippetPhoto(account: context.account, userLocation: .peer(message.id.peerId), 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, presentationData.dateTimeFormat, message, associatedData, attributes, image, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout } else if let wallpaper = media as? WallpaperPreviewMedia { let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, wallpaper, imageDateAndStatus, .full, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) initialWidth = initialImageWidth + horizontalInsets.left + horizontalInsets.right refineContentImageLayout = refineLayout if case let .file(_, _, _, _, isTheme, _) = wallpaper.content, isTheme { skipStandardStatus = true } } else if let story = media as? TelegramMediaStory { var media: Media? if let storyValue = message.associatedStories[story.storyId]?.get(Stories.StoredItem.self), case let .item(item) = storyValue { media = item.media } var automaticDownload = false if let media { automaticDownload = shouldDownloadMediaAutomatically(settings: automaticDownloadSettings, peerType: associatedData.automaticDownloadPeerType, networkType: associatedData.automaticDownloadNetworkType, authorPeerId: message.author?.id, contactsPeerIds: associatedData.contactsPeerIds, media: media) } let (_, initialImageWidth, refineLayout) = contentImageLayout(context, presentationData, presentationData.dateTimeFormat, message, associatedData, attributes, story, imageDateAndStatus, automaticDownload ? .full : .none, associatedData.automaticDownloadPeerType, associatedData.automaticDownloadPeerId, .constrained(CGSize(width: constrainedSize.width - horizontalInsets.left - horizontalInsets.right, height: constrainedSize.height)), layoutConstants, contentMode, controllerInteraction.presentationContext) 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) var lineInsets = insets //insets.top += 4.0 //insets.bottom += 4.0 switch position { case .linear(.None, _): insets.top += 10.0 insets.bottom += 8.0 lineInsets.top += 10.0 + 8.0 default: break } let textConstrainedSize = CGSize(width: constrainedSize.width - insets.left - insets.right, height: constrainedSize.height - insets.top - insets.bottom) var updatedAdditionalImageBadge: ChatMessageInteractiveMediaBadge? if let _ = additionalImageBadgeContent { updatedAdditionalImageBadge = currentAdditionalImageBadgeNode ?? ChatMessageInteractiveMediaBadge() } let upatedTextCutout = textCutout let (topTitleLayout, topTitleApply) = topTitleAsyncLayout(TextNodeLayoutArguments(attributedString: topTitleString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (textLayout, textApply) = textAsyncLayout(TextNodeLayoutArguments(attributedString: textString, backgroundColor: nil, maximumNumberOfLines: 12, truncationType: .end, constrainedSize: textConstrainedSize, alignment: .natural, cutout: upatedTextCutout, insets: UIEdgeInsets())) var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? if statusInText, let textStatusType = textStatusType { let trailingContentWidth: CGFloat if textLayout.hasRTL { trailingContentWidth = 10000.0 } else { trailingContentWidth = textLayout.trailingLineWidth } statusSuggestedWidthAndContinue = statusLayout(ChatMessageDateAndStatusNode.Arguments( context: context, presentationData: presentationData, edited: edited, impressionCount: viewCount, dateText: dateText, type: textStatusType, layoutInput: .trailingContent(contentWidth: trailingContentWidth, reactionSettings: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions) ? ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: true, preferAdditionalInset: false) : nil), constrainedSize: textConstrainedSize, availableReactions: associatedData.availableReactions, reactions: dateReactionsAndPeers.reactions, reactionPeers: dateReactionsAndPeers.peers, displayAllReactionPeers: message.id.peerId.namespace == Namespaces.Peer.CloudUser, replyCount: dateReplies, isPinned: message.tags.contains(.pinned) && !associatedData.isInPinnedListMode && !isReplyThread, hasAutoremove: message.isSelfExpiring, canViewReactionList: canViewMessageReactionList(message: message), animationCache: controllerInteraction.presentationContext.animationCache, animationRenderer: controllerInteraction.presentationContext.animationRenderer )) } let _ = statusSuggestedWidthAndContinue var textFrame = CGRect(origin: CGPoint(), size: textLayout.size) textFrame = textFrame.offsetBy(dx: insets.left, dy: insets.top) let mainColor: UIColor if !incoming { mainColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor } else { var authorNameColor: UIColor? let author = message.author if [Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudChannel].contains(message.id.peerId.namespace), author?.id.namespace == Namespaces.Peer.CloudUser { authorNameColor = author.flatMap { chatMessagePeerIdColors[Int(clamping: $0.id.id._internalGetInt64Value() % 7)] } if let rawAuthorNameColor = authorNameColor { var dimColors = false switch presentationData.theme.theme.name { case .builtin(.nightAccent), .builtin(.night): dimColors = true default: break } if dimColors { var hue: CGFloat = 0.0 var saturation: CGFloat = 0.0 var brightness: CGFloat = 0.0 rawAuthorNameColor.getHue(&hue, saturation: &saturation, brightness: &brightness, alpha: nil) authorNameColor = UIColor(hue: hue, saturation: saturation * 0.7, brightness: min(1.0, brightness * 1.2), alpha: 1.0) } } } if let authorNameColor { mainColor = authorNameColor } else { mainColor = presentationData.theme.theme.chat.message.incoming.accentTextColor } } var boundingSize = textFrame.size var lineHeight = textLayout.rawTextSize.height if titleBeforeMedia { lineHeight += topTitleLayout.size.height + 4.0 boundingSize.height += topTitleLayout.size.height + 4.0 boundingSize.width = max(boundingSize.width, topTitleLayout.size.width) } if let inlineImageSize = inlineImageSize { if boundingSize.height < inlineImageSize.height { boundingSize.height = inlineImageSize.height } if lineHeight < inlineImageSize.height { lineHeight = inlineImageSize.height } } if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { boundingSize.width = max(boundingSize.width, statusSuggestedWidthAndContinue.0) } var finalizeContentImageLayout: ((CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, 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, ListViewItemUpdateAnimation, ListViewItemApply?) -> 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 += lineInsets.top + lineInsets.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.message.incoming.mediaPlaceholderColor : presentationData.theme.theme.chat.message.outgoing.mediaPlaceholderColor) imageApply = imageLayout(arguments) } var continueActionButtonLayout: ((CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode))? if let actionTitle = actionTitle, !isPreview { let buttonImage: UIImage let buttonHighlightedImage: UIImage var buttonIconImage: UIImage? var buttonHighlightedIconImage: UIImage? var cornerIcon = false let titleColor: UIColor let titleHighlightedColor: UIColor if incoming { buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonIncoming(presentationData.theme.theme)! buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIncoming(presentationData.theme.theme)! if let actionIcon { switch actionIcon { case .instant: buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantIncoming(presentationData.theme.theme)! buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! case .link: buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkIncoming(presentationData.theme.theme)! buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkIncoming(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! cornerIcon = true } } titleColor = presentationData.theme.theme.chat.message.incoming.accentTextColor let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: true, wallpaper: !presentationData.theme.wallpaper.isEmpty) titleHighlightedColor = bubbleColor.fill[0] } else { buttonImage = PresentationResourcesChat.chatMessageAttachedContentButtonOutgoing(presentationData.theme.theme)! buttonHighlightedImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonOutgoing(presentationData.theme.theme)! if let actionIcon { switch actionIcon { case .instant: buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconInstantOutgoing(presentationData.theme.theme)! buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconInstantOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! case .link: buttonIconImage = PresentationResourcesChat.chatMessageAttachedContentButtonIconLinkOutgoing(presentationData.theme.theme)! buttonHighlightedIconImage = PresentationResourcesChat.chatMessageAttachedContentHighlightedButtonIconLinkOutgoing(presentationData.theme.theme, wallpaper: !presentationData.theme.wallpaper.isEmpty)! cornerIcon = true } } titleColor = presentationData.theme.theme.chat.message.outgoing.accentTextColor let bubbleColor = bubbleColorComponents(theme: presentationData.theme.theme, incoming: false, wallpaper: !presentationData.theme.wallpaper.isEmpty) titleHighlightedColor = bubbleColor.fill[0] } let (buttonWidth, continueLayout) = makeButtonLayout(constrainedSize.width, buttonImage, buttonHighlightedImage, buttonIconImage, buttonHighlightedIconImage, cornerIcon, actionTitle, titleColor, titleHighlightedColor, false) 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, (ListViewItemUpdateAnimation, 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 } var contentFileSizeAndApply: (CGSize, (Bool, ListViewItemUpdateAnimation, ListViewItemApply?) -> 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 } else { imageHeightAddition += 7.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 - 12.0 - insets.right) actionButtonSizeAndApply = (size, apply) adjustedBoundingSize.width = max(adjustedBoundingSize.width, insets.left + size.width + insets.right) adjustedBoundingSize.height += 7.0 + size.height } var statusSizeAndApply: ((CGSize), (ListViewItemUpdateAnimation) -> Void)? if let statusSuggestedWidthAndContinue = statusSuggestedWidthAndContinue { statusSizeAndApply = statusSuggestedWidthAndContinue.1(boundingWidth - insets.left - insets.right) } if let statusSizeAndApply = statusSizeAndApply { adjustedBoundingSize.height += statusSizeAndApply.0.height adjustedLineHeight += statusSizeAndApply.0.height if let imageFrame = imageFrame, statusSizeAndApply.0.height == 0.0 { if statusInText { adjustedBoundingSize.height = max(adjustedBoundingSize.height, imageFrame.maxY + 8.0 + 15.0) } } } adjustedBoundingSize.width = max(boundingWidth, adjustedBoundingSize.width) var contentMediaHeight: CGFloat? if let (contentImageSize, _) = contentImageSizeAndApply { contentMediaHeight = contentImageSize.height } if let (contentFileSize, _) = contentFileSizeAndApply { contentMediaHeight = contentFileSize.height } if let (videoLayout, _) = contentInstantVideoSizeAndApply { contentMediaHeight = videoLayout.contentSize.height } var textVerticalOffset: CGFloat = 0.0 if titleBeforeMedia { textVerticalOffset += topTitleLayout.size.height + 4.0 } if let contentMediaHeight = contentMediaHeight, let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { textVerticalOffset += contentMediaHeight + 7.0 } let adjustedTextFrame = textFrame.offsetBy(dx: 0.0, dy: textVerticalOffset) var statusFrame: CGRect? if let statusSizeAndApply = statusSizeAndApply { var finalStatusFrame = CGRect(origin: CGPoint(x: adjustedTextFrame.minX, y: adjustedTextFrame.maxY), size: statusSizeAndApply.0) if let imageFrame = imageFrame { if finalStatusFrame.maxY < imageFrame.maxY + 10.0 { finalStatusFrame.origin.y = max(finalStatusFrame.minY, imageFrame.maxY + 2.0) if finalStatusFrame.height == 0.0 { finalStatusFrame.origin.y += 14.0 adjustedBoundingSize.height += 14.0 adjustedLineHeight += 14.0 } } } statusFrame = finalStatusFrame } return (adjustedBoundingSize, { [weak self] animation, synchronousLoads, applyInfo in if let strongSelf = self { strongSelf.context = context strongSelf.message = message strongSelf.media = mediaAndFlags?.0 strongSelf.theme = presentationData.theme let backgroundView: UIImageView if let current = strongSelf.backgroundView { backgroundView = current } else { backgroundView = UIImageView() strongSelf.backgroundView = backgroundView strongSelf.view.insertSubview(backgroundView, at: 0) } if backgroundView.image == nil { backgroundView.image = PresentationResourcesChat.chatReplyBackgroundTemplateImage(presentationData.theme.theme) } backgroundView.tintColor = mainColor animation.animator.updateFrame(layer: backgroundView.layer, frame: CGRect(origin: CGPoint(x: 11.0, y: insets.top), size: CGSize(width: adjustedBoundingSize.width - 1.0 - insets.right, height: adjustedLineHeight - insets.top - insets.bottom - 2.0)), completion: nil) backgroundView.isHidden = !displayLine strongSelf.textNode.textNode.displaysAsynchronously = !isPreview let _ = topTitleApply() strongSelf.topTitleNode.frame = CGRect(origin: CGPoint(x: textFrame.minX, y: insets.top), size: topTitleLayout.size) let _ = textApply(TextNodeWithEntities.Arguments( context: context, cache: animationCache, renderer: animationRenderer, placeholderColor: messageTheme.mediaPlaceholderColor, attemptSynchronous: synchronousLoads )) switch strongSelf.visibility { case .none: strongSelf.textNode.visibilityRect = nil case let .visible(_, subRect): var subRect = subRect subRect.origin.x = 0.0 subRect.size.width = 10000.0 strongSelf.textNode.visibilityRect = subRect } if let imageFrame = imageFrame { if let updateImageSignal = updateInlineImageSignal { strongSelf.inlineImageNode.setSignal(updateImageSignal) } animation.animator.updateFrame(layer: strongSelf.inlineImageNode.layer, frame: imageFrame, completion: nil) if strongSelf.inlineImageNode.supernode == nil { strongSelf.addSubnode(strongSelf.inlineImageNode) } if let imageApply = imageApply { imageApply() } } else if strongSelf.inlineImageNode.supernode != nil { strongSelf.inlineImageNode.removeFromSupernode() } if let (contentImageSize, contentImageApply) = contentImageSizeAndApply { let contentImageNode = contentImageApply(animation, synchronousLoads) if strongSelf.contentImageNode !== contentImageNode { strongSelf.contentImageNode = contentImageNode contentImageNode.activatePinch = { sourceNode in controllerInteraction.activateMessagePinch(sourceNode) } strongSelf.addSubnode(contentImageNode) contentImageNode.activateLocalContent = { [weak strongSelf] mode in if let strongSelf = strongSelf { strongSelf.openMedia?(mode) } } contentImageNode.updateMessageReaction = { [weak controllerInteraction] message, value in guard let controllerInteraction = controllerInteraction else { return } controllerInteraction.updateMessageReaction(message, value) } contentImageNode.visibility = strongSelf.visibility != .none } let _ = contentImageApply(animation, synchronousLoads) var contentImageFrame: CGRect if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) { contentImageFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: contentImageSize) if titleBeforeMedia { contentImageFrame.origin.y += topTitleLayout.size.height + 4.0 } } 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 } else if let contentImageNode = strongSelf.contentImageNode { contentImageNode.visibility = false 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 { let contentFileNode = contentFileApply(synchronousLoads, animation, applyInfo) if strongSelf.contentFileNode !== contentFileNode { strongSelf.contentFileNode = contentFileNode strongSelf.addSubnode(contentFileNode) contentFileNode.activateLocalContent = { [weak strongSelf] in if let strongSelf = strongSelf { strongSelf.openMedia?(.default) } } contentFileNode.requestUpdateLayout = { [weak strongSelf] _ in if let strongSelf = strongSelf { strongSelf.requestUpdateLayout?() } } } 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 : 7.0)), size: contentFileSize) } } else if let contentFileNode = strongSelf.contentFileNode { contentFileNode.removeFromSupernode() strongSelf.contentFileNode = nil } if let (videoLayout, apply) = contentInstantVideoSizeAndApply { let contentInstantVideoNode = apply(.unconstrained(width: boundingWidth - insets.left - insets.right), animation) 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 } strongSelf.textNode.textNode.frame = adjustedTextFrame if let statusSizeAndApply = statusSizeAndApply, let statusFrame = statusFrame { if strongSelf.statusNode.supernode == nil { strongSelf.addSubnode(strongSelf.statusNode) strongSelf.statusNode.frame = statusFrame statusSizeAndApply.1(.None) } else { animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: statusFrame, completion: nil) statusSizeAndApply.1(animation) } } else if strongSelf.statusNode.supernode != nil { strongSelf.statusNode.removeFromSupernode() } 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: 12.0, y: adjustedLineHeight - insets.top - insets.bottom - 2.0 + 6.0), size: size) } else if let buttonNode = strongSelf.buttonNode { buttonNode.removeFromSupernode() strongSelf.buttonNode = nil } } }) }) }) } } public 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 } public func transitionNode(media: Media) -> (ASDisplayNode, CGRect, () -> (UIView?, UIView?))? { if let contentImageNode = self.contentImageNode, let image = self.media as? TelegramMediaImage, image.isEqual(to: media) { return (contentImageNode, contentImageNode.bounds, { [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, contentImageNode.bounds, { [weak contentImageNode] in return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) }) } else if let contentImageNode = self.contentImageNode, let story = self.media as? TelegramMediaStory, story.isEqual(to: media) { return (contentImageNode, contentImageNode.bounds, { [weak contentImageNode] in return (contentImageNode?.view.snapshotContentTree(unhide: true), nil) }) } return nil } public func hasActionAtPoint(_ point: CGPoint) -> Bool { if let buttonNode = self.buttonNode, buttonNode.frame.contains(point) { return true } return false } public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { let textNodeFrame = self.textNode.textNode.frame if let (index, attributes) = self.textNode.textNode.attributesAtPoint(CGPoint(x: point.x - textNodeFrame.minX, y: point.y - textNodeFrame.minY)) { if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { var concealed = true if let (attributeText, fullText) = self.textNode.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) } return .url(url: url, concealed: concealed) } else if let peerMention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { return .peerMention(peerId: peerMention.peerId, mention: peerMention.mention, openProfile: false) } else if let peerName = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { return .textMention(peerName) } else if let botCommand = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { return .botCommand(botCommand) } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { return .hashtag(hashtag.peerName, hashtag.hashtag) } else { return .none } } else { return .none } } public 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.textNode.frame if let (index, attributes) = self.textNode.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, TelegramTextAttributes.BankCard ] for name in possibleNames { if let _ = attributes[NSAttributedString.Key(rawValue: name)] { rects = self.textNode.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.message.incoming.linkHighlightColor : theme.theme.chat.message.outgoing.linkHighlightColor) self.linkHighlightingNode = linkHighlightingNode self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) } linkHighlightingNode.frame = self.textNode.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() }) } } } public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { if !self.statusNode.isHidden { if let result = self.statusNode.reactionView(value: value) { return result } } if let result = self.contentFileNode?.dateAndStatusNode.reactionView(value: value) { return result } if let result = self.contentImageNode?.dateAndStatusNode.reactionView(value: value) { return result } if let result = self.contentInstantVideoNode?.dateAndStatusNode.reactionView(value: value) { return result } return nil } public func playMediaWithSound() -> ((Double?) -> Void, Bool, Bool, Bool, ASDisplayNode?)? { return self.contentImageNode?.playMediaWithSound() } }