import Foundation import UIKit import Display import ContextUI import SwiftSignalKit import AccountContext import TelegramPresentationData import AsyncDisplayKit import ReactionSelectionNode import TelegramCore import UIKitRuntimeUtils import UndoUI import TextSelectionNode private let animationDurationFactor: Double = 1.0 func convertFrame(_ frame: CGRect, from fromView: UIView, to toView: UIView) -> CGRect { let sourceWindowFrame = fromView.convert(frame, to: nil) var targetWindowFrame = toView.convert(sourceWindowFrame, from: nil) if let fromWindow = fromView.window, let toWindow = toView.window { targetWindowFrame.origin.x += toWindow.bounds.width - fromWindow.bounds.width } return targetWindowFrame } final class ContextControllerNode: ViewControllerTracingNode, ASScrollViewDelegate { private weak var controller: ContextControllerImpl? private let context: AccountContext? private var presentationData: PresentationData private let configuration: ContextController.Configuration private let legacySource: ContextContentSource private var legacyItems: Signal let beginDismiss: (ContextMenuActionResult) -> Void private let beganAnimatingOut: () -> Void private let attemptTransitionControllerIntoNavigation: () -> Void var dismissedForCancel: (() -> Void)? private let getController: () -> ContextControllerProtocol? private weak var gesture: ContextGesture? private var didSetItemsReady = false let itemsReady = Promise() let contentReady = Promise() private var currentItems: ContextController.Items? private var currentActionsMinHeight: ContextController.ActionsHeight? private var validLayout: ContainerViewLayout? private let effectView: UIVisualEffectView private var propertyAnimator: AnyObject? private var displayLinkAnimator: DisplayLinkAnimator? private let dimNode: ASDisplayNode private let withoutBlurDimNode: ASDisplayNode private let dismissNode: ASDisplayNode private let dismissAccessibilityArea: AccessibilityAreaNode private var sourceContainer: ContextSourceContainer? private let clippingNode: ASDisplayNode private let scrollNode: ASScrollNode private var originalProjectedContentViewFrame: (CGRect, CGRect)? private var contentAreaInScreenSpace: CGRect? private var customPosition: CGPoint? private let contentContainerNode: ContextContentContainerNode private var actionsContainerNode: ContextActionsContainerNode private var didCompleteAnimationIn = false private var initialContinueGesturePoint: CGPoint? private var didMoveFromInitialGesturePoint = false private var highlightedActionNode: ContextActionNodeProtocol? private var highlightedReaction: ReactionItem.Reaction? private let hapticFeedback = HapticFeedback() private var animatedIn = false private var isAnimatingOut = false private let itemsDisposable = MetaDisposable() private let blurBackground: Bool var overlayWantsToBeBelowKeyboard: Bool { guard let sourceContainer = self.sourceContainer else { return false } return sourceContainer.overlayWantsToBeBelowKeyboard } init( controller: ContextControllerImpl, context: AccountContext?, presentationData: PresentationData, configuration: ContextController.Configuration, beginDismiss: @escaping (ContextMenuActionResult) -> Void, recognizer: TapLongTapOrDoubleTapGestureRecognizer?, gesture: ContextGesture?, beganAnimatingOut: @escaping () -> Void, attemptTransitionControllerIntoNavigation: @escaping () -> Void ) { self.controller = controller self.context = context self.presentationData = presentationData self.configuration = configuration self.beginDismiss = beginDismiss self.beganAnimatingOut = beganAnimatingOut self.attemptTransitionControllerIntoNavigation = attemptTransitionControllerIntoNavigation self.gesture = gesture self.legacySource = configuration.sources[0].source self.legacyItems = configuration.sources[0].items self.getController = { [weak controller] in return controller } self.effectView = UIVisualEffectView() if #available(iOS 9.0, *) { } else { if presentationData.theme.rootController.keyboardColor == .dark { self.effectView.effect = UIBlurEffect(style: .dark) } else { self.effectView.effect = UIBlurEffect(style: .light) } self.effectView.alpha = 0.0 } self.dimNode = ASDisplayNode() self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor self.dimNode.alpha = 0.0 self.withoutBlurDimNode = ASDisplayNode() self.withoutBlurDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.4) self.withoutBlurDimNode.alpha = 0.0 self.dismissNode = ASDisplayNode() self.dismissAccessibilityArea = AccessibilityAreaNode() self.dismissAccessibilityArea.accessibilityLabel = presentationData.strings.VoiceOver_DismissContextMenu self.dismissAccessibilityArea.accessibilityTraits = .button self.clippingNode = ASDisplayNode() self.clippingNode.clipsToBounds = true self.scrollNode = ASScrollNode() self.scrollNode.canCancelAllTouchesInViews = true self.scrollNode.view.delaysContentTouches = false self.scrollNode.view.showsVerticalScrollIndicator = false if #available(iOS 11.0, *) { self.scrollNode.view.contentInsetAdjustmentBehavior = .never } self.contentContainerNode = ContextContentContainerNode() var feedbackTap: (() -> Void)? var updateLayout: (() -> Void)? var blurBackground = true if let mainSource = configuration.sources.first(where: { $0.id == configuration.initialId }) { if case .reference = mainSource.source { blurBackground = false } else if case let .extracted(extractedSource) = mainSource.source, !extractedSource.blurBackground { blurBackground = false } } self.blurBackground = blurBackground self.actionsContainerNode = ContextActionsContainerNode(presentationData: presentationData, items: ContextController.Items(), getController: { [weak controller] in return controller }, actionSelected: { result in beginDismiss(result) }, requestLayout: { updateLayout?() }, feedbackTap: { feedbackTap?() }, blurBackground: blurBackground) super.init() feedbackTap = { [weak self] in self?.hapticFeedback.tap() } updateLayout = { [weak self] in self?.updateLayout() } self.scrollNode.view.delegate = self.wrappedScrollViewDelegate if blurBackground { self.view.addSubview(self.effectView) self.addSubnode(self.dimNode) self.addSubnode(self.withoutBlurDimNode) } self.addSubnode(self.clippingNode) self.clippingNode.addSubnode(self.scrollNode) self.scrollNode.addSubnode(self.dismissNode) self.scrollNode.addSubnode(self.dismissAccessibilityArea) self.scrollNode.addSubnode(self.actionsContainerNode) if let recognizer = recognizer { recognizer.externalUpdated = { [weak self, weak recognizer] view, point in guard let strongSelf = self, let _ = recognizer else { return } let localPoint = strongSelf.view.convert(point, from: view) let initialPoint: CGPoint if let current = strongSelf.initialContinueGesturePoint { initialPoint = current } else { initialPoint = localPoint strongSelf.initialContinueGesturePoint = localPoint } if strongSelf.didCompleteAnimationIn { if !strongSelf.didMoveFromInitialGesturePoint { let distance = abs(localPoint.y - initialPoint.y) if distance > 12.0 { strongSelf.didMoveFromInitialGesturePoint = true } } if strongSelf.didMoveFromInitialGesturePoint { if let sourceContainer = strongSelf.sourceContainer { let presentationPoint = strongSelf.view.convert(localPoint, to: sourceContainer.view) sourceContainer.highlightGestureMoved(location: presentationPoint, hover: false) } else { let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) let actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) if strongSelf.highlightedActionNode !== actionNode { strongSelf.highlightedActionNode?.setIsHighlighted(false) strongSelf.highlightedActionNode = actionNode if let actionNode = actionNode { actionNode.setIsHighlighted(true) strongSelf.hapticFeedback.tap() } } } } } } recognizer.externalEnded = { [weak self, weak recognizer] viewAndPoint in guard let strongSelf = self, let recognizer = recognizer else { return } recognizer.externalUpdated = nil if strongSelf.didMoveFromInitialGesturePoint { if let sourceContainer = strongSelf.sourceContainer { sourceContainer.highlightGestureFinished(performAction: viewAndPoint != nil) } else { if let (_, _) = viewAndPoint { if let highlightedActionNode = strongSelf.highlightedActionNode { strongSelf.highlightedActionNode = nil highlightedActionNode.performAction() } } else { if let highlightedActionNode = strongSelf.highlightedActionNode { strongSelf.highlightedActionNode = nil highlightedActionNode.setIsHighlighted(false) } } } } } } else if let gesture = gesture { gesture.externalUpdated = { [weak self, weak gesture] view, point in guard let strongSelf = self, let _ = gesture else { return } let localPoint: CGPoint if let layout = strongSelf.validLayout, layout.metrics.isTablet, layout.size.width > layout.size.height, let view { localPoint = view.convert(point, to: nil) } else { localPoint = strongSelf.view.convert(point, from: view) } let initialPoint: CGPoint if let current = strongSelf.initialContinueGesturePoint { initialPoint = current } else { initialPoint = localPoint strongSelf.initialContinueGesturePoint = localPoint } if strongSelf.didCompleteAnimationIn { if !strongSelf.didMoveFromInitialGesturePoint { let distance = abs(localPoint.y - initialPoint.y) if distance > 4.0 { strongSelf.didMoveFromInitialGesturePoint = true } } if strongSelf.didMoveFromInitialGesturePoint { if let sourceContainer = strongSelf.sourceContainer { let presentationPoint = strongSelf.view.convert(localPoint, to: sourceContainer.view) sourceContainer.highlightGestureMoved(location: presentationPoint, hover: false) } else { let actionPoint = strongSelf.view.convert(localPoint, to: strongSelf.actionsContainerNode.view) var actionNode = strongSelf.actionsContainerNode.actionNode(at: actionPoint) if let actionNodeValue = actionNode, !actionNodeValue.isActionEnabled { actionNode = nil } if strongSelf.highlightedActionNode !== actionNode { strongSelf.highlightedActionNode?.setIsHighlighted(false) strongSelf.highlightedActionNode = actionNode if let actionNode = actionNode { actionNode.setIsHighlighted(true) strongSelf.hapticFeedback.tap() } } } } } } gesture.externalEnded = { [weak self, weak gesture] viewAndPoint in guard let strongSelf = self, let gesture = gesture else { return } gesture.externalUpdated = nil if strongSelf.didMoveFromInitialGesturePoint { if let sourceContainer = strongSelf.sourceContainer { sourceContainer.highlightGestureFinished(performAction: viewAndPoint != nil) } else { if let (_, _) = viewAndPoint { if let highlightedActionNode = strongSelf.highlightedActionNode { strongSelf.highlightedActionNode = nil highlightedActionNode.performAction() } } else { if let highlightedActionNode = strongSelf.highlightedActionNode { strongSelf.highlightedActionNode = nil highlightedActionNode.setIsHighlighted(false) } } } } } } self.initializeContent() self.dismissAccessibilityArea.activate = { [weak self] in self?.dimNodeTapped() return true } if controller.disableScreenshots { setLayerDisableScreenshots(self.layer, true) } } deinit { if let propertyAnimator = self.propertyAnimator { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator propertyAnimator?.stopAnimation(true) } } self.itemsDisposable.dispose() } override func didLoad() { super.didLoad() self.dismissNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimNodeTapped))) if #available(iOS 13.0, *) { self.view.addGestureRecognizer(UIHoverGestureRecognizer(target: self, action: #selector(self.hoverGesture(_:)))) } } @objc private func dimNodeTapped() { guard self.animatedIn else { return } self.dismissedForCancel?() self.beginDismiss(.default) } @available(iOS 13.0, *) @objc private func hoverGesture(_ gestureRecognizer: UIHoverGestureRecognizer) { guard self.didCompleteAnimationIn else { return } let localPoint = gestureRecognizer.location(in: self.view) switch gestureRecognizer.state { case .changed: if let sourceContainer = self.sourceContainer { let presentationPoint = self.view.convert(localPoint, to: sourceContainer.view) sourceContainer.highlightGestureMoved(location: presentationPoint, hover: true) } else { let actionPoint = self.view.convert(localPoint, to: self.actionsContainerNode.view) let actionNode = self.actionsContainerNode.actionNode(at: actionPoint) if self.highlightedActionNode !== actionNode { self.highlightedActionNode?.setIsHighlighted(false) self.highlightedActionNode = actionNode if let actionNode = actionNode { actionNode.setIsHighlighted(true) } } } case .ended, .cancelled: if let sourceContainer = self.sourceContainer { sourceContainer.highlightGestureMoved(location: CGPoint(x: -1, y: -1), hover: true) } else { if let highlightedActionNode = self.highlightedActionNode { self.highlightedActionNode = nil highlightedActionNode.setIsHighlighted(false) } } default: break } } private func initializeContent() { if self.configuration.sources.count == 1 { switch self.configuration.sources[0].source { case .location: break case let .reference(source): if let controller = self.getController() as? ContextControllerImpl, controller.workaroundUseLegacyImplementation { self.contentReady.set(.single(true)) let transitionInfo = source.transitionInfo() if let transitionInfo { let referenceView = transitionInfo.referenceView self.contentContainerNode.contentNode = .reference(view: referenceView) self.contentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace self.customPosition = transitionInfo.customPosition var projectedFrame = convertFrame(referenceView.bounds, from: referenceView, to: self.view) projectedFrame.origin.x += transitionInfo.insets.left projectedFrame.size.width -= transitionInfo.insets.left + transitionInfo.insets.right projectedFrame.origin.y += transitionInfo.insets.top projectedFrame.size.width -= transitionInfo.insets.top + transitionInfo.insets.bottom self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) } self.itemsDisposable.set((self.configuration.sources[0].items |> deliverOnMainQueue).start(next: { [weak self] items in self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale) })) return } case .extracted: break case let .controller(source): if let controller = self.getController() as? ContextControllerImpl, controller.workaroundUseLegacyImplementation { self.contentReady.set(source.controller.ready.get()) let transitionInfo = source.transitionInfo() if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() { let contentParentNode = ContextControllerContentNode(sourceView: sourceView, controller: source.controller, tapped: { [weak self] in self?.attemptTransitionControllerIntoNavigation() }) self.contentContainerNode.contentNode = .controller(contentParentNode) self.scrollNode.addSubnode(self.contentContainerNode) self.contentContainerNode.clipsToBounds = true self.contentContainerNode.cornerRadius = 30.0 self.contentContainerNode.addSubnode(contentParentNode) let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view) self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) } self.itemsDisposable.set((self.configuration.sources[0].items |> deliverOnMainQueue).start(next: { [weak self] items in self?.setItems(items: items, minHeight: nil, previousActionsTransition: .scale) })) return } } } if let controller = self.controller { let sourceContainer = ContextSourceContainer(controller: controller, configuration: self.configuration, context: self.context) self.contentReady.set(sourceContainer.ready.get()) self.itemsReady.set(.single(true)) self.sourceContainer = sourceContainer self.addSubnode(sourceContainer) } } func animateIn() { self.gesture?.endPressedAppearance() self.hapticFeedback.impact() if let sourceContainer = self.sourceContainer { self.didCompleteAnimationIn = true sourceContainer.animateIn() return } switch self.legacySource { case .location, .reference: break case .extracted: if let contentAreaInScreenSpace = self.contentAreaInScreenSpace, let maybeContentNode = self.contentContainerNode.contentNode, case .extracted = maybeContentNode { var updatedContentAreaInScreenSpace = contentAreaInScreenSpace updatedContentAreaInScreenSpace.origin.x = 0.0 updatedContentAreaInScreenSpace.size.width = self.bounds.width self.clippingNode.layer.animateFrame(from: updatedContentAreaInScreenSpace, to: self.clippingNode.frame, duration: 0.18 * animationDurationFactor, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) self.clippingNode.layer.animateBoundsOriginYAdditive(from: updatedContentAreaInScreenSpace.minY, to: 0.0, duration: 0.18 * animationDurationFactor, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) } case let .controller(source): let transitionInfo = source.transitionInfo() if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() { let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view) self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace updatedContentAreaInScreenSpace.origin.x = 0.0 updatedContentAreaInScreenSpace.size.width = self.bounds.width self.contentAreaInScreenSpace = updatedContentAreaInScreenSpace } } if let validLayout = self.validLayout { self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil) } if !self.dimNode.isHidden { self.dimNode.alpha = 1.0 self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) } else { self.withoutBlurDimNode.alpha = 1.0 self.withoutBlurDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) } if #available(iOS 10.0, *) { if let propertyAnimator = self.propertyAnimator { let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator propertyAnimator?.stopAnimation(true) } self.effectView.effect = makeCustomZoomBlurEffect(isLight: presentationData.theme.rootController.keyboardColor == .light) self.effectView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) self.propertyAnimator = UIViewPropertyAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), curve: .easeInOut, animations: { }) } if let _ = self.propertyAnimator { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), from: 0.0, to: 1.0, update: { [weak self] value in (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value }, completion: { [weak self] in self?.didCompleteAnimationIn = true self?.hapticFeedback.prepareTap() self?.actionsContainerNode.animateIn() }) } } else { UIView.animate(withDuration: 0.2 * animationDurationFactor, animations: { self.effectView.effect = makeCustomZoomBlurEffect(isLight: self.presentationData.theme.rootController.keyboardColor == .light) }, completion: { [weak self] _ in self?.didCompleteAnimationIn = true self?.actionsContainerNode.animateIn() }) } if let contentNode = self.contentContainerNode.contentNode { switch contentNode { case .reference: let springDuration: Double = 0.42 * animationDurationFactor let springDamping: CGFloat = 104.0 self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) let localContentSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.contentContainerNode.view.superview) self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y) self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in self?.animatedIn = true }) } case .extractedContainer: break case let .extracted(extracted, keepInPlace): let springDuration: Double = 0.42 * animationDurationFactor var springDamping: CGFloat = 104.0 if case let .extracted(source) = self.legacySource, source.centerVertically { springDamping = 124.0 } self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let contentParentNode = extracted let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) var actionsDuration = springDuration var actionsOffset: CGFloat = 0.0 var contentDuration = springDuration if case let .extracted(source) = self.legacySource, source.centerVertically { actionsOffset = -(originalProjectedContentViewFrame.1.height - originalProjectedContentViewFrame.0.height) * 0.57 actionsDuration *= 1.0 contentDuration *= 0.9 } let localContentSourceFrame: CGRect if keepInPlace { localContentSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.contentContainerNode.view.superview) } else { localContentSourceFrame = localSourceFrame } self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: actionsDuration, initialVelocity: 0.0, damping: springDamping, additive: true) let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: contentDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in self?.clippingNode.view.mask = nil self?.animatedIn = true }) contentParentNode.applyAbsoluteOffsetSpring?(-contentContainerOffset.y, springDuration, springDamping) } extracted.willUpdateIsExtractedToContextPreview?(true, .animated(duration: 0.2, curve: .easeInOut)) case .controller: let springDuration: Double = 0.52 * animationDurationFactor let springDamping: CGFloat = 110.0 self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor) self.actionsContainerNode.layer.animateSpring(from: 0.1 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) self.contentContainerNode.allowsGroupOpacity = true self.contentContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2 * animationDurationFactor, completion: { [weak self] _ in self?.contentContainerNode.allowsGroupOpacity = false }) if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let localSourceFrame = self.view.convert(CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: CGSize(width: originalProjectedContentViewFrame.1.width, height: originalProjectedContentViewFrame.1.height)), to: self.scrollNode.view) self.contentContainerNode.layer.animateSpring(from: min(localSourceFrame.width / self.contentContainerNode.frame.width, localSourceFrame.height / self.contentContainerNode.frame.height) as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping) switch self.legacySource { case let .controller(controller): controller.animatedIn() default: break } let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y) if let contentNode = self.contentContainerNode.contentNode, case let .controller(controller) = contentNode { let snapshotView: UIView? = nil// controller.sourceNode.view.snapshotContentTree() if let snapshotView = snapshotView { controller.sourceView.isHidden = true self.view.insertSubview(snapshotView, belowSubview: self.contentContainerNode.view) snapshotView.layer.animateSpring(from: NSValue(cgPoint: localSourceFrame.center), to: NSValue(cgPoint: CGPoint(x: self.contentContainerNode.frame.midX, y: self.contentContainerNode.frame.minY + localSourceFrame.height / 2.0)), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false) //snapshotView.layer.animateSpring(from: 1.0 as NSNumber, to: (self.contentContainerNode.frame.width / localSourceFrame.width) as NSNumber, keyPath: "transform.scale", duration: springDuration, initialVelocity: 0.0, damping: springDamping, removeOnCompletion: false) snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() }) } } self.actionsContainerNode.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true) self.contentContainerNode.layer.animateSpring(from: NSValue(cgPoint: contentContainerOffset), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, initialVelocity: 0.0, damping: springDamping, additive: true, completion: { [weak self] _ in self?.animatedIn = true }) } } } } private var delayLayoutUpdate = false func animateOut(result initialResult: ContextMenuActionResult, completion: @escaping () -> Void) { self.isUserInteractionEnabled = false self.beganAnimatingOut() if let sourceContainer = self.sourceContainer { sourceContainer.animateOut(result: initialResult, completion: completion) return } var transitionDuration: Double = 0.2 var transitionCurve: ContainedViewLayoutTransitionCurve = .easeInOut var result = initialResult switch self.legacySource { case let .location(source): let transitionInfo = source.transitionInfo() if transitionInfo == nil { result = .dismissWithoutContent } switch result { case let .custom(value): switch value { case let .animated(duration, curve): transitionDuration = duration transitionCurve = curve default: break } default: break } self.isUserInteractionEnabled = false self.isAnimatingOut = true self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) if !self.dimNode.isHidden { self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) } else { self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) } self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15 * animationDurationFactor, removeOnCompletion: false, completion: { _ in completion() }) self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) let animateOutToItem: Bool switch result { case .default, .custom: animateOutToItem = true case .dismissWithoutContent: animateOutToItem = false } if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) } case let .reference(source): guard let maybeContentNode = self.contentContainerNode.contentNode, case let .reference(referenceView) = maybeContentNode else { return } let transitionInfo = source.transitionInfo() if transitionInfo == nil { result = .dismissWithoutContent } switch result { case let .custom(value): switch value { case let .animated(duration, curve): transitionDuration = duration transitionCurve = curve default: break } default: break } self.isUserInteractionEnabled = false self.isAnimatingOut = true self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) if let transitionInfo = transitionInfo, let parentSuperview = referenceView.superview { self.originalProjectedContentViewFrame = (convertFrame(referenceView.frame, from: parentSuperview, to: self.view), convertFrame(referenceView.bounds, from: referenceView, to: self.view)) var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace updatedContentAreaInScreenSpace.origin.x = 0.0 updatedContentAreaInScreenSpace.size.width = self.bounds.width self.clippingNode.layer.animateFrame(from: self.clippingNode.frame, to: updatedContentAreaInScreenSpace, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) } if !self.dimNode.isHidden { self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) } else { self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) } self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15 * animationDurationFactor, removeOnCompletion: false, completion: { _ in completion() }) self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) let animateOutToItem: Bool switch result { case .default, .custom: animateOutToItem = true case .dismissWithoutContent: animateOutToItem = false } if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) } case let .extracted(source): guard let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode, keepInPlace) = maybeContentNode else { return } let putBackInfo = source.putBack() if putBackInfo == nil { result = .dismissWithoutContent } switch result { case let .custom(value): switch value { case let .animated(duration, curve): transitionDuration = duration transitionCurve = curve default: break } default: break } self.isUserInteractionEnabled = false self.isAnimatingOut = true self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) var completedEffect = false var completedContentNode = false var completedActionsNode = false if let putBackInfo = putBackInfo, let parentSupernode = contentParentNode.supernode { self.originalProjectedContentViewFrame = (convertFrame(contentParentNode.frame, from: parentSupernode.view, to: self.view), convertFrame(contentParentNode.contentRect, from: contentParentNode.view, to: self.view)) var updatedContentAreaInScreenSpace = putBackInfo.contentAreaInScreenSpace updatedContentAreaInScreenSpace.origin.x = 0.0 updatedContentAreaInScreenSpace.size.width = self.bounds.width self.clippingNode.view.mask = putBackInfo.maskView let previousFrame = self.clippingNode.frame self.clippingNode.position = updatedContentAreaInScreenSpace.center self.clippingNode.bounds = CGRect(origin: CGPoint(), size: updatedContentAreaInScreenSpace.size) self.clippingNode.layer.animatePosition(from: previousFrame.center, to: updatedContentAreaInScreenSpace.center, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: true) self.clippingNode.layer.animateBounds(from: CGRect(origin: CGPoint(), size: previousFrame.size), to: CGRect(origin: CGPoint(), size: updatedContentAreaInScreenSpace.size), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: true) //self.clippingNode.layer.animateFrame(from: previousFrame, to: updatedContentAreaInScreenSpace, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) //self.clippingNode.layer.animateBoundsOriginYAdditive(from: 0.0, to: updatedContentAreaInScreenSpace.minY, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) } let intermediateCompletion: () -> Void = { [weak self, weak contentParentNode] in if completedEffect && completedContentNode && completedActionsNode { switch result { case .default, .custom: if let contentParentNode = contentParentNode { contentParentNode.addSubnode(contentParentNode.contentNode) contentParentNode.isExtractedToContextPreview = false contentParentNode.isExtractedToContextPreviewUpdated?(false) } case .dismissWithoutContent: break } self?.clippingNode.view.mask = nil completion() } } if #available(iOS 10.0, *) { if let propertyAnimator = self.propertyAnimator { let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator propertyAnimator?.stopAnimation(true) } self.propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration * UIView.animationDurationFactor(), curve: .easeInOut, animations: { //self?.effectView.effect = nil }) } if let _ = self.propertyAnimator { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), from: 0.0, to: 0.999, update: { [weak self] value in (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value }, completion: { completedEffect = true intermediateCompletion() }) } self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, delay: 0.0, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) } else { UIView.animate(withDuration: 0.21 * animationDurationFactor, animations: { if #available(iOS 9.0, *) { self.effectView.effect = nil } else { self.effectView.alpha = 0.0 } }, completion: { _ in completedEffect = true intermediateCompletion() }) } if !self.dimNode.isHidden { self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) } else { self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) } self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15 * animationDurationFactor, removeOnCompletion: false, completion: { _ in completedActionsNode = true intermediateCompletion() }) self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) let animateOutToItem: Bool switch result { case .default, .custom: animateOutToItem = true case .dismissWithoutContent: animateOutToItem = false } if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let localSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.scrollNode.view) let localContentSourceFrame: CGRect if keepInPlace { localContentSourceFrame = self.view.convert(originalProjectedContentViewFrame.1, to: self.contentContainerNode.view.superview) } else { localContentSourceFrame = localSourceFrame } var actionsOffset: CGFloat = 0.0 if case let .extracted(source) = self.legacySource, source.centerVertically { actionsOffset = -localSourceFrame.width * 0.6 } self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y + actionsOffset), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) let contentContainerOffset = CGPoint(x: localContentSourceFrame.center.x - self.contentContainerNode.frame.center.x - contentParentNode.contentRect.minX, y: localContentSourceFrame.center.y - self.contentContainerNode.frame.center.y - contentParentNode.contentRect.minY) self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true, completion: { _ in completedContentNode = true intermediateCompletion() }) contentParentNode.updateAbsoluteRect?(self.contentContainerNode.frame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y + contentContainerOffset.y), self.bounds.size) contentParentNode.applyAbsoluteOffset?(CGPoint(x: 0.0, y: -contentContainerOffset.y), transitionCurve, transitionDuration) contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut)) } else { if let snapshotView = contentParentNode.contentNode.view.snapshotContentTree(keepTransform: true) { self.contentContainerNode.view.addSubview(snapshotView) } contentParentNode.addSubnode(contentParentNode.contentNode) contentParentNode.isExtractedToContextPreview = false contentParentNode.isExtractedToContextPreviewUpdated?(false) self.contentContainerNode.allowsGroupOpacity = true self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false, completion: { _ in completedContentNode = true intermediateCompletion() }) contentParentNode.contentNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) contentParentNode.willUpdateIsExtractedToContextPreview?(false, .animated(duration: 0.2, curve: .easeInOut)) } case let .controller(source): guard let maybeContentNode = self.contentContainerNode.contentNode, case let .controller(controller) = maybeContentNode else { return } let transitionInfo = source.transitionInfo() if transitionInfo == nil { result = .dismissWithoutContent } switch result { case let .custom(value): switch value { case let .animated(duration, curve): transitionDuration = duration transitionCurve = curve default: break } default: break } self.isUserInteractionEnabled = false self.isAnimatingOut = true self.scrollNode.view.setContentOffset(self.scrollNode.view.contentOffset, animated: false) var completedEffect = false var completedContentNode = false var completedActionsNode = false if let transitionInfo = transitionInfo, let (sourceView, sourceNodeRect) = transitionInfo.sourceNode() { let projectedFrame = convertFrame(sourceNodeRect, from: sourceView, to: self.view) self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) var updatedContentAreaInScreenSpace = transitionInfo.contentAreaInScreenSpace updatedContentAreaInScreenSpace.origin.x = 0.0 updatedContentAreaInScreenSpace.size.width = self.bounds.width } let intermediateCompletion: () -> Void = { if completedEffect && completedContentNode && completedActionsNode { switch result { case .default, .custom: break case .dismissWithoutContent: break } completion() } } if #available(iOS 10.0, *) { if let propertyAnimator = self.propertyAnimator { let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator propertyAnimator?.stopAnimation(true) } self.propertyAnimator = UIViewPropertyAnimator(duration: transitionDuration * UIView.animationDurationFactor(), curve: .easeInOut, animations: { [weak self] in self?.effectView.effect = nil }) } if let _ = self.propertyAnimator { if #available(iOSApplicationExtension 10.0, iOS 10.0, *) { self.displayLinkAnimator = DisplayLinkAnimator(duration: 0.2 * animationDurationFactor * UIView.animationDurationFactor(), from: 0.0, to: 0.999, update: { [weak self] value in (self?.propertyAnimator as? UIViewPropertyAnimator)?.fractionComplete = value }, completion: { completedEffect = true intermediateCompletion() }) } self.effectView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.05 * animationDurationFactor, delay: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false) } else { UIView.animate(withDuration: 0.21 * animationDurationFactor, animations: { if #available(iOS 9.0, *) { self.effectView.effect = nil } else { self.effectView.alpha = 0.0 } }, completion: { _ in completedEffect = true intermediateCompletion() }) } if !self.dimNode.isHidden { self.dimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) } else { self.withoutBlurDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false) } self.actionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { _ in completedActionsNode = true intermediateCompletion() }) self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2 * animationDurationFactor, removeOnCompletion: false, completion: { _ in }) self.actionsContainerNode.layer.animateScale(from: 1.0, to: 0.1, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) self.contentContainerNode.layer.animateScale(from: 1.0, to: 0.01, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false) let animateOutToItem: Bool switch result { case .default, .custom: animateOutToItem = true case .dismissWithoutContent: animateOutToItem = false } if animateOutToItem, let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let localSourceFrame = self.view.convert(CGRect(origin: CGPoint(x: originalProjectedContentViewFrame.1.minX, y: originalProjectedContentViewFrame.1.minY), size: CGSize(width: originalProjectedContentViewFrame.1.width, height: originalProjectedContentViewFrame.1.height)), to: self.scrollNode.view) self.actionsContainerNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: localSourceFrame.center.x - self.actionsContainerNode.position.x, y: localSourceFrame.center.y - self.actionsContainerNode.position.y), duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true) let contentContainerOffset = CGPoint(x: localSourceFrame.center.x - self.contentContainerNode.frame.center.x, y: localSourceFrame.center.y - self.contentContainerNode.frame.center.y) self.contentContainerNode.layer.animatePosition(from: CGPoint(), to: contentContainerOffset, duration: transitionDuration * animationDurationFactor, timingFunction: transitionCurve.timingFunction, removeOnCompletion: false, additive: true, completion: { [weak self] _ in completedContentNode = true if let strongSelf = self, let contentNode = strongSelf.contentContainerNode.contentNode, case let .controller(controller) = contentNode { controller.sourceView.isHidden = false } intermediateCompletion() }) } else { if let contentNode = self.contentContainerNode.contentNode, case let .controller(controller) = contentNode { controller.sourceView.isHidden = false } if let snapshotView = controller.view.snapshotContentTree(keepTransform: true) { self.contentContainerNode.view.addSubview(snapshotView) } self.contentContainerNode.allowsGroupOpacity = true self.contentContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: transitionDuration * animationDurationFactor, removeOnCompletion: false, completion: { _ in completedContentNode = true intermediateCompletion() }) } } } func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { if let sourceContainer = self.sourceContainer { sourceContainer.addRelativeContentOffset(offset, transition: transition) } } func cancelReactionAnimation() { if let sourceContainer = self.sourceContainer { sourceContainer.cancelReactionAnimation() } } func animateOutToReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: @escaping () -> Void) { if let sourceContainer = self.sourceContainer { sourceContainer.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: completion) } } func animateDismissalIfNeeded() { guard let layout = self.validLayout, layout.metrics.isTablet else { return } if let sourceContainer = self.sourceContainer { sourceContainer.animateOut(result: .dismissWithoutContent, completion: {}) return } } func getActionsMinHeight() -> ContextController.ActionsHeight? { if !self.actionsContainerNode.bounds.height.isZero { return ContextController.ActionsHeight( minY: self.actionsContainerNode.frame.minY, contentOffset: self.scrollNode.view.contentOffset.y ) } else { return nil } } func setItemsSignal(items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition, animated: Bool) { if let sourceContainer = self.sourceContainer { sourceContainer.setItems(items: items, animated: animated) } else { self.legacyItems = items self.itemsDisposable.set((items |> deliverOnMainQueue).start(next: { [weak self] items in guard let strongSelf = self else { return } strongSelf.setItems(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition) })) } } private func setItems(items: ContextController.Items, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { if let sourceContainer = self.sourceContainer { let disableAnimations = self.getController()?.immediateItemsTransitionAnimation == true sourceContainer.setItems(items: .single(items), animated: !disableAnimations) if !self.didSetItemsReady { self.didSetItemsReady = true self.itemsReady.set(.single(true)) } return } if let _ = self.currentItems, !self.didCompleteAnimationIn && self.getController()?.immediateItemsTransitionAnimation == true { return } self.currentItems = items self.currentActionsMinHeight = minHeight let previousActionsContainerNode = self.actionsContainerNode let previousActionsContainerFrame = previousActionsContainerNode.view.convert(previousActionsContainerNode.bounds, to: self.view) self.actionsContainerNode = ContextActionsContainerNode(presentationData: self.presentationData, items: items, getController: { [weak self] in return self?.getController() }, actionSelected: { [weak self] result in self?.beginDismiss(result) }, requestLayout: { [weak self] in self?.updateLayout() }, feedbackTap: { [weak self] in self?.hapticFeedback.tap() }, blurBackground: self.blurBackground) self.scrollNode.insertSubnode(self.actionsContainerNode, aboveSubnode: previousActionsContainerNode) if let layout = self.validLayout { self.updateLayout(layout: layout, transition: self.didSetItemsReady ? .animated(duration: 0.3, curve: .spring) : .immediate, previousActionsContainerNode: previousActionsContainerNode, previousActionsContainerFrame: previousActionsContainerFrame, previousActionsTransition: previousActionsTransition) } else { previousActionsContainerNode.removeFromSupernode() } if !self.didSetItemsReady { self.didSetItemsReady = true self.itemsReady.set(.single(true)) } } func pushItems(items: Signal) { if let sourceContainer = self.sourceContainer { sourceContainer.pushItems(items: items) } } func popItems() { if let sourceContainer = self.sourceContainer { sourceContainer.popItems() } } func updateTheme(presentationData: PresentationData) { self.presentationData = presentationData self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor self.actionsContainerNode.updateTheme(presentationData: presentationData) if let validLayout = self.validLayout { self.updateLayout(layout: validLayout, transition: .immediate, previousActionsContainerNode: nil, previousActionsContainerFrame: nil) } } func updateLayout() { if let layout = self.validLayout { self.updateLayout(layout: layout, transition: .immediate, previousActionsContainerNode: nil) } } func updateLayout(layout: ContainerViewLayout, transition: ContainedViewLayoutTransition, previousActionsContainerNode: ContextActionsContainerNode?, previousActionsContainerFrame: CGRect? = nil, previousActionsTransition: ContextController.PreviousActionsTransition = .scale) { if self.isAnimatingOut || self.delayLayoutUpdate { return } self.validLayout = layout if let sourceContainer = self.sourceContainer { transition.updateFrame(node: sourceContainer, frame: CGRect(origin: CGPoint(), size: layout.size)) sourceContainer.update( presentationData: self.presentationData, layout: layout, transition: transition ) return } var actionsContainerTransition = transition if previousActionsContainerNode != nil { actionsContainerTransition = .immediate } transition.updateFrame(view: self.effectView, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.withoutBlurDimNode, frame: CGRect(origin: CGPoint(), size: layout.size)) switch layout.metrics.widthClass { case .compact: if case .reference = self.legacySource { } else if case let .extracted(extractedSource) = self.legacySource, !extractedSource.blurBackground { } else if self.effectView.superview == nil { self.view.insertSubview(self.effectView, at: 0) if #available(iOS 10.0, *) { if let propertyAnimator = self.propertyAnimator { let propertyAnimator = propertyAnimator as? UIViewPropertyAnimator propertyAnimator?.stopAnimation(true) } } self.effectView.effect = makeCustomZoomBlurEffect(isLight: presentationData.theme.rootController.keyboardColor == .light) self.dimNode.alpha = 1.0 } self.dimNode.isHidden = false self.withoutBlurDimNode.isHidden = true case .regular: if case .reference = self.legacySource { } else if case let .extracted(extractedSource) = self.legacySource, !extractedSource.blurBackground { } else if self.effectView.superview != nil { self.effectView.removeFromSuperview() self.withoutBlurDimNode.alpha = 1.0 } self.dimNode.isHidden = true self.withoutBlurDimNode.isHidden = false } transition.updateFrame(node: self.clippingNode, frame: CGRect(origin: CGPoint(), size: layout.size)) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: layout.size)) let actionsSideInset: CGFloat = layout.safeInsets.left + 12.0 let contentTopInset: CGFloat = max(11.0, layout.statusBarHeight ?? 0.0) let actionsBottomInset: CGFloat = 11.0 if let contentNode = self.contentContainerNode.contentNode { switch contentNode { case let .reference(referenceNode): let contentActionsSpacing: CGFloat = 8.0 if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, presentation: .inline, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: actionsContainerTransition) let adjustedActionsSize = realActionsSize self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize) let contentSize = originalProjectedContentViewFrame.1.size self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition) let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - adjustedActionsSize.height) let originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) let preferredActionsX = originalProjectedContentViewFrame.1.minX var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - adjustedActionsSize.width - actionsSideInset, preferredActionsX)), y: originalActionsY), size: realActionsSize) let originalContentX: CGFloat = originalProjectedContentViewFrame.1.minX let originalContentY = originalProjectedContentViewFrame.1.minY var originalContentFrame = CGRect(origin: CGPoint(x: originalContentX, y: originalContentY), size: originalProjectedContentViewFrame.1.size) let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) let bottomEdge = min(layout.size.height - layout.intrinsicInsets.bottom, self.contentAreaInScreenSpace?.maxY ?? layout.size.height) if originalContentFrame.minY < topEdge { let requiredOffset = topEdge - originalContentFrame.minY let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) let offset = min(requiredOffset, availableOffset) originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) } else if originalActionsFrame.maxY > bottomEdge { let requiredOffset = bottomEdge - originalActionsFrame.maxY let offset = requiredOffset originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) } var contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) contentHeight = max(contentHeight, adjustedActionsSize.height + originalActionsFrame.minY + actionsBottomInset) var overflowOffset: CGFloat var contentContainerFrame: CGRect overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset) let contentParentNode = referenceNode contentContainerFrame = originalContentFrame if !overflowOffset.isZero { let offsetDelta = contentParentNode.frame.height + 4.0 overflowOffset += offsetDelta overflowOffset = min(0.0, overflowOffset) originalActionsFrame.origin.x -= contentParentNode.frame.width + 14.0 originalActionsFrame.origin.x = max(actionsSideInset, originalActionsFrame.origin.x) if originalActionsFrame.minX < contentContainerFrame.minX { contentContainerFrame.origin.x = min(originalActionsFrame.maxX + 14.0, layout.size.width - actionsSideInset) } originalActionsFrame.origin.y += offsetDelta if originalActionsFrame.maxY < originalContentFrame.maxY { originalActionsFrame.origin.y += contentParentNode.frame.height originalActionsFrame.origin.y = min(originalActionsFrame.origin.y, layout.size.height - originalActionsFrame.height - actionsBottomInset) } contentHeight -= offsetDelta } if let customPosition = self.customPosition { originalActionsFrame.origin.x = floor(originalContentFrame.center.x - originalActionsFrame.width / 2.0) + customPosition.x originalActionsFrame.origin.y = floor(originalContentFrame.center.y - originalActionsFrame.height / 2.0) + customPosition.y } let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) if self.scrollNode.view.contentSize != scrollContentSize { self.scrollNode.view.contentSize = scrollContentSize } self.actionsContainerNode.panSelectionGestureEnabled = scrollContentSize.height <= layout.size.height transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) if isInitialLayout { let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) if overflowOffset < 0.0 { transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) } } } case .extractedContainer: break case let .extracted(contentParentNode, keepInPlace): var centerVertically = false if case let .extracted(source) = self.legacySource, source.centerVertically { centerVertically = true } let contentActionsSpacing: CGFloat = keepInPlace ? 16.0 : 8.0 if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) let constrainedActionsHeight: CGFloat let constrainedActionsBottomInset: CGFloat if let currentActionsMinHeight = self.currentActionsMinHeight { constrainedActionsBottomInset = actionsBottomInset + layout.intrinsicInsets.bottom constrainedActionsHeight = layout.size.height - currentActionsMinHeight.minY - constrainedActionsBottomInset } else { constrainedActionsHeight = layout.size.height constrainedActionsBottomInset = 0.0 } let realActionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, presentation: .inline, constrainedWidth: layout.size.width - actionsSideInset * 2.0, constrainedHeight: constrainedActionsHeight, transition: actionsContainerTransition) let adjustedActionsSize = realActionsSize self.actionsContainerNode.updateSize(containerSize: realActionsSize, contentSize: realActionsSize) let contentSize = originalProjectedContentViewFrame.1.size self.contentContainerNode.updateLayout(size: contentSize, scaledSize: contentSize, transition: transition) let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - adjustedActionsSize.height) let preferredActionsX: CGFloat var originalActionsY: CGFloat if centerVertically { originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) preferredActionsX = originalProjectedContentViewFrame.1.maxX - adjustedActionsSize.width } else if keepInPlace { originalActionsY = originalProjectedContentViewFrame.1.minY - contentActionsSpacing - adjustedActionsSize.height preferredActionsX = max(actionsSideInset, originalProjectedContentViewFrame.1.maxX - adjustedActionsSize.width) } else { originalActionsY = min(originalProjectedContentViewFrame.1.maxY + contentActionsSpacing, maximumActionsFrameOrigin) preferredActionsX = originalProjectedContentViewFrame.1.minX } if let currentActionsMinHeight = self.currentActionsMinHeight { originalActionsY = currentActionsMinHeight.minY } var originalActionsFrame = CGRect(origin: CGPoint(x: max(actionsSideInset, min(layout.size.width - adjustedActionsSize.width - actionsSideInset, preferredActionsX)), y: originalActionsY), size: realActionsSize) let originalContentX: CGFloat = originalProjectedContentViewFrame.1.minX let originalContentY: CGFloat if keepInPlace { originalContentY = originalProjectedContentViewFrame.1.minY } else { originalContentY = originalActionsFrame.minY - contentActionsSpacing - originalProjectedContentViewFrame.1.size.height } var originalContentFrame = CGRect(origin: CGPoint(x: originalContentX, y: originalContentY), size: originalProjectedContentViewFrame.1.size) let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) if originalContentFrame.minY < topEdge { let requiredOffset = topEdge - originalContentFrame.minY let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) let offset = min(requiredOffset, availableOffset) originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) } var contentHeight: CGFloat if keepInPlace { contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalActionsFrame.minY + contentTopInset) } else { if self.currentActionsMinHeight != nil { contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset + layout.intrinsicInsets.bottom)) } else { contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset + layout.intrinsicInsets.bottom) - originalContentFrame.minY + contentTopInset) } } var overflowOffset: CGFloat var contentContainerFrame: CGRect if centerVertically { overflowOffset = 0.0 if layout.size.width > layout.size.height, case .compact = layout.metrics.widthClass { let totalWidth = originalContentFrame.width + originalActionsFrame.width + contentActionsSpacing contentContainerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - totalWidth) / 2.0 + originalContentFrame.width * 0.1), y: floor((layout.size.height - originalContentFrame.height) / 2.0)), size: originalContentFrame.size) originalActionsFrame.origin.x = contentContainerFrame.maxX + contentActionsSpacing + 14.0 originalActionsFrame.origin.y = contentContainerFrame.origin.y contentHeight = layout.size.height } else { let totalHeight = originalContentFrame.height + originalActionsFrame.height contentContainerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - originalContentFrame.width) / 2.0), y: floor((layout.size.height - totalHeight) / 2.0)), size: originalContentFrame.size) originalActionsFrame.origin.y = contentContainerFrame.maxY + contentActionsSpacing } } else if keepInPlace { overflowOffset = min(0.0, originalActionsFrame.minY - contentTopInset) contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -contentParentNode.contentRect.minY) if !overflowOffset.isZero { let offsetDelta = contentParentNode.contentRect.height + 4.0 overflowOffset += offsetDelta overflowOffset = min(0.0, overflowOffset) originalActionsFrame.origin.x -= contentParentNode.contentRect.maxX - contentParentNode.contentRect.minX + 14.0 originalActionsFrame.origin.x = max(actionsSideInset, originalActionsFrame.origin.x) //originalActionsFrame.origin.y += contentParentNode.contentRect.height if originalActionsFrame.minX < contentContainerFrame.minX { contentContainerFrame.origin.x = min(originalActionsFrame.maxX + 14.0, layout.size.width - actionsSideInset) } originalActionsFrame.origin.y += offsetDelta if originalActionsFrame.maxY < originalContentFrame.maxY { originalActionsFrame.origin.y += contentParentNode.contentRect.height originalActionsFrame.origin.y = min(originalActionsFrame.origin.y, layout.size.height - originalActionsFrame.height - actionsBottomInset) } contentHeight -= offsetDelta } } else { overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) contentContainerFrame = originalContentFrame.offsetBy(dx: -contentParentNode.contentRect.minX, dy: -overflowOffset - contentParentNode.contentRect.minY) if contentContainerFrame.maxX > layout.size.width { contentContainerFrame = CGRect(origin: CGPoint(x: layout.size.width - contentContainerFrame.width - 11.0, y: contentContainerFrame.minY), size: contentContainerFrame.size) } } let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) if self.scrollNode.view.contentSize != scrollContentSize { self.scrollNode.view.contentSize = scrollContentSize } self.actionsContainerNode.panSelectionGestureEnabled = scrollContentSize.height <= layout.size.height transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame) actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) if isInitialLayout { //let previousContentOffset = self.scrollNode.view.contentOffset.y if !keepInPlace { if let currentActionsMinHeight = self.currentActionsMinHeight { self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: currentActionsMinHeight.contentOffset) } else { self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) } } let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) var offset: CGFloat = 0.0 //offset -= previousContentOffset - self.scrollNode.view.contentOffset.y offset += previousContainerFrame.minY - currentContainerFrame.minY transition.animatePositionAdditive(node: self.contentContainerNode, offset: CGPoint(x: 0.0, y: offset)) if overflowOffset < 0.0 { let _ = currentContainerFrame let _ = previousContainerFrame } } let absoluteContentRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) contentParentNode.updateAbsoluteRect?(absoluteContentRect, layout.size) } case let .controller(contentParentNode): var projectedFrame: CGRect = convertFrame(contentParentNode.sourceView.bounds, from: contentParentNode.sourceView, to: self.view) switch self.legacySource { case let .controller(source): let transitionInfo = source.transitionInfo() if let (sourceView, sourceRect) = transitionInfo?.sourceNode() { projectedFrame = convertFrame(sourceRect, from: sourceView, to: self.view) } default: break } self.originalProjectedContentViewFrame = (projectedFrame, projectedFrame) if let originalProjectedContentViewFrame = self.originalProjectedContentViewFrame { let contentActionsSpacing: CGFloat = actionsSideInset let topEdge = max(contentTopInset, self.contentAreaInScreenSpace?.minY ?? 0.0) let isInitialLayout = self.actionsContainerNode.frame.size.width.isZero let previousContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) let constrainedWidth: CGFloat if layout.size.width < layout.size.height { constrainedWidth = layout.size.width } else { constrainedWidth = floor(layout.size.width / 2.0) } let actionsSize = self.actionsContainerNode.updateLayout(widthClass: layout.metrics.widthClass, presentation: .inline, constrainedWidth: constrainedWidth - actionsSideInset * 2.0, constrainedHeight: layout.size.height, transition: actionsContainerTransition) let contentScale = (constrainedWidth - actionsSideInset * 2.0) / constrainedWidth var contentUnscaledSize: CGSize if case .compact = layout.metrics.widthClass { self.actionsContainerNode.updateSize(containerSize: actionsSize, contentSize: actionsSize) let proposedContentHeight: CGFloat if layout.size.width < layout.size.height { proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset } else { proposedContentHeight = layout.size.height - topEdge - topEdge let maxActionsHeight = layout.size.height - topEdge - topEdge self.actionsContainerNode.updateSize(containerSize: CGSize(width: actionsSize.width, height: min(actionsSize.height, maxActionsHeight)), contentSize: actionsSize) } contentUnscaledSize = CGSize(width: constrainedWidth, height: max(100.0, proposedContentHeight)) if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, 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)) { contentUnscaledSize = preferredSize } } else { let maxActionsHeight = layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height self.actionsContainerNode.updateSize(containerSize: CGSize(width: actionsSize.width, height: min(actionsSize.height, maxActionsHeight)), contentSize: actionsSize) let proposedContentHeight = layout.size.height - topEdge - contentActionsSpacing - actionsSize.height - layout.intrinsicInsets.bottom - actionsBottomInset contentUnscaledSize = CGSize(width: min(layout.size.width, 340.0), height: min(568.0, proposedContentHeight)) if let preferredSize = contentParentNode.controller.preferredContentSizeForLayout(ContainerViewLayout(size: contentUnscaledSize, 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)) { contentUnscaledSize = preferredSize } } let contentSize = CGSize(width: floor(contentUnscaledSize.width * contentScale), height: floor(contentUnscaledSize.height * contentScale)) self.contentContainerNode.updateLayout(size: contentUnscaledSize, scaledSize: contentSize, transition: transition) let maximumActionsFrameOrigin = max(60.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - actionsSize.height) var originalActionsFrame: CGRect var originalContentFrame: CGRect var contentHeight: CGFloat if case .compact = layout.metrics.widthClass { if layout.size.width < layout.size.height { let sideInset = floor((layout.size.width - max(contentSize.width, actionsSize.width)) / 2.0) originalActionsFrame = CGRect(origin: CGPoint(x: sideInset, y: min(maximumActionsFrameOrigin, floor((layout.size.height - contentActionsSpacing - contentSize.height) / 2.0) + contentSize.height + contentActionsSpacing)), size: actionsSize) originalContentFrame = CGRect(origin: CGPoint(x: sideInset, y: originalActionsFrame.minY - contentActionsSpacing - contentSize.height), size: contentSize) if originalContentFrame.minY < topEdge { let requiredOffset = topEdge - originalContentFrame.minY let availableOffset = max(0.0, layout.size.height - layout.intrinsicInsets.bottom - actionsBottomInset - originalActionsFrame.maxY) let offset = min(requiredOffset, availableOffset) originalActionsFrame = originalActionsFrame.offsetBy(dx: 0.0, dy: offset) originalContentFrame = originalContentFrame.offsetBy(dx: 0.0, dy: offset) } contentHeight = max(layout.size.height, max(layout.size.height, originalActionsFrame.maxY + actionsBottomInset) - originalContentFrame.minY + contentTopInset) } else { originalContentFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - contentSize.width - actionsSideInset - actionsSize.width) / 2.0), y: floor((layout.size.height - contentSize.height) / 2.0)), size: contentSize) originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.maxX + actionsSideInset, y: max(topEdge, originalContentFrame.minY)), size: actionsSize) contentHeight = max(layout.size.height, max(originalContentFrame.maxY, originalActionsFrame.maxY)) } } else { originalContentFrame = CGRect(origin: CGPoint(x: floor(originalProjectedContentViewFrame.1.midX - contentSize.width / 2.0), y: floor(originalProjectedContentViewFrame.1.midY - contentSize.height / 2.0)), size: contentSize) originalContentFrame.origin.x = min(originalContentFrame.origin.x, layout.size.width - actionsSideInset - contentSize.width) originalContentFrame.origin.x = max(originalContentFrame.origin.x, actionsSideInset) originalContentFrame.origin.y = min(originalContentFrame.origin.y, layout.size.height - layout.intrinsicInsets.bottom - actionsSideInset - contentSize.height) originalContentFrame.origin.y = max(originalContentFrame.origin.y, contentTopInset) if originalContentFrame.maxX <= layout.size.width - actionsSideInset - actionsSize.width - contentActionsSpacing { originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.maxX + contentActionsSpacing, y: originalContentFrame.minY), size: actionsSize) if originalActionsFrame.maxX > layout.size.width - actionsSideInset { let offset = originalActionsFrame.maxX - (layout.size.width - actionsSideInset) originalActionsFrame.origin.x -= offset originalContentFrame.origin.x -= offset } } else { originalActionsFrame = CGRect(origin: CGPoint(x: originalContentFrame.minX - contentActionsSpacing - actionsSize.width, y: originalContentFrame.minY), size: actionsSize) if originalActionsFrame.minX < actionsSideInset { let offset = actionsSideInset - originalActionsFrame.minX originalActionsFrame.origin.x += offset originalContentFrame.origin.x += offset } } contentHeight = layout.size.height contentHeight = max(contentHeight, originalActionsFrame.maxY + actionsBottomInset) contentHeight = max(contentHeight, originalContentFrame.maxY + actionsBottomInset) } let scrollContentSize = CGSize(width: layout.size.width, height: contentHeight) if self.scrollNode.view.contentSize != scrollContentSize { self.scrollNode.view.contentSize = scrollContentSize } self.actionsContainerNode.panSelectionGestureEnabled = true let overflowOffset = min(0.0, originalContentFrame.minY - contentTopInset) let contentContainerFrame = originalContentFrame transition.updateFrame(node: self.contentContainerNode, frame: contentContainerFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) actionsContainerTransition.updateFrame(node: self.actionsContainerNode, frame: originalActionsFrame.offsetBy(dx: 0.0, dy: -overflowOffset)) if isInitialLayout { self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y: -overflowOffset) let currentContainerFrame = self.view.convert(self.contentContainerNode.frame, from: self.scrollNode.view) if overflowOffset < 0.0 { transition.animateOffsetAdditive(node: self.scrollNode, offset: currentContainerFrame.minY - previousContainerFrame.minY) } } } } } if let previousActionsContainerNode = previousActionsContainerNode { if transition.isAnimated && self.getController()?.immediateItemsTransitionAnimation == false { if previousActionsContainerNode.hasAdditionalActions && !self.actionsContainerNode.hasAdditionalActions && self.getController()?.useComplexItemsTransitionAnimation == true { var initialFrame = self.actionsContainerNode.frame let delta = (previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height) initialFrame.origin.y = self.actionsContainerNode.frame.minY + previousActionsContainerNode.frame.height - self.actionsContainerNode.frame.height transition.animateFrame(node: self.actionsContainerNode, from: initialFrame) transition.animatePosition(node: previousActionsContainerNode, to: CGPoint(x: 0.0, y: -delta), removeOnCompletion: false, additive: true) previousActionsContainerNode.animateOut(offset: delta, transition: transition) previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in previousActionsContainerNode?.removeFromSupernode() }) self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } else { if let previousActionsContainerFrame = previousActionsContainerFrame { previousActionsContainerNode.frame = self.view.convert(previousActionsContainerFrame, to: self.actionsContainerNode.view.superview!) } switch previousActionsTransition { case .scale: transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.1) previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in previousActionsContainerNode?.removeFromSupernode() }) transition.animateTransformScale(node: self.actionsContainerNode, from: 0.1) self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) case let .slide(forward): let deltaY = self.actionsContainerNode.frame.minY - previousActionsContainerNode.frame.minY var previousNodePosition = previousActionsContainerNode.position.offsetBy(dx: 0.0, dy: deltaY) let additionalHorizontalOffset: CGFloat = 20.0 let currentNodeOffset: CGFloat if forward { previousNodePosition = previousNodePosition.offsetBy(dx: -previousActionsContainerNode.frame.width / 2.0 - additionalHorizontalOffset, dy: -previousActionsContainerNode.frame.height / 2.0) currentNodeOffset = self.actionsContainerNode.bounds.width / 2.0 + additionalHorizontalOffset } else { previousNodePosition = previousNodePosition.offsetBy(dx: previousActionsContainerNode.frame.width / 2.0 + additionalHorizontalOffset, dy: -previousActionsContainerNode.frame.height / 2.0) currentNodeOffset = -self.actionsContainerNode.bounds.width / 2.0 - additionalHorizontalOffset } transition.updatePosition(node: previousActionsContainerNode, position: previousNodePosition) transition.updateTransformScale(node: previousActionsContainerNode, scale: 0.01) previousActionsContainerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak previousActionsContainerNode] _ in previousActionsContainerNode?.removeFromSupernode() }) transition.animatePositionAdditive(node: self.actionsContainerNode, offset: CGPoint(x: currentNodeOffset, y: -deltaY - self.actionsContainerNode.bounds.height / 2.0)) transition.animateTransformScale(node: self.actionsContainerNode, from: 0.01) self.actionsContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } } else { previousActionsContainerNode.removeFromSupernode() } } transition.updateFrame(node: self.dismissNode, frame: CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)) self.dismissAccessibilityArea.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize) } func scrollViewDidScroll(_ scrollView: UIScrollView) { guard let layout = self.validLayout else { return } if let maybeContentNode = self.contentContainerNode.contentNode, case let .extracted(contentParentNode, keepInPlace) = maybeContentNode { let contentContainerFrame = self.contentContainerNode.frame let absoluteRect: CGRect if keepInPlace { absoluteRect = contentContainerFrame } else { absoluteRect = contentContainerFrame.offsetBy(dx: 0.0, dy: -self.scrollNode.view.contentOffset.y) } contentParentNode.updateAbsoluteRect?(absoluteRect, layout.size) } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if !self.bounds.contains(point) { return nil } if !self.isUserInteractionEnabled { return nil } if let controller = self.getController() as? ContextController { var innerResult: UIView? controller.forEachController { c in if let c = c as? UndoOverlayController { if let result = c.view.hitTest(self.view.convert(point, to: c.view), with: event) { innerResult = result return false } } return true } if let innerResult = innerResult { return innerResult } } if let sourceContainer = self.sourceContainer { return sourceContainer.hitTest(self.view.convert(point, to: sourceContainer.view), with: event) } let mappedPoint = self.view.convert(point, to: self.scrollNode.view) var maybePassthrough: ContextControllerImpl.HandledTouchEvent? if let maybeContentNode = self.contentContainerNode.contentNode { switch maybeContentNode { case .reference: if let controller = self.getController() as? ContextControllerImpl, let passthroughTouchEvent = controller.passthroughTouchEvent { maybePassthrough = passthroughTouchEvent(self.view, point) } case let .extracted(contentParentNode, _): if case let .extracted(source) = self.legacySource { if !source.ignoreContentTouches { let contentPoint = self.view.convert(point, to: contentParentNode.contentNode.view) if let result = contentParentNode.contentNode.customHitTest?(contentPoint) { return result } else if let result = contentParentNode.contentNode.hitTest(contentPoint, with: event) { if result is TextSelectionNodeView { return result } else if contentParentNode.contentRect.contains(contentPoint) { return contentParentNode.contentNode.view } } } } case .extractedContainer: break case let .controller(controller): var passthrough = false switch self.legacySource { case let .controller(controllerSource): passthrough = controllerSource.passthroughTouches default: break } if passthrough { let controllerPoint = self.view.convert(point, to: controller.controller.view) if let result = controller.controller.view.hitTest(controllerPoint, with: event) { return result } } } } if self.actionsContainerNode.frame.contains(mappedPoint) { return self.actionsContainerNode.hitTest(self.view.convert(point, to: self.actionsContainerNode.view), with: event) } if let maybePassthrough = maybePassthrough { switch maybePassthrough { case .ignore: break case let .dismiss(consume, hitTestResult): self.getController()?.dismiss(completion: nil) if let hitTestResult = hitTestResult { return hitTestResult } if !consume { return nil } } } return self.dismissNode.view } fileprivate func performHighlightedAction() { self.sourceContainer?.performHighlightedAction() } fileprivate func decreaseHighlightedIndex() { self.sourceContainer?.decreaseHighlightedIndex() } fileprivate func increaseHighlightedIndex() { self.sourceContainer?.increaseHighlightedIndex() } } public final class ContextControllerImpl: ViewController, ContextController, StandalonePresentableController, ContextControllerProtocol, KeyShortcutResponder { private let context: AccountContext? private var presentationData: PresentationData private let configuration: ContextController.Configuration private let _ready = Promise() override public var ready: Promise { return self._ready } private weak var recognizer: TapLongTapOrDoubleTapGestureRecognizer? private weak var gesture: ContextGesture? private var animatedDidAppear = false private var wasDismissed = false private var dismissOnInputClose: (result: ContextMenuActionResult, completion: (() -> Void)?)? private var dismissToReactionOnInputClose: (value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, completion: (() -> Void)?)? override public var overlayWantsToBeBelowKeyboard: Bool { if self.isNodeLoaded { return self.controllerNode.overlayWantsToBeBelowKeyboard } else { return false } } var controllerNode: ContextControllerNode { return self.displayNode as! ContextControllerNode } public var dismissed: (() -> Void)? public var dismissedForCancel: (() -> Void)? { didSet { self.controllerNode.dismissedForCancel = self.dismissedForCancel } } public var useComplexItemsTransitionAnimation = false public var immediateItemsTransitionAnimation = false let workaroundUseLegacyImplementation: Bool let disableScreenshots: Bool let hideReactionPanelTail: Bool public var passthroughTouchEvent: ((UIView, CGPoint) -> HandledTouchEvent)? private var shouldBeDismissedDisposable: Disposable? public var reactionSelected: ((UpdateMessageReaction, Bool) -> Void)? public var premiumReactionsSelected: (() -> Void)? public var getOverlayViews: (() -> [UIView])? public init( context: AccountContext? = nil, presentationData: PresentationData, configuration: ContextController.Configuration, recognizer: TapLongTapOrDoubleTapGestureRecognizer? = nil, gesture: ContextGesture? = nil, workaroundUseLegacyImplementation: Bool = false, disableScreenshots: Bool = false, hideReactionPanelTail: Bool = false ) { self.context = context self.presentationData = presentationData self.configuration = configuration self.recognizer = recognizer self.gesture = gesture self.workaroundUseLegacyImplementation = workaroundUseLegacyImplementation self.disableScreenshots = disableScreenshots self.hideReactionPanelTail = hideReactionPanelTail super.init(navigationBarPresentationData: nil) if let mainSource = configuration.sources.first(where: { $0.id == configuration.initialId }) { switch mainSource.source { case let .location(locationSource): self.statusBar.statusBarStyle = .Ignore self.shouldBeDismissedDisposable = (locationSource.shouldBeDismissed |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.dismiss(result: .default, completion: {}) }).strict() case let .reference(referenceSource): self.statusBar.statusBarStyle = .Ignore self.shouldBeDismissedDisposable = (referenceSource.shouldBeDismissed |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.dismiss(result: .default, completion: {}) }).strict() case let .extracted(extractedSource): if extractedSource.blurBackground { self.statusBar.statusBarStyle = .Hide } else { self.statusBar.statusBarStyle = .Ignore } self.shouldBeDismissedDisposable = (extractedSource.shouldBeDismissed |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let strongSelf = self else { return } strongSelf.dismiss(result: .default, completion: {}) }).strict() case .controller: self.statusBar.statusBarStyle = .Hide } } self.lockOrientation = true self.blocksBackgroundWhenInOverlay = true } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { self.shouldBeDismissedDisposable?.dispose() } override public func loadDisplayNode() { self.displayNode = ContextControllerNode(controller: self, context: self.context, presentationData: self.presentationData, configuration: self.configuration, beginDismiss: { [weak self] result in self?.dismiss(result: result, completion: nil) }, recognizer: self.recognizer, gesture: self.gesture, beganAnimatingOut: { [weak self] in guard let strongSelf = self else { return } strongSelf.statusBar.statusBarStyle = .Ignore strongSelf.forEachController { c in if let c = c as? UndoOverlayController { c.dismiss() } return true } }, attemptTransitionControllerIntoNavigation: { }) self.controllerNode.dismissedForCancel = self.dismissedForCancel self.displayNodeDidLoad() self._ready.set(combineLatest(queue: .mainQueue(), self.controllerNode.itemsReady.get(), self.controllerNode.contentReady.get()) |> map { values in return values.0 && values.1 }) } override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) self.controllerNode.updateLayout(layout: layout, transition: transition, previousActionsContainerNode: nil) if (layout.inputHeight ?? 0.0) == 0.0 { if let dismissOnInputClose = self.dismissOnInputClose { self.dismissOnInputClose = nil DispatchQueue.main.async { self.dismiss(result: dismissOnInputClose.result, completion: dismissOnInputClose.completion) } } else if let args = self.dismissToReactionOnInputClose { self.dismissToReactionOnInputClose = nil DispatchQueue.main.async { self.dismissWithReactionImpl(value: args.value, targetView: args.targetView, hideNode: args.hideNode, animateTargetContainer: args.animateTargetContainer, addStandaloneReactionAnimation: args.addStandaloneReactionAnimation, reducedCurve: true, onHit: nil, completion: args.completion) } } } } override public func viewDidAppear(_ animated: Bool) { if self.ignoreAppearanceMethodInvocations() { return } super.viewDidAppear(animated) if !self.wasDismissed && !self.animatedDidAppear { self.animatedDidAppear = true self.controllerNode.animateIn() } } public func getActionsMinHeight() -> ContextController.ActionsHeight? { if self.isNodeLoaded { return self.controllerNode.getActionsMinHeight() } return nil } public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, animated: Bool) { //self.items = items if self.isNodeLoaded { self.immediateItemsTransitionAnimation = false self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: .scale, animated: animated) } else { assertionFailure() } } public func setItems(_ items: Signal, minHeight: ContextController.ActionsHeight?, previousActionsTransition: ContextController.PreviousActionsTransition) { //self.items = items if self.isNodeLoaded { self.controllerNode.setItemsSignal(items: items, minHeight: minHeight, previousActionsTransition: previousActionsTransition, animated: true) } else { assertionFailure() } } public func pushItems(items: Signal) { if !self.isNodeLoaded { return } self.controllerNode.pushItems(items: items) } public func popItems() { if !self.isNodeLoaded { return } self.controllerNode.popItems() } public func updateTheme(presentationData: PresentationData) { self.presentationData = presentationData if self.isNodeLoaded { self.controllerNode.updateTheme(presentationData: presentationData) } } public func dismiss(result: ContextMenuActionResult, completion: (() -> Void)?) { if let mainSource = self.configuration.sources.first(where: { $0.id == self.configuration.initialId }), case let .reference(source) = mainSource.source, source.forceDisplayBelowKeyboard { } else if viewTreeContainsFirstResponder(view: self.view) { self.dismissOnInputClose = (result, completion) self.view.endEditing(true) return } if !self.wasDismissed { self.wasDismissed = true self.controllerNode.animateOut(result: result, completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() }) self.dismissed?() } } override public func dismiss(completion: (() -> Void)? = nil) { self.dismiss(result: .default, completion: completion) } public func dismissWithCustomTransition(transition: ContainedViewLayoutTransition, completion: (() -> Void)? = nil) { self.dismiss(result: .custom(transition), completion: nil) } public func dismissWithoutContent() { self.dismiss(result: .dismissWithoutContent, completion: nil) } public func dismissNow() { self.presentingViewController?.dismiss(animated: false, completion: nil) self.dismissed?() } public func dismissWithReaction(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, onHit: (() -> Void)?, completion: (() -> Void)?) { self.dismissWithReactionImpl(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: false, onHit: onHit, completion: completion) } private func dismissWithReactionImpl(value: MessageReaction.Reaction, targetView: UIView, hideNode: Bool, animateTargetContainer: UIView?, addStandaloneReactionAnimation: ((StandaloneReactionAnimation) -> Void)?, reducedCurve: Bool, onHit: (() -> Void)?, completion: (() -> Void)?) { if viewTreeContainsFirstResponder(view: self.view) { self.dismissToReactionOnInputClose = (value, targetView, hideNode, animateTargetContainer, addStandaloneReactionAnimation, completion) self.view.endEditing(true) return } if !self.wasDismissed { self.wasDismissed = true self.controllerNode.animateOutToReaction(value: value, targetView: targetView, hideNode: hideNode, animateTargetContainer: animateTargetContainer, addStandaloneReactionAnimation: addStandaloneReactionAnimation, reducedCurve: reducedCurve, onHit: onHit, completion: { [weak self] in self?.presentingViewController?.dismiss(animated: false, completion: nil) completion?() }) self.dismissed?() } } public func animateDismissalIfNeeded() { self.controllerNode.animateDismissalIfNeeded() } public func cancelReactionAnimation() { self.controllerNode.cancelReactionAnimation() } public func addRelativeContentOffset(_ offset: CGPoint, transition: ContainedViewLayoutTransition) { self.controllerNode.addRelativeContentOffset(offset, transition: transition) } public var keyShortcuts: [KeyShortcut] { return [ KeyShortcut( input: UIKeyCommand.inputEscape, modifiers: [], action: { [weak self] in self?.dismissWithoutContent() } ), KeyShortcut( input: "W", modifiers: [.command], action: { [weak self] in self?.dismissWithoutContent() } ), KeyShortcut( input: "\r", modifiers: [], action: { [weak self] in self?.controllerNode.performHighlightedAction() } ), KeyShortcut( input: UIKeyCommand.inputUpArrow, modifiers: [], action: { [weak self] in self?.controllerNode.decreaseHighlightedIndex() } ), KeyShortcut( input: UIKeyCommand.inputDownArrow, modifiers: [], action: { [weak self] in self?.controllerNode.increaseHighlightedIndex() } ) ] } }