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 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 ChatMessageTransitionNode: ASDisplayNode, 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 inputPanelSearch(itemNode: StickerPaneSearchStickerItemNode) 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 DecorationItemNode: ASDisplayNode { let itemNode: ChatMessageItemView let contentView: UIView private let getContentAreaInScreenSpace: () -> CGRect private let scrollingContainer: ASDisplayNode private let containerNode: ASDisplayNode private let clippingNode: ASDisplayNode fileprivate weak var overlayController: OverlayTransitionContainerController? init(itemNode: ChatMessageItemView, contentView: UIView, getContentAreaInScreenSpace: @escaping () -> CGRect) { self.itemNode = itemNode self.contentView = contentView 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) 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 } 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) } } private final class AnimatingItemNode: ASDisplayNode { let itemNode: ChatMessageItemView private let contextSourceNode: ContextExtractedContentContainingNode private let source: ChatMessageTransitionNode.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: ChatMessageItemView, contextSourceNode: ContextExtractedContentContainingNode, source: ChatMessageTransitionNode.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 = ChatMessageTransitionNode.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 = ChatMessageTransitionNode.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 = ChatMessageTransitionNode.horizontalAnimationCurve let horizontalTransition: ContainedViewLayoutTransition = .animated(duration: horizontalDuration, curve: horizontalCurve) let verticalCurve = ChatMessageTransitionNode.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: textInput, transition: combinedTransition) if let sourceReplyPanel = sourceReplyPanel { itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) } } else if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode { itemNode.animateContentFromTextInputField(textInput: textInput, transition: combinedTransition) if let sourceReplyPanel = sourceReplyPanel { itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) } } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode { itemNode.animateContentFromTextInputField(textInput: textInput, transition: combinedTransition) if let sourceReplyPanel = sourceReplyPanel { itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, 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 .inputPanelSearch(sourceItemNode): stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: nil, imageLayer: nil, relativeSourceRect: sourceItemNode.imageNode.frame) sourceAbsoluteRect = sourceItemNode.view.convert(sourceItemNode.imageNode.frame, to: 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: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) if let itemNode = self.itemNode as? ChatMessageAnimatedStickerItemNode { itemNode.animateContentFromStickerGridItem(stickerSource: stickerSource, transition: combinedTransition) if let sourceAnimationNode = stickerSource.animationNode { itemNode.animationNode?.setFrameIndex(sourceAnimationNode.currentFrameIndex) } if let sourceReplyPanel = sourceReplyPanel { itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, transition: combinedTransition) } } else if let itemNode = self.itemNode as? ChatMessageStickerItemNode { itemNode.animateContentFromStickerGridItem(stickerSource: stickerSource, transition: combinedTransition) if let sourceReplyPanel = sourceReplyPanel { itemNode.animateReplyPanel(sourceReplyPanel: sourceReplyPanel, 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: ChatMessageTransitionNode.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), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.midY - targetAbsoluteRect.midY), ChatMessageTransitionNode.verticalAnimationCurve, verticalDuration) self.containerNode.layer.animatePosition(from: CGPoint(x: sourceAbsoluteRect.midX - targetAbsoluteRect.midX, y: 0.0), to: CGPoint(), duration: horizontalDuration, delay: delay, mediaTimingFunction: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true) switch stickerMediaInput { case .inputPanel, .universal: break case let .mediaPanel(sourceItemNode): sourceItemNode.isHidden = true case let .inputPanelSearch(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: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.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: ChatMessageTransitionNode.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: ChatMessageTransitionNode.horizontalAnimationCurve.mediaTimingFunction, additive: true) } } } } case let .videoMessage(videoMessage): let combinedTransition = CombinedTransition(horizontal: .animated(duration: horizontalDuration, curve: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.verticalAnimationCurve)) if let itemNode = self.itemNode as? ChatMessageInstantVideoItemNode { 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: ChatMessageTransitionNode.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: ChatMessageTransitionNode.verticalAnimationCurve.mediaTimingFunction, additive: true, completion: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.endAnimation() }) itemNode.animateFromSnapshot(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: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.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: ChatMessageTransitionNode.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: ChatMessageTransitionNode.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), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNode.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: ChatMessageTransitionNode.horizontalAnimationCurve), vertical: .animated(duration: verticalDuration, curve: ChatMessageTransitionNode.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: ChatMessageTransitionNode.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: ChatMessageTransitionNode.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), ChatMessageTransitionNode.horizontalAnimationCurve, horizontalDuration) self.contextSourceNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: sourceAbsoluteRect.maxY - targetAbsoluteRect.maxY), ChatMessageTransitionNode.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?) { 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: ChatHistoryListNode private let getContentAreaInScreenSpace: () -> CGRect private let onTransitionEvent: (ContainedViewLayoutTransition) -> Void private var currentPendingItems: [Int64: (Source, () -> Void)] = [:] private var animatingItemNodes: [AnimatingItemNode] = [] private var decorationItemNodes: [DecorationItemNode] = [] private var messageReactionContexts: [MessageReactionContext] = [] var hasScheduledTransitions: Bool { return !self.currentPendingItems.isEmpty } var hasOngoingTransitions: Bool { return !self.animatingItemNodes.isEmpty } init(listNode: ChatHistoryListNode, 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) } func add(decorationView: UIView, itemNode: ChatMessageItemView) -> DecorationItemNode { let decorationItemNode = DecorationItemNode(itemNode: itemNode, contentView: decorationView, getContentAreaInScreenSpace: self.getContentAreaInScreenSpace) decorationItemNode.updateLayout(size: self.bounds.size) self.decorationItemNodes.append(decorationItemNode) self.addSubnode(decorationItemNode) // let overlayController = OverlayTransitionContainerController() // overlayController.displayNode.isUserInteractionEnabled = false // overlayController.displayNode.addSubnode(decorationItemNode) // decorationItemNode.overlayController = overlayController // itemNode.item?.context.sharedContext.mainWindow?.presentInGlobalOverlay(overlayController) return decorationItemNode } func remove(decorationNode: DecorationItemNode) { self.decorationItemNodes.removeAll(where: { $0 === decorationNode }) decorationNode.removeFromSupernode() decorationNode.overlayController?.dismiss() } private func beginAnimation(itemNode: ChatMessageItemView, 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 itemNode.item?.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, let item = animatingItemNode.itemNode.item { for (message, _) in item.content { strongSelf.listNode.requestMessageUpdate(stableId: message.stableId) break } } } animatingItemNode.frame = self.bounds animatingItemNode.beginAnimation() self.onTransitionEvent(.animated(duration: ChatMessageTransitionNode.animationDuration, curve: ChatMessageTransitionNode.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? ChatMessageItemView { if let item = itemNode.item { for (message, _) in item.content { if message.id == messageId { messageItemNode = itemNode break } } } } } 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?) { 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) } } for messageReactionContext in self.messageReactionContexts { messageReactionContext.addExternalOffset(offset: offset, transition: transition, itemNode: itemNode) } } 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) } } for messageReactionContext in self.messageReactionContexts { messageReactionContext.addContentOffset(offset: offset, itemNode: itemNode) } } func isAnimatingMessage(stableId: UInt32) -> Bool { for itemNode in self.animatingItemNodes { if let item = itemNode.itemNode.item { for (message, _) in item.content { if message.stableId == stableId { return true } } } } return false } func scheduleUpdateMessageAfterAnimationCompleted(stableId: UInt32) { for itemNode in self.animatingItemNodes { if let item = itemNode.itemNode.item { for (message, _) in item.content { if message.stableId == stableId { itemNode.updateAfterCompletion = true } } } } } func hasScheduledUpdateMessageAfterAnimationCompleted(stableId: UInt32) -> Bool { for itemNode in self.animatingItemNodes { if let item = itemNode.itemNode.item { for (message, _) in item.content { if message.stableId == stableId { return itemNode.updateAfterCompletion } } } } return false } }