import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import TelegramUIPreferences import AccountContext import TelegramStringFormatting import UIKitRuntimeUtils import MediaResources import LegacyMessageInputPanel import LegacyMessageInputPanelInputView import AttachmentTextInputPanelNode public enum AttachmentButtonType: Equatable { case gallery case file case location case quickReply case contact case poll case app(AttachMenuBot) case gift case standalone public static func ==(lhs: AttachmentButtonType, rhs: AttachmentButtonType) -> Bool { switch lhs { case .gallery: if case .gallery = rhs { return true } else { return false } case .file: if case .file = rhs { return true } else { return false } case .location: if case .location = rhs { return true } else { return false } case .quickReply: if case .quickReply = rhs { return true } else { return false } case .contact: if case .contact = rhs { return true } else { return false } case .poll: if case .poll = rhs { return true } else { return false } case let .app(lhsBot): if case let .app(rhsBot) = rhs, lhsBot.peer.id == rhsBot.peer.id { return true } else { return false } case .gift: if case .gift = rhs { return true } else { return false } case .standalone: if case .standalone = rhs { return true } else { return false } } } } public protocol AttachmentContainable: ViewController { var requestAttachmentMenuExpansion: () -> Void { get set } var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void { get set } var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void { get set } var cancelPanGesture: () -> Void { get set } var isContainerPanning: () -> Bool { get set } var isContainerExpanded: () -> Bool { get set } var isPanGestureEnabled: (() -> Bool)? { get } var mediaPickerContext: AttachmentMediaPickerContext? { get } func isContainerPanningUpdated(_ panning: Bool) func resetForReuse() func prepareForReuse() func requestDismiss(completion: @escaping () -> Void) func shouldDismissImmediately() -> Bool } public extension AttachmentContainable { func isContainerPanningUpdated(_ panning: Bool) { } func resetForReuse() { } func prepareForReuse() { } func requestDismiss(completion: @escaping () -> Void) { completion() } func shouldDismissImmediately() -> Bool { return true } var isPanGestureEnabled: (() -> Bool)? { return nil } } public enum AttachmentMediaPickerSendMode { case generic case silently case whenOnline } public enum AttachmentMediaPickerAttachmentMode { case media case files } public protocol AttachmentMediaPickerContext { var selectionCount: Signal { get } var caption: Signal { get } var loadingProgress: Signal { get } var mainButtonState: Signal { get } func mainButtonAction() func setCaption(_ caption: NSAttributedString) func send(mode: AttachmentMediaPickerSendMode, attachmentMode: AttachmentMediaPickerAttachmentMode) func schedule() } private func generateShadowImage() -> UIImage? { return generateImage(CGSize(width: 140.0, height: 140.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.saveGState() context.setShadow(offset: CGSize(), blur: 60.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor) let path = UIBezierPath(roundedRect: CGRect(x: 60.0, y: 60.0, width: 20.0, height: 20.0), cornerRadius: 10.0).cgPath context.addPath(path) context.fillPath() context.restoreGState() context.setBlendMode(.clear) context.addPath(path) context.fillPath() })?.stretchableImage(withLeftCapWidth: 70, topCapHeight: 70) } private func generateMaskImage() -> UIImage? { return generateImage(CGSize(width: 390.0, height: 220.0), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) context.setFillColor(UIColor.white.cgColor) let path = UIBezierPath(roundedRect: CGRect(x: 0.0, y: 0.0, width: 390.0, height: 209.0), cornerRadius: 10.0).cgPath context.addPath(path) context.fillPath() try? drawSvgPath(context, path: "M183.219,208.89 H206.781 C205.648,208.89 204.567,209.371 203.808,210.214 L197.23,217.523 C196.038,218.848 193.962,218.848 192.77,217.523 L186.192,210.214 C185.433,209.371 184.352,208.89 183.219,208.89 Z ") })?.stretchableImage(withLeftCapWidth: 195, topCapHeight: 110) } public class AttachmentController: ViewController { private let context: AccountContext private let updatedPresentationData: (initial: PresentationData, signal: Signal)? private let chatLocation: ChatLocation? private let isScheduledMessages: Bool private let buttons: [AttachmentButtonType] private let initialButton: AttachmentButtonType private let fromMenu: Bool private let hasTextInput: Bool private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? public var animateAppearance: Bool = false public var willDismiss: () -> Void = {} public var didDismiss: () -> Void = {} public var mediaPickerContext: AttachmentMediaPickerContext? { get { return self.node.mediaPickerContext } set { self.node.mediaPickerContext = newValue } } private let _ready = Promise() override public var ready: Promise { return self._ready } private final class Node: ASDisplayNode { private weak var controller: AttachmentController? private let dim: ASDisplayNode private let shadowNode: ASImageNode fileprivate let container: AttachmentContainer private let makeEntityInputView: () -> AttachmentTextInputPanelInputView? let panel: AttachmentPanel private var currentType: AttachmentButtonType? private var currentControllers: [AttachmentContainable] = [] private var validLayout: ContainerViewLayout? private var modalProgress: CGFloat = 0.0 fileprivate var isDismissing = false private let captionDisposable = MetaDisposable() private let mediaSelectionCountDisposable = MetaDisposable() private let loadingProgressDisposable = MetaDisposable() private let mainButtonStateDisposable = MetaDisposable() private var selectionCount: Int = 0 fileprivate var mediaPickerContext: AttachmentMediaPickerContext? { didSet { if let mediaPickerContext = self.mediaPickerContext { self.captionDisposable.set((mediaPickerContext.caption |> deliverOnMainQueue).startStrict(next: { [weak self] caption in if let strongSelf = self { strongSelf.panel.updateCaption(caption ?? NSAttributedString()) } })) self.mediaSelectionCountDisposable.set((mediaPickerContext.selectionCount |> deliverOnMainQueue).startStrict(next: { [weak self] count in if let strongSelf = self { strongSelf.updateSelectionCount(count) } })) self.loadingProgressDisposable.set((mediaPickerContext.loadingProgress |> deliverOnMainQueue).startStrict(next: { [weak self] progress in if let strongSelf = self { strongSelf.panel.updateLoadingProgress(progress) if let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.4, curve: .spring)) } } })) self.mainButtonStateDisposable.set((mediaPickerContext.mainButtonState |> deliverOnMainQueue).startStrict(next: { [weak self] mainButtonState in if let strongSelf = self { let _ = (strongSelf.panel.animatingTransitionPromise.get() |> filter { value in return !value } |> take(1)).startStandalone(next: { [weak self] _ in if let strongSelf = self { strongSelf.panel.updateMainButtonState(mainButtonState) if let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.4, curve: .spring)) } } }) } })) } else { self.updateSelectionCount(0) self.mediaSelectionCountDisposable.set(nil) self.loadingProgressDisposable.set(nil) self.mainButtonStateDisposable.set(nil) } } } private let wrapperNode: ASDisplayNode init(controller: AttachmentController, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView?) { self.controller = controller self.makeEntityInputView = makeEntityInputView self.dim = ASDisplayNode() self.dim.alpha = 0.0 self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25) self.shadowNode = ASImageNode() self.shadowNode.isUserInteractionEnabled = false self.wrapperNode = ASDisplayNode() self.wrapperNode.clipsToBounds = true self.container = AttachmentContainer() self.container.canHaveKeyboardFocus = true self.panel = AttachmentPanel(context: controller.context, chatLocation: controller.chatLocation, isScheduledMessages: controller.isScheduledMessages, updatedPresentationData: controller.updatedPresentationData, makeEntityInputView: makeEntityInputView) self.panel.fromMenu = controller.fromMenu self.panel.isStandalone = controller.isStandalone super.init() self.addSubnode(self.dim) self.addSubnode(self.shadowNode) self.addSubnode(self.wrapperNode) self.container.controllerRemoved = { [weak self] controller in if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing { strongSelf.currentControllers = strongSelf.currentControllers.filter { $0 !== controller } strongSelf.containerLayoutUpdated(layout, transition: .immediate) } } self.container.updateModalProgress = { [weak self] progress, transition in if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing { var transition = transition if strongSelf.container.supernode == nil { transition = .animated(duration: 0.4, curve: .spring) } strongSelf.controller?.updateModalStyleOverlayTransitionFactor(progress, transition: transition) strongSelf.modalProgress = progress strongSelf.containerLayoutUpdated(layout, transition: transition) } } self.container.isReadyUpdated = { [weak self] in if let strongSelf = self, let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.4, curve: .spring)) } } self.container.interactivelyDismissed = { [weak self] in if let strongSelf = self { strongSelf.controller?.dismiss(animated: true) } } self.container.isPanningUpdated = { [weak self] value in if let strongSelf = self, let currentController = strongSelf.currentControllers.last, !value { currentController.isContainerPanningUpdated(value) } } self.container.isPanGestureEnabled = { [weak self] in guard let self, let currentController = self.currentControllers.last else { return true } if let isPanGestureEnabled = currentController.isPanGestureEnabled { return isPanGestureEnabled() } else { return true } } self.container.shouldCancelPanGesture = { [weak self] in if let strongSelf = self, let currentController = strongSelf.currentControllers.last { if !currentController.shouldDismissImmediately() { return true } else { return false } } else { return false } } self.container.requestDismiss = { [weak self] in if let strongSelf = self, let currentController = strongSelf.currentControllers.last { currentController.requestDismiss { [weak self] in if let strongSelf = self { strongSelf.controller?.dismiss(animated: true) } } } } self.panel.selectionChanged = { [weak self] type in if let strongSelf = self { return strongSelf.switchToController(type) } else { return false } } self.panel.longPressed = { [weak self] _ in if let strongSelf = self, let currentController = strongSelf.currentControllers.last { currentController.longTapWithTabBar?() } } self.panel.beganTextEditing = { [weak self] in if let strongSelf = self { strongSelf.container.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) } } self.panel.textUpdated = { [weak self] text in if let strongSelf = self { strongSelf.mediaPickerContext?.setCaption(text) } } self.panel.sendMessagePressed = { [weak self] mode in if let strongSelf = self { switch mode { case .generic: strongSelf.mediaPickerContext?.send(mode: .generic, attachmentMode: .media) case .silent: strongSelf.mediaPickerContext?.send(mode: .silently, attachmentMode: .media) case .schedule: strongSelf.mediaPickerContext?.schedule() case .whenOnline: strongSelf.mediaPickerContext?.send(mode: .whenOnline, attachmentMode: .media) } } } self.panel.mainButtonPressed = { [weak self] in if let strongSelf = self { strongSelf.mediaPickerContext?.mainButtonAction() } } self.panel.requestLayout = { [weak self] in if let strongSelf = self, let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.2, curve: .easeInOut)) } } self.panel.present = { [weak self] c in if let strongSelf = self { strongSelf.controller?.present(c, in: .window(.root)) } } self.panel.presentInGlobalOverlay = { [weak self] c in if let strongSelf = self { strongSelf.controller?.presentInGlobalOverlay(c, with: nil) } } } deinit { self.captionDisposable.dispose() self.mediaSelectionCountDisposable.dispose() self.loadingProgressDisposable.dispose() self.mainButtonStateDisposable.dispose() } private var inputContainerHeight: CGFloat? private var inputContainerNode: ASDisplayNode? override func didLoad() { super.didLoad() self.view.disablesInteractiveModalDismiss = true self.dim.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) if let controller = self.controller { let _ = self.switchToController(controller.initialButton) if case let .app(bot) = controller.initialButton { if let index = controller.buttons.firstIndex(where: { if case let .app(otherBot) = $0, otherBot.peer.id == bot.peer.id { return true } else { return false } }) { self.panel.updateSelectedIndex(index) } } else if controller.initialButton != .standalone { if let index = controller.buttons.firstIndex(where: { if $0 == controller.initialButton { return true } else { return false } }) { self.panel.updateSelectedIndex(index) } } } if let (inputContainerHeight, inputContainerNode, _) = self.controller?.getInputContainerNode() { self.inputContainerHeight = inputContainerHeight self.inputContainerNode = inputContainerNode self.addSubnode(inputContainerNode) } } fileprivate func updateSelectionCount(_ count: Int, animated: Bool = true) { self.selectionCount = count if let layout = self.validLayout { self.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate) } } @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { guard !self.isDismissing else { return } if case .ended = recognizer.state { if let controller = self.currentControllers.last { controller.requestDismiss(completion: { [weak self] in self?.controller?.dismiss(animated: true) }) } else { self.controller?.dismiss(animated: true) } } } func switchToController(_ type: AttachmentButtonType, animated: Bool = true) -> Bool { guard self.currentType != type else { if self.animating { return false } if let controller = self.currentControllers.last { controller.scrollToTopWithTabBar?() controller.requestAttachmentMenuExpansion() } return true } let previousType = self.currentType self.currentType = type self.controller?.requestController(type, { [weak self] controller, mediaPickerContext in if let strongSelf = self { if let controller = controller { strongSelf.controller?._ready.set(controller.ready.get()) controller._presentedInModal = true controller.navigation_setPresenting(strongSelf.controller) controller.requestAttachmentMenuExpansion = { [weak self] in if let strongSelf = self, !strongSelf.container.isTracking { strongSelf.container.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) } } controller.updateNavigationStack = { [weak self] f in if let strongSelf = self { let (controllers, mediaPickerContext) = f(strongSelf.currentControllers) strongSelf.currentControllers = controllers strongSelf.mediaPickerContext = mediaPickerContext if let layout = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.4, curve: .spring)) } } } controller.updateTabBarAlpha = { [weak self, weak controller] alpha, transition in if let strongSelf = self, strongSelf.currentControllers.contains(where: { $0 === controller }) { strongSelf.panel.updateBackgroundAlpha(alpha, transition: transition) } } controller.cancelPanGesture = { [weak self] in if let strongSelf = self { strongSelf.container.cancelPanGesture() } } controller.isContainerPanning = { [weak self] in if let strongSelf = self { return strongSelf.container.isPanning } else { return false } } controller.isContainerExpanded = { [weak self] in if let strongSelf = self { return strongSelf.container.isExpanded } else { return false } } let previousController = strongSelf.currentControllers.last strongSelf.currentControllers = [controller] if previousType != nil && animated { strongSelf.animateSwitchTransition(controller, previousController: previousController) } if let layout = strongSelf.validLayout { strongSelf.switchingController = true strongSelf.containerLayoutUpdated(layout, transition: animated ? .animated(duration: 0.3, curve: .spring) : .immediate) strongSelf.switchingController = false } } strongSelf.mediaPickerContext = mediaPickerContext } }) return true } private func animateSwitchTransition(_ controller: AttachmentContainable, previousController: AttachmentContainable?) { guard let snapshotView = self.container.container.view.snapshotView(afterScreenUpdates: false) else { return } snapshotView.frame = self.container.container.frame self.container.clipNode.view.addSubview(snapshotView) let _ = (controller.ready.get() |> filter { $0 } |> take(1) |> deliverOnMainQueue).startStandalone(next: { [weak self, weak snapshotView] _ in guard let strongSelf = self, let layout = strongSelf.validLayout else { return } if case .compact = layout.metrics.widthClass { let offset = 25.0 let initialPosition = strongSelf.container.clipNode.layer.position let targetPosition = initialPosition.offsetBy(dx: 0.0, dy: offset) var startPosition = initialPosition if let presentation = strongSelf.container.clipNode.layer.presentation() { startPosition = presentation.position } strongSelf.container.clipNode.layer.animatePosition(from: startPosition, to: targetPosition, duration: 0.2, removeOnCompletion: false, completion: { [weak self] finished in if let strongSelf = self, finished { strongSelf.container.clipNode.layer.animateSpring(from: NSValue(cgPoint: targetPosition), to: NSValue(cgPoint: initialPosition), keyPath: "position", duration: 0.4, delay: 0.0, initialVelocity: 0.0, damping: 70.0, removeOnCompletion: false, completion: { [weak self] finished in if finished { self?.container.clipNode.layer.removeAllAnimations() } }) } }) } snapshotView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.23, removeOnCompletion: false, completion: { [weak snapshotView] _ in snapshotView?.removeFromSuperview() previousController?.resetForReuse() }) }) } private var animating = false func animateIn() { guard let layout = self.validLayout, let controller = self.controller else { return } self.animating = true if case .regular = layout.metrics.widthClass { if controller.animateAppearance { let targetPosition = self.position let startPosition = targetPosition.offsetBy(dx: 0.0, dy: layout.size.height) self.position = startPosition let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) transition.animateView(allowUserInteraction: true, { self.position = targetPosition }, completion: { _ in self.animating = false }) } else { self.animating = false } ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 0.1) } else { ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0) let targetPosition = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0) let startPosition = targetPosition.offsetBy(dx: 0.0, dy: layout.size.height) self.container.position = startPosition let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring) transition.animateView(allowUserInteraction: true, { self.container.position = targetPosition }, completion: { _ in self.animating = false }) } } func animateOut(completion: @escaping () -> Void = {}) { guard let controller = self.controller else { return } self.isDismissing = true guard let layout = self.validLayout else { return } self.animating = true if case .regular = layout.metrics.widthClass { self.layer.allowsGroupOpacity = true self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in let _ = self?.container.dismiss(transition: .immediate, completion: completion) self?.animating = false self?.layer.removeAllAnimations() }) } else { let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0), completion: { [weak self] _ in let _ = self?.container.dismiss(transition: .immediate, completion: completion) self?.animating = false }) let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) alphaTransition.updateAlpha(node: self.dim, alpha: 0.0) self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition) if controller.fromMenu && self.hasButton, let (_, _, getTransition) = controller.getInputContainerNode(), let inputTransition = getTransition() { self.panel.animateTransitionOut(inputTransition: inputTransition, dismissed: true, transition: positionTransition) self.containerLayoutUpdated(layout, transition: positionTransition) } } } func scrollToTop() { self.currentControllers.last?.scrollToTop?() } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let controller = self.controller, controller.isInteractionDisabled() { return self.view } else { let result = super.hitTest(point, with: event) if result == self.wrapperNode.view { return self.dim.view } return result } } private var isUpdatingContainer = false private var switchingController = false private var hasButton = false func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.validLayout = layout guard let controller = self.controller else { return } transition.updateFrame(node: self.dim, frame: CGRect(origin: CGPoint(), size: layout.size)) let fromMenu = controller.fromMenu var containerLayout = layout let containerRect: CGRect var isCompact = true if case .regular = layout.metrics.widthClass { isCompact = false let availableHeight = layout.size.height - (layout.inputHeight ?? 0.0) - 60.0 let size = CGSize(width: 390.0, height: min(620.0, availableHeight)) let insets = layout.insets(options: [.input]) let masterWidth = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0)) let position: CGPoint let positionY = layout.size.height - size.height - insets.bottom - 40.0 if let sourceRect = controller.getSourceRect?() { position = CGPoint(x: min(layout.size.width - size.width - 28.0, floor(sourceRect.midX - size.width / 2.0)), y: min(positionY, sourceRect.minY - size.height)) } else { position = CGPoint(x: masterWidth - 174.0, y: positionY) } if controller.isStandalone && !controller.forceSourceRect { var containerY = floorToScreenPixels((layout.size.height - size.height) / 2.0) if let inputHeight = layout.inputHeight, inputHeight > 88.0 { containerY = layout.size.height - inputHeight - size.height - 80.0 } containerRect = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - size.width) / 2.0), y: containerY), size: size) } else { containerRect = CGRect(origin: position, size: size) } containerLayout.size = containerRect.size containerLayout.intrinsicInsets.bottom = 12.0 containerLayout.inputHeight = nil if controller.isStandalone { self.wrapperNode.cornerRadius = 10.0 } else if self.wrapperNode.view.mask == nil { let maskView = UIImageView() maskView.image = generateMaskImage() maskView.contentMode = .scaleToFill self.wrapperNode.view.mask = maskView } if let maskView = self.wrapperNode.view.mask { transition.updateFrame(view: maskView, frame: CGRect(origin: CGPoint(), size: size)) } self.shadowNode.alpha = 1.0 if self.shadowNode.image == nil { self.shadowNode.image = generateShadowImage() } } else { let containerHeight: CGFloat if fromMenu { if let inputContainerHeight = self.inputContainerHeight { containerHeight = layout.size.height - inputContainerHeight } else { containerHeight = layout.size.height } } else { containerHeight = layout.size.height } containerRect = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: containerHeight)) self.wrapperNode.cornerRadius = 0.0 self.shadowNode.alpha = 0.0 self.wrapperNode.view.mask = nil } var containerInsets = containerLayout.intrinsicInsets var hasPanel = false let previousHasButton = self.hasButton let hasButton = self.panel.isButtonVisible && !self.isDismissing self.hasButton = hasButton if let controller = self.controller, controller.buttons.count > 1 || controller.hasTextInput { hasPanel = true } let isEffecitvelyCollapsedUpdated = (self.selectionCount > 0) != (self.panel.isSelecting) var panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isSelecting: self.selectionCount > 0, elevateProgress: !hasPanel && !hasButton, transition: transition) if fromMenu && !hasButton, let inputContainerHeight = self.inputContainerHeight { panelHeight = inputContainerHeight } if hasPanel || hasButton || (fromMenu && isCompact) { containerInsets.bottom = panelHeight } var transitioning = false if fromMenu && previousHasButton != hasButton, let (_, _, getTransition) = controller.getInputContainerNode(), let inputTransition = getTransition() { if hasButton { self.panel.animateTransitionIn(inputTransition: inputTransition, transition: transition) } else { self.panel.animateTransitionOut(inputTransition: inputTransition, dismissed: false, transition: transition) } transitioning = true } var panelTransition = transition if isEffecitvelyCollapsedUpdated { panelTransition = .animated(duration: 0.25, curve: .easeInOut) } var panelY = containerRect.height - panelHeight if fromMenu && isCompact { panelY = layout.size.height - panelHeight } else if !hasPanel && !hasButton { panelY = containerRect.height } if fromMenu && isCompact { if hasButton { self.panel.isHidden = false self.inputContainerNode?.isHidden = true } else if !transitioning { if !self.panel.animatingTransition { self.panel.isHidden = true self.inputContainerNode?.isHidden = false } } } panelTransition.updateFrame(node: self.panel, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: containerRect.width, height: panelHeight)), completion: { [weak self] finished in if transitioning && finished, isCompact { self?.panel.isHidden = !hasButton self?.inputContainerNode?.isHidden = hasButton } }) var shadowFrame = containerRect.insetBy(dx: -60.0, dy: -60.0) shadowFrame.size.height -= 12.0 transition.updateFrame(node: self.shadowNode, frame: shadowFrame) transition.updateFrame(node: self.wrapperNode, frame: containerRect) if !self.isUpdatingContainer && !self.isDismissing { self.isUpdatingContainer = true let containerTransition: ContainedViewLayoutTransition if self.container.supernode == nil { containerTransition = .immediate } else { containerTransition = transition } let controllers = self.currentControllers if !self.animating { containerTransition.updateFrame(node: self.container, frame: CGRect(origin: CGPoint(), size: containerRect.size)) } let containerLayout = containerLayout.withUpdatedIntrinsicInsets(containerInsets) self.container.update(layout: containerLayout, controllers: controllers, coveredByModalTransition: 0.0, transition: self.switchingController ? .immediate : transition) if self.container.supernode == nil, !controllers.isEmpty && self.container.isReady && !self.isDismissing { self.wrapperNode.addSubnode(self.container) if fromMenu, let _ = controller.getInputContainerNode() { self.addSubnode(self.panel) } else { self.container.addSubnode(self.panel) } self.animateIn() } self.isUpdatingContainer = false } } } public var requestController: (AttachmentButtonType, @escaping (AttachmentContainable?, AttachmentMediaPickerContext?) -> Void) -> Void = { _, completion in completion(nil, nil) } public var getInputContainerNode: () -> (CGFloat, ASDisplayNode, () -> AttachmentController.InputPanelTransition?)? = { return nil } public var getSourceRect: (() -> CGRect?)? public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)? = nil, chatLocation: ChatLocation?, isScheduledMessages: Bool = false, buttons: [AttachmentButtonType], initialButton: AttachmentButtonType = .gallery, fromMenu: Bool = false, hasTextInput: Bool = true, makeEntityInputView: @escaping () -> AttachmentTextInputPanelInputView? = { return nil}) { self.context = context self.updatedPresentationData = updatedPresentationData self.chatLocation = chatLocation self.isScheduledMessages = isScheduledMessages self.buttons = buttons self.initialButton = initialButton self.fromMenu = fromMenu self.hasTextInput = hasTextInput self.makeEntityInputView = makeEntityInputView super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Ignore self.blocksBackgroundWhenInOverlay = true self.acceptsFocusWhenInOverlay = true self.scrollToTop = { [weak self] in if let strongSelf = self { strongSelf.node.scrollToTop() } } } public required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } public var forceSourceRect = false fileprivate var isStandalone: Bool { return self.buttons.contains(.standalone) } public func updateSelectionCount(_ count: Int) { self.node.updateSelectionCount(count, animated: false) } private var node: Node { return self.displayNode as! Node } open override func loadDisplayNode() { self.displayNode = Node(controller: self, makeEntityInputView: self.makeEntityInputView) self.displayNodeDidLoad() } private var dismissedFlag = false public func _dismiss() { super.dismiss(animated: false, completion: {}) } public var ensureUnfocused = true public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { if self.ensureUnfocused { self.view.endEditing(true) } if flag { if !self.dismissedFlag { self.dismissedFlag = true self.willDismiss() self.node.animateOut(completion: { [weak self] in self?.didDismiss() self?._dismiss() completion?() self?.dismissedFlag = false self?.node.isDismissing = false self?.node.container.removeFromSupernode() }) } } else { self.didDismiss() self._dismiss() completion?() self.node.isDismissing = false self.node.container.removeFromSupernode() } } private func isInteractionDisabled() -> Bool { return false } private var validLayout: ContainerViewLayout? override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { let previousSize = self.validLayout?.size super.containerLayoutUpdated(layout, transition: transition) self.validLayout = layout if let previousSize, previousSize != layout.size { Queue.mainQueue().after(0.1) { self.node.containerLayoutUpdated(layout, transition: transition) } } self.node.containerLayoutUpdated(layout, transition: transition) } public final class InputPanelTransition { let inputNode: ASDisplayNode let accessoryPanelNode: ASDisplayNode? let menuButtonNode: ASDisplayNode let menuButtonBackgroundNode: ASDisplayNode let menuIconNode: ASDisplayNode let menuTextNode: ASDisplayNode let prepareForDismiss: () -> Void public init( inputNode: ASDisplayNode, accessoryPanelNode: ASDisplayNode?, menuButtonNode: ASDisplayNode, menuButtonBackgroundNode: ASDisplayNode, menuIconNode: ASDisplayNode, menuTextNode: ASDisplayNode, prepareForDismiss: @escaping () -> Void ) { self.inputNode = inputNode self.accessoryPanelNode = accessoryPanelNode self.menuButtonNode = menuButtonNode self.menuButtonBackgroundNode = menuButtonBackgroundNode self.menuIconNode = menuIconNode self.menuTextNode = menuTextNode self.prepareForDismiss = prepareForDismiss } } public static func preloadAttachBotIcons(context: AccountContext) -> DisposableSet { let disposableSet = DisposableSet() let _ = (context.engine.messages.attachMenuBots() |> take(1) |> deliverOnMainQueue).startStandalone(next: { bots in for bot in bots { for (name, file) in bot.icons { if [.iOSAnimated, .placeholder].contains(name), let peer = PeerReference(bot.peer._asPeer()) { if case .placeholder = name { let path = context.account.postbox.mediaBox.cachedRepresentationCompletePath(file.resource.id, representation: CachedPreparedSvgRepresentation()) if !FileManager.default.fileExists(atPath: path) { let accountFullSizeData = Signal<(Data?, Bool), NoError> { subscriber in let accountResource = context.account.postbox.mediaBox.cachedResourceRepresentation(file.resource, representation: CachedPreparedSvgRepresentation(), complete: false, fetch: true) let fetchedFullSize = fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: MediaResourceUserContentType(file: file), reference: .media(media: .attachBot(peer: peer, media: file), resource: file.resource)) let fetchedFullSizeDisposable = fetchedFullSize.start() let fullSizeDisposable = accountResource.start() return ActionDisposable { fetchedFullSizeDisposable.dispose() fullSizeDisposable.dispose() } } disposableSet.add(accountFullSizeData.start()) } } else { disposableSet.add(freeMediaFileInteractiveFetched(account: context.account, userLocation: .other, fileReference: .attachBot(peer: peer, media: file)).start()) } } } } }) return disposableSet } }