diff --git a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift index bff3646adc..8d5bda8478 100644 --- a/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift +++ b/submodules/ChatSendMessageActionUI/Sources/ChatSendMessageActionSheetControllerNode.swift @@ -177,7 +177,9 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, private let messageClipNode: ASDisplayNode private let messageBackgroundNode: ASImageNode + private let fromMessageTextScrollView: UIScrollView private let fromMessageTextNode: ChatInputTextNode + private let toMessageTextScrollView: UIScrollView private let toMessageTextNode: ChatInputTextNode private let scrollNode: ASScrollNode @@ -225,10 +227,16 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.messageBackgroundNode = ASImageNode() self.messageBackgroundNode.isUserInteractionEnabled = true self.fromMessageTextNode = ChatInputTextNode() + self.fromMessageTextNode.textView.isScrollEnabled = false self.fromMessageTextNode.isUserInteractionEnabled = false + self.fromMessageTextScrollView = UIScrollView() + self.fromMessageTextScrollView.isUserInteractionEnabled = false self.toMessageTextNode = ChatInputTextNode() - self.toMessageTextNode.alpha = 0.0 + self.toMessageTextNode.textView.isScrollEnabled = false self.toMessageTextNode.isUserInteractionEnabled = false + self.toMessageTextScrollView = UIScrollView() + self.toMessageTextScrollView.alpha = 0.0 + self.toMessageTextScrollView.isUserInteractionEnabled = false self.scrollNode = ASScrollNode() self.scrollNode.transform = CATransform3DMakeScale(1.0, -1.0, 1.0) @@ -304,8 +312,10 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.addSubnode(self.sendButtonNode) self.scrollNode.addSubnode(self.messageClipNode) self.messageClipNode.addSubnode(self.messageBackgroundNode) - self.messageClipNode.addSubnode(self.fromMessageTextNode) - self.messageClipNode.addSubnode(self.toMessageTextNode) + self.messageClipNode.view.addSubview(self.fromMessageTextScrollView) + self.fromMessageTextScrollView.addSubview(self.fromMessageTextNode.view) + self.messageClipNode.view.addSubview(self.toMessageTextScrollView) + self.toMessageTextScrollView.addSubview(self.toMessageTextNode.view) self.contentNodes.forEach(self.contentContainerNode.addSubnode) @@ -469,12 +479,12 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, self.sourceSendButton.isHidden = true if self.animateInputField { - self.fromMessageTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) - self.toMessageTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false) + self.fromMessageTextScrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false) + self.toMessageTextScrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, removeOnCompletion: false) } else { self.messageBackgroundNode.isHidden = true - self.fromMessageTextNode.isHidden = true - self.toMessageTextNode.isHidden = true + self.fromMessageTextScrollView.isHidden = true + self.toMessageTextScrollView.isHidden = true } let duration = 0.4 @@ -514,8 +524,8 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, if self.textInputView.numberOfLines == 1 && self.textInputView.isRTL { textXOffset = initialWidth - self.messageClipNode.bounds.width } - self.fromMessageTextNode.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) - self.toMessageTextNode.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.fromMessageTextScrollView.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) + self.toMessageTextScrollView.layer.animatePosition(from: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), to: CGPoint(), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, additive: true) let contentOffset = CGPoint(x: self.sendButtonFrame.midX - self.contentContainerNode.frame.midX, y: self.sendButtonFrame.midY - self.contentContainerNode.frame.midY) @@ -574,8 +584,8 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, if self.animateInputField { if cancel { - self.fromMessageTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: 0.15, removeOnCompletion: false) - self.toMessageTextNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false) + self.fromMessageTextScrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3, delay: 0.15, removeOnCompletion: false) + self.toMessageTextScrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false) self.messageBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, delay: 0.15, removeOnCompletion: false, completion: { _ in completedAlpha = true intermediateCompletion() @@ -636,8 +646,8 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, if self.textInputView.numberOfLines == 1 && self.textInputView.isRTL { textXOffset = initialWidth - self.messageClipNode.bounds.width } - self.fromMessageTextNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) - self.toMessageTextNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + self.fromMessageTextScrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + self.toMessageTextScrollView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: textXOffset, y: delta * 2.0 + textYOffset), duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) } else { completedBubble = true } @@ -772,10 +782,12 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode, textFrame.origin.x -= messageOriginDelta } - self.fromMessageTextNode.frame = textFrame + self.fromMessageTextScrollView.frame = textFrame + self.fromMessageTextNode.frame = CGRect(origin: CGPoint(), size: textFrame.size) self.fromMessageTextNode.updateLayout(size: textFrame.size) - self.toMessageTextNode.frame = textFrame + self.toMessageTextScrollView.frame = textFrame + self.toMessageTextNode.frame = CGRect(origin: CGPoint(), size: textFrame.size) self.toMessageTextNode.updateLayout(size: textFrame.size) } diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 8922a6dd6e..6dedb8bc91 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -2070,7 +2070,21 @@ public final class ControlledTransition { if layer.bounds == bounds { return } - let fromValue = layer.presentation()?.bounds ?? layer.bounds + let fromValue: CGRect + if let animationKeys = layer.animationKeys(), animationKeys.contains(where: { key in + guard let animation = layer.animation(forKey: key) as? CAPropertyAnimation else { + return false + } + if animation.keyPath == "bounds" { + return true + } else { + return false + } + }) { + fromValue = layer.presentation()?.bounds ?? layer.bounds + } else { + fromValue = layer.bounds + } layer.bounds = bounds self.add(animation: ControlledTransitionProperty( layer: layer, diff --git a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift index 706d9e1fa9..048938b924 100644 --- a/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatInputTextNode/Sources/ChatInputTextNode.swift @@ -558,6 +558,10 @@ public final class ChatInputTextView: ChatInputTextViewImpl, NSLayoutManagerDele } } + override public func setNeedsLayout() { + super.setNeedsLayout() + } + override public func layoutSubviews() { let isLayoutUpdated = self.validLayoutSize != self.bounds.size self.validLayoutSize = self.bounds.size diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift index 8e8db827e7..20600c37c4 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageAttachedContentNode/Sources/ChatMessageAttachedContentNode.swift @@ -65,7 +65,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { private var contentFile: ChatMessageInteractiveFileNode? private var actionButton: ChatMessageAttachedContentButtonNode? private var actionButtonSeparator: SimpleLayer? - public let statusNode: ChatMessageDateAndStatusNode + public var statusNode: ChatMessageDateAndStatusNode? private var inlineMediaValue: Media? @@ -109,12 +109,10 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { override public init() { self.transformContainer = ASDisplayNode() - self.statusNode = ChatMessageDateAndStatusNode() super.init() self.addSubnode(self.transformContainer) - self.addSubnode(self.statusNode) } deinit { @@ -148,7 +146,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { let makeContentMedia = ChatMessageInteractiveMediaNode.asyncLayout(self.contentMedia) let makeContentFile = ChatMessageInteractiveFileNode.asyncLayout(self.contentFile) let makeActionButtonLayout = ChatMessageAttachedContentButtonNode.asyncLayout(self.actionButton) - let makeStatusLayout = self.statusNode.asyncLayout() + let makeStatusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode) return { [weak self] 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 @@ -604,35 +602,44 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } } - let statusLayoutAndContinue = makeStatusLayout(ChatMessageDateAndStatusNode.Arguments( - context: context, - presentationData: presentationData, - edited: edited, - impressionCount: viewCount, - dateText: dateText, - type: statusType, - layoutInput: .trailingContent( - contentWidth: trailingContentWidth, - reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions), preferAdditionalInset: false) - ), - constrainedSize: CGSize(width: maxStatusContentWidth, height: CGFloat.greatestFiniteMagnitude), - 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 - )) - actualWidth = max(actualWidth, statusLayoutAndContinue.0) + var statusLayoutAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? + if case let .linear(_, bottom) = position { + switch bottom { + case .None, .Neighbour(_, .footer, _): + let statusLayoutAndContinueValue = makeStatusLayout(ChatMessageDateAndStatusNode.Arguments( + context: context, + presentationData: presentationData, + edited: edited, + impressionCount: viewCount, + dateText: dateText, + type: statusType, + layoutInput: .trailingContent( + contentWidth: trailingContentWidth, + reactionSettings: ChatMessageDateAndStatusNode.TrailingReactionSettings(displayInline: shouldDisplayInlineDateReactions(message: message, isPremium: associatedData.isPremium, forceInline: associatedData.forceInlineReactions), preferAdditionalInset: false) + ), + constrainedSize: CGSize(width: maxStatusContentWidth, height: CGFloat.greatestFiniteMagnitude), + 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 + )) + statusLayoutAndContinue = statusLayoutAndContinueValue + actualWidth = max(actualWidth, statusLayoutAndContinueValue.0) + default: + break + } + } actualWidth += insets.left + insets.right return (actualWidth, { resultingWidth in - let statusSizeAndApply = statusLayoutAndContinue.1(resultingWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0) + let statusSizeAndApply = statusLayoutAndContinue?.1(resultingWidth - layoutConstants.text.bubbleInsets.left - layoutConstants.text.bubbleInsets.right - 6.0) let contentMediaSizeAndApply: (CGSize, (ListViewItemUpdateAnimation, Bool) -> ChatMessageInteractiveMediaNode)? if let contentMediaFinalizeLayout { @@ -816,7 +823,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } } - if case let .linear(_, bottom) = position { + if case let .linear(_, bottom) = position, let statusSizeAndApply { switch bottom { case .None, .Neighbour(_, .footer, _): let bottomStatusContentHeight = statusBackgroundSpacing + statusSizeAndApply.0.height @@ -1099,7 +1106,7 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { if let item = contentDisplayOrder.first(where: { $0.item == .actionButton }), let (actionButtonSize, actionButtonApply) = actionButtonSizeAndApply { var actionButtonFrame = CGRect(origin: CGPoint(x: insets.left, y: item.offsetY), size: actionButtonSize) - if let _ = message.adAttribute { + if let _ = message.adAttribute, let statusSizeAndApply { actionButtonFrame.origin.y += statusSizeAndApply.0.height } @@ -1151,38 +1158,40 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { actionButtonSeparator.removeFromSuperlayer() } - do { - statusSizeAndApply.1(animation) - - var statusFrame = CGRect(origin: CGPoint(x: actualSize.width - insets.right - statusSizeAndApply.0.width, y: actualSize.height - layoutConstants.text.bubbleInsets.bottom - statusSizeAndApply.0.height), size: statusSizeAndApply.0) + if let statusSizeAndApply { + var statusFrame = CGRect(origin: CGPoint(x: actualSize.width - backgroundInsets.right - statusSizeAndApply.0.width, y: actualSize.height - layoutConstants.text.bubbleInsets.bottom - statusSizeAndApply.0.height), size: statusSizeAndApply.0) if let _ = message.adAttribute, let (actionButtonSize, _) = actionButtonSizeAndApply { statusFrame.origin.y -= actionButtonSize.height + statusBackgroundSpacing } - animation.animator.updateFrame(layer: self.statusNode.layer, frame: statusFrame, completion: nil) - self.statusNode.reactionSelected = { [weak self] value in - guard let self, let message = self.message else { - return + let statusNode = statusSizeAndApply.1(self.statusNode == nil ? .None : animation) + if self.statusNode !== statusNode { + self.statusNode?.removeFromSupernode() + self.statusNode = statusNode + self.addSubnode(statusNode) + + statusNode.reactionSelected = { [weak self] value in + guard let self, let message = self.message else { + return + } + controllerInteraction.updateMessageReaction(message, .reaction(value)) } - controllerInteraction.updateMessageReaction(message, .reaction(value)) - } - - self.statusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in - guard let self, let message = self.message else { - gesture?.cancel() - return - } - controllerInteraction.openMessageReactionContextMenu(message, sourceNode, gesture, value) - } - - if case let .linear(_, bottom) = position { - switch bottom { - case .None, .Neighbour(_, .footer, _): - animation.animator.updateAlpha(layer: self.statusNode.layer, alpha: 1.0, completion: nil) - default: - animation.animator.updateAlpha(layer: self.statusNode.layer, alpha: 0.0, completion: nil) + + statusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in + guard let self, let message = self.message else { + gesture?.cancel() + return + } + controllerInteraction.openMessageReactionContextMenu(message, sourceNode, gesture, value) } + + statusNode.frame = statusFrame + } else { + animation.animator.updateFrame(layer: statusNode.layer, frame: statusFrame, completion: nil) } + } else if let statusNode = self.statusNode { + self.statusNode = nil + statusNode.removeFromSupernode() } }) }) @@ -2239,8 +2248,8 @@ public final class ChatMessageAttachedContentNode: ASDisplayNode { } public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { - if !self.statusNode.isHidden { - if let result = self.statusNode.reactionView(value: value) { + if let statusNode = self.statusNode, !statusNode.isHidden { + if let result = statusNode.reactionView(value: value) { return result } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index 2e077398bf..94d38ec9c8 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -2832,7 +2832,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI if case let .System(duration, _) = animation { legacyTransition = .animated(duration: duration, curve: .spring) - if let subject = item.associatedData.subject, case .messageOptions = subject { + if let subject = item.associatedData.subject, case .messageOptions = subject, !"".isEmpty { useDisplayLinkAnimations = true } } @@ -3343,6 +3343,9 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI strongSelf.contentContainersWrapperNode.view.mask = nil } + var animateTextAndWebpagePositionSwap: Bool? + var bottomStatusNodeAnimationSourcePosition: CGPoint? + if removedContentNodeIndices?.count ?? 0 != 0 || addedContentNodes?.count ?? 0 != 0 || updatedContentNodeOrder { var updatedContentNodes = strongSelf.contentNodes @@ -3415,6 +3418,26 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI assert(sortedContentNodes.count == updatedContentNodes.count) + if animation.isAnimated, let fromTextIndex = strongSelf.contentNodes.firstIndex(where: { $0 is ChatMessageTextBubbleContentNode }), let fromWebpageIndex = strongSelf.contentNodes.firstIndex(where: { $0 is ChatMessageWebpageBubbleContentNode }) { + if let toTextIndex = sortedContentNodes.firstIndex(where: { $0 is ChatMessageTextBubbleContentNode }), let toWebpageIndex = sortedContentNodes.firstIndex(where: { $0 is ChatMessageWebpageBubbleContentNode }) { + if fromTextIndex == toWebpageIndex && fromWebpageIndex == toTextIndex { + animateTextAndWebpagePositionSwap = fromTextIndex < toTextIndex + + if let textNode = strongSelf.contentNodes[fromTextIndex] as? ChatMessageTextBubbleContentNode, let webpageNode = strongSelf.contentNodes[fromWebpageIndex] as? ChatMessageWebpageBubbleContentNode { + if fromTextIndex > toTextIndex { + if let statusNode = textNode.statusNode, let contentSuperview = textNode.view.superview, statusNode.view.isDescendant(of: contentSuperview) { + bottomStatusNodeAnimationSourcePosition = statusNode.view.convert(CGPoint(x: statusNode.bounds.width, y: statusNode.bounds.height), to: contentSuperview) + } + } else { + if let statusNode = webpageNode.contentNode.statusNode, let contentSuperview = webpageNode.view.superview, statusNode.view.isDescendant(of: contentSuperview) { + bottomStatusNodeAnimationSourcePosition = statusNode.view.convert(CGPoint(x: statusNode.bounds.width, y: statusNode.bounds.height), to: contentSuperview) + } + } + } + } + } + } + strongSelf.contentNodes = sortedContentNodes } @@ -3463,7 +3486,47 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI }) strongSelf.setAnimationForKey("contentNode\(contentNodeIndex)Frame", animation: animation) } else { - animation.animator.updateFrame(layer: contentNode.layer, frame: contentNodeFrame, completion: nil) + if let animateTextAndWebpagePositionSwap, let contentNode = contentNode as? ChatMessageTextBubbleContentNode, let snapshotLayer = contentNode.layer.snapshotContentTree() { + let clippingLayer = SimpleLayer() + clippingLayer.masksToBounds = true + clippingLayer.frame = contentNode.frame + + clippingLayer.addSublayer(snapshotLayer) + snapshotLayer.frame = CGRect(origin: CGPoint(), size: contentNode.bounds.size) + + contentNode.layer.superlayer?.insertSublayer(clippingLayer, below: contentNode.layer) + + animation.animator.updateAlpha(layer: clippingLayer, alpha: 0.0, completion: { [weak clippingLayer] _ in + clippingLayer?.removeFromSuperlayer() + }) + + let positionOffset: CGFloat = animateTextAndWebpagePositionSwap ? -1.0 : 1.0 + + animation.animator.updatePosition(layer: snapshotLayer, position: CGPoint(x: snapshotLayer.position.x, y: snapshotLayer.position.y + positionOffset * contentNode.frame.height), completion: nil) + + contentNode.frame = contentNodeFrame + + if let statusNode = contentNode.statusNode, let contentSuperview = contentNode.view.superview, statusNode.view.isDescendant(of: contentSuperview), let bottomStatusNodeAnimationSourcePosition { + let localSourcePosition = statusNode.view.convert(bottomStatusNodeAnimationSourcePosition, from: contentSuperview) + let offset = CGPoint(x: statusNode.bounds.width - localSourcePosition.x, y: statusNode.bounds.height - localSourcePosition.y) + animation.animator.animatePosition(layer: statusNode.layer, from: statusNode.layer.position.offsetBy(dx: -offset.x, dy: -offset.y), to: statusNode.layer.position, completion: nil) + } + + contentNode.animateClippingTransition(offset: positionOffset * contentNodeFrame.height, animation: animation) + + contentNode.alpha = 0.0 + animation.animator.updateAlpha(layer: contentNode.layer, alpha: 1.0, completion: nil) + } else if animateTextAndWebpagePositionSwap != nil, let contentNode = contentNode as? ChatMessageWebpageBubbleContentNode { + if let statusNode = contentNode.contentNode.statusNode, let contentSuperview = contentNode.view.superview, statusNode.view.isDescendant(of: contentSuperview), let bottomStatusNodeAnimationSourcePosition { + let localSourcePosition = statusNode.view.convert(bottomStatusNodeAnimationSourcePosition, from: contentSuperview) + let offset = CGPoint(x: statusNode.bounds.width - localSourcePosition.x, y: statusNode.bounds.height - localSourcePosition.y) + animation.animator.animatePosition(layer: statusNode.layer, from: statusNode.layer.position.offsetBy(dx: -offset.x, dy: -offset.y), to: statusNode.layer.position, completion: nil) + } + + animation.animator.updateFrame(layer: contentNode.layer, frame: contentNodeFrame, completion: nil) + } else { + animation.animator.updateFrame(layer: contentNode.layer, frame: contentNodeFrame, completion: nil) + } } } else if animateAlpha { contentNode.frame = contentNodeFrame diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode/Sources/ChatMessageGameBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode/Sources/ChatMessageGameBubbleContentNode.swift index b763bd5e92..1d9a30a9f1 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode/Sources/ChatMessageGameBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageGameBubbleContentNode/Sources/ChatMessageGameBubbleContentNode.swift @@ -144,8 +144,8 @@ public final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNod } override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { - if !self.contentNode.statusNode.isHidden { - return self.contentNode.statusNode.reactionView(value: value) + if let statusNode = self.contentNode.statusNode, !statusNode.isHidden { + return statusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift index d2d3f5bb71..59a16518f5 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageInvoiceBubbleContentNode/Sources/ChatMessageInvoiceBubbleContentNode.swift @@ -140,8 +140,8 @@ public final class ChatMessageInvoiceBubbleContentNode: ChatMessageBubbleContent } override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { - if !self.contentNode.statusNode.isHidden { - return self.contentNode.statusNode.reactionView(value: value) + if let statusNode = self.contentNode.statusNode, !statusNode.isHidden { + return statusNode.reactionView(value: value) } return nil } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift index 57e7536ef2..4635ac4ec9 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageTextBubbleContentNode/Sources/ChatMessageTextBubbleContentNode.swift @@ -54,12 +54,13 @@ private final class CachedChatMessageText { } public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { + private let containerNode: ASDisplayNode private let textNode: TextNodeWithEntities private var spoilerTextNode: TextNodeWithEntities? private var dustNode: InvisibleInkDustNode? private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode - private let statusNode: ChatMessageDateAndStatusNode + public var statusNode: ChatMessageDateAndStatusNode? private var linkHighlightingNode: LinkHighlightingNode? private var shimmeringNode: ShimmeringLinkNode? private var textSelectionNode: TextSelectionNode? @@ -99,40 +100,26 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } required public init() { - self.textNode = TextNodeWithEntities() + self.containerNode = ASDisplayNode() - self.statusNode = ChatMessageDateAndStatusNode() + self.textNode = TextNodeWithEntities() self.textAccessibilityOverlayNode = TextAccessibilityOverlayNode() super.init() + self.addSubnode(self.containerNode) + self.textNode.textNode.isUserInteractionEnabled = false self.textNode.textNode.contentMode = .topLeft self.textNode.textNode.contentsScale = UIScreenScale self.textNode.textNode.displaysAsynchronously = true - self.addSubnode(self.textNode.textNode) - self.addSubnode(self.textAccessibilityOverlayNode) + self.containerNode.addSubnode(self.textNode.textNode) + self.containerNode.addSubnode(self.textAccessibilityOverlayNode) self.textAccessibilityOverlayNode.openUrl = { [weak self] url in self?.item?.controllerInteraction.openUrl(ChatControllerInteraction.OpenUrl(url: url, concealed: false, external: false)) } - - self.statusNode.reactionSelected = { [weak self] value in - guard let strongSelf = self, let item = strongSelf.item else { - return - } - item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) - } - - self.statusNode.openReactionPreview = { [weak self] gesture, sourceNode, value in - guard let strongSelf = self, let item = strongSelf.item else { - gesture?.cancel() - return - } - - item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) - } } required public init?(coder aDecoder: NSCoder) { @@ -147,7 +134,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override public func asyncLayoutContent() -> (_ item: ChatMessageBubbleContentItem, _ layoutConstants: ChatMessageItemLayoutConstants, _ preparePosition: ChatMessageBubblePreparePosition, _ messageSelection: Bool?, _ constrainedSize: CGSize, _ avatarInset: CGFloat) -> (ChatMessageBubbleContentProperties, CGSize?, CGFloat, (CGSize, ChatMessageBubbleContentPosition) -> (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation, Bool, ListViewItemApply?) -> Void))) { let textLayout = TextNodeWithEntities.asyncLayout(self.textNode) let spoilerTextLayout = TextNodeWithEntities.asyncLayout(self.spoilerTextNode) - let statusLayout = self.statusNode.asyncLayout() + let statusLayout = ChatMessageDateAndStatusNode.asyncLayout(self.statusNode) let currentCachedChatMessageText = self.cachedChatMessageText @@ -454,7 +441,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { spoilerTextLayoutAndApply = nil } - var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> Void))? + var statusSuggestedWidthAndContinue: (CGFloat, (CGFloat) -> (CGSize, (ListViewItemUpdateAnimation) -> ChatMessageDateAndStatusNode))? if let statusType = statusType { var isReplyThread = false if case .replyThread = item.chatLocation { @@ -527,6 +514,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.cachedChatMessageText = updatedCachedChatMessageText } + strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: boundingSize) + let cachedLayout = strongSelf.textNode.textNode.cachedLayout if case .System = animation { @@ -538,7 +527,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { fadeNode.contents = textContents fadeNode.frame = strongSelf.textNode.textNode.frame fadeNode.isLayerBacked = true - strongSelf.addSubnode(fadeNode) + strongSelf.containerNode.addSubnode(fadeNode) fadeNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak fadeNode] _ in fadeNode?.removeFromSupernode() }) @@ -559,7 +548,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { spoilerTextNode.textNode.contentMode = .topLeft spoilerTextNode.textNode.contentsScale = UIScreenScale spoilerTextNode.textNode.displaysAsynchronously = false - strongSelf.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode) + strongSelf.containerNode.insertSubnode(spoilerTextNode.textNode, aboveSubnode: strongSelf.textAccessibilityOverlayNode) strongSelf.spoilerTextNode = spoilerTextNode } @@ -572,7 +561,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { dustNode = InvisibleInkDustNode(textNode: spoilerTextNode.textNode, enableAnimations: item.context.sharedContext.energyUsageSettings.fullTranslucency) strongSelf.dustNode = dustNode - strongSelf.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode) + strongSelf.containerNode.insertSubnode(dustNode, aboveSubnode: spoilerTextNode.textNode) } dustNode.frame = textFrame.insetBy(dx: -3.0, dy: -3.0).offsetBy(dx: 0.0, dy: 3.0) dustNode.update(size: dustNode.frame.size, color: messageTheme.secondaryTextColor, textColor: messageTheme.primaryTextColor, rects: textLayout.spoilers.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }, wordRects: textLayout.spoilerWords.map { $0.1.offsetBy(dx: 3.0, dy: 3.0).insetBy(dx: 1.0, dy: 1.0) }) @@ -611,27 +600,48 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { strongSelf.updateIsTranslating(isTranslating) - if let statusSizeAndApply = statusSizeAndApply { - animation.animator.updateFrame(layer: strongSelf.statusNode.layer, frame: CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0), completion: nil) - if strongSelf.statusNode.supernode == nil { - strongSelf.addSubnode(strongSelf.statusNode) - statusSizeAndApply.1(.None) + if let statusSizeAndApply { + let statusNode = statusSizeAndApply.1(strongSelf.statusNode == nil ? .None : animation) + let statusFrame = CGRect(origin: CGPoint(x: textFrameWithoutInsets.minX, y: textFrameWithoutInsets.maxY), size: statusSizeAndApply.0) + + if strongSelf.statusNode !== statusNode { + strongSelf.statusNode?.removeFromSupernode() + strongSelf.statusNode = statusNode + + strongSelf.addSubnode(statusNode) + + statusNode.reactionSelected = { [weak strongSelf] value in + guard let strongSelf, let item = strongSelf.item else { + return + } + item.controllerInteraction.updateMessageReaction(item.message, .reaction(value)) + } + statusNode.openReactionPreview = { [weak strongSelf] gesture, sourceNode, value in + guard let strongSelf, let item = strongSelf.item else { + gesture?.cancel() + return + } + + item.controllerInteraction.openMessageReactionContextMenu(item.topMessage, sourceNode, gesture, value) + } + statusNode.frame = statusFrame } else { - statusSizeAndApply.1(animation) + animation.animator.updateFrame(layer: statusNode.layer, frame: statusFrame, completion: nil) } - } else if strongSelf.statusNode.supernode != nil { - strongSelf.statusNode.removeFromSupernode() + } else if let statusNode = strongSelf.statusNode { + strongSelf.statusNode = nil + statusNode.removeFromSupernode() } - if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported) { - strongSelf.statusNode.pressed = { - guard let strongSelf = self else { + if let forwardInfo = item.message.forwardInfo, forwardInfo.flags.contains(.isImported), let statusNode = strongSelf.statusNode { + statusNode.pressed = { + guard let strongSelf = self, let statusNode = strongSelf.statusNode else { return } - item.controllerInteraction.displayImportedMessageTooltip(strongSelf.statusNode) + item.controllerInteraction.displayImportedMessageTooltip(statusNode) } } else { - strongSelf.statusNode.pressed = nil + strongSelf.statusNode?.pressed = nil } if let subject = item.associatedData.subject, case let .messageOptions(_, _, info) = subject { @@ -688,17 +698,17 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { override public func animateInsertion(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.statusNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } override public func animateAdded(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + self.statusNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } override public func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.textNode.textNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) - self.statusNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) + self.statusNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false) } override public func tapActionAtPoint(_ point: CGPoint, gesture: TapLongTapOrDoubleTapGesture, isEstimating: Bool) -> ChatMessageBubbleContentTapAction { @@ -785,7 +795,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } } } else { - if let _ = self.statusNode.hitTest(self.view.convert(point, to: self.statusNode.view), with: nil) { + if let statusNode = self.statusNode, let _ = statusNode.hitTest(self.view.convert(point, to: statusNode.view), with: nil) { return ChatMessageBubbleContentTapAction(content: .ignore) } return ChatMessageBubbleContentTapAction(content: .none) @@ -793,7 +803,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { - if self.statusNode.supernode != nil, let result = self.statusNode.hitTest(self.view.convert(point, to: self.statusNode.view), with: event) { + if let statusNode = self.statusNode, statusNode.supernode != nil, let result = statusNode.hitTest(self.view.convert(point, to: statusNode.view), with: event) { return result } return super.hitTest(point, with: event) @@ -815,7 +825,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { shimmeringNode.updateLayout(self.textNode.textNode.frame.size) shimmeringNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) self.shimmeringNode = shimmeringNode - self.insertSubnode(shimmeringNode, belowSubnode: self.textNode.textNode) + self.containerNode.insertSubnode(shimmeringNode, belowSubnode: self.textNode.textNode) } } else if let shimmeringNode = self.shimmeringNode { self.shimmeringNode = nil @@ -862,7 +872,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { linkHighlightingNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor) self.linkHighlightingNode = linkHighlightingNode - self.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) + self.containerNode.insertSubnode(linkHighlightingNode, belowSubnode: self.textNode.textNode) } linkHighlightingNode.frame = self.textNode.textNode.frame linkHighlightingNode.updateRects(rects) @@ -893,7 +903,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.textHighlightColor : item.presentationData.theme.theme.chat.message.outgoing.textHighlightColor) self.textHighlightingNodes.append(textHighlightNode) - self.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) + self.containerNode.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) } textHighlightNode.frame = self.textNode.textNode.frame textHighlightNode.updateRects(rects) @@ -927,7 +937,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { textHighlightNode = LinkHighlightingNode(color: item.message.effectivelyIncoming(item.context.account.peerId) ? item.presentationData.theme.theme.chat.message.incoming.linkHighlightColor.withMultipliedAlpha(0.5) : item.presentationData.theme.theme.chat.message.outgoing.linkHighlightColor.withMultipliedAlpha(0.5)) self.linkPreviewHighlightingNodes.append(textHighlightNode) - self.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) + self.containerNode.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) } textHighlightNode.frame = self.textNode.textNode.frame textHighlightNode.updateRects(rects) @@ -951,7 +961,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { linkProgressView = TextLoadingEffectView(frame: CGRect()) self.linkProgressView = linkProgressView - self.view.addSubview(linkProgressView) + self.containerNode.view.addSubview(linkProgressView) } linkProgressView.frame = self.textNode.textNode.frame @@ -1050,7 +1060,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } else { textHighlightNode = LinkHighlightingNode(color: color) self.quoteHighlightingNode = textHighlightNode - self.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) + self.containerNode.insertSubnode(textHighlightNode, belowSubnode: self.textNode.textNode) } textHighlightNode.frame = self.textNode.textNode.frame textHighlightNode.updateRects(rects) @@ -1157,8 +1167,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { textSelectionNode.enableShare = enableOtherActions textSelectionNode.menuSkipCoordnateConversion = !enableOtherActions self.textSelectionNode = textSelectionNode - self.addSubnode(textSelectionNode) - self.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode.textNode) + self.containerNode.addSubnode(textSelectionNode) + self.containerNode.insertSubnode(textSelectionNode.highlightAreaNode, belowSubnode: self.textNode.textNode) textSelectionNode.frame = self.textNode.textNode.frame textSelectionNode.highlightAreaNode.frame = self.textNode.textNode.frame } @@ -1180,8 +1190,8 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } override public func reactionTargetView(value: MessageReaction.Reaction) -> UIView? { - if !self.statusNode.isHidden { - return self.statusNode.reactionView(value: value) + if let statusNode = self.statusNode, !statusNode.isHidden { + return statusNode.reactionView(value: value) } return nil } @@ -1191,7 +1201,7 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } public func animateFrom(sourceView: UIView, scrollOffset: CGFloat, widthDifference: CGFloat, transition: CombinedTransition) { - self.view.addSubview(sourceView) + self.containerNode.view.addSubview(sourceView) sourceView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak sourceView] _ in sourceView?.removeFromSuperview() @@ -1206,8 +1216,10 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { transition.vertical.animatePositionAdditive(node: self.textNode.textNode, offset: offset) transition.updatePosition(layer: sourceView.layer, position: CGPoint(x: sourceView.layer.position.x - offset.x, y: sourceView.layer.position.y - offset.y)) - self.statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) - transition.horizontal.animatePositionAdditive(node: self.statusNode, offset: CGPoint(x: -widthDifference, y: 0.0)) + if let statusNode = self.statusNode { + statusNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + transition.horizontal.animatePositionAdditive(node: statusNode, offset: CGPoint(x: -widthDifference, y: 0.0)) + } } public func beginTextSelection(range: NSRange?, displayMenu: Bool = true) { @@ -1260,4 +1272,17 @@ public class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { return (substring, entities) } + + public func animateClippingTransition(offset: CGFloat, animation: ListViewItemUpdateAnimation) { + self.containerNode.clipsToBounds = true + self.containerNode.bounds = CGRect(origin: CGPoint(x: 0.0, y: offset), size: self.containerNode.bounds.size) + self.containerNode.alpha = 0.0 + animation.animator.updateAlpha(layer: self.containerNode.layer, alpha: 1.0, completion: nil) + animation.animator.updateBounds(layer: self.containerNode.layer, bounds: CGRect(origin: CGPoint(), size: self.containerNode.bounds.size), completion: { [weak self] completed in + guard let self, completed else { + return + } + self.containerNode.clipsToBounds = false + }) + } } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift index 14f33a7653..27f5d03d33 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageWebpageBubbleContentNode/Sources/ChatMessageWebpageBubbleContentNode.swift @@ -25,7 +25,7 @@ private let titleFont: UIFont = Font.semibold(15.0) public final class ChatMessageWebpageBubbleContentNode: ChatMessageBubbleContentNode { private var webPage: TelegramMediaWebpage? - private var contentNode: ChatMessageAttachedContentNode + public private(set) var contentNode: ChatMessageAttachedContentNode override public var visibility: ListViewItemNodeVisibility { didSet {