Swiftgram/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift
2021-12-23 22:30:30 +04:00

892 lines
47 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import ContextUI
import AnimatedStickerNode
import SwiftSignalKit
import ContextUI
import Postbox
import TelegramCore
import ReactionSelectionNode
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<Bool>()
override public var ready: Promise<Bool> {
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 {
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: GenericAnimatedStickerNode?
let placeholderNode: ASDisplayNode?
let relativeSourceRect: CGRect
init(imageNode: TransformImageNode, animationNode: GenericAnimatedStickerNode?, placeholderNode: ASDisplayNode?, relativeSourceRect: CGRect) {
self.imageNode = imageNode
self.animationNode = animationNode
self.placeholderNode = placeholderNode
self.relativeSourceRect = relativeSourceRect
}
}
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 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
}
}
case textInput(textInput: TextInput, replyPanel: ReplyAccessoryPanelNode?)
case stickerMediaInput(input: StickerInput, replyPanel: ReplyAccessoryPanelNode?)
case audioMicInput(AudioMicInput)
case videoMessage(VideoMessage)
case mediaInput(MediaInput)
}
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, relativeSourceRect: sourceItemNode.imageNode.frame)
sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view)
case let .mediaPanel(sourceItemNode):
stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: sourceItemNode.placeholderNode, relativeSourceRect: sourceItemNode.imageNode.frame)
sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view)
case let .inputPanelSearch(sourceItemNode):
stickerSource = Sticker(imageNode: sourceItemNode.imageNode, animationNode: sourceItemNode.animationNode, placeholderNode: nil, relativeSourceRect: sourceItemNode.imageNode.frame)
sourceAbsoluteRect = sourceItemNode.view.convert(stickerSource.imageNode.frame, to: self.view)
case let .emptyPanel(sourceItemNode):
stickerSource = Sticker(imageNode: sourceItemNode.stickerNode.imageNode, animationNode: sourceItemNode.stickerNode.animationNode, placeholderNode: 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:
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() {
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)
}
}
}
}
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 currentPendingItem: (Int64, Source, () -> Void)?
private var animatingItemNodes: [AnimatingItemNode] = []
private var decorationItemNodes: [DecorationItemNode] = []
private var messageReactionContexts: [MessageReactionContext] = []
var hasScheduledTransitions: Bool {
return self.currentPendingItem != nil
}
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.animationCorrelationMessageFound = { [weak self] itemNode, correlationId in
guard let strongSelf = self, let (currentId, currentSource, initiated) = strongSelf.currentPendingItem else {
return
}
if currentId == correlationId {
strongSelf.currentPendingItem = nil
strongSelf.beginAnimation(itemNode: itemNode, source: currentSource)
initiated()
}
}
}
func add(correlationId: Int64, source: Source, initiated: @escaping () -> Void) {
self.currentPendingItem = (correlationId, source, initiated)
self.listNode.setCurrentSendAnimationCorrelationId(correlationId)
}
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:
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
}
}