Swiftgram/submodules/TelegramUI/Sources/ChatMessageTransitionNode.swift
2024-06-11 17:54:13 +04:00

1196 lines
64 KiB
Swift

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<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 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<Int64>()
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
}
}