import Foundation import UIKit import AsyncDisplayKit import Display import TelegramPresentationData import TextSelectionNode import TelegramCore import SwiftSignalKit import ReactionSelectionNode import UndoUI private extension ContextControllerTakeViewInfo.ContainingItem { var contentRect: CGRect { switch self { case let .node(containingNode): return containingNode.contentRect case let .view(containingView): return containingView.contentRect } } var customHitTest: ((CGPoint) -> UIView?)? { switch self { case let .node(containingNode): return containingNode.contentNode.customHitTest case let .view(containingView): return containingView.contentView.customHitTest } } var view: UIView { switch self { case let .node(containingNode): return containingNode.view case let .view(containingView): return containingView } } var contentView: UIView { switch self { case let .node(containingNode): return containingNode.contentNode.view case let .view(containingView): return containingView.contentView } } func contentHitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { switch self { case let .node(containingNode): return containingNode.contentNode.hitTest(point, with: event) case let .view(containingView): return containingView.contentView.hitTest(point, with: event) } } var isExtractedToContextPreview: Bool { get { switch self { case let .node(containingNode): return containingNode.isExtractedToContextPreview case let .view(containingView): return containingView.isExtractedToContextPreview } } set(value) { switch self { case let .node(containingNode): containingNode.isExtractedToContextPreview = value case let .view(containingView): containingView.isExtractedToContextPreview = value } } } var willUpdateIsExtractedToContextPreview: ((Bool, ContainedViewLayoutTransition) -> Void)? { switch self { case let .node(containingNode): return containingNode.willUpdateIsExtractedToContextPreview case let .view(containingView): return containingView.willUpdateIsExtractedToContextPreview } } var isExtractedToContextPreviewUpdated: ((Bool) -> Void)? { switch self { case let .node(containingNode): return containingNode.isExtractedToContextPreviewUpdated case let .view(containingView): return containingView.isExtractedToContextPreviewUpdated } } var layoutUpdated: ((CGSize, ListViewItemUpdateAnimation) -> Void)? { get { switch self { case let .node(containingNode): return containingNode.layoutUpdated case let .view(containingView): return containingView.layoutUpdated } } set(value) { switch self { case let .node(containingNode): containingNode.layoutUpdated = value case let .view(containingView): containingView.layoutUpdated = value } } } } final class ContextControllerExtractedPresentationNode: ASDisplayNode, ContextControllerPresentationNode, UIScrollViewDelegate { enum ContentSource { case location(ContextLocationContentSource) case reference(ContextReferenceContentSource) case extracted(ContextExtractedContentSource) case controller(ContextControllerContentSource) } private final class ItemContentNode: ASDisplayNode { let offsetContainerNode: ASDisplayNode var containingItem: ContextControllerTakeViewInfo.ContainingItem var animateClippingFromContentAreaInScreenSpace: CGRect? var storedGlobalFrame: CGRect? init(containingItem: ContextControllerTakeViewInfo.ContainingItem) { self.offsetContainerNode = ASDisplayNode() self.containingItem = containingItem super.init() self.addSubnode(self.offsetContainerNode) } func update(presentationData: PresentationData, size: CGSize, transition: ContainedViewLayoutTransition) { } func takeContainingNode() { switch self.containingItem { case let .node(containingNode): if containingNode.contentNode.supernode !== self.offsetContainerNode { self.offsetContainerNode.addSubnode(containingNode.contentNode) } case let .view(containingView): if containingView.contentView.superview !== self.offsetContainerNode.view { self.offsetContainerNode.view.addSubview(containingView.contentView) } } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } if !self.containingItem.contentRect.contains(point) { return nil } return self.view } } private final class ControllerContentNode: ASDisplayNode { let controller: ViewController let passthroughTouches: Bool var storedContentHeight: CGFloat? init(controller: ViewController, passthroughTouches: Bool) { self.controller = controller self.passthroughTouches = passthroughTouches super.init() self.clipsToBounds = true self.cornerRadius = 14.0 self.addSubnode(self.controller.displayNode) } func update(presentationData: PresentationData, parentLayout: ContainerViewLayout, size: CGSize, transition: ContainedViewLayoutTransition) { transition.updateFrame(node: self.controller.displayNode, frame: CGRect(origin: CGPoint(), size: size)) self.controller.containerLayoutUpdated( ContainerViewLayout( size: size, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: parentLayout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false ), transition: transition ) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } if self.passthroughTouches { let controllerPoint = self.view.convert(point, to: self.controller.view) if let result = self.controller.view.hitTest(controllerPoint, with: event) { return result } } return self.view } } private final class AnimatingOutState { var currentContentScreenFrame: CGRect init( currentContentScreenFrame: CGRect ) { self.currentContentScreenFrame = currentContentScreenFrame } } private let _ready = Promise() private var didSetReady: Bool = false var ready: Signal { return self._ready.get() } private let getController: () -> ContextControllerProtocol? private let requestUpdate: (ContainedViewLayoutTransition) -> Void private let requestUpdateOverlayWantsToBeBelowKeyboard: (ContainedViewLayoutTransition) -> Void private let requestDismiss: (ContextMenuActionResult) -> Void private let requestAnimateOut: (ContextMenuActionResult, @escaping () -> Void) -> Void private let source: ContentSource private let dismissTapNode: ASDisplayNode private let dismissAccessibilityArea: AccessibilityAreaNode private let clippingNode: ASDisplayNode private let scroller: UIScrollView private let scrollNode: ASDisplayNode private var reactionContextNode: ReactionContextNode? private var reactionContextNodeIsAnimatingOut: Bool = false private var itemContentNode: ItemContentNode? private var controllerContentNode: ControllerContentNode? private let contentRectDebugNode: ASDisplayNode private var actionsContainerNode: ASDisplayNode private let actionsStackNode: ContextControllerActionsStackNode private let additionalActionsStackNode: ContextControllerActionsStackNode private var validLayout: ContainerViewLayout? private var animatingOutState: AnimatingOutState? private var strings: PresentationStrings? private enum OverscrollMode { case unrestricted case topOnly case disabled } private var overscrollMode: OverscrollMode = .unrestricted private weak var currentUndoController: ViewController? init( getController: @escaping () -> ContextControllerProtocol?, requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, requestUpdateOverlayWantsToBeBelowKeyboard: @escaping (ContainedViewLayoutTransition) -> Void, requestDismiss: @escaping (ContextMenuActionResult) -> Void, requestAnimateOut: @escaping (ContextMenuActionResult, @escaping () -> Void) -> Void, source: ContentSource ) { self.getController = getController self.requestUpdate = requestUpdate self.requestUpdateOverlayWantsToBeBelowKeyboard = requestUpdateOverlayWantsToBeBelowKeyboard self.requestDismiss = requestDismiss self.requestAnimateOut = requestAnimateOut self.source = source self.dismissTapNode = ASDisplayNode() self.dismissAccessibilityArea = AccessibilityAreaNode() self.dismissAccessibilityArea.accessibilityTraits = .button self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true self.scroller = UIScrollView() self.scroller.canCancelContentTouches = true self.scroller.delaysContentTouches = false self.scroller.showsVerticalScrollIndicator = false if #available(iOS 11.0, *) { self.scroller.contentInsetAdjustmentBehavior = .never } self.scroller.alwaysBounceVertical = true self.scrollNode = ASDisplayNode() self.scrollNode.view.addGestureRecognizer(self.scroller.panGestureRecognizer) self.contentRectDebugNode = ASDisplayNode() self.contentRectDebugNode.isUserInteractionEnabled = false self.contentRectDebugNode.backgroundColor = UIColor.red.withAlphaComponent(0.2) self.actionsContainerNode = ASDisplayNode() self.actionsStackNode = ContextControllerActionsStackNode( getController: getController, requestDismiss: { result in requestDismiss(result) }, requestUpdate: requestUpdate ) self.additionalActionsStackNode = ContextControllerActionsStackNode( getController: getController, requestDismiss: { result in requestDismiss(result) }, requestUpdate: requestUpdate ) super.init() self.view.addSubview(self.scroller) self.scroller.isHidden = true self.addSubnode(self.clippingNode) self.clippingNode.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.dismissTapNode) self.scrollNode.addSubnode(self.dismissAccessibilityArea) self.scrollNode.addSubnode(self.actionsContainerNode) self.actionsContainerNode.addSubnode(self.additionalActionsStackNode) self.actionsContainerNode.addSubnode(self.actionsStackNode) #if DEBUG //self.addSubnode(self.contentRectDebugNode) #endif self.scroller.delegate = self self.dismissTapNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dismissTapGesture(_:)))) self.dismissAccessibilityArea.activate = { [weak self] in self?.requestDismiss(.default) return true } } @objc func dismissTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.requestDismiss(.default) } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if self.bounds.contains(point) { if let reactionContextNode = self.reactionContextNode { if let result = reactionContextNode.hitTest(self.view.convert(point, to: reactionContextNode.view), with: event) { return result } } if case let .extracted(source) = self.source, !source.ignoreContentTouches, let contentNode = self.itemContentNode { let contentPoint = self.view.convert(point, to: contentNode.containingItem.contentView) if let result = contentNode.containingItem.customHitTest?(contentPoint) { return result } else if let result = contentNode.containingItem.contentHitTest(contentPoint, with: event) { if result is TextSelectionNodeView { return result } else if contentNode.containingItem.contentRect.contains(contentPoint) { return contentNode.containingItem.contentView } } } else if case .controller = self.source, let contentNode = self.controllerContentNode { let contentPoint = self.view.convert(point, to: contentNode.view) let _ = contentPoint //TODO: } if let result = self.scrollNode.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event) { if let reactionContextNode = self.reactionContextNode, reactionContextNode.isExpanded { if result === self.actionsContainerNode.view { return self.dismissTapNode.view } } return result } return nil } else { return nil } } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if let reactionContextNode = self.reactionContextNode, (reactionContextNode.isExpanded || !reactionContextNode.canBeExpanded) { self.overscrollMode = .disabled self.scroller.alwaysBounceVertical = false } else { if scrollView.contentSize.height > scrollView.bounds.height { self.overscrollMode = .unrestricted self.scroller.alwaysBounceVertical = true } else { if self.reactionContextNode != nil { self.overscrollMode = .topOnly self.scroller.alwaysBounceVertical = true } else { self.overscrollMode = .disabled self.scroller.alwaysBounceVertical = false } } } } func scrollViewDidScroll(_ scrollView: UIScrollView) { var adjustedBounds = scrollView.bounds var topOverscroll: CGFloat = 0.0 switch self.overscrollMode { case .unrestricted: if adjustedBounds.origin.y < 0.0 { topOverscroll = -adjustedBounds.origin.y } case .disabled: break case .topOnly: if scrollView.contentSize.height <= scrollView.bounds.height { if adjustedBounds.origin.y > 0.0 { adjustedBounds.origin.y = 0.0 } else { adjustedBounds.origin.y = floorToScreenPixels(adjustedBounds.origin.y * 0.35) topOverscroll = -adjustedBounds.origin.y } } else { if adjustedBounds.origin.y < 0.0 { adjustedBounds.origin.y = floorToScreenPixels(adjustedBounds.origin.y * 0.35) topOverscroll = -adjustedBounds.origin.y } else if adjustedBounds.origin.y + adjustedBounds.height > scrollView.contentSize.height { adjustedBounds.origin.y = scrollView.contentSize.height - adjustedBounds.height } } } self.scrollNode.bounds = adjustedBounds if let reactionContextNode = self.reactionContextNode { let isIntersectingContent = adjustedBounds.minY >= 10.0 reactionContextNode.updateIsIntersectingContent(isIntersectingContent: isIntersectingContent, transition: .animated(duration: 0.25, curve: .easeInOut)) if !reactionContextNode.isExpanded && reactionContextNode.canBeExpanded { if topOverscroll > 30.0 && self.scroller.isDragging { self.scroller.panGestureRecognizer.state = .cancelled reactionContextNode.expand() } else { reactionContextNode.updateExtension(distance: topOverscroll) } } } } func highlightGestureMoved(location: CGPoint, hover: Bool) { self.actionsStackNode.highlightGestureMoved(location: self.view.convert(location, to: self.actionsStackNode.view)) if let reactionContextNode = self.reactionContextNode { reactionContextNode.highlightGestureMoved(location: self.view.convert(location, to: reactionContextNode.view), hover: hover) } } func highlightGestureFinished(performAction: Bool) { self.actionsStackNode.highlightGestureFinished(performAction: performAction) if let reactionContextNode = self.reactionContextNode { reactionContextNode.highlightGestureFinished(performAction: performAction) } } func decreaseHighlightedIndex() { self.actionsStackNode.decreaseHighlightedIndex() } func increaseHighlightedIndex() { self.actionsStackNode.increaseHighlightedIndex() } func wantsDisplayBelowKeyboard() -> Bool { if let reactionContextNode = self.reactionContextNode { return reactionContextNode.wantsDisplayBelowKeyboard() } else { return false } } func replaceItems(items: ContextController.Items, animated: Bool?) { if case .twoLists = items.content { let stackItems = makeContextControllerActionsStackItem(items: items) self.actionsStackNode.replace(item: stackItems.first!, animated: animated) self.additionalActionsStackNode.replace(item: stackItems.last!, animated: animated) } else { self.actionsStackNode.replace(item: makeContextControllerActionsStackItem(items: items).first!, animated: animated) } } func pushItems(items: ContextController.Items) { let currentScrollingState = self.getCurrentScrollingState() var positionLock: CGFloat? if !items.disablePositionLock { positionLock = self.getActionsStackPositionLock() } if self.actionsStackNode.topPositionLock == nil { if let contentNode = self.controllerContentNode, contentNode.bounds.height != 0.0 { contentNode.storedContentHeight = contentNode.bounds.height } } self.actionsStackNode.push(item: makeContextControllerActionsStackItem(items: items).first!, currentScrollingState: currentScrollingState, positionLock: positionLock, animated: true) } func popItems() { self.actionsStackNode.pop() if self.actionsStackNode.topPositionLock == nil { if let contentNode = self.controllerContentNode { contentNode.storedContentHeight = nil } } } private func getCurrentScrollingState() -> CGFloat { return self.scrollNode.bounds.minY } private func getActionsStackPositionLock() -> CGFloat? { switch self.source { case .location, .reference: return nil case .extracted, .controller: return self.actionsStackNode.view.convert(CGPoint(), to: self.view).y } } private var proposedReactionsPositionLock: CGFloat? private var currentReactionsPositionLock: CGFloat? private func setCurrentReactionsPositionLock() { self.currentReactionsPositionLock = self.proposedReactionsPositionLock } private func getCurrentReactionsPositionLock() -> CGFloat? { return self.currentReactionsPositionLock } func update( presentationData: PresentationData, layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, stateTransition: ContextControllerPresentationNodeStateTransition? ) { self.validLayout = layout var contentActionsSpacing: CGFloat = 7.0 let actionsEdgeInset: CGFloat let actionsSideInset: CGFloat let topInset: CGFloat = layout.insets(options: .statusBar).top + 8.0 let bottomInset: CGFloat = 10.0 let itemContentNode: ItemContentNode? let controllerContentNode: ControllerContentNode? var contentTransition = transition if self.strings !== presentationData.strings { self.strings = presentationData.strings self.dismissAccessibilityArea.accessibilityLabel = presentationData.strings.VoiceOver_DismissContextMenu } switch self.source { case .location, .reference: actionsEdgeInset = 16.0 actionsSideInset = 6.0 case .extracted: actionsEdgeInset = 12.0 actionsSideInset = 6.0 case .controller: actionsEdgeInset = 12.0 actionsSideInset = -2.0 contentActionsSpacing += 3.0 } transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) if self.scrollNode.frame != CGRect(origin: CGPoint(), size: layout.size) { transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) transition.updateFrame(view: self.scroller, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) } if let current = self.itemContentNode { itemContentNode = current } else { switch self.source { case .location, .reference, .controller: itemContentNode = nil case let .extracted(source): guard let takeInfo = source.takeView() else { return } let contentNodeValue = ItemContentNode(containingItem: takeInfo.containingItem) contentNodeValue.animateClippingFromContentAreaInScreenSpace = takeInfo.contentAreaInScreenSpace self.scrollNode.insertSubnode(contentNodeValue, aboveSubnode: self.actionsContainerNode) self.itemContentNode = contentNodeValue itemContentNode = contentNodeValue contentTransition = .immediate } } if let current = self.controllerContentNode { controllerContentNode = current } else { switch self.source { case let .controller(source): let controllerContentNodeValue = ControllerContentNode(controller: source.controller, passthroughTouches: source.passthroughTouches) //source.controller.viewWillAppear(false) //source.controller.setIgnoreAppearanceMethodInvocations(true) self.scrollNode.insertSubnode(controllerContentNodeValue, aboveSubnode: self.actionsContainerNode) self.controllerContentNode = controllerContentNodeValue controllerContentNode = controllerContentNodeValue contentTransition = .immediate //source.controller.setIgnoreAppearanceMethodInvocations(false) //source.controller.viewDidAppear(false) case .location, .reference, .extracted: controllerContentNode = nil } } var animateReactionsIn = false var contentTopInset: CGFloat = topInset var removedReactionContextNode: ReactionContextNode? if let reactionItems = self.actionsStackNode.topReactionItems, !reactionItems.reactionItems.isEmpty { let reactionContextNode: ReactionContextNode if let current = self.reactionContextNode { reactionContextNode = current } else { reactionContextNode = ReactionContextNode( context: reactionItems.context, animationCache: reactionItems.animationCache, presentationData: presentationData, items: reactionItems.reactionItems, selectedItems: reactionItems.selectedReactionItems, title: reactionItems.reactionsTitle, alwaysAllowPremiumReactions: reactionItems.alwaysAllowPremiumReactions, getEmojiContent: reactionItems.getEmojiContent, isExpandedUpdated: { [weak self] transition in guard let strongSelf = self else { return } strongSelf.setCurrentReactionsPositionLock() strongSelf.requestUpdate(transition) }, requestLayout: { [weak self] transition in guard let strongSelf = self else { return } strongSelf.requestUpdate(transition) }, requestUpdateOverlayWantsToBeBelowKeyboard: { [weak self] transition in guard let strongSelf = self else { return } strongSelf.requestUpdateOverlayWantsToBeBelowKeyboard(transition) } ) self.reactionContextNode = reactionContextNode self.addSubnode(reactionContextNode) if transition.isAnimated { animateReactionsIn = true } reactionContextNode.reactionSelected = { [weak self] reaction, isLarge in guard let strongSelf = self, let controller = strongSelf.getController() as? ContextController else { return } controller.reactionSelected?(reaction, isLarge) } let context = reactionItems.context reactionContextNode.premiumReactionsSelected = { [weak self] file in guard let strongSelf = self, let validLayout = strongSelf.validLayout, let controller = strongSelf.getController() as? ContextController else { return } if let file = file, let reactionContextNode = strongSelf.reactionContextNode { let position: UndoOverlayController.Position let insets = validLayout.insets(options: .statusBar) if reactionContextNode.hasSpaceInTheBottom(insets: insets, height: 100.0) { position = .bottom } else { position = .top } var animateInAsReplacement = false if let currentUndoController = strongSelf.currentUndoController { currentUndoController.dismiss() animateInAsReplacement = true } let presentationData = context.sharedContext.currentPresentationData.with { $0 } let undoController = UndoOverlayController(presentationData: presentationData, content: .sticker(context: context, file: file, loop: true, title: nil, text: presentationData.strings.Chat_PremiumReactionToastTitle, undoText: presentationData.strings.Chat_PremiumReactionToastAction, customAction: { [weak controller] in controller?.premiumReactionsSelected?() }), elevatedLayout: false, position: position, animateInAsReplacement: animateInAsReplacement, action: { _ in true }) strongSelf.currentUndoController = undoController controller.present(undoController, in: .current) } else { controller.premiumReactionsSelected?() } } } contentTopInset += reactionContextNode.contentHeight + 18.0 } else if let reactionContextNode = self.reactionContextNode { self.reactionContextNode = nil removedReactionContextNode = reactionContextNode } if let contentNode = itemContentNode { switch stateTransition { case .animateIn, .animateOut: contentNode.storedGlobalFrame = convertFrame(contentNode.containingItem.contentRect, from: contentNode.containingItem.view, to: self.view) case .none: if contentNode.storedGlobalFrame == nil { contentNode.storedGlobalFrame = convertFrame(contentNode.containingItem.contentRect, from: contentNode.containingItem.view, to: self.view) } } } let contentParentGlobalFrame: CGRect var contentRect: CGRect var isContentResizeableVertically: Bool = false let _ = isContentResizeableVertically switch self.source { case let .location(location): if let transitionInfo = location.transitionInfo() { contentRect = CGRect(origin: transitionInfo.location, size: CGSize(width: 1.0, height: 1.0)) contentParentGlobalFrame = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minX), size: CGSize(width: layout.size.width, height: contentRect.height)) } else { return } case let .reference(reference): if let transitionInfo = reference.transitionInfo() { contentRect = convertFrame(transitionInfo.referenceView.bounds.inset(by: transitionInfo.insets), from: transitionInfo.referenceView, to: self.view).insetBy(dx: -2.0, dy: 0.0) contentRect.size.width += 5.0 contentParentGlobalFrame = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minX), size: CGSize(width: layout.size.width, height: contentRect.height)) } else { return } case .extracted: if let contentNode = itemContentNode { contentParentGlobalFrame = convertFrame(contentNode.containingItem.view.bounds, from: contentNode.containingItem.view, to: self.view) let contentRectGlobalFrame = CGRect(origin: CGPoint(x: contentNode.containingItem.contentRect.minX, y: (contentNode.storedGlobalFrame?.maxY ?? 0.0) - contentNode.containingItem.contentRect.height), size: contentNode.containingItem.contentRect.size) contentRect = CGRect(origin: CGPoint(x: contentRectGlobalFrame.minX, y: contentRectGlobalFrame.maxY - contentNode.containingItem.contentRect.size.height), size: contentNode.containingItem.contentRect.size) if case .animateOut = stateTransition { contentRect.origin.y = self.contentRectDebugNode.frame.maxY - contentRect.size.height } } else { return } case let .controller(source): if let contentNode = controllerContentNode { var defaultContentSize = CGSize(width: layout.size.width - 12.0 * 2.0, height: layout.size.height - 12.0 * 2.0 - contentTopInset - layout.safeInsets.bottom) if case .regular = layout.metrics.widthClass { defaultContentSize.width = min(defaultContentSize.width, 400.0) } defaultContentSize.height = min(defaultContentSize.height, 460.0) let contentSize: CGSize if let preferredSize = contentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: defaultContentSize, metrics: LayoutMetrics(widthClass: .compact, heightClass: .compact, orientation: nil), deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: false)) { contentSize = preferredSize } else if let storedContentHeight = contentNode.storedContentHeight { contentSize = CGSize(width: defaultContentSize.width, height: storedContentHeight) } else { contentSize = defaultContentSize isContentResizeableVertically = true } if case .regular = layout.metrics.widthClass { if let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() { let sourcePoint = sourceView.convert(sourceRect.center, to: self.view) contentRect = CGRect(origin: CGPoint(x: sourcePoint.x - floor(contentSize.width * 0.5), y: sourcePoint.y - floor(contentSize.height * 0.5)), size: contentSize) if contentRect.origin.x < 0.0 { contentRect.origin.x = 0.0 } if contentRect.origin.y < 0.0 { contentRect.origin.y = 0.0 } if contentRect.origin.x + contentRect.width > layout.size.width { contentRect.origin.x = layout.size.width - contentRect.width } if contentRect.origin.y + contentRect.height > layout.size.height { contentRect.origin.y = layout.size.height - contentRect.height } } else { contentRect = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) * 0.5), y: floor((layout.size.height - contentSize.height) * 0.5)), size: contentSize) } } else { contentRect = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width) * 0.5), y: floor((layout.size.height - contentSize.height) * 0.5)), size: contentSize) } contentParentGlobalFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height)) } else { return } } let keepInPlace: Bool let actionsHorizontalAlignment: ContextActionsHorizontalAlignment switch self.source { case .location, .reference: keepInPlace = true actionsHorizontalAlignment = .default case let .extracted(source): keepInPlace = source.keepInPlace actionsHorizontalAlignment = source.actionsHorizontalAlignment case .controller: //TODO: keepInPlace = false actionsHorizontalAlignment = .default } var defaultScrollY: CGFloat = 0.0 if self.animatingOutState == nil { if let contentNode = itemContentNode { contentNode.update( presentationData: presentationData, size: contentNode.containingItem.view.bounds.size, transition: contentTransition ) } let actionsConstrainedHeight: CGFloat if let actionsPositionLock = self.actionsStackNode.topPositionLock { actionsConstrainedHeight = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - actionsPositionLock } else { if case let .reference(reference) = self.source, reference.keepInPlace { actionsConstrainedHeight = layout.size.height - contentRect.maxY - contentActionsSpacing - bottomInset - layout.intrinsicInsets.bottom } else { actionsConstrainedHeight = layout.size.height - contentTopInset - contentRect.height - contentActionsSpacing - bottomInset - layout.intrinsicInsets.bottom } } let actionsStackPresentation: ContextControllerActionsStackNode.Presentation switch self.source { case .location, .reference, .controller: actionsStackPresentation = .inline case .extracted: actionsStackPresentation = .modal } let additionalActionsSize = self.additionalActionsStackNode.update( presentationData: presentationData, constrainedSize: CGSize(width: layout.size.width, height: actionsConstrainedHeight), presentation: .additional, transition: transition ) self.additionalActionsStackNode.isHidden = additionalActionsSize.height.isZero let actionsSize = self.actionsStackNode.update( presentationData: presentationData, constrainedSize: CGSize(width: layout.size.width, height: actionsConstrainedHeight), presentation: actionsStackPresentation, transition: transition ) if isContentResizeableVertically && self.actionsStackNode.topPositionLock == nil { var contentHeight = layout.size.height - contentTopInset - contentActionsSpacing - bottomInset - layout.intrinsicInsets.bottom - actionsSize.height contentHeight = min(contentHeight, contentRect.height) contentHeight = max(contentHeight, 200.0) if case .regular = layout.metrics.widthClass { } else { contentRect = CGRect(origin: CGPoint(x: contentRect.minX, y: floor(contentRect.midY - contentHeight * 0.5)), size: CGSize(width: contentRect.width, height: contentHeight)) } } var isAnimatingOut = false if case .animateOut = stateTransition { isAnimatingOut = true } else { if let currentReactionsPositionLock = self.currentReactionsPositionLock, let reactionContextNode = self.reactionContextNode { contentRect.origin.y = currentReactionsPositionLock + reactionContextNode.contentHeight + 18.0 + reactionContextNode.visibleExtensionDistance } else if let topPositionLock = self.actionsStackNode.topPositionLock { contentRect.origin.y = topPositionLock - contentActionsSpacing - contentRect.height } else if keepInPlace { } else { if contentRect.minY < contentTopInset { contentRect.origin.y = contentTopInset } var combinedBounds = CGRect(origin: CGPoint(x: 0.0, y: contentRect.minY), size: CGSize(width: layout.size.width, height: contentRect.height + contentActionsSpacing + actionsSize.height)) if combinedBounds.maxY > layout.size.height - bottomInset - layout.intrinsicInsets.bottom { combinedBounds.origin.y = layout.size.height - bottomInset - layout.intrinsicInsets.bottom - combinedBounds.height } if combinedBounds.minY < contentTopInset { combinedBounds.origin.y = contentTopInset } contentRect.origin.y = combinedBounds.minY } } if let reactionContextNode = self.reactionContextNode { var reactionContextNodeTransition = transition if reactionContextNode.frame.isEmpty { reactionContextNodeTransition = .immediate } reactionContextNodeTransition.updateFrame(node: reactionContextNode, frame: CGRect(origin: CGPoint(), size: layout.size), beginWithCurrentState: true) var reactionAnchorRect = contentRect.offsetBy(dx: contentParentGlobalFrame.minX, dy: 0.0) let bottomInset = layout.insets(options: [.input]).bottom var isCoveredByInput = false if reactionAnchorRect.minY > layout.size.height - bottomInset { reactionAnchorRect.origin.y = layout.size.height - bottomInset isCoveredByInput = true } reactionContextNode.updateLayout(size: layout.size, insets: UIEdgeInsets(top: topInset, left: layout.safeInsets.left, bottom: 0.0, right: layout.safeInsets.right), anchorRect: reactionAnchorRect, isCoveredByInput: isCoveredByInput, isAnimatingOut: isAnimatingOut, transition: reactionContextNodeTransition) if reactionContextNode.alwaysAllowPremiumReactions { self.proposedReactionsPositionLock = contentRect.minY - 18.0 - reactionContextNode.contentHeight } else { self.proposedReactionsPositionLock = contentRect.minY - 18.0 - reactionContextNode.contentHeight - (46.0 + 54.0 - 4.0) } } else { self.proposedReactionsPositionLock = nil } if let _ = self.currentReactionsPositionLock { transition.updateAlpha(node: self.actionsStackNode, alpha: 0.0) } else { transition.updateAlpha(node: self.actionsStackNode, alpha: 1.0) } if let removedReactionContextNode = removedReactionContextNode { removedReactionContextNode.animateOut(to: contentRect, animatingOutToReaction: false) transition.updateAlpha(node: removedReactionContextNode, alpha: 0.0, completion: { [weak removedReactionContextNode] _ in removedReactionContextNode?.removeFromSupernode() }) } transition.updateFrame(node: self.contentRectDebugNode, frame: contentRect, beginWithCurrentState: true) var actionsFrame: CGRect if case let .reference(source) = self.source, let actionsPosition = source.transitionInfo()?.actionsPosition, case .top = actionsPosition { actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.minY - contentActionsSpacing - actionsSize.height), size: actionsSize) } else { actionsFrame = CGRect(origin: CGPoint(x: actionsSideInset, y: contentRect.maxY + contentActionsSpacing), size: actionsSize) } var contentVerticalOffset: CGFloat = 0.0 if keepInPlace, case .extracted = self.source { actionsFrame.origin.y = contentRect.minY - contentActionsSpacing - actionsFrame.height let statusBarHeight = (layout.statusBarHeight ?? 0.0) if actionsFrame.origin.y < statusBarHeight { let updatedActionsOriginY = statusBarHeight + contentActionsSpacing let delta = updatedActionsOriginY - actionsFrame.origin.y actionsFrame.origin.y = updatedActionsOriginY contentVerticalOffset = delta } } var additionalVisibleOffsetY: CGFloat = 0.0 if let reactionContextNode = self.reactionContextNode { additionalVisibleOffsetY += reactionContextNode.visibleExtensionDistance } if case .center = actionsHorizontalAlignment { actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0) if actionsFrame.maxX > layout.size.width - actionsEdgeInset { actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width } if actionsFrame.minX < actionsEdgeInset { actionsFrame.origin.x = actionsEdgeInset } } else { if case .location = self.source { actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.minX + actionsSideInset - 4.0 } else if case .right = actionsHorizontalAlignment { actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0 } else { if contentRect.midX < layout.size.width / 2.0 { actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.minX + actionsSideInset - 4.0 } else { switch self.source { case .location, .reference: actionsFrame.origin.x = floor(contentParentGlobalFrame.minX + contentRect.midX - actionsFrame.width / 2.0) if actionsFrame.maxX > layout.size.width - actionsEdgeInset { actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width } if actionsFrame.minX < actionsEdgeInset { actionsFrame.origin.x = actionsEdgeInset } case .extracted: actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0 case .controller: //TODO: actionsFrame.origin.x = contentParentGlobalFrame.minX + contentRect.maxX - actionsSideInset - actionsSize.width - 1.0 } } } if actionsFrame.maxX > layout.size.width - actionsEdgeInset { actionsFrame.origin.x = layout.size.width - actionsEdgeInset - actionsFrame.width } if actionsFrame.minX < actionsEdgeInset { actionsFrame.origin.x = actionsEdgeInset } } if case let .reference(reference) = self.source, let transitionInfo = reference.transitionInfo(), let customPosition = transitionInfo.customPosition { actionsFrame = actionsFrame.offsetBy(dx: customPosition.x, dy: customPosition.y) } var additionalActionsFrame: CGRect let combinedActionsFrame: CGRect if additionalActionsSize.height > 0.0 { additionalActionsFrame = CGRect(origin: actionsFrame.origin, size: additionalActionsSize) actionsFrame = actionsFrame.offsetBy(dx: 0.0, dy: additionalActionsSize.height + 10.0) combinedActionsFrame = actionsFrame.union(additionalActionsFrame) } else { additionalActionsFrame = .zero combinedActionsFrame = actionsFrame } transition.updateFrame(node: self.actionsContainerNode, frame: combinedActionsFrame.offsetBy(dx: 0.0, dy: additionalVisibleOffsetY)) transition.updateFrame(node: self.actionsStackNode, frame: CGRect(origin: CGPoint(x: 0.0, y: combinedActionsFrame.height - actionsSize.height), size: actionsSize), beginWithCurrentState: true) transition.updateFrame(node: self.additionalActionsStackNode, frame: CGRect(origin: .zero, size: additionalActionsSize), beginWithCurrentState: true) if let contentNode = itemContentNode { var contentFrame = CGRect(origin: CGPoint(x: contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX, y: contentRect.minY - contentNode.containingItem.contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentNode.containingItem.view.bounds.size) if case let .extracted(extracted) = self.source { if extracted.centerVertically { if combinedActionsFrame.height.isZero { contentFrame.origin.y = floorToScreenPixels((layout.size.height - contentFrame.height) / 2.0) } else if contentFrame.midX > layout.size.width / 2.0 { contentFrame.origin.x = layout.size.width - contentFrame.maxX } } } contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true) } if let contentNode = controllerContentNode { //TODO: var contentFrame = CGRect(origin: CGPoint(x: contentRect.minX, y: contentRect.minY + contentVerticalOffset + additionalVisibleOffsetY), size: contentRect.size) if case let .extracted(extracted) = self.source, extracted.centerVertically { if combinedActionsFrame.height.isZero { contentFrame.origin.y = floorToScreenPixels((layout.size.height - contentFrame.height) / 2.0) } else if contentFrame.midX > layout.size.width / 2.0 { contentFrame.origin.x = layout.size.width - contentFrame.maxX } } contentTransition.updateFrame(node: contentNode, frame: contentFrame, beginWithCurrentState: true) contentNode.update( presentationData: presentationData, parentLayout: layout, size: contentFrame.size, transition: contentTransition ) } let contentHeight: CGFloat if self.actionsStackNode.topPositionLock != nil || self.currentReactionsPositionLock != nil { contentHeight = layout.size.height } else { if keepInPlace, case .extracted = self.source { contentHeight = (layout.statusBarHeight ?? 0.0) + actionsFrame.height + abs(actionsFrame.minY) + bottomInset + layout.intrinsicInsets.bottom } else { contentHeight = actionsFrame.maxY + bottomInset + layout.intrinsicInsets.bottom } } let contentSize = CGSize(width: layout.size.width, height: contentHeight) if self.scroller.contentSize != contentSize { let previousContentOffset = self.scroller.contentOffset self.scroller.contentSize = contentSize if let storedScrollingState = self.actionsStackNode.storedScrollingState { self.actionsStackNode.clearStoredScrollingState() self.scroller.contentOffset = CGPoint(x: 0.0, y: storedScrollingState) } if case .none = stateTransition, transition.isAnimated { let contentOffset = self.scroller.contentOffset transition.animateOffsetAdditive(layer: self.scrollNode.layer, offset: previousContentOffset.y - contentOffset.y) } } self.actionsStackNode.updatePanSelection(isEnabled: contentSize.height <= layout.size.height) defaultScrollY = contentSize.height - layout.size.height if defaultScrollY < 0.0 { defaultScrollY = 0.0 } self.dismissTapNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: max(contentSize.height, layout.size.height))) self.dismissAccessibilityArea.frame = CGRect(origin: CGPoint(), size: CGSize(width: contentSize.width, height: max(contentSize.height, layout.size.height))) } switch stateTransition { case .animateIn: let actionsSize = self.actionsContainerNode.bounds.size if let contentNode = itemContentNode { contentNode.takeContainingNode() } let duration: Double = 0.42 let springDamping: CGFloat = 104.0 self.scroller.contentOffset = CGPoint(x: 0.0, y: defaultScrollY) var animationInContentYDistance: CGFloat let currentContentScreenFrame: CGRect if let contentNode = itemContentNode { if let animateClippingFromContentAreaInScreenSpace = contentNode.animateClippingFromContentAreaInScreenSpace { self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(x: 0.0, y: animateClippingFromContentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: animateClippingFromContentAreaInScreenSpace.height)), to: CGRect(origin: CGPoint(), size: layout.size), duration: 0.2) self.clippingNode.layer.animateBoundsOriginYAdditive(from: animateClippingFromContentAreaInScreenSpace.minY, to: 0.0, duration: 0.2) } currentContentScreenFrame = convertFrame(contentNode.containingItem.contentRect, from: contentNode.containingItem.view, to: self.view) let currentContentLocalFrame = convertFrame(contentRect, from: self.scrollNode.view, to: self.view) animationInContentYDistance = currentContentLocalFrame.maxY - currentContentScreenFrame.maxY var animationInContentXDistance: CGFloat = 0.0 let contentX = contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX let contentWidth = contentNode.containingItem.view.bounds.size.width let contentHeight = contentNode.containingItem.view.bounds.size.height if case let .extracted(extracted) = self.source, extracted.centerVertically { if actionsSize.height.isZero { var initialContentRect = contentRect initialContentRect.origin.y += extracted.initialAppearanceOffset.y let fixedContentY = floorToScreenPixels((layout.size.height - contentHeight) / 2.0) animationInContentYDistance = fixedContentY - initialContentRect.minY } else if contentX + contentWidth > layout.size.width / 2.0, actionsSize.height > 0.0 { let fixedContentX = layout.size.width - (contentX + contentWidth) animationInContentXDistance = fixedContentX - contentX contentNode.layer.animateSpring( from: -animationInContentXDistance as NSNumber, to: 0.0 as NSNumber, keyPath: "position.x", duration: duration, delay: 0.0, initialVelocity: 0.0, damping: springDamping, additive: true ) } } contentNode.layer.animateSpring( from: -animationInContentYDistance as NSNumber, to: 0.0 as NSNumber, keyPath: "position.y", duration: duration, delay: 0.0, initialVelocity: 0.0, damping: springDamping, additive: true ) } else if let contentNode = controllerContentNode { if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() { let sourcePoint = sourceView.convert(sourceRect.center, to: self.view) animationInContentYDistance = contentRect.midY - sourcePoint.y } else { animationInContentYDistance = 0.0 } currentContentScreenFrame = contentRect contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) contentNode.layer.animateSpring( from: -animationInContentYDistance as NSNumber, to: 0.0 as NSNumber, keyPath: "position.y", duration: duration, delay: 0.0, initialVelocity: 0.0, damping: springDamping, additive: true ) contentNode.layer.animateSpring( from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, delay: 0.0, initialVelocity: 0.0, damping: springDamping, additive: false ) } else { animationInContentYDistance = 0.0 currentContentScreenFrame = contentRect } self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: self.actionsContainerNode.alpha, duration: 0.05) self.actionsContainerNode.layer.animateSpring( from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: duration, delay: 0.0, initialVelocity: 0.0, damping: springDamping, additive: false ) var actionsPositionDeltaXDistance: CGFloat = 0.0 if case .center = actionsHorizontalAlignment { actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX } if case .reference = self.source { actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX } let actionsVerticalTransitionDirection: CGFloat if let contentNode = itemContentNode { if contentNode.frame.minY < self.actionsContainerNode.frame.minY { actionsVerticalTransitionDirection = -1.0 } else { actionsVerticalTransitionDirection = 1.0 } } else { if contentRect.minY < self.actionsContainerNode.frame.minY { actionsVerticalTransitionDirection = -1.0 } else { actionsVerticalTransitionDirection = 1.0 } } let actionsPositionDeltaYDistance = -animationInContentYDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing self.actionsContainerNode.layer.animateSpring( from: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: duration, delay: 0.0, initialVelocity: 0.0, damping: springDamping, additive: true ) if let reactionContextNode = self.reactionContextNode { let reactionsPositionDeltaYDistance = -animationInContentYDistance reactionContextNode.layer.animateSpring( from: NSValue(cgPoint: CGPoint(x: 0.0, y: reactionsPositionDeltaYDistance)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: duration, delay: 0.0, initialVelocity: 0.0, damping: springDamping, additive: true ) reactionContextNode.animateIn(from: currentContentScreenFrame) } self.actionsStackNode.animateIn() if let contentNode = itemContentNode { contentNode.containingItem.isExtractedToContextPreview = true contentNode.containingItem.isExtractedToContextPreviewUpdated?(true) contentNode.containingItem.willUpdateIsExtractedToContextPreview?(true, transition) contentNode.containingItem.layoutUpdated = { [weak self] _, animation in guard let strongSelf = self else { return } if let _ = strongSelf.animatingOutState { } else { strongSelf.requestUpdate(animation.transition) } } } if let overlayViews = self.getController()?.getOverlayViews?(), !overlayViews.isEmpty { for view in overlayViews { if let snapshotView = view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = view.convert(view.bounds, to: nil) self.view.addSubview(snapshotView) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) } } } case let .animateOut(result, completion): let actionsSize = self.actionsContainerNode.bounds.size let duration: Double let timingFunction: String switch result { case .default, .dismissWithoutContent: duration = self.reactionContextNodeIsAnimatingOut ? 0.25 : 0.2 timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue case let .custom(customTransition): switch customTransition { case let .animated(customDuration, curve): duration = customDuration timingFunction = curve.timingFunction case .immediate: duration = self.reactionContextNodeIsAnimatingOut ? 0.25 : 0.2 timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue } } let currentContentScreenFrame: CGRect switch self.source { case let .location(location): if let putBackInfo = location.transitionInfo() { self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, timingFunction: timingFunction, removeOnCompletion: false) self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) currentContentScreenFrame = CGRect(origin: putBackInfo.location, size: CGSize(width: 1.0, height: 1.0)) } else { return } case let .reference(source): if let putBackInfo = source.transitionInfo() { self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, timingFunction: timingFunction, removeOnCompletion: false) self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) currentContentScreenFrame = convertFrame(putBackInfo.referenceView.bounds.inset(by: putBackInfo.insets), from: putBackInfo.referenceView, to: self.view) } else { return } case let .extracted(source): let putBackInfo = source.putBack() if let putBackInfo = putBackInfo { self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, timingFunction: timingFunction, removeOnCompletion: false) self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false) } if let contentNode = itemContentNode { currentContentScreenFrame = convertFrame(contentNode.containingItem.contentRect, from: contentNode.containingItem.view, to: self.view) } else { return } case let .controller(source): if let putBackInfo = source.transitionInfo() { let _ = putBackInfo /*self.clippingNode.layer.animateFrame(from: CGRect(origin: CGPoint(), size: layout.size), to: CGRect(origin: CGPoint(x: 0.0, y: putBackInfo.contentAreaInScreenSpace.minY), size: CGSize(width: layout.size.width, height: putBackInfo.contentAreaInScreenSpace.height)), duration: duration, timingFunction: timingFunction, removeOnCompletion: false) self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: putBackInfo.contentAreaInScreenSpace.minY, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)*/ //TODO: currentContentScreenFrame = CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)) } else { return } } self.animatingOutState = AnimatingOutState( currentContentScreenFrame: currentContentScreenFrame ) let currentContentLocalFrame = convertFrame(contentRect, from: self.scrollNode.view, to: self.view) var animationInContentYDistance: CGFloat switch result { case .default, .custom: animationInContentYDistance = currentContentLocalFrame.minY - currentContentScreenFrame.minY case .dismissWithoutContent: animationInContentYDistance = 0.0 if let contentNode = itemContentNode { contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) } } let actionsVerticalTransitionDirection: CGFloat if let contentNode = itemContentNode { if contentNode.frame.minY < self.actionsContainerNode.frame.minY { actionsVerticalTransitionDirection = -1.0 } else { actionsVerticalTransitionDirection = 1.0 } } else { if contentRect.minY < self.actionsContainerNode.frame.minY { actionsVerticalTransitionDirection = -1.0 } else { actionsVerticalTransitionDirection = 1.0 } } let completeWithActionStack = itemContentNode == nil && controllerContentNode == nil if let contentNode = itemContentNode { contentNode.containingItem.willUpdateIsExtractedToContextPreview?(false, transition) var animationInContentXDistance: CGFloat = 0.0 let contentX = contentParentGlobalFrame.minX + contentRect.minX - contentNode.containingItem.contentRect.minX let contentWidth = contentNode.containingItem.view.bounds.size.width if case let .extracted(extracted) = self.source, extracted.centerVertically { if actionsSize.height.isZero { // let fixedContentY = floorToScreenPixels((layout.size.height - contentHeight) / 2.0) animationInContentYDistance = 0.0 //contentY - fixedContentY } else if contentX + contentWidth > layout.size.width / 2.0{ let fixedContentX = layout.size.width - (contentX + contentWidth) animationInContentXDistance = contentX - fixedContentX contentNode.offsetContainerNode.layer.animate( from: -animationInContentXDistance as NSNumber, to: 0.0 as NSNumber, keyPath: "position.x", timingFunction: timingFunction, duration: duration, delay: 0.0, additive: true ) } } contentNode.offsetContainerNode.position = contentNode.offsetContainerNode.position.offsetBy(dx: animationInContentXDistance, dy: -animationInContentYDistance) let reactionContextNodeIsAnimatingOut = self.reactionContextNodeIsAnimatingOut contentNode.offsetContainerNode.layer.animate( from: animationInContentYDistance as NSNumber, to: 0.0 as NSNumber, keyPath: "position.y", timingFunction: timingFunction, duration: duration, delay: 0.0, additive: true, completion: { [weak self] _ in Queue.mainQueue().after(reactionContextNodeIsAnimatingOut ? 0.2 * UIView.animationDurationFactor() : 0.0, { contentNode.containingItem.isExtractedToContextPreview = false contentNode.containingItem.isExtractedToContextPreviewUpdated?(false) if let strongSelf = self, let contentNode = strongSelf.itemContentNode { switch contentNode.containingItem { case let .node(containingNode): containingNode.addSubnode(containingNode.contentNode) case let .view(containingView): containingView.addSubview(containingView.contentView) } } completion() }) } ) } if let contentNode = controllerContentNode { if case let .controller(source) = self.source, let transitionInfo = source.transitionInfo(), let (sourceView, sourceRect) = transitionInfo.sourceNode() { let sourcePoint = sourceView.convert(sourceRect.center, to: self.view) animationInContentYDistance = contentRect.midY - sourcePoint.y } else { animationInContentYDistance = 0.0 } contentNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.8, removeOnCompletion: false, completion: { _ in completion() }) contentNode.layer.animate( from: 0.0 as NSNumber, to: -animationInContentYDistance as NSNumber, keyPath: "position.y", timingFunction: timingFunction, duration: duration, delay: 0.0, removeOnCompletion: false, additive: true ) contentNode.layer.animate( from: 1.0 as NSNumber, to: 0.01 as NSNumber, keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: 0.0, removeOnCompletion: false, additive: false ) } self.actionsContainerNode.layer.animateAlpha(from: self.actionsContainerNode.alpha, to: 0.0, duration: duration, removeOnCompletion: false) self.actionsContainerNode.layer.animate( from: 1.0 as NSNumber, to: 0.01 as NSNumber, keyPath: "transform.scale", timingFunction: timingFunction, duration: duration, delay: 0.0, removeOnCompletion: false, completion: { _ in if completeWithActionStack { completion() } } ) var actionsPositionDeltaXDistance: CGFloat = 0.0 if case .center = actionsHorizontalAlignment { actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX } if case .reference = self.source { actionsPositionDeltaXDistance = currentContentScreenFrame.midX - self.actionsContainerNode.frame.midX } let actionsPositionDeltaYDistance = -animationInContentYDistance + actionsVerticalTransitionDirection * actionsSize.height / 2.0 - contentActionsSpacing self.actionsContainerNode.layer.animate( from: NSValue(cgPoint: CGPoint()), to: NSValue(cgPoint: CGPoint(x: actionsPositionDeltaXDistance, y: actionsPositionDeltaYDistance)), keyPath: "position", timingFunction: timingFunction, duration: duration, delay: 0.0, removeOnCompletion: false, additive: true ) if let reactionContextNode = self.reactionContextNode { reactionContextNode.animateOut(to: currentContentScreenFrame, animatingOutToReaction: self.reactionContextNodeIsAnimatingOut) } if let overlayViews = self.getController()?.getOverlayViews?(), !overlayViews.isEmpty { for view in overlayViews { if let snapshotView = view.snapshotView(afterScreenUpdates: false) { snapshotView.frame = view.convert(view.bounds, to: nil) self.view.addSubview(snapshotView) snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) } } } case .none: if animateReactionsIn, let reactionContextNode = self.reactionContextNode { reactionContextNode.animateIn(from: contentRect) } } } func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, completion: @escaping () -> Void) { guard let reactionContextNode = self.reactionContextNode else { self.requestAnimateOut(.default, completion) return } var contentCompleted = false var reactionCompleted = false let intermediateCompletion: () -> Void = { if contentCompleted && reactionCompleted { completion() } } self.reactionContextNodeIsAnimatingOut = true reactionContextNode.willAnimateOutToReaction(value: value) let result: ContextMenuActionResult if reducedCurve { result = .custom(.animated(duration: 0.5, curve: .spring)) } else { result = .default } self.requestAnimateOut(result, { contentCompleted = true intermediateCompletion() }) reactionContextNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, completion: { [weak self] in guard let strongSelf = self else { return } strongSelf.reactionContextNode?.removeFromSupernode() strongSelf.reactionContextNode = nil reactionCompleted = true intermediateCompletion() }) } func cancelReactionAnimation() { self.reactionContextNode?.cancelReactionAnimation() } func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { if self.reactionContextNodeIsAnimatingOut, let reactionContextNode = self.reactionContextNode { reactionContextNode.bounds = reactionContextNode.bounds.offsetBy(dx: 0.0, dy: offset.y) transition.animateOffsetAdditive(node: reactionContextNode, offset: -offset.y) } } }