mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1181 lines
63 KiB
Swift
1181 lines
63 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
|
|
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, 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)
|
|
}
|
|
}
|
|
|
|
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) -> DecorationItemNode {
|
|
let decorationItemNode = DecorationItemNodeImpl(itemNode: itemNode, contentView: decorationView, 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
|
|
}
|
|
}
|