2023-10-17 01:41:53 +04:00

1223 lines
77 KiB
Swift

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: HighlightTrackingButtonNode {
private var backgroundView: UIImageView?
private var lineDashView: 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?
private var buttonSeparatorLayer: SimpleLayer?
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
}
}
}
}
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 = false
self.inlineImageNode.displaysAsynchronously = false
self.statusNode = ChatMessageDateAndStatusNode()
super.init(pointerStyle: .default)
self.addSubnode(self.topTitleNode)
self.addSubnode(self.textNode.textNode)
self.addSubnode(self.statusNode)
self.highligthedChanged = { [weak self] highlighted in
guard let self else {
return
}
if self.bounds.width < 1.0 {
return
}
let transition: ContainedViewLayoutTransition = .animated(duration: highlighted ? 0.1 : 0.2, curve: .easeInOut)
let scale: CGFloat = highlighted ? ((self.bounds.width - 10.0) / self.bounds.width) : 1.0
transition.updateSublayerTransformScale(node: self, scale: scale)
}
self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside)
}
@objc private func pressed() {
self.activateAction?()
}
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 * 14.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 += 10.0
horizontalInsets.right += 9.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 mainColor: UIColor
var secondaryColor: 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?.nameColor?.color
secondaryColor = author?.nameColor?.dashColors.1
// 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
}
}
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: incoming ? mainColor : messageTheme.accentTextColor))
} else {
string.append(NSAttributedString(string: title, font: titleFont, textColor: incoming ? mainColor : 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: 53.0, height: 53.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: 0.0, right: horizontalInsets.right)
switch position {
case let .linear(topNeighbor, bottomNeighbor):
switch topNeighbor {
case .None:
insets.top += 10.0
default:
break
}
switch bottomNeighbor {
case .None:
insets.bottom += 12.0
default:
insets.bottom += 0.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)
var boundingSize = textFrame.size
if titleBeforeMedia {
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 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)
}
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, CGFloat) -> (CGSize, () -> ChatMessageAttachedContentButtonNode))?
if let actionTitle = actionTitle, !isPreview {
var buttonIconImage: UIImage?
var buttonHighlightedIconImage: UIImage?
var cornerIcon = false
let titleColor: UIColor
let titleHighlightedColor: UIColor
if incoming {
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 {
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, nil, nil, 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 imageFrame: CGRect?
if let inlineImageSize = inlineImageSize {
imageFrame = CGRect(origin: CGPoint(x: boundingWidth - inlineImageSize.width - insets.right + 4.0, 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 + 7.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
}
if let (videoLayout, _) = contentInstantVideoSizeAndApply {
let imageHeightAddition = videoLayout.contentSize.height + 6.0
adjustedBoundingSize.height += imageHeightAddition// + 5.0
}
var actionButtonSizeAndApply: ((CGSize, () -> ChatMessageAttachedContentButtonNode))?
if let continueActionButtonLayout = continueActionButtonLayout {
let (size, apply) = continueActionButtonLayout(boundingWidth - 5.0 - insets.right, 38.0)
actionButtonSizeAndApply = (size, apply)
adjustedBoundingSize.height += 4.0 + size.height
if let text, !text.isEmpty {
if contentImageSizeAndApply == nil {
adjustedBoundingSize.height += 5.0
} else if let (_, flags) = mediaAndFlags, flags.contains(.preferMediaBeforeText) {
adjustedBoundingSize.height += 5.0
}
}
}
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
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
}
}
}
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
let backgroundFrame = CGRect(origin: CGPoint(x: 11.0, y: insets.top - 3.0), size: CGSize(width: adjustedBoundingSize.width - 4.0 - insets.right, height: adjustedBoundingSize.height - insets.top - insets.bottom + 4.0))
animation.animator.updateFrame(layer: backgroundView.layer, frame: backgroundFrame, completion: nil)
backgroundView.isHidden = !displayLine
if let secondaryColor {
let lineDashView: UIImageView
if let current = strongSelf.lineDashView {
lineDashView = current
} else {
lineDashView = UIImageView(image: PresentationResourcesChat.chatReplyLineDashTemplateImage(presentationData.theme.theme))
strongSelf.lineDashView = lineDashView
strongSelf.view.insertSubview(lineDashView, aboveSubview: backgroundView)
}
lineDashView.tintColor = secondaryColor
lineDashView.frame = CGRect(origin: backgroundFrame.origin, size: CGSize(width: 3.0, height: backgroundFrame.height))
} else {
if let lineDashView = strongSelf.lineDashView {
strongSelf.lineDashView = nil
lineDashView.removeFromSuperview()
}
}
//strongSelf.borderColor = UIColor.red.cgColor
//strongSelf.borderWidth = 2.0
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()
let buttonFrame = CGRect(origin: CGPoint(x: 12.0, y: adjustedBoundingSize.height - insets.bottom - size.height), size: size)
if buttonNode !== strongSelf.buttonNode {
strongSelf.buttonNode?.removeFromSupernode()
strongSelf.buttonNode = buttonNode
buttonNode.isUserInteractionEnabled = false
strongSelf.addSubnode(buttonNode)
buttonNode.pressed = {
if let strongSelf = self {
strongSelf.activateAction?()
}
}
buttonNode.frame = buttonFrame
} else {
animation.animator.updateFrame(layer: buttonNode.layer, frame: buttonFrame, completion: nil)
}
let buttonSeparatorFrame = CGRect(origin: CGPoint(x: buttonFrame.minX + 8.0, y: buttonFrame.minY - 2.0), size: CGSize(width: buttonFrame.width - 8.0 - 8.0, height: UIScreenPixel))
let buttonSeparatorLayer: SimpleLayer
if let current = strongSelf.buttonSeparatorLayer {
buttonSeparatorLayer = current
animation.animator.updateFrame(layer: buttonSeparatorLayer, frame: buttonSeparatorFrame, completion: nil)
} else {
buttonSeparatorLayer = SimpleLayer()
strongSelf.buttonSeparatorLayer = buttonSeparatorLayer
strongSelf.layer.addSublayer(buttonSeparatorLayer)
buttonSeparatorLayer.frame = buttonSeparatorFrame
}
buttonSeparatorLayer.backgroundColor = mainColor.withMultipliedAlpha(0.5).cgColor
} else {
if let buttonNode = strongSelf.buttonNode {
strongSelf.buttonNode = nil
buttonNode.removeFromSupernode()
}
if let buttonSeparatorLayer = strongSelf.buttonSeparatorLayer {
strongSelf.buttonSeparatorLayer = nil
buttonSeparatorLayer.removeFromSuperlayer()
}
}
}
})
})
})
}
}
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 .ignore
}
} else {
return .ignore
}
}
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()
}
}