import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import AccountContext import ComponentFlow import ViewControllerComponent import EntityKeyboard import PagerComponent private final class StickerSelectionComponent: Component { public typealias EnvironmentType = Empty public let theme: PresentationTheme public let strings: PresentationStrings public let deviceMetrics: DeviceMetrics public let stickerContent: EmojiPagerContentComponent public let backgroundColor: UIColor public let separatorColor: UIColor public init( theme: PresentationTheme, strings: PresentationStrings, deviceMetrics: DeviceMetrics, stickerContent: EmojiPagerContentComponent, backgroundColor: UIColor, separatorColor: UIColor ) { self.theme = theme self.strings = strings self.deviceMetrics = deviceMetrics self.stickerContent = stickerContent self.backgroundColor = backgroundColor self.separatorColor = separatorColor } public static func ==(lhs: StickerSelectionComponent, rhs: StickerSelectionComponent) -> Bool { if lhs.theme !== rhs.theme { return false } if lhs.strings != rhs.strings { return false } if lhs.deviceMetrics != rhs.deviceMetrics { return false } if lhs.stickerContent != rhs.stickerContent { return false } if lhs.backgroundColor != rhs.backgroundColor { return false } if lhs.separatorColor != rhs.separatorColor { return false } return true } public final class View: UIView { private let keyboardView: ComponentView private let keyboardClippingView: UIView private let panelHostView: PagerExternalTopPanelContainer private let panelBackgroundView: BlurredBackgroundView private let panelSeparatorView: UIView private var component: StickerSelectionComponent? private weak var state: EmptyComponentState? override init(frame: CGRect) { self.keyboardView = ComponentView() self.keyboardClippingView = UIView() self.panelHostView = PagerExternalTopPanelContainer() self.panelBackgroundView = BlurredBackgroundView(color: .clear, enableBlur: true) self.panelSeparatorView = UIView() super.init(frame: frame) self.addSubview(self.keyboardClippingView) self.addSubview(self.panelBackgroundView) self.addSubview(self.panelSeparatorView) self.addSubview(self.panelHostView) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { } func update(component: StickerSelectionComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { self.backgroundColor = component.backgroundColor let panelBackgroundColor = component.backgroundColor.withMultipliedAlpha(0.85) self.panelBackgroundView.updateColor(color: panelBackgroundColor, transition: .immediate) self.panelSeparatorView.backgroundColor = component.separatorColor self.component = component self.state = state let topPanelHeight: CGFloat = 42.0 let keyboardSize = self.keyboardView.update( transition: transition,//.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), component: AnyComponent(EntityKeyboardComponent( theme: component.theme, strings: component.strings, isContentInFocus: true, containerInsets: UIEdgeInsets(top: topPanelHeight - 34.0, left: 0.0, bottom: 0.0, right: 0.0), topPanelInsets: UIEdgeInsets(top: 0.0, left: 4.0, bottom: 0.0, right: 4.0), emojiContent: nil, stickerContent: component.stickerContent, gifContent: nil, hasRecentGifs: false, availableGifSearchEmojies: [], defaultToEmojiTab: false, externalTopPanelContainer: self.panelHostView, topPanelExtensionUpdated: { _, _ in }, hideInputUpdated: { _, _, _ in }, hideTopPanelUpdated: { _, _ in }, switchToTextInput: {}, switchToGifSubject: { _ in }, reorderItems: { _, _ in }, makeSearchContainerNode: { _ in return nil }, deviceMetrics: component.deviceMetrics, hiddenInputHeight: 0.0, inputHeight: 0.0, displayBottomPanel: false, isExpanded: true )), environment: {}, containerSize: availableSize ) if let keyboardComponentView = self.keyboardView.view { if keyboardComponentView.superview == nil { self.keyboardClippingView.addSubview(keyboardComponentView) } if panelBackgroundColor.alpha < 0.01 { self.keyboardClippingView.clipsToBounds = true } else { self.keyboardClippingView.clipsToBounds = false } transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight))) transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight), size: keyboardSize)) transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight - 34.0), size: CGSize(width: keyboardSize.width, height: 0.0))) transition.setFrame(view: self.panelBackgroundView, frame: CGRect(origin: CGPoint(), size: CGSize(width: keyboardSize.width, height: topPanelHeight))) self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel))) transition.setAlpha(view: self.panelSeparatorView, alpha: 1.0) } return availableSize } } public func makeView() -> View { return View(frame: CGRect()) } public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } public class StickerPickerScreen: ViewController { final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { private var presentationData: PresentationData private weak var controller: StickerPickerScreen? private let theme: PresentationTheme let dim: ASDisplayNode let wrappingView: UIView let containerView: UIView let scrollView: UIScrollView let hostView: ComponentHostView private var stickerContent: EmojiPagerContentComponent? private let stickerContentDisposable = MetaDisposable() private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation? private(set) var isExpanded = false private var panGestureRecognizer: UIPanGestureRecognizer? private var panGestureArguments: (topInset: CGFloat, offset: CGFloat, scrollView: UIScrollView?, listNode: ListView?)? private var currentIsVisible: Bool = false private var currentLayout: (layout: ContainerViewLayout, navigationHeight: CGFloat)? fileprivate var temporaryDismiss = false init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.controller = controller self.theme = theme self.dim = ASDisplayNode() self.dim.alpha = 0.0 self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) self.wrappingView = UIView() self.containerView = UIView() self.scrollView = UIScrollView() self.hostView = ComponentHostView() super.init() self.scrollView.delegate = self self.scrollView.showsVerticalScrollIndicator = false self.containerView.clipsToBounds = true self.containerView.backgroundColor = .clear self.addSubnode(self.dim) self.view.addSubview(self.wrappingView) self.wrappingView.addSubview(self.containerView) self.containerView.addSubview(self.scrollView) self.scrollView.addSubview(self.hostView) self.stickerContentDisposable.set(( EmojiPagerContentComponent.stickerInputData( context: context, animationCache: context.animationCache, animationRenderer: context.animationRenderer, stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], chatPeerId: context.account.peerId ) |> deliverOnMainQueue).start(next: { [weak self] content in if let strongSelf = self { strongSelf.updateContent(content) } }) ) } deinit { self.stickerContentDisposable.dispose() } func updateContent(_ content: EmojiPagerContentComponent) { self.stickerContent = content content.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction( performItemAction: { [weak self] _, item, _, _, _, _ in guard let strongSelf = self, let file = item.itemFile else { return } strongSelf.controller?.completion(file) strongSelf.controller?.dismiss(animated: true) }, deleteBackwards: {}, openStickerSettings: { // guard let controllerInteraction = controllerInteraction else { // return // } // let controller = installedStickerPacksController(context: context, mode: .modal) // controller.navigationPresentation = .modal // controllerInteraction.navigationController()?.pushViewController(controller) }, openFeatured: { // guard let controllerInteraction = controllerInteraction else { // return // } // // controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen( // context: context, // highlightedPackId: nil, // sendSticker: { [weak controllerInteraction] fileReference, sourceNode, sourceRect in // guard let controllerInteraction = controllerInteraction else { // return false // } // return controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, []) // } // )) }, openSearch: {}, addGroupAction: { [weak self] groupId, isPremiumLocked in guard let strongSelf = self, let controller = strongSelf.controller, let collectionId = groupId.base as? ItemCollectionId else { return } let context = controller.context if isPremiumLocked { // let controller = PremiumIntroScreen(context: context, source: .stickers) // controllerInteraction.navigationController()?.pushViewController(controller) return } let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks) let _ = (context.account.postbox.combinedView(keys: [viewKey]) |> take(1) |> deliverOnMainQueue).start(next: { views in guard let view = views.views[viewKey] as? OrderedItemListView else { return } for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) { if featuredStickerPack.info.id == collectionId { let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false) |> mapToSignal { result -> Signal in switch result { case let .result(info, items, installed): if installed { return .complete() } else { return context.engine.stickers.addStickerPackInteractively(info: info, items: items) } case .fetching: break case .none: break } return .complete() } |> deliverOnMainQueue).start(completed: { }) break } } }) }, clearGroup: { [weak self] groupId in guard let strongSelf = self, let controller = strongSelf.controller else { return } if groupId == AnyHashable("popular") { let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 } let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize)) var items: [ActionSheetItem] = [] let context = controller.context items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true)) items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() guard let strongSelf = self else { return } strongSelf.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular")) let _ = context.engine.stickers.clearRecentlyUsedReactions().start() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet) } }, pushController: { c in }, presentController: { c in }, presentGlobalOverlayController: { c in }, navigationController: { [weak self] in return self?.controller?.navigationController as? NavigationController }, requestUpdate: { _ in }, updateSearchQuery: { _, _ in }, chatPeerId: nil, peekBehavior: nil, customLayout: nil, externalBackground: nil, externalExpansionView: nil, useOpaqueTheme: false ) if let (layout, navigationHeight) = self.currentLayout { self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) } } override func didLoad() { super.didLoad() let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) panRecognizer.delegate = self panRecognizer.delaysTouchesBegan = false panRecognizer.cancelsTouchesInView = true self.panGestureRecognizer = panRecognizer self.wrappingView.addGestureRecognizer(panRecognizer) self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate) } @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.controller?.completion(nil) self.controller?.dismiss(animated: true) } } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { if let (layout, _) = self.currentLayout { if case .regular = layout.metrics.widthClass { return false } } return true } func scrollViewDidScroll(_ scrollView: UIScrollView) { let contentOffset = self.scrollView.contentOffset.y self.controller?.navigationBar?.updateBackgroundAlpha(min(30.0, contentOffset) / 30.0, transition: .immediate) } func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer { return true } return false } private var isDismissing = false func animateIn() { ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) let targetPosition = self.containerView.center let startPosition = targetPosition.offsetBy(dx: 0.0, dy: self.bounds.height) self.containerView.center = startPosition let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) transition.animateView(allowUserInteraction: true, { self.containerView.center = targetPosition }, completion: { _ in }) } func animateOut(completion: @escaping () -> Void = {}) { self.isDismissing = true let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) positionTransition.updatePosition(layer: self.containerView.layer, position: CGPoint(x: self.containerView.center.x, y: self.bounds.height + self.containerView.bounds.height / 2.0), completion: { [weak self] _ in self?.controller?.dismiss(animated: false, completion: completion) }) let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) if !self.temporaryDismiss { self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) } } func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { self.currentLayout = (layout, navigationHeight) self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) var effectiveExpanded = self.isExpanded if case .regular = layout.metrics.widthClass { effectiveExpanded = true } let isLandscape = layout.orientation == .landscape let edgeTopInset = isLandscape ? 0.0 : self.defaultTopInset let topInset: CGFloat if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments { if effectiveExpanded { topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset)) } else { topInset = max(0.0, panInitialTopInset + min(0.0, panOffset)) } } else { topInset = effectiveExpanded ? 0.0 : edgeTopInset } transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) let clipFrame: CGRect if layout.metrics.widthClass == .compact { self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.25) if isLandscape { self.containerView.layer.cornerRadius = 0.0 } else { self.containerView.layer.cornerRadius = 10.0 } if #available(iOS 11.0, *) { if layout.safeInsets.bottom.isZero { self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner] } else { self.containerView.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner] } } if isLandscape { clipFrame = CGRect(origin: CGPoint(), size: layout.size) } else { let coveredByModalTransition: CGFloat = 0.0 var containerTopInset: CGFloat = 10.0 if let statusBarHeight = layout.statusBarHeight { containerTopInset += statusBarHeight } let unscaledFrame = CGRect(origin: CGPoint(x: 0.0, y: containerTopInset - coveredByModalTransition * 10.0), size: CGSize(width: layout.size.width, height: layout.size.height - containerTopInset)) let maxScale: CGFloat = (layout.size.width - 16.0 * 2.0) / layout.size.width let containerScale = 1.0 * (1.0 - coveredByModalTransition) + maxScale * coveredByModalTransition let maxScaledTopInset: CGFloat = containerTopInset - 10.0 let scaledTopInset: CGFloat = containerTopInset * (1.0 - coveredByModalTransition) + maxScaledTopInset * coveredByModalTransition let containerFrame = unscaledFrame.offsetBy(dx: 0.0, dy: scaledTopInset - (unscaledFrame.midY - containerScale * unscaledFrame.height / 2.0)) clipFrame = CGRect(x: containerFrame.minX, y: containerFrame.minY, width: containerFrame.width, height: containerFrame.height) } } else { self.dim.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.4) self.containerView.layer.cornerRadius = 10.0 let verticalInset: CGFloat = 44.0 let maxSide = max(layout.size.width, layout.size.height) let minSide = min(layout.size.width, layout.size.height) let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0) clipFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize) } transition.setFrame(view: self.containerView, frame: clipFrame) transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: clipFrame.size), completion: nil) if let stickerContent = self.stickerContent { var stickersTransition: Transition = transition if let scheduledEmojiContentAnimationHint = self.scheduledEmojiContentAnimationHint { self.scheduledEmojiContentAnimationHint = nil let contentAnimation = scheduledEmojiContentAnimationHint stickersTransition = Transition(animation: .curve(duration: 0.4, curve: .spring)).withUserData(contentAnimation) } var contentSize = self.hostView.update( transition: stickersTransition, component: AnyComponent( StickerSelectionComponent( theme: self.theme, strings: self.presentationData.strings, deviceMetrics: layout.deviceMetrics, stickerContent: stickerContent, backgroundColor: self.theme.list.itemBlocksBackgroundColor, separatorColor: self.theme.list.blocksBackgroundColor ) ), environment: {}, forceUpdate: true, containerSize: CGSize(width: clipFrame.size.width, height: clipFrame.height) ) contentSize.height = max(layout.size.height - navigationHeight, contentSize.height) transition.setFrame(view: self.hostView, frame: CGRect(origin: CGPoint(), size: contentSize), completion: nil) self.scrollView.contentSize = contentSize } } private var didPlayAppearAnimation = false func updateIsVisible(isVisible: Bool) { if self.currentIsVisible == isVisible { return } self.currentIsVisible = isVisible guard let currentLayout = self.currentLayout else { return } self.containerLayoutUpdated(layout: currentLayout.layout, navigationHeight: currentLayout.navigationHeight, transition: .immediate) if !self.didPlayAppearAnimation { self.didPlayAppearAnimation = true self.animateIn() } } private var defaultTopInset: CGFloat { guard let (layout, _) = self.currentLayout else{ return 210.0 } if case .compact = layout.metrics.widthClass { var factor: CGFloat = 0.2488 if layout.size.width <= 320.0 { factor = 0.15 } return floor(max(layout.size.width, layout.size.height) * factor) } else { return 210.0 } } private func findScrollView(view: UIView?) -> (UIScrollView, ListView?)? { if let view = view { if let view = view as? UIScrollView, view.contentSize.width < view.contentSize.height { return (view, nil) } if let node = view.asyncdisplaykit_node as? ListView { return (node.scroller, node) } return findScrollView(view: view.superview) } else { return nil } } @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { guard let (layout, navigationHeight) = self.currentLayout else { return } let isLandscape = layout.orientation == .landscape let edgeTopInset = isLandscape ? 0.0 : defaultTopInset switch recognizer.state { case .began: let point = recognizer.location(in: self.view) let currentHitView = self.hitTest(point, with: nil) var scrollViewAndListNode = self.findScrollView(view: currentHitView) if scrollViewAndListNode?.0.frame.height == self.frame.width { scrollViewAndListNode = nil } let scrollView = scrollViewAndListNode?.0 let listNode = scrollViewAndListNode?.1 let topInset: CGFloat if self.isExpanded { topInset = 0.0 } else { topInset = edgeTopInset } self.panGestureArguments = (topInset, 0.0, scrollView, listNode) case .changed: guard let (topInset, panOffset, scrollView, listNode) = self.panGestureArguments else { return } let visibleContentOffset = listNode?.visibleContentOffset() let contentOffset = scrollView?.contentOffset.y ?? 0.0 var translation = recognizer.translation(in: self.view).y var currentOffset = topInset + translation let epsilon = 1.0 if case let .known(value) = visibleContentOffset, value <= epsilon { if let scrollView = scrollView { scrollView.bounces = false scrollView.setContentOffset(CGPoint(x: 0.0, y: 0.0), animated: false) } } else if let scrollView = scrollView, contentOffset <= -scrollView.contentInset.top + epsilon { scrollView.bounces = false scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) } else if let scrollView = scrollView { translation = panOffset currentOffset = topInset + translation if self.isExpanded { recognizer.setTranslation(CGPoint(), in: self.view) } else if currentOffset > 0.0 { scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) } } self.panGestureArguments = (topInset, translation, scrollView, listNode) if !self.isExpanded { if currentOffset > 0.0, let scrollView = scrollView { scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView) } } var bounds = self.bounds if self.isExpanded { bounds.origin.y = -max(0.0, translation - edgeTopInset) } else { bounds.origin.y = -translation } bounds.origin.y = min(0.0, bounds.origin.y) self.bounds = bounds self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: .immediate) case .ended: guard let (currentTopInset, panOffset, scrollView, listNode) = self.panGestureArguments else { return } self.panGestureArguments = nil let visibleContentOffset = listNode?.visibleContentOffset() let contentOffset = scrollView?.contentOffset.y ?? 0.0 let translation = recognizer.translation(in: self.view).y var velocity = recognizer.velocity(in: self.view) if self.isExpanded { if case let .known(value) = visibleContentOffset, value > 0.1 { velocity = CGPoint() } else if case .unknown = visibleContentOffset { velocity = CGPoint() } else if contentOffset > 0.1 { velocity = CGPoint() } } var bounds = self.bounds if self.isExpanded { bounds.origin.y = -max(0.0, translation - edgeTopInset) } else { bounds.origin.y = -translation } bounds.origin.y = min(0.0, bounds.origin.y) scrollView?.bounces = true let offset = currentTopInset + panOffset let topInset: CGFloat = edgeTopInset var dismissing = false if bounds.minY < -60 || (bounds.minY < 0.0 && velocity.y > 300.0) || (self.isExpanded && bounds.minY.isZero && velocity.y > 1800.0) { self.controller?.dismiss(animated: true, completion: nil) dismissing = true } else if self.isExpanded { if velocity.y > 300.0 || offset > topInset / 2.0 { self.isExpanded = false if let listNode = listNode { listNode.scroller.setContentOffset(CGPoint(), animated: false) } else if let scrollView = scrollView { scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) } let distance = topInset - offset let initialVelocity: CGFloat = distance.isZero ? 0.0 : abs(velocity.y / distance) let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) } else { self.isExpanded = true self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) } } else if (velocity.y < -300.0 || offset < topInset / 2.0) { if velocity.y > -2200.0 && velocity.y < -300.0, let listNode = listNode { DispatchQueue.main.async { listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } } let initialVelocity: CGFloat = offset.isZero ? 0.0 : abs(velocity.y / offset) let transition = ContainedViewLayoutTransition.animated(duration: 0.45, curve: .customSpring(damping: 124.0, initialVelocity: initialVelocity)) self.isExpanded = true self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) } else { if let listNode = listNode { listNode.scroller.setContentOffset(CGPoint(), animated: false) } else if let scrollView = scrollView { scrollView.setContentOffset(CGPoint(x: 0.0, y: -scrollView.contentInset.top), animated: false) } self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) } if !dismissing { var bounds = self.bounds let previousBounds = bounds bounds.origin.y = 0.0 self.bounds = bounds self.layer.animateBounds(from: previousBounds, to: self.bounds, duration: 0.3, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue) } case .cancelled: self.panGestureArguments = nil self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(.animated(duration: 0.3, curve: .easeInOut))) default: break } } func update(isExpanded: Bool, transition: ContainedViewLayoutTransition) { guard isExpanded != self.isExpanded else { return } self.isExpanded = isExpanded guard let (layout, navigationHeight) = self.currentLayout else { return } self.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) } } var node: Node { return self.displayNode as! Node } private let context: AccountContext private let theme: PresentationTheme private var currentLayout: ContainerViewLayout? public var pushController: (ViewController) -> Void = { _ in } public var presentController: (ViewController) -> Void = { _ in } var completion: (TelegramMediaFile?) -> Void = { _ in } public init(context: AccountContext) { self.context = context self.theme = defaultDarkColorPresentationTheme super.init(navigationBarPresentationData: nil) self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.statusBar.statusBarStyle = .Ignore } required public init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } @objc private func cancelPressed() { self.dismiss(animated: true, completion: nil) } override open func loadDisplayNode() { self.displayNode = Node(context: self.context, controller: self, theme: self.theme) self.displayNodeDidLoad() } public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { self.view.endEditing(true) if flag { self.node.animateOut(completion: { super.dismiss(animated: false, completion: {}) completion?() }) } else { super.dismiss(animated: false, completion: {}) completion?() } } override open func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.node.updateIsVisible(isVisible: true) } override open func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.node.updateIsVisible(isVisible: false) } override open func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.currentLayout = layout super.containerLayoutUpdated(layout, transition: transition) let navigationHeight: CGFloat = 56.0 self.node.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: Transition(transition)) } }