import Foundation import UIKit import AsyncDisplayKit import Display import ContextUI import AnimatedStickerNode import SwiftSignalKit import ContextUI import Postbox import TelegramCore import ReactionSelectionNode import ChatControllerInteraction import FeaturedStickersScreen import ChatTextInputMediaRecordingButton import ReplyAccessoryPanelNode import ChatMessageStickerItemNode import ChatMessageInstantVideoItemNode import ChatMessageAnimatedStickerItemNode import ChatMessageTransitionNode import ChatMessageBubbleItemNode import ChatEmptyNode import ChatMediaInputStickerGridItem import AccountContext private func convertAnimatingSourceRect(_ rect: CGRect, fromView: UIView, toView: UIView?) -> CGRect { if let presentationLayer = fromView.layer.presentation() { return presentationLayer.convert(rect, to: toView?.layer) } else { return fromView.layer.convert(rect, to: toView?.layer) } } private final class OverlayTransitionContainerNode: ViewControllerTracingNode { override init() { super.init() } deinit { } override func didLoad() { super.didLoad() } func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return nil } } private final class OverlayTransitionContainerController: ViewController, StandalonePresentableController { private let _ready = Promise() override public var ready: Promise { return self._ready } private var controllerNode: OverlayTransitionContainerNode { return self.displayNode as! OverlayTransitionContainerNode } private var wasDismissed: Bool = false init() { super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Ignore } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } override public func loadDisplayNode() { self.displayNode = OverlayTransitionContainerNode() self.displayNodeDidLoad() self._ready.set(.single(true)) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.updateLayout(layout: layout, transition: transition) } override public func viewDidAppear(_ animated: Bool) { if self.ignoreAppearanceMethodInvocations() { return } super.viewDidAppear(animated) } override public func dismiss(completion: (() -> Void)? = nil) { if !self.wasDismissed { self.wasDismissed = true self.presentingViewController?.dismiss(animated: false, completion: nil) completion?() } } } public final class ChatMessageTransitionNodeImpl: ASDisplayNode, ChatMessageTransitionNode, ChatMessageTransitionProtocol { static let animationDuration: Double = 0.3 static let verticalAnimationControlPoints: (Float, Float, Float, Float) = (0.19919472913616398, 0.010644531250000006, 0.27920937042459737, 0.91025390625) static let verticalAnimationCurve: ContainedViewLayoutTransitionCurve = .custom(verticalAnimationControlPoints.0, verticalAnimationControlPoints.1, verticalAnimationControlPoints.2, verticalAnimationControlPoints.3) static let horizontalAnimationCurve: ContainedViewLayoutTransitionCurve = .custom(0.23, 1.0, 0.32, 1.0) final class ReplyPanel { let titleNode: ASDisplayNode let textNode: ASDisplayNode let lineNode: ASDisplayNode let imageNode: ASDisplayNode let relativeSourceRect: CGRect let relativeTargetRect: CGRect init(titleNode: ASDisplayNode, textNode: ASDisplayNode, lineNode: ASDisplayNode, imageNode: ASDisplayNode, relativeSourceRect: CGRect, relativeTargetRect: CGRect) { self.titleNode = titleNode self.textNode = textNode self.lineNode = lineNode self.imageNode = imageNode self.relativeSourceRect = relativeSourceRect self.relativeTargetRect = relativeTargetRect } } final class Sticker { let imageNode: TransformImageNode? let animationNode: AnimatedStickerNode? let placeholderNode: ASDisplayNode? let imageLayer: CALayer? let relativeSourceRect: CGRect var sourceFrame: CGRect { if let imageNode = self.imageNode { return imageNode.frame } else if let imageLayer = self.imageLayer { return imageLayer.bounds } else { return CGRect(origin: CGPoint(), size: relativeSourceRect.size) } } var sourceLayer: CALayer? { if let imageNode = self.imageNode { return imageNode.layer } else if let imageLayer = self.imageLayer { return imageLayer } else { return nil } } init(imageNode: TransformImageNode?, animationNode: AnimatedStickerNode?, placeholderNode: ASDisplayNode?, imageLayer: CALayer?, relativeSourceRect: CGRect) { self.imageNode = imageNode self.animationNode = animationNode self.placeholderNode = placeholderNode self.imageLayer = imageLayer self.relativeSourceRect = relativeSourceRect } func snapshotContentTree() -> UIView? { if let animationNode = self.animationNode { return animationNode.view.snapshotContentTree() } else if let imageNode = self.imageNode { return imageNode.view.snapshotContentTree() } else if let sourceLayer = self.imageLayer { return sourceLayer.snapshotContentTreeAsView() } else { return nil } } } enum Source { final class TextInput { let backgroundView: UIView let contentView: UIView let sourceRect: CGRect let scrollOffset: CGFloat init(backgroundView: UIView, contentView: UIView, sourceRect: CGRect, scrollOffset: CGFloat) { self.backgroundView = backgroundView self.contentView = contentView self.sourceRect = sourceRect self.scrollOffset = scrollOffset } } enum StickerInput { case inputPanel(itemNode: ChatMediaInputStickerGridItemNode) case mediaPanel(itemNode: HorizontalStickerGridItemNode) case universal(sourceContainerView: UIView, sourceRect: CGRect, sourceLayer: CALayer) case emptyPanel(itemNode: ChatEmptyNodeStickerContentNode) } final class AudioMicInput { let micButton: ChatTextInputMediaRecordingButton init(micButton: ChatTextInputMediaRecordingButton) { self.micButton = micButton } } final class VideoMessage { let view: UIView init(view: UIView) { self.view = view } } final class MediaInput { let extractSnapshot: () -> UIView? init(extractSnapshot: @escaping () -> UIView?) { self.extractSnapshot = extractSnapshot } } final class GroupedMediaInput { let extractSnapshots: () -> [UIView] init(extractSnapshots: @escaping () -> [UIView]) { self.extractSnapshots = extractSnapshots } } case textInput(textInput: TextInput, replyPanel: ReplyAccessoryPanelNode?) case stickerMediaInput(input: StickerInput, replyPanel: ReplyAccessoryPanelNode?) case audioMicInput(AudioMicInput) case videoMessage(VideoMessage) case mediaInput(MediaInput) case groupedMediaInput(GroupedMediaInput) } final class DecorationItemNodeImpl: ASDisplayNode, ChatMessageTransitionNode.DecorationItemNode { let itemNode: ChatMessageItemNodeProtocol let contentView: UIView var globalPortalSourceView: PortalSourceView? let aboveEverything: Bool private let getContentAreaInScreenSpace: () -> CGRect private let scrollingContainer: ASDisplayNode private let containerNode: ASDisplayNode private let clippingNode: ASDisplayNode fileprivate weak var overlayController: OverlayTransitionContainerController? init(itemNode: ChatMessageItemNodeProtocol, contentView: UIView, aboveEverything: Bool, getContentAreaInScreenSpace: @escaping () -> CGRect) { self.itemNode = itemNode self.contentView = contentView self.aboveEverything = aboveEverything self.getContentAreaInScreenSpace = getContentAreaInScreenSpace self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true self.scrollingContainer = ASDisplayNode() self.containerNode = ASDisplayNode() super.init() self.addSubnode(self.clippingNode) self.clippingNode.addSubnode(self.scrollingContainer) self.scrollingContainer.addSubnode(self.containerNode) if aboveEverything { let globalPortalSourceView = PortalSourceView() globalPortalSourceView.needsGlobalPortal = true self.globalPortalSourceView = globalPortalSourceView globalPortalSourceView.addSubview(self.contentView) self.containerNode.view.addSubview(globalPortalSourceView) } else { self.containerNode.view.addSubview(self.contentView) } } func updateLayout(size: CGSize) { self.clippingNode.frame = CGRect(origin: CGPoint(), size: size) let absoluteRect = self.itemNode.view.convert(self.itemNode.view.bounds, to: self.itemNode.supernode?.supernode?.view) self.containerNode.frame = absoluteRect if let globalPortalSourceView = self.globalPortalSourceView { globalPortalSourceView.frame = CGRect(origin: CGPoint(), size: size) } } func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition) { if transition.isAnimated { assert(true) } self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: -offset) transition.animateOffsetAdditive(node: self.scrollingContainer, offset: offset) } func addContentOffset(offset: CGFloat) { self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset) } } final class CustomOffsetHandlerImpl { weak var itemNode: ChatMessageItemNodeProtocol? let update: (CGFloat, ContainedViewLayoutTransition) -> Bool init(itemNode: ChatMessageItemNodeProtocol, update: @escaping (CGFloat, ContainedViewLayoutTransition) -> Bool) { self.itemNode = itemNode self.update = update } } private final class AnimatingItemNode: ASDisplayNode { let itemNode: ChatMessageItemNodeProtocol private let contextSourceNode: ContextExtractedContentContainingNode private let source: ChatMessageTransitionNodeImpl.Source private let getContentAreaInScreenSpace: () -> CGRect private let scrollingContainer: ASDisplayNode private let containerNode: ASDisplayNode private let clippingNode: ASDisplayNode weak var overlayController: OverlayTransitionContainerController? var animationEnded: (() -> Void)? var updateAfterCompletion: Bool = false init(itemNode: ChatMessageItemNodeProtocol, contextSourceNode: ContextExtractedContentContainingNode, source: ChatMessageTransitionNodeImpl.Source, getContentAreaInScreenSpace: @escaping () -> CGRect) { self.itemNode = itemNode self.getContentAreaInScreenSpace = getContentAreaInScreenSpace self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true self.scrollingContainer = ASDisplayNode() self.containerNode = ASDisplayNode() self.contextSourceNode = contextSourceNode self.source = source super.init() self.addSubnode(self.clippingNode) self.clippingNode.addSubnode(self.scrollingContainer) self.scrollingContainer.addSubnode(self.containerNode) } deinit { self.contextSourceNode.addSubnode(self.contextSourceNode.contentNode) } func updateLayout(size: CGSize) { self.clippingNode.frame = CGRect(origin: CGPoint(), size: size) } func beginAnimation() { let verticalDuration: Double = ChatMessageTransitionNodeImpl.animationDuration let horizontalDuration: Double = verticalDuration let delay: Double = 0.0 var updatedContentAreaInScreenSpace = self.getContentAreaInScreenSpace() updatedContentAreaInScreenSpace.size.width = updatedContentAreaInScreenSpace.origin.x + self.clippingNode.bounds.width updatedContentAreaInScreenSpace.origin.x = 0.0 let clippingOffset = updatedContentAreaInScreenSpace.minY - self.clippingNode.frame.minY self.clippingNode.frame = CGRect(origin: CGPoint(x: 0.0, y: updatedContentAreaInScreenSpace.minY), size: CGSize(width: updatedContentAreaInScreenSpace.size.width, height: self.clippingNode.bounds.height)) self.clippingNode.bounds = CGRect(origin: CGPoint(x: 0.0, y: clippingOffset), size: self.clippingNode.bounds.size) switch self.source { case let .textInput(initialTextInput, replyPanel): self.contextSourceNode.isExtractedToContextPreview = true self.contextSourceNode.isExtractedToContextPreviewUpdated?(true) var currentContentRect = self.contextSourceNode.contentRect let contextSourceNode = self.contextSourceNode self.contextSourceNode.layoutUpdated = { [weak self, weak contextSourceNode] size, _ in guard let strongSelf = self, let contextSourceNode = contextSourceNode, strongSelf.contextSourceNode === contextSourceNode else { return } let updatedContentRect = contextSourceNode.contentRect let deltaY = updatedContentRect.height - currentContentRect.height if !deltaY.isZero { currentContentRect = updatedContentRect strongSelf.addContentOffset(offset: deltaY, itemNode: nil) } } self.containerNode.addSubnode(self.contextSourceNode.contentNode) let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view) let sourceRect = self.view.convert(initialTextInput.sourceRect, from: nil) let sourceBackgroundAbsoluteRect = initialTextInput.backgroundView.frame.offsetBy(dx: sourceRect.minX, dy: sourceRect.minY) let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.minX, y: sourceBackgroundAbsoluteRect.maxY - self.contextSourceNode.contentRect.height), size: self.contextSourceNode.contentRect.size) let textInput = ChatMessageTransitionNodeImpl.Source.TextInput(backgroundView: initialTextInput.backgroundView, contentView: initialTextInput.contentView, sourceRect: sourceRect, scrollOffset: initialTextInput.scrollOffset) textInput.backgroundView.frame = CGRect(origin: CGPoint(x: 0.0, y: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height), size: textInput.backgroundView.bounds.size) textInput.contentView.frame = textInput.contentView.frame.offsetBy(dx: 0.0, dy: sourceAbsoluteRect.height - sourceBackgroundAbsoluteRect.height) var sourceReplyPanel: ReplyPanel? if let replyPanel = replyPanel, let replyPanelParentView = replyPanel.view.superview { let replyPanelFrame = replyPanel.originalFrameBeforeDismissed ?? replyPanel.frame var replySourceAbsoluteFrame = replyPanelParentView.convert(replyPanelFrame, to: self.view) replySourceAbsoluteFrame.origin.x -= sourceAbsoluteRect.minX - self.contextSourceNode.contentRect.minX replySourceAbsoluteFrame.origin.y -= sourceAbsoluteRect.minY - self.contextSourceNode.contentRect.minY var globalTargetFrame = replySourceAbsoluteFrame.offsetBy(dx: 0.0, dy: replyPanelFrame.height) globalTargetFrame.origin.x += sourceAbsoluteRect.minX - targetAbsoluteRect.minX globalTargetFrame.origin.y += sourceAbsoluteRect.minY - targetAbsoluteRect.minY sourceReplyPanel = ReplyPanel(titleNode: replyPanel.titleNode, textNode: replyPanel.textNode, lineNode: replyPanel.lineNode, imageNode: replyPanel.imageNode, relativeSourceRect: replySourceAbsoluteFrame, relativeTargetRect: globalTargetFrame) } self.itemNode.cancelInsertionAnimations() let horizontalCurve = ChatMessageTransitionNodeImpl.horizontalAnimationCurve let horizontalTransition: ContainedViewLayoutTransition = .animated(duration: horizontalDuration, curve: horizontalCurve) let verticalCurve = ChatMessageTransitionNodeImpl.verticalAnimationCurve let verticalTransition: ContainedViewLayoutTransition = .animated(duration: verticalDuration, curve: verticalCurve) let combinedTransition = CombinedTransition(horizontal: horizontalTransition, vertical: verticalTransition) self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: verticalCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.endAnimation() }) self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: horizontalCurve.mediaTimingFunction, additive: true) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), horizontalCurve, horizontalDuration) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), verticalCurve, verticalDuration) if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { itemNode.animateContentFromTextInputField( textInput: ChatMessageBubbleItemNode.AnimationTransitionTextInput( backgroundView: textInput.backgroundView, contentView: textInput.contentView, sourceRect: textInput.sourceRect, scrollOffset: textInput.scrollOffset ), transition: combinedTransition ) if let sourceReplyPanel = sourceReplyPanel { itemNode.animateReplyPanel( sourceReplyPanel: ChatMessageBubbleItemNode.AnimationTransitionReplyPanel( titleNode: sourceReplyPanel.titleNode, textNode: sourceReplyPanel.textNode, lineNode: sourceReplyPanel.lineNode, imageNode: sourceReplyPanel.imageNode, relativeSourceRect: sourceReplyPanel.relativeSourceRect, relativeTargetRect: sourceReplyPanel.relativeTargetRect ), transition: combinedTransition ) } } else if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode { itemNode.animateContentFromTextInputField( textInput: ChatMessageAnimatedStickerItemNode.AnimationTransitionTextInput( backgroundView: textInput.backgroundView, contentView: textInput.contentView, sourceRect: textInput.sourceRect, scrollOffset: textInput.scrollOffset ), transition: combinedTransition ) if let sourceReplyPanel = sourceReplyPanel { itemNode.animateReplyPanel( sourceReplyPanel: ChatMessageAnimatedStickerItemNode.AnimationTransitionReplyPanel( titleNode: sourceReplyPanel.titleNode, textNode: sourceReplyPanel.textNode, lineNode: sourceReplyPanel.lineNode, imageNode: sourceReplyPanel.imageNode, relativeSourceRect: sourceReplyPanel.relativeSourceRect, relativeTargetRect: sourceReplyPanel.relativeTargetRect ), transition: combinedTransition ) } } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode { itemNode.animateContentFromTextInputField( textInput: ChatMessageStickerItemNode.AnimationTransitionTextInput( backgroundView: textInput.backgroundView, contentView: textInput.contentView, sourceRect: textInput.sourceRect, scrollOffset: textInput.scrollOffset ), transition: combinedTransition ) if let sourceReplyPanel = sourceReplyPanel { itemNode.animateReplyPanel( sourceReplyPanel: ChatMessageStickerItemNode.AnimationTransitionReplyPanel( titleNode: sourceReplyPanel.titleNode, textNode: sourceReplyPanel.textNode, lineNode: sourceReplyPanel.lineNode, imageNode: sourceReplyPanel.imageNode, relativeSourceRect: sourceReplyPanel.relativeSourceRect, relativeTargetRect: sourceReplyPanel.relativeTargetRect ), transition: combinedTransition ) } } case let .stickerMediaInput(stickerMediaInput, replyPanel): self.itemNode.cancelInsertionAnimations() self.contextSourceNode.isExtractedToContextPreview = true self.contextSourceNode.isExtractedToContextPreviewUpdated?(true) self.containerNode.addSubnode(self.contextSourceNode.contentNode) let stickerSource: Sticker let sourceAbsoluteRect: CGRect switch stickerMediaInput { case let .inputPanel(sourceItemNode): stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, imageLayer: nil, relativeSourceRect: sourceItemNode.imageNode.frame) sourceAbsoluteRect = sourceItemNode.view.convert(sourceItemNode.imageNode.frame, to: self.view) case let .mediaPanel(sourceItemNode): stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, imageLayer: nil, relativeSourceRect: sourceItemNode.imageNode.frame) sourceAbsoluteRect = sourceItemNode.view.convert(sourceItemNode.imageNode.frame, to: self.view) case let .universal(sourceContainerView, sourceRect, sourceLayer): stickerSource = Sticker(imageNode: nil, animationNode: nil, placeholderNode: nil, imageLayer: sourceLayer, relativeSourceRect: sourceLayer.frame) sourceAbsoluteRect = convertAnimatingSourceRect(sourceRect, fromView: sourceContainerView, toView: self.view) case let .emptyPanel(sourceItemNode): stickerSource = Sticker(imageNode: sourceItemNode.stickerNode.imageNode, animationNode: sourceItemNode.stickerNode.animationNode, placeholderNode: nil, imageLayer: nil, relativeSourceRect: sourceItemNode.stickerNode.imageNode.frame) sourceAbsoluteRect = sourceItemNode.stickerNode.view.convert(sourceItemNode.stickerNode.imageNode.frame, to: self.view) } let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view) var sourceReplyPanel: ReplyPanel? if let replyPanel = replyPanel, let replyPanelParentView = replyPanel.view.superview { var replySourceAbsoluteFrame = replyPanelParentView.convert(replyPanel.originalFrameBeforeDismissed ?? replyPanel.frame, to: self.view) replySourceAbsoluteFrame.origin.x -= sourceAbsoluteRect.midX - self.contextSourceNode.contentRect.midX replySourceAbsoluteFrame.origin.y -= sourceAbsoluteRect.midY - self.contextSourceNode.contentRect.midY sourceReplyPanel = ReplyPanel(titleNode: replyPanel.titleNode, textNode: replyPanel.textNode, lineNode: replyPanel.lineNode, imageNode: replyPanel.imageNode, relativeSourceRect: replySourceAbsoluteFrame, relativeTargetRect: replySourceAbsoluteFrame.offsetBy(dx: 0.0, dy: replySourceAbsoluteFrame.height)) } let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode { itemNode.animateContentFromStickerGridItem( stickerSource: ChatMessageAnimatedStickerItemNode.AnimationTransitionSticker( imageNode: stickerSource.imageNode, animationNode: stickerSource.animationNode, placeholderNode: stickerSource.placeholderNode, imageLayer: stickerSource.imageLayer, relativeSourceRect: stickerSource.relativeSourceRect ), transition: combinedTransition ) if let sourceAnimationNode = stickerSource.animationNode { itemNode.animationNode?.setFrameIndex(sourceAnimationNode.currentFrameIndex) } if let sourceReplyPanel = sourceReplyPanel { itemNode.animateReplyPanel( sourceReplyPanel: ChatMessageAnimatedStickerItemNode.AnimationTransitionReplyPanel( titleNode: sourceReplyPanel.titleNode, textNode: sourceReplyPanel.textNode, lineNode: sourceReplyPanel.lineNode, imageNode: sourceReplyPanel.imageNode, relativeSourceRect: sourceReplyPanel.relativeSourceRect, relativeTargetRect: sourceReplyPanel.relativeTargetRect ), transition: combinedTransition ) } } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode { itemNode.animateContentFromStickerGridItem( stickerSource: ChatMessageStickerItemNode.AnimationTransitionSticker( imageNode: stickerSource.imageNode, animationNode: stickerSource.animationNode, placeholderNode: stickerSource.placeholderNode, imageLayer: stickerSource.imageLayer, relativeSourceRect: stickerSource.relativeSourceRect ), transition: combinedTransition ) if let sourceReplyPanel = sourceReplyPanel { itemNode.animateReplyPanel( sourceReplyPanel: ChatMessageStickerItemNode.AnimationTransitionReplyPanel( titleNode: sourceReplyPanel.titleNode, textNode: sourceReplyPanel.textNode, lineNode: sourceReplyPanel.lineNode, imageNode: sourceReplyPanel.imageNode, relativeSourceRect: sourceReplyPanel.relativeSourceRect, relativeTargetRect: sourceReplyPanel.relativeTargetRect ), transition: combinedTransition ) } } self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.endAnimation() }) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), ChatMessageTransitionNodeImpl.horizontalAnimationCurve, horizontalDuration) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), ChatMessageTransitionNodeImpl.verticalAnimationCurve, verticalDuration) self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true) switch stickerMediaInput { case .inputPanel, .universal: break case let .mediaPanel(sourceItemNode): sourceItemNode.isHidden = true case let .emptyPanel(sourceItemNode): sourceItemNode.isHidden = true } case let .audioMicInput(audioMicInput): if let (container, localRect) = audioMicInput.micButton.contentContainer { let snapshotView = container.snapshotView(afterScreenUpdates: false) if let snapshotView = snapshotView { let sourceAbsoluteRect = container.convert(localRect, to: self.view) snapshotView.frame = sourceAbsoluteRect container.isHidden = true let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { if let contextContainer = itemNode.animateFromMicInput(micInputNode: snapshotView, transition: combinedTransition) { self.containerNode.addSubnode(contextContainer.contentNode) let targetAbsoluteRect = contextContainer.view.convert(contextContainer.contentRect, to: self.view) self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -contextContainer.contentRect.minX, dy: -contextContainer.contentRect.minY) contextContainer.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self, weak contextContainer, weak container] _ in guard let strongSelf = self else { return } if let contextContainer = contextContainer { contextContainer.isExtractedToContextPreview = false contextContainer.isExtractedToContextPreviewUpdated?(false) contextContainer.addSubnode(contextContainer.contentNode) } container?.isHidden = false strongSelf.endAnimation() }) self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true) } } } } case let .videoMessage(videoMessage): let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { itemNode.cancelInsertionAnimations() self.contextSourceNode.isExtractedToContextPreview = true self.contextSourceNode.isExtractedToContextPreviewUpdated?(true) self.containerNode.addSubnode(self.contextSourceNode.contentNode) let sourceAbsoluteRect = videoMessage.view.frame let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view) videoMessage.view.frame = videoMessage.view.frame.offsetBy(dx: targetAbsoluteRect.midX - sourceAbsoluteRect.midX, dy: targetAbsoluteRect.midY - sourceAbsoluteRect.midY) self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.verticalAnimationCurve.mediaTimingFunction, additive: true, completion: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.endAnimation() }) itemNode.animateInstantVideoFromSnapshot(snapshotView: videoMessage.view, transition: combinedTransition) } case let .mediaInput(mediaInput): if let snapshotView = mediaInput.extractSnapshot() { Queue.mainQueue().justDispatch { if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { itemNode.cancelInsertionAnimations() self.contextSourceNode.isExtractedToContextPreview = true self.contextSourceNode.isExtractedToContextPreviewUpdated?(true) self.containerNode.addSubnode(self.contextSourceNode.contentNode) let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view) let sourceBackgroundAbsoluteRect = snapshotView.frame let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.midX - self.contextSourceNode.contentRect.size.width / 2.0, y: sourceBackgroundAbsoluteRect.midY - self.contextSourceNode.contentRect.size.height / 2.0), size: self.contextSourceNode.contentRect.size) let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { itemNode.animateContentFromMediaInput(snapshotView: snapshotView, transition: combinedTransition) } self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) snapshotView.center = targetAbsoluteRect.center.offsetBy(dx: -self.containerNode.frame.minX, dy: -self.containerNode.frame.minY) self.containerNode.view.addSubview(snapshotView) self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.endAnimation() }) combinedTransition.horizontal.animateTransformScale(node: self.contextSourceNode.contentNode, from: CGPoint(x: sourceBackgroundAbsoluteRect.width / targetAbsoluteRect.width, y: sourceBackgroundAbsoluteRect.height / targetAbsoluteRect.height)) combinedTransition.horizontal.updateTransformScale(layer: snapshotView.layer, scale: CGPoint(x: 1.0 / (sourceBackgroundAbsoluteRect.width / targetAbsoluteRect.width), y: 1.0 / (sourceBackgroundAbsoluteRect.height / targetAbsoluteRect.height))) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), ChatMessageTransitionNodeImpl.horizontalAnimationCurve, horizontalDuration) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNodeImpl.verticalAnimationCurve, verticalDuration) } } } else { self.endAnimation() } case let .groupedMediaInput(groupedMediaInput): let snapshotViews = groupedMediaInput.extractSnapshots() if snapshotViews.isEmpty { self.endAnimation() return } Queue.mainQueue().justDispatch { if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { itemNode.cancelInsertionAnimations() self.contextSourceNode.isExtractedToContextPreview = true self.contextSourceNode.isExtractedToContextPreviewUpdated?(true) self.containerNode.addSubnode(self.contextSourceNode.contentNode) let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNodeImpl.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) var targetContentRects: [CGRect] = [] if let itemNode = self.itemNode as? ChatMessageBubbleItemNode { targetContentRects = itemNode.animateContentFromGroupedMediaInput(transition: combinedTransition) } let targetAbsoluteRect = self.contextSourceNode.view.convert(self.contextSourceNode.contentRect, to: self.view) func boundingRect(for views: [UIView]) -> CGRect { var minX: CGFloat = .greatestFiniteMagnitude var minY: CGFloat = .greatestFiniteMagnitude var maxX: CGFloat = .leastNonzeroMagnitude var maxY: CGFloat = .leastNonzeroMagnitude for view in views { let rect = view.frame if rect.minX < minX { minX = rect.minX } if rect.minY < minY { minY = rect.minY } if rect.maxX > maxX { maxX = rect.maxX } if rect.maxY > maxY { maxY = rect.maxY } } return CGRect(origin: CGPoint(x: minX, y: minY), size: CGSize(width: maxX - minX, height: maxY - minY)) } let sourceBackgroundAbsoluteRect = boundingRect(for: snapshotViews) let sourceAbsoluteRect = CGRect(origin: CGPoint(x: sourceBackgroundAbsoluteRect.midX - self.contextSourceNode.contentRect.size.width / 2.0, y: sourceBackgroundAbsoluteRect.midY - self.contextSourceNode.contentRect.size.height / 2.0), size: self.contextSourceNode.contentRect.size) self.containerNode.frame = targetAbsoluteRect.offsetBy(dx: -self.contextSourceNode.contentRect.minX, dy: -self.contextSourceNode.contentRect.minY) self.contextSourceNode.updateAbsoluteRect?(self.containerNode.frame, UIScreen.main.bounds.size) self.containerNode.layer.animatePosition(from: CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.horizontalAnimationCurve.mediaTimingFunction, additive: true, force: true) self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: verticalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNodeImpl.verticalAnimationCurve.mediaTimingFunction, additive: true, force: true, completion: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.endAnimation() }) combinedTransition.horizontal.animateTransformScale(node: self.contextSourceNode.contentNode, from: CGPoint(x: sourceBackgroundAbsoluteRect.width / targetAbsoluteRect.width, y: sourceBackgroundAbsoluteRect.height / targetAbsoluteRect.height)) var index = 0 for snapshotView in snapshotViews { let targetContentRect = targetContentRects[index] let targetAbsoluteContentRect = targetContentRect.offsetBy(dx: targetAbsoluteRect.minX, dy: targetAbsoluteRect.minY) snapshotView.center = targetAbsoluteContentRect.center.offsetBy(dx: -self.containerNode.frame.minX, dy: -self.containerNode.frame.minY) self.containerNode.view.addSubview(snapshotView) combinedTransition.horizontal.updateTransformScale(layer: snapshotView.layer, scale: CGPoint(x: 1.0 / (snapshotView.frame.width / targetContentRect.width), y: 1.0 / (snapshotView.frame.height / targetContentRect.height))) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.12, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) index += 1 } self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: sourceAbsoluteRect.minX - targetAbsoluteRect.minX, y: 0.0), ChatMessageTransitionNodeImpl.horizontalAnimationCurve, horizontalDuration) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNodeImpl.verticalAnimationCurve, verticalDuration) } } } } private func endAnimation() { self.contextSourceNode.isExtractedToContextPreview = false self.contextSourceNode.isExtractedToContextPreviewUpdated?(false) self.animationEnded?() } func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?) { var applyOffset = false if let itemNode = itemNode { if itemNode === self.itemNode { applyOffset = true } } else { applyOffset = true } if applyOffset { if transition.isAnimated { assert(true) } self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: -offset) transition.animateOffsetAdditive(node: self.scrollingContainer, offset: offset) } } func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { var applyOffset = false if let itemNode = itemNode { if itemNode === self.itemNode { applyOffset = true } } else { applyOffset = true } if applyOffset { self.scrollingContainer.bounds = self.scrollingContainer.bounds.offsetBy(dx: 0.0, dy: offset) } } } private final class MessageReactionContext { private(set) weak var itemNode: ListViewItemNode? private(set) weak var contextController: ContextController? private(set) weak var standaloneReactionAnimation: StandaloneReactionAnimation? var isEmpty: Bool { return self.contextController == nil && self.standaloneReactionAnimation == nil } init(itemNode: ListViewItemNode, contextController: ContextController?, standaloneReactionAnimation: StandaloneReactionAnimation?) { self.itemNode = itemNode self.contextController = contextController self.standaloneReactionAnimation = standaloneReactionAnimation } func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?, isRotated: Bool) { guard let currentItemNode = self.itemNode else { return } if itemNode == nil || itemNode === currentItemNode { if let contextController = self.contextController { contextController.addRelativeContentOffset(CGPoint(x: 0.0, y: offset), transition: transition) } if let standaloneReactionAnimation = self.standaloneReactionAnimation { standaloneReactionAnimation.addRelativeContentOffset(CGPoint(x: 0.0, y: -offset), transition: transition) } } } func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { } func dismiss() { if let contextController = self.contextController { contextController.cancelReactionAnimation() contextController.view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak contextController] _ in contextController?.dismissNow() }) } if let standaloneReactionAnimation = self.standaloneReactionAnimation { standaloneReactionAnimation.cancel() standaloneReactionAnimation.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak standaloneReactionAnimation] _ in standaloneReactionAnimation?.removeFromSupernode() }) } } } private let listNode: ChatHistoryListNodeImpl private let getContentAreaInScreenSpace: () -> CGRect private let onTransitionEvent: (ContainedViewLayoutTransition) -> Void private var currentPendingItems: [Int64: (Source, () -> Void)] = [:] private var animatingItemNodes: [AnimatingItemNode] = [] private var decorationItemNodes: [DecorationItemNodeImpl] = [] private var messageReactionContexts: [MessageReactionContext] = [] private var customOffsetHandlers: [CustomOffsetHandlerImpl] = [] var hasScheduledTransitions: Bool { return !self.currentPendingItems.isEmpty } var hasOngoingTransitions: Bool { return !self.animatingItemNodes.isEmpty } init(listNode: ChatHistoryListNodeImpl, getContentAreaInScreenSpace: @escaping () -> CGRect, onTransitionEvent: @escaping (ContainedViewLayoutTransition) -> Void) { self.listNode = listNode self.getContentAreaInScreenSpace = getContentAreaInScreenSpace self.onTransitionEvent = onTransitionEvent super.init() self.listNode.animationCorrelationMessagesFound = { [weak self] itemNodeAndCorrelationIds in guard let strongSelf = self else { return } for (correlationId, itemNode) in itemNodeAndCorrelationIds { if let (currentSource, initiated) = strongSelf.currentPendingItems[correlationId] { strongSelf.beginAnimation(itemNode: itemNode, source: currentSource) initiated() } } if itemNodeAndCorrelationIds.count == strongSelf.currentPendingItems.count { strongSelf.currentPendingItems = [:] } } } func add(correlationId: Int64, source: Source, initiated: @escaping () -> Void) { self.currentPendingItems = [correlationId: (source, initiated)] self.listNode.setCurrentSendAnimationCorrelationIds(Set([correlationId])) } func add(grouped: [(correlationId: Int64, source: Source, initiated: () -> Void)]) { var currentPendingItems: [Int64: (Source, () -> Void)] = [:] var correlationIds = Set() for (correlationId, source, initiated) in grouped { currentPendingItems[correlationId] = (source, initiated) correlationIds.insert(correlationId) } self.currentPendingItems = currentPendingItems self.listNode.setCurrentSendAnimationCorrelationIds(correlationIds) } public func add(decorationView: UIView, itemNode: ChatMessageItemNodeProtocol, aboveEverything: Bool) -> DecorationItemNode { let decorationItemNode = DecorationItemNodeImpl(itemNode: itemNode, contentView: decorationView, aboveEverything: aboveEverything, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) decorationItemNode.updateLayout(size: self.bounds.size) self.decorationItemNodes.append(decorationItemNode) self.addSubnode(decorationItemNode) return decorationItemNode } public func remove(decorationNode: DecorationItemNode) { self.decorationItemNodes.removeAll(where: { $0 === decorationNode }) decorationNode.removeFromSupernode() if let decorationNode = decorationNode as? DecorationItemNodeImpl { decorationNode.overlayController?.dismiss() } } public func addCustomOffsetHandler(itemNode: ChatMessageItemNodeProtocol, update: @escaping (CGFloat, ContainedViewLayoutTransition) -> Bool) -> Disposable { let handler = CustomOffsetHandlerImpl(itemNode: itemNode, update: update) self.customOffsetHandlers.append(handler) return ActionDisposable { [weak self, weak handler] in Queue.mainQueue().async { guard let self, let handler else { return } self.customOffsetHandlers.removeAll(where: { $0 === handler }) } } } private func beginAnimation(itemNode: ChatMessageItemNodeProtocol, source: Source) { var contextSourceNode: ContextExtractedContentContainingNode? if let itemNode = itemNode as? ChatMessageBubbleItemNode { contextSourceNode = itemNode.mainContextSourceNode } else if let itemNode = itemNode as? ChatMessageStickerItemNode { contextSourceNode = itemNode.contextSourceNode } else if let itemNode = itemNode as? ChatMessageAnimatedStickerItemNode { contextSourceNode = itemNode.contextSourceNode } else if let itemNode = itemNode as? ChatMessageInstantVideoItemNode { contextSourceNode = itemNode.contextSourceNode } if let contextSourceNode = contextSourceNode { let animatingItemNode = AnimatingItemNode(itemNode: itemNode, contextSourceNode: contextSourceNode, source: source, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) animatingItemNode.updateLayout(size: self.bounds.size) self.animatingItemNodes.append(animatingItemNode) switch source { case .audioMicInput, .videoMessage, .mediaInput, .groupedMediaInput: let overlayController = OverlayTransitionContainerController() overlayController.displayNode.addSubnode(animatingItemNode) animatingItemNode.overlayController = overlayController self.listNode.context.sharedContext.mainWindow?.presentInGlobalOverlay(overlayController) default: self.addSubnode(animatingItemNode) } animatingItemNode.animationEnded = { [weak self, weak animatingItemNode] in guard let strongSelf = self, let animatingItemNode = animatingItemNode else { return } animatingItemNode.removeFromSupernode() animatingItemNode.overlayController?.dismiss() if let index = strongSelf.animatingItemNodes.firstIndex(where: { $0 === animatingItemNode }) { strongSelf.animatingItemNodes.remove(at: index) } if animatingItemNode.updateAfterCompletion { for message in animatingItemNode.itemNode.messages() { strongSelf.listNode.requestMessageUpdate(stableId: message.stableId) break } } } animatingItemNode.frame = self.bounds animatingItemNode.beginAnimation() self.onTransitionEvent(.animated(duration: ChatMessageTransitionNodeImpl.animationDuration, curve: ChatMessageTransitionNodeImpl.verticalAnimationCurve)) } } override public func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { return nil } private func removeEmptyMessageReactionContexts() { for i in (0 ..< self.messageReactionContexts.count).reversed() { if self.messageReactionContexts[i].isEmpty { self.messageReactionContexts.remove(at: i) } } } func dismissMessageReactionContexts(itemNode: ListViewItemNode? = nil) { for i in (0 ..< self.messageReactionContexts.count).reversed() { let messageReactionContext = self.messageReactionContexts[i] if itemNode == nil || messageReactionContext.itemNode === itemNode { self.messageReactionContexts.remove(at: i) messageReactionContext.dismiss() } } } func addMessageContextController(messageId: MessageId, contextController: ContextController) { self.addMessageReactionContextContext(messageId: messageId, contextController: contextController, standaloneReactionAnimation: nil) } func addMessageStandaloneReactionAnimation(messageId: MessageId, standaloneReactionAnimation: StandaloneReactionAnimation) { self.addMessageReactionContextContext(messageId: messageId, contextController: nil, standaloneReactionAnimation: standaloneReactionAnimation) } private func addMessageReactionContextContext(messageId: MessageId, contextController: ContextController?, standaloneReactionAnimation: StandaloneReactionAnimation?) { self.removeEmptyMessageReactionContexts() var messageItemNode: ListViewItemNode? self.listNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemNodeProtocol { if itemNode.matchesMessage(id: messageId) { messageItemNode = itemNode } } } if let messageItemNode = messageItemNode { for i in 0 ..< self.messageReactionContexts.count { if self.messageReactionContexts[i].itemNode === messageItemNode { self.messageReactionContexts[i].dismiss() self.messageReactionContexts.remove(at: i) break } } self.messageReactionContexts.append(MessageReactionContext(itemNode: messageItemNode, contextController: contextController, standaloneReactionAnimation: standaloneReactionAnimation)) } } func addExternalOffset(offset: CGFloat, transition: ContainedViewLayoutTransition, itemNode: ListViewItemNode?, isRotated: Bool) { for animatingItemNode in self.animatingItemNodes { animatingItemNode.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) } if itemNode == nil { for decorationItemNode in self.decorationItemNodes { decorationItemNode.addExternalOffset(offset: offset, transition: transition) } var removeCustomOffsetHandlers: [CustomOffsetHandlerImpl] = [] for customOffsetHandler in self.customOffsetHandlers { if !customOffsetHandler.update(offset, transition) { removeCustomOffsetHandlers.append(customOffsetHandler) } } for customOffsetHandler in removeCustomOffsetHandlers { self.customOffsetHandlers.removeAll(where: { $0 === customOffsetHandler}) } } for messageReactionContext in self.messageReactionContexts { messageReactionContext.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode, isRotated: isRotated) } } func addContentOffset(offset: CGFloat, itemNode: ListViewItemNode?) { for animatingItemNode in self.animatingItemNodes { animatingItemNode.addContentOffset(offset: offset, itemNode: itemNode) } if itemNode == nil { for decorationItemNode in self.decorationItemNodes { decorationItemNode.addContentOffset(offset: offset) } var removeCustomOffsetHandlers: [CustomOffsetHandlerImpl] = [] for customOffsetHandler in self.customOffsetHandlers { if !customOffsetHandler.update(offset, .immediate) { removeCustomOffsetHandlers.append(customOffsetHandler) } } for customOffsetHandler in removeCustomOffsetHandlers { self.customOffsetHandlers.removeAll(where: { $0 === customOffsetHandler}) } } for messageReactionContext in self.messageReactionContexts { messageReactionContext.addContentOffset(offset: offset, itemNode: itemNode) } } func isAnimatingMessage(stableId: UInt32) -> Bool { for itemNode in self.animatingItemNodes { for message in itemNode.itemNode.messages() { if message.stableId == stableId { return true } } } return false } func scheduleUpdateMessageAfterAnimationCompleted(stableId: UInt32) { for itemNode in self.animatingItemNodes { for message in itemNode.itemNode.messages() { if message.stableId == stableId { itemNode.updateAfterCompletion = true } } } } func hasScheduledUpdateMessageAfterAnimationCompleted(stableId: UInt32) -> Bool { for itemNode in self.animatingItemNodes { for message in itemNode.itemNode.messages() { if message.stableId == stableId { return itemNode.updateAfterCompletion } } } return false } }