import Foundation import AsyncDisplayKit import Postbox import SwiftSignalKit import Display import TelegramCore private final class ChatControllerNodeView: UITracingLayerView, WindowInputAccessoryHeightProvider, PreviewingHostView { var inputAccessoryHeight: (() -> CGFloat)? var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)? var previewingDelegate: PreviewingHostViewDelegate? { return PreviewingHostViewDelegate(controllerForLocation: { [weak self] sourceView, point in return self?.controller?.previewingController(from: sourceView, for: point) }, commitController: { [weak self] controller in self?.controller?.previewingCommit(controller) }) } weak var controller: ChatController? func getWindowInputAccessoryHeight() -> CGFloat { return self.inputAccessoryHeight?() ?? 0.0 } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let result = self.hitTestImpl?(point, event) { return result } return super.hitTest(point, with: event) } } private final class ScrollContainerNode: ASScrollNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if super.hitTest(point, with: event) == self.view { return nil } return super.hitTest(point, with: event) } } private struct ChatControllerNodeDerivedLayoutState { var inputContextPanelsFrame: CGRect var inputContextPanelsOverMainPanelFrame: CGRect var inputNodeHeight: CGFloat? var upperInputPositionBound: CGFloat? } class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { let account: Account let chatLocation: ChatLocation let controllerInteraction: ChatControllerInteraction private weak var controller: ChatController? let navigationBar: NavigationBar? private var backgroundEffectNode: ASDisplayNode? private var containerBackgroundNode: ASImageNode? private var scrollContainerNode: ScrollContainerNode? private var containerNode: ASDisplayNode? private var overlayNavigationBar: ChatOverlayNavigationBar? var peerView: PeerView? { didSet { self.overlayNavigationBar?.peerView = self.peerView } } let backgroundNode: ASDisplayNode let historyNode: ChatHistoryListNode let historyNodeContainer: ASDisplayNode let loadingNode: ChatLoadingNode private var emptyNode: ChatEmptyNode? private var validEmptyNodeLayout: (CGSize, UIEdgeInsets)? var restrictedNode: ChatRecentActionsEmptyNode? private var validLayout: (ContainerViewLayout, CGFloat)? private var searchNavigationNode: ChatSearchNavigationContentNode? private let inputPanelBackgroundNode: ASDisplayNode private let inputPanelBackgroundSeparatorNode: ASDisplayNode private let titleAccessoryPanelContainer: ChatControllerTitlePanelNodeContainer private var titleAccessoryPanelNode: ChatTitleAccessoryPanelNode? private var inputPanelNode: ChatInputPanelNode? private var accessoryPanelNode: AccessoryPanelNode? private var inputContextPanelNode: ChatInputContextPanelNode? private var overlayContextPanelNode: ChatInputContextPanelNode? private var inputNode: ChatInputNode? private var textInputPanelNode: ChatTextInputPanelNode? private var inputMediaNode: ChatMediaInputNode? let navigateButtons: ChatHistoryNavigationButtons private var ignoreUpdateHeight = false private var animateInAsOverlayCompletion: (() -> Void)? private var dismissAsOverlayCompletion: (() -> Void)? private var dismissedAsOverlay = false private var scheduledAnimateInAsOverlayFromNode: ASDisplayNode? private var dismissAsOverlayLayout: ContainerViewLayout? private var hapticFeedback: HapticFeedback? private var scrollViewDismissStatus = false var chatPresentationInterfaceState: ChatPresentationInterfaceState var automaticMediaDownloadSettings: AutomaticMediaDownloadSettings private let selectedMessagesPromise = Promise?>(nil) var selectedMessages: Set? { didSet { if self.selectedMessages != oldValue { self.selectedMessagesPromise.set(.single(self.selectedMessages)) } } } var requestUpdateChatInterfaceState: (Bool, (ChatInterfaceState) -> ChatInterfaceState) -> Void = { _, _ in } var requestUpdateInterfaceState: (ContainedViewLayoutTransition, Bool, (ChatPresentationInterfaceState) -> ChatPresentationInterfaceState) -> Void = { _, _, _ in } var sendMessages: ([EnqueueMessage]) -> Void = { _ in } var displayAttachmentMenu: () -> Void = { } var paste: (ChatTextInputPanelPasteData) -> Void = { _ in } var updateTypingActivity: (Bool) -> Void = { _ in } var dismissUrlPreview: () -> Void = { } var setupSendActionOnViewUpdate: (@escaping () -> Void) -> Void = { _ in } var requestLayout: (ContainedViewLayoutTransition) -> Void = { _ in } var dismissAsOverlay: () -> Void = { } var interfaceInteraction: ChatPanelInterfaceInteraction? private var messageActionSheetController: (ChatMessageActionSheetController, UInt32)? private var messageActionSheetControllerAdditionalInset: CGFloat? private var messageActionSheetTopDimNode: ASDisplayNode? private var messageActionSheetBottomDimNode: ASDisplayNode? private var expandedInputDimNode: ASDisplayNode? private var dropDimNode: ASDisplayNode? private var containerLayoutAndNavigationBarHeight: (ContainerViewLayout, CGFloat)? private var scheduledLayoutTransitionRequestId: Int = 0 private var scheduledLayoutTransitionRequest: (Int, ContainedViewLayoutTransition)? private var panRecognizer: WindowPanRecognizer? private let keyboardGestureRecognizerDelegate = WindowKeyboardGestureRecognizerDelegate() private var upperInputPositionBound: CGFloat? private var keyboardGestureBeginLocation: CGPoint? private var keyboardGestureAccessoryHeight: CGFloat? private var derivedLayoutState: ChatControllerNodeDerivedLayoutState? private var isLoading: Bool = false { didSet { if self.isLoading != oldValue { if self.isLoading { self.historyNodeContainer.supernode?.insertSubnode(self.loadingNode, aboveSubnode: self.historyNodeContainer) } else { self.loadingNode.removeFromSupernode() } } } } init(account: Account, chatLocation: ChatLocation, messageId: MessageId?, controllerInteraction: ChatControllerInteraction, chatPresentationInterfaceState: ChatPresentationInterfaceState, automaticMediaDownloadSettings: AutomaticMediaDownloadSettings, navigationBar: NavigationBar?, controller: ChatController?) { self.account = account self.chatLocation = chatLocation self.controllerInteraction = controllerInteraction self.chatPresentationInterfaceState = chatPresentationInterfaceState self.automaticMediaDownloadSettings = automaticMediaDownloadSettings self.navigationBar = navigationBar self.controller = controller self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.backgroundNode.contentMode = .scaleAspectFill self.backgroundNode.displaysAsynchronously = false self.backgroundNode.clipsToBounds = true self.titleAccessoryPanelContainer = ChatControllerTitlePanelNodeContainer() self.titleAccessoryPanelContainer.clipsToBounds = true self.historyNode = ChatHistoryListNode(account: account, chatLocation: chatLocation, tagMask: nil, messageId: messageId, controllerInteraction: controllerInteraction, selectedMessages: self.selectedMessagesPromise.get()) self.historyNode.rotated = true self.historyNodeContainer = ASDisplayNode() self.historyNodeContainer.addSubnode(self.historyNode) self.loadingNode = ChatLoadingNode(theme: self.chatPresentationInterfaceState.theme, chatWallpaper: self.chatPresentationInterfaceState.chatWallpaper) self.inputPanelBackgroundNode = ASDisplayNode() self.inputPanelBackgroundNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor self.inputPanelBackgroundNode.isLayerBacked = true self.inputPanelBackgroundSeparatorNode = ASDisplayNode() self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelStrokeColor self.inputPanelBackgroundSeparatorNode.isLayerBacked = true self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme) super.init() self.setViewBlock({ return ChatControllerNodeView() }) (self.view as? ChatControllerNodeView)?.inputAccessoryHeight = { [weak self] in if let strongSelf = self { return strongSelf.getWindowInputAccessoryHeight() } else { return 0.0 } } (self.view as? ChatControllerNodeView)?.hitTestImpl = { [weak self] point, event in return self?.hitTest(point, with: event) } assert(Queue.mainQueue().isCurrent()) self.historyNode.setLoadStateUpdated { [weak self] loadState, animated in if let strongSelf = self { if case .loading = loadState { strongSelf.isLoading = true } else { strongSelf.isLoading = false } var isEmpty = false if case .empty = loadState { isEmpty = true } strongSelf.updateIsEmpty(isEmpty, animated: animated) } } self.backgroundNode.contents = chatControllerBackgroundImage(wallpaper: chatPresentationInterfaceState.chatWallpaper, postbox: account.postbox)?.cgImage self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) self.addSubnode(self.backgroundNode) self.addSubnode(self.historyNodeContainer) self.addSubnode(self.titleAccessoryPanelContainer) self.addSubnode(self.inputPanelBackgroundNode) self.addSubnode(self.inputPanelBackgroundSeparatorNode) self.addSubnode(self.navigateButtons) self.historyNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))) self.textInputPanelNode = ChatTextInputPanelNode(theme: chatPresentationInterfaceState.theme, presentController: { [weak self] controller in self?.interfaceInteraction?.presentController(controller, nil) }) self.textInputPanelNode?.updateHeight = { [weak self] in if let strongSelf = self, let _ = strongSelf.inputPanelNode as? ChatTextInputPanelNode, !strongSelf.ignoreUpdateHeight { strongSelf.requestLayout(.animated(duration: 0.1, curve: .easeInOut)) } } var lastSendTimestamp = 0.0 self.textInputPanelNode?.sendMessage = { [weak self] in if let strongSelf = self, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { if textInputPanelNode.textInputNode?.isFirstResponder() ?? false { applyKeyboardAutocorrection() } var effectivePresentationInterfaceState = strongSelf.chatPresentationInterfaceState if let textInputPanelNode = strongSelf.textInputPanelNode { effectivePresentationInterfaceState = effectivePresentationInterfaceState.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputPanelNode.inputTextState) } } if let _ = effectivePresentationInterfaceState.interfaceState.editMessage { strongSelf.interfaceInteraction?.editMessage() } else { let timestamp = CACurrentMediaTime() if lastSendTimestamp + 0.15 > timestamp { return } lastSendTimestamp = timestamp strongSelf.updateTypingActivity(false) var messages: [EnqueueMessage] = [] for text in breakChatInputText(trimChatInputText(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)) { if text.length != 0 { var attributes: [MessageAttribute] = [] let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) if !entities.isEmpty { attributes.append(TextEntitiesMessageAttribute(entities: entities)) } var webpage: TelegramMediaWebpage? if strongSelf.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil { attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews])) } else { webpage = strongSelf.chatPresentationInterfaceState.urlPreview?.1 } messages.append(.message(text: text.string, attributes: attributes, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: strongSelf.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil)) } } if !messages.isEmpty || strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil { strongSelf.setupSendActionOnViewUpdate({ [weak strongSelf] in if let strongSelf = strongSelf, let textInputPanelNode = strongSelf.inputPanelNode as? ChatTextInputPanelNode { strongSelf.ignoreUpdateHeight = true textInputPanelNode.text = "" strongSelf.requestUpdateChatInterfaceState(false, { $0.withUpdatedReplyMessageId(nil).withUpdatedForwardMessageIds(nil).withUpdatedComposeDisableUrlPreview(nil) }) strongSelf.ignoreUpdateHeight = false } }) if let forwardMessageIds = strongSelf.chatPresentationInterfaceState.interfaceState.forwardMessageIds { for id in forwardMessageIds { messages.append(.forward(source: id, grouping: .auto)) } } if case .peer = strongSelf.chatLocation { strongSelf.sendMessages(messages) } } } } } self.textInputPanelNode?.paste = { [weak self] data in self?.paste(data) } self.textInputPanelNode?.displayAttachmentMenu = { [weak self] in self?.displayAttachmentMenu() } self.textInputPanelNode?.updateActivity = { [weak self] in self?.updateTypingActivity(true) } } override func didLoad() { super.didLoad() let recognizer = WindowPanRecognizer(target: nil, action: nil) recognizer.cancelsTouchesInView = false recognizer.delaysTouchesBegan = false recognizer.delaysTouchesEnded = false recognizer.delegate = self.keyboardGestureRecognizerDelegate recognizer.began = { [weak self] point in guard let strongSelf = self else { return } strongSelf.panGestureBegan(location: point) } recognizer.moved = { [weak self] point in guard let strongSelf = self else { return } strongSelf.panGestureMoved(location: point) } recognizer.ended = { [weak self] point, velocity in guard let strongSelf = self else { return } strongSelf.panGestureEnded(location: point, velocity: velocity) } self.panRecognizer = recognizer self.view.addGestureRecognizer(recognizer) self.view.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in guard let strongSelf = self else { return false } if let _ = strongSelf.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState { return true } return false } (self.view as? ChatControllerNodeView)?.controller = self.controller } private func updateIsEmpty(_ isEmpty: Bool, animated: Bool) { if isEmpty && self.emptyNode == nil { let emptyNode = ChatEmptyNode(accountPeerId: self.account.peerId) if let (size, insets) = self.validEmptyNodeLayout { emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, size: size, insets: insets, transition: .immediate) } emptyNode.isHidden = self.restrictedNode != nil self.emptyNode = emptyNode self.historyNodeContainer.supernode?.insertSubnode(emptyNode, aboveSubnode: self.historyNodeContainer) if animated { emptyNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } else if let emptyNode = self.emptyNode { self.emptyNode = nil if animated { emptyNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak emptyNode] _ in emptyNode?.removeFromSupernode() }) } else { emptyNode.removeFromSupernode() } } } func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition protoTransition: ContainedViewLayoutTransition, listViewTransaction: (ListViewUpdateSizeAndInsets, CGFloat, Bool) -> Void) { let transition: ContainedViewLayoutTransition if let _ = self.scheduledAnimateInAsOverlayFromNode { transition = .immediate } else { transition = protoTransition } self.scheduledLayoutTransitionRequest = nil if case .overlay = self.chatPresentationInterfaceState.mode { if self.backgroundEffectNode == nil { let backgroundEffectNode = ASDisplayNode() backgroundEffectNode.backgroundColor = self.chatPresentationInterfaceState.theme.chatList.backgroundColor.withAlphaComponent(0.8) self.insertSubnode(backgroundEffectNode, at: 0) self.backgroundEffectNode = backgroundEffectNode backgroundEffectNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.backgroundEffectTap(_:)))) } if self.scrollContainerNode == nil { let scrollContainerNode = ScrollContainerNode() scrollContainerNode.view.delaysContentTouches = false scrollContainerNode.view.delegate = self scrollContainerNode.view.alwaysBounceVertical = true if #available(iOSApplicationExtension 11.0, *) { scrollContainerNode.view.contentInsetAdjustmentBehavior = .never } self.insertSubnode(scrollContainerNode, aboveSubnode: self.backgroundEffectNode!) self.scrollContainerNode = scrollContainerNode } if self.containerBackgroundNode == nil { let containerBackgroundNode = ASImageNode() containerBackgroundNode.displaysAsynchronously = false containerBackgroundNode.displayWithoutProcessing = true containerBackgroundNode.image = PresentationResourcesRootController.inAppNotificationBackground(self.chatPresentationInterfaceState.theme) self.scrollContainerNode?.addSubnode(containerBackgroundNode) self.containerBackgroundNode = containerBackgroundNode } if self.containerNode == nil { let containerNode = ASDisplayNode() containerNode.clipsToBounds = true containerNode.cornerRadius = 15.0 containerNode.addSubnode(self.backgroundNode) containerNode.addSubnode(self.historyNodeContainer) if let restrictedNode = self.restrictedNode { containerNode.addSubnode(restrictedNode) } self.containerNode = containerNode self.scrollContainerNode?.addSubnode(containerNode) self.navigationBar?.isHidden = true } if self.overlayNavigationBar == nil { let overlayNavigationBar = ChatOverlayNavigationBar(theme: self.chatPresentationInterfaceState.theme, close: { [weak self] in self?.dismissAsOverlay() }) overlayNavigationBar.peerView = self.peerView self.overlayNavigationBar = overlayNavigationBar self.containerNode?.addSubnode(overlayNavigationBar) } } else { if let backgroundEffectNode = self.backgroundEffectNode { backgroundEffectNode.removeFromSupernode() self.backgroundEffectNode = nil } if let scrollContainerNode = self.scrollContainerNode { scrollContainerNode.removeFromSupernode() self.scrollContainerNode = nil } if let containerNode = self.containerNode { self.containerNode = nil containerNode.removeFromSupernode() self.insertSubnode(self.backgroundNode, at: 0) self.insertSubnode(self.historyNodeContainer, aboveSubnode: self.backgroundNode) if let restrictedNode = self.restrictedNode { self.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer) } self.navigationBar?.isHidden = false } if let overlayNavigationBar = self.overlayNavigationBar { overlayNavigationBar.removeFromSupernode() self.overlayNavigationBar = nil } } var dismissedInputByDragging = false if let (validLayout, _) = self.validLayout { var wasDraggingKeyboard = false if validLayout.inputHeight != nil && validLayout.inputHeightIsInteractivellyChanging { wasDraggingKeyboard = true } var wasDraggingInputNode = false if let derivedLayoutState = self.derivedLayoutState, let inputNodeHeight = derivedLayoutState.inputNodeHeight, !inputNodeHeight.isZero, let upperInputPositionBound = derivedLayoutState.upperInputPositionBound { let normalizedHeight = max(0.0, layout.size.height - upperInputPositionBound) if normalizedHeight < inputNodeHeight { wasDraggingInputNode = true } } if wasDraggingKeyboard || wasDraggingInputNode { var isDraggingKeyboard = wasDraggingKeyboard if layout.inputHeight == 0.0 && validLayout.inputHeightIsInteractivellyChanging && !layout.inputHeightIsInteractivellyChanging { isDraggingKeyboard = false } var isDraggingInputNode = false if self.upperInputPositionBound != nil { isDraggingInputNode = true } if !isDraggingKeyboard && !isDraggingInputNode { dismissedInputByDragging = true } } } self.validLayout = (layout, navigationBarHeight) let cleanInsets = layout.intrinsicInsets var previousInputHeight: CGFloat = 0.0 if let (previousLayout, _) = self.containerLayoutAndNavigationBarHeight { previousInputHeight = previousLayout.insets(options: [.input]).bottom } if let inputNode = self.inputNode { previousInputHeight = inputNode.bounds.size.height } var previousInputPanelOrigin = CGPoint(x: 0.0, y: layout.size.height - previousInputHeight) if let inputPanelNode = self.inputPanelNode { previousInputPanelOrigin.y -= inputPanelNode.bounds.size.height } self.containerLayoutAndNavigationBarHeight = (layout, navigationBarHeight) let transitionIsAnimated: Bool if case .immediate = transition { transitionIsAnimated = false } else { transitionIsAnimated = true } if let _ = self.chatPresentationInterfaceState.search, let interfaceInteraction = self.interfaceInteraction { var activate = false if self.searchNavigationNode == nil { activate = true self.searchNavigationNode = ChatSearchNavigationContentNode(theme: self.chatPresentationInterfaceState.theme, strings: self.chatPresentationInterfaceState.strings, chatLocation: self.chatPresentationInterfaceState.chatLocation, interaction: interfaceInteraction) } self.navigationBar?.setContentNode(self.searchNavigationNode, animated: transitionIsAnimated) self.searchNavigationNode?.update(presentationInterfaceState: self.chatPresentationInterfaceState) if activate { self.searchNavigationNode?.activate() } } else if let _ = self.searchNavigationNode { self.searchNavigationNode = nil self.navigationBar?.setContentNode(nil, animated: transitionIsAnimated) } var dismissedTitleAccessoryPanelNode: ChatTitleAccessoryPanelNode? var immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance = false var titleAccessoryPanelHeight: CGFloat? if let titleAccessoryPanelNode = titlePanelForChatPresentationInterfaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.titleAccessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { if self.titleAccessoryPanelNode != titleAccessoryPanelNode { dismissedTitleAccessoryPanelNode = self.titleAccessoryPanelNode self.titleAccessoryPanelNode = titleAccessoryPanelNode immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance = true self.titleAccessoryPanelContainer.addSubnode(titleAccessoryPanelNode) } titleAccessoryPanelHeight = titleAccessoryPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) } else if let titleAccessoryPanelNode = self.titleAccessoryPanelNode { dismissedTitleAccessoryPanelNode = titleAccessoryPanelNode self.titleAccessoryPanelNode = nil } var inputPanelNodeBaseHeight: CGFloat = 0.0 if let inputPanelNode = self.inputPanelNode { inputPanelNodeBaseHeight = inputPanelNode.minimalHeight(interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) } let maximumInputNodeHeight = layout.size.height - max(navigationBarHeight, layout.safeInsets.top) - inputPanelNodeBaseHeight var dismissedInputNode: ChatInputNode? var immediatelyLayoutInputNodeAndAnimateAppearance = false var inputNodeHeightAndOverflow: (CGFloat, CGFloat)? if let inputNode = inputNodeForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentNode: self.inputNode, interfaceInteraction: self.interfaceInteraction, inputMediaNode: self.inputMediaNode, controllerInteraction: self.controllerInteraction, inputPanelNode: self.inputPanelNode) { if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { inputTextPanelNode.ensureUnfocused() } if let inputMediaNode = inputNode as? ChatMediaInputNode, self.inputMediaNode == nil { self.inputMediaNode = inputMediaNode } if self.inputNode != inputNode { dismissedInputNode = self.inputNode self.inputNode = inputNode inputNode.alpha = 1.0 inputNode.layer.removeAnimation(forKey: "opacity") immediatelyLayoutInputNodeAndAnimateAppearance = true if let inputPanelNode = self.inputPanelNode, inputPanelNode.supernode != nil { self.insertSubnode(inputNode, aboveSubnode: inputPanelNode) } else { self.insertSubnode(inputNode, aboveSubnode: self.inputPanelBackgroundNode) } } inputNodeHeightAndOverflow = inputNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelNodeBaseHeight, transition: immediatelyLayoutInputNodeAndAnimateAppearance ? .immediate : transition, interfaceState: self.chatPresentationInterfaceState) } else if let inputNode = self.inputNode { dismissedInputNode = inputNode self.inputNode = nil } var effectiveInputNodeHeight: CGFloat? if let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { if let upperInputPositionBound = self.upperInputPositionBound { effectiveInputNodeHeight = min(layout.size.height - max(0.0, upperInputPositionBound), inputNodeHeightAndOverflow.0) } else { effectiveInputNodeHeight = inputNodeHeightAndOverflow.0 } } var insets: UIEdgeInsets var bottomOverflowOffset: CGFloat = 0.0 if let effectiveInputNodeHeight = effectiveInputNodeHeight, let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { insets = layout.insets(options: []) insets.bottom = max(effectiveInputNodeHeight, insets.bottom) bottomOverflowOffset = inputNodeHeightAndOverflow.1 } else { insets = layout.insets(options: [.input]) } if case .overlay = self.chatPresentationInterfaceState.mode { insets.top = 44.0 } else { insets.top += navigationBarHeight } var wrappingInsets = UIEdgeInsets() if case .overlay = self.chatPresentationInterfaceState.mode { let containerWidth = horizontalContainerFillingSizeForLayout(layout: layout, sideInset: 8.0 + layout.safeInsets.left) wrappingInsets.left = floor((layout.size.width - containerWidth) / 2.0) wrappingInsets.right = wrappingInsets.left wrappingInsets.top = 8.0 if let statusBarHeight = layout.statusBarHeight, CGFloat(40.0).isLess(than: statusBarHeight) { wrappingInsets.top += statusBarHeight } } var dismissedInputPanelNode: ASDisplayNode? var dismissedAccessoryPanelNode: ASDisplayNode? var dismissedInputContextPanelNode: ChatInputContextPanelNode? var dismissedOverlayContextPanelNode: ChatInputContextPanelNode? let previewing: Bool if case .standard(true) = self.chatPresentationInterfaceState.mode { previewing = true } else { previewing = false } var inputPanelSize: CGSize? var immediatelyLayoutInputPanelAndAnimateAppearance = false if let inputPanelNode = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.inputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction), !previewing { if inputPanelNode !== self.inputPanelNode { if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { inputTextPanelNode.ensureUnfocused() let _ = inputTextPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) } dismissedInputPanelNode = self.inputPanelNode immediatelyLayoutInputPanelAndAnimateAppearance = true let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, transition: .immediate, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) self.inputPanelNode = inputPanelNode self.insertSubnode(inputPanelNode, aboveSubnode: self.inputPanelBackgroundNode) } else { let inputPanelHeight = inputPanelNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, maxHeight: layout.size.height - insets.top - insets.bottom, transition: transition, interfaceState: self.chatPresentationInterfaceState, metrics: layout.metrics) inputPanelSize = CGSize(width: layout.size.width, height: inputPanelHeight) } } else { dismissedInputPanelNode = self.inputPanelNode self.inputPanelNode = nil } if let inputMediaNode = self.inputMediaNode, inputMediaNode != self.inputNode { let _ = inputMediaNode.updateLayout(width: layout.size.width, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, bottomInset: cleanInsets.bottom, standardInputHeight: layout.standardInputHeight, inputHeight: layout.inputHeight ?? 0.0, maximumHeight: maximumInputNodeHeight, inputPanelHeight: inputPanelSize?.height ?? 0.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } transition.updateFrame(node: self.titleAccessoryPanelContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: 56.0))) var titleAccessoryPanelFrame: CGRect? if let _ = self.titleAccessoryPanelNode, let panelHeight = titleAccessoryPanelHeight { titleAccessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: panelHeight)) insets.top += panelHeight } var duration: Double = 0.0 var curve: UInt = 0 switch transition { case .immediate: break case let .animated(animationDuration, animationCurve): duration = animationDuration switch animationCurve { case .easeInOut: break case .spring: curve = 7 } } let contentBounds = CGRect(x: 0.0, y: -bottomOverflowOffset, width: layout.size.width - wrappingInsets.left - wrappingInsets.right, height: layout.size.height - wrappingInsets.top - wrappingInsets.bottom) if let backgroundEffectNode = self.backgroundEffectNode { transition.updateFrame(node: backgroundEffectNode, frame: CGRect(origin: CGPoint(), size: layout.size)) } transition.updateFrame(node: self.backgroundNode, frame: contentBounds) transition.updateFrame(node: self.historyNodeContainer, frame: contentBounds) transition.updateBounds(node: self.historyNode, bounds: CGRect(origin: CGPoint(), size: contentBounds.size)) transition.updatePosition(node: self.historyNode, position: CGPoint(x: contentBounds.size.width / 2.0, y: contentBounds.size.height / 2.0)) transition.updateFrame(node: self.loadingNode, frame: contentBounds) if let restrictedNode = self.restrictedNode { transition.updateFrame(node: restrictedNode, frame: contentBounds) restrictedNode.updateLayout(size: contentBounds.size, transition: transition) } let listViewCurve: ListViewAnimationCurve if curve == 7 { listViewCurve = .Spring(duration: duration) } else { listViewCurve = .Default(duration: duration) } var accessoryPanelSize: CGSize? var immediatelyLayoutAccessoryPanelAndAnimateAppearance = false if let accessoryPanelNode = accessoryPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.accessoryPanelNode, interfaceInteraction: self.interfaceInteraction) { accessoryPanelSize = accessoryPanelNode.measure(CGSize(width: layout.size.width, height: layout.size.height)) accessoryPanelNode.updateState(size: CGSize(width: layout.size.width, height: layout.size.height), interfaceState: self.chatPresentationInterfaceState) if accessoryPanelNode !== self.accessoryPanelNode { dismissedAccessoryPanelNode = self.accessoryPanelNode self.accessoryPanelNode = accessoryPanelNode if let inputPanelNode = self.inputPanelNode { self.insertSubnode(accessoryPanelNode, belowSubnode: inputPanelNode) } else { self.insertSubnode(accessoryPanelNode, aboveSubnode: self.navigateButtons) } accessoryPanelNode.dismiss = { [weak self, weak accessoryPanelNode] in if let strongSelf = self, let accessoryPanelNode = accessoryPanelNode, strongSelf.accessoryPanelNode === accessoryPanelNode { if let _ = accessoryPanelNode as? ReplyAccessoryPanelNode { strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedReplyMessageId(nil) }) } else if let _ = accessoryPanelNode as? ForwardAccessoryPanelNode { strongSelf.requestUpdateChatInterfaceState(true, { $0.withUpdatedForwardMessageIds(nil) }) } else if let _ = accessoryPanelNode as? EditAccessoryPanelNode { strongSelf.interfaceInteraction?.setupEditMessage(nil) } else if let _ = accessoryPanelNode as? WebpagePreviewAccessoryPanelNode { strongSelf.dismissUrlPreview() } } } immediatelyLayoutAccessoryPanelAndAnimateAppearance = true } } else if let accessoryPanelNode = self.accessoryPanelNode { dismissedAccessoryPanelNode = accessoryPanelNode self.accessoryPanelNode = nil } var immediatelyLayoutInputContextPanelAndAnimateAppearance = false if let inputContextPanelNode = inputContextPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.inputContextPanelNode, controllerInteraction: self.controllerInteraction, interfaceInteraction: self.interfaceInteraction) { if inputContextPanelNode !== self.inputContextPanelNode { dismissedInputContextPanelNode = self.inputContextPanelNode self.inputContextPanelNode = inputContextPanelNode self.addSubnode(inputContextPanelNode) immediatelyLayoutInputContextPanelAndAnimateAppearance = true } } else if let inputContextPanelNode = self.inputContextPanelNode { dismissedInputContextPanelNode = inputContextPanelNode self.inputContextPanelNode = nil } var immediatelyLayoutOverlayContextPanelAndAnimateAppearance = false if let overlayContextPanelNode = chatOverlayContextPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.overlayContextPanelNode, interfaceInteraction: self.interfaceInteraction) { if overlayContextPanelNode !== self.overlayContextPanelNode { dismissedOverlayContextPanelNode = self.overlayContextPanelNode self.overlayContextPanelNode = overlayContextPanelNode self.addSubnode(overlayContextPanelNode) immediatelyLayoutOverlayContextPanelAndAnimateAppearance = true } } else if let overlayContextPanelNode = self.overlayContextPanelNode { dismissedOverlayContextPanelNode = overlayContextPanelNode self.overlayContextPanelNode = nil } var inputPanelsHeight: CGFloat = 0.0 var inputPanelFrame: CGRect? if self.inputPanelNode != nil { assert(inputPanelSize != nil) inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight - inputPanelSize!.height), size: CGSize(width: layout.size.width, height: inputPanelSize!.height)) if self.dismissedAsOverlay { inputPanelFrame = inputPanelFrame!.offsetBy(dx: 0.0, dy: inputPanelsHeight + inputPanelSize!.height) } inputPanelsHeight += inputPanelSize!.height } var accessoryPanelFrame: CGRect? if self.accessoryPanelNode != nil { assert(accessoryPanelSize != nil) accessoryPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomOverflowOffset - insets.bottom - inputPanelsHeight - accessoryPanelSize!.height), size: CGSize(width: layout.size.width, height: accessoryPanelSize!.height)) if self.dismissedAsOverlay { accessoryPanelFrame = accessoryPanelFrame!.offsetBy(dx: 0.0, dy: inputPanelsHeight + accessoryPanelSize!.height) } inputPanelsHeight += accessoryPanelSize!.height } if self.dismissedAsOverlay { inputPanelsHeight = 0.0 } let inputBackgroundInset: CGFloat if cleanInsets.bottom < insets.bottom { inputBackgroundInset = 0.0 } else { inputBackgroundInset = cleanInsets.bottom } let inputBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight), size: CGSize(width: layout.size.width, height: inputPanelsHeight + inputBackgroundInset)) let additionalScrollDistance: CGFloat = 0.0 var scrollToTop = false if dismissedInputByDragging { if !self.historyNode.trackingOffset.isZero { if self.historyNode.beganTrackingAtTopOrigin { scrollToTop = true } } } var emptyNodeInsets = insets emptyNodeInsets.bottom += inputPanelsHeight self.validEmptyNodeLayout = (contentBounds.size, emptyNodeInsets) if let emptyNode = self.emptyNode { emptyNode.updateLayout(interfaceState: self.chatPresentationInterfaceState, size: contentBounds.size, insets: emptyNodeInsets, transition: transition) transition.updateFrame(node: emptyNode, frame: contentBounds) } var contentBottomInset: CGFloat = inputPanelsHeight + 4.0 if let scrollContainerNode = self.scrollContainerNode { transition.updateFrame(node: scrollContainerNode, frame: CGRect(origin: CGPoint(), size: layout.size)) } var containerInsets = insets if let dismissAsOverlayLayout = self.dismissAsOverlayLayout { if let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { containerInsets = dismissAsOverlayLayout.insets(options: []) containerInsets.bottom = max(inputNodeHeightAndOverflow.0 + inputNodeHeightAndOverflow.1, insets.bottom) } else { containerInsets = dismissAsOverlayLayout.insets(options: [.input]) } } self.loadingNode.updateLayout(size: contentBounds.size, insets: UIEdgeInsetsMake(containerInsets.top, 0.0, containerInsets.bottom + contentBottomInset, 0.0), transition: transition) if let containerNode = self.containerNode { contentBottomInset += 8.0 let containerNodeFrame = CGRect(origin: CGPoint(x: wrappingInsets.left, y: wrappingInsets.top), size: CGSize(width: contentBounds.size.width, height: contentBounds.size.height - containerInsets.bottom - inputPanelsHeight - 8.0)) transition.updateFrame(node: containerNode, frame: containerNodeFrame) if let containerBackgroundNode = self.containerBackgroundNode { transition.updateFrame(node: containerBackgroundNode, frame: CGRect(origin: CGPoint(x: containerNodeFrame.minX - 8.0 * 2.0, y: containerNodeFrame.minY - 8.0 * 2.0), size: CGSize(width: containerNodeFrame.size.width + 8.0 * 4.0, height: containerNodeFrame.size.height + 8.0 * 2.0 + 20.0))) } } if let overlayNavigationBar = self.overlayNavigationBar { let barFrame = CGRect(origin: CGPoint(), size: CGSize(width: contentBounds.size.width, height: 44.0)) transition.updateFrame(node: overlayNavigationBar, frame: barFrame) overlayNavigationBar.updateLayout(size: barFrame.size, transition: transition) } var listInsets = UIEdgeInsets(top: containerInsets.bottom + contentBottomInset, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left) let listScrollIndicatorInsets = UIEdgeInsets(top: containerInsets.bottom + inputPanelsHeight, left: containerInsets.right, bottom: containerInsets.top, right: containerInsets.left) if case .standard = self.chatPresentationInterfaceState.mode { listInsets.left += layout.safeInsets.left listInsets.right += layout.safeInsets.right if case .regular = layout.metrics.widthClass, case .regular = layout.metrics.heightClass { listInsets.left += 6.0 listInsets.right += 6.0 listInsets.top += 6.0 } } var displayTopDimNode = false var ensureTopInsetForOverlayHighlightedItems: CGFloat? if let (controller, _) = self.messageActionSheetController { displayTopDimNode = true let globalSelfOrigin = self.view.convert(CGPoint(), to: nil) let menuHeight = controller.controllerNode.updateLayout(layout: layout, horizontalOrigin: globalSelfOrigin.x, transition: transition) ensureTopInsetForOverlayHighlightedItems = menuHeight let bottomInset = containerInsets.bottom + inputPanelsHeight + UIScreenPixel let bottomFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - bottomInset), size: CGSize(width: layout.size.width, height: max(0.0, bottomInset - (layout.inputHeight ?? 0.0)))) let messageActionSheetBottomDimNode: ASDisplayNode if let current = self.messageActionSheetBottomDimNode { messageActionSheetBottomDimNode = current transition.updateFrame(node: messageActionSheetBottomDimNode, frame: bottomFrame) } else { messageActionSheetBottomDimNode = ASDisplayNode() messageActionSheetBottomDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) messageActionSheetBottomDimNode.alpha = 0.0 messageActionSheetBottomDimNode.isLayerBacked = true self.messageActionSheetBottomDimNode = messageActionSheetBottomDimNode self.addSubnode(messageActionSheetBottomDimNode) transition.updateAlpha(node: messageActionSheetBottomDimNode, alpha: 1.0) messageActionSheetBottomDimNode.frame = bottomFrame } } else { if let messageActionSheetBottomDimNode = self.messageActionSheetBottomDimNode { self.messageActionSheetBottomDimNode = nil transition.updateAlpha(node: messageActionSheetBottomDimNode, alpha: 0.0, completion: { [weak messageActionSheetBottomDimNode] _ in messageActionSheetBottomDimNode?.removeFromSupernode() }) } } var expandTopDimNode = false if case let .media(_, expanded) = self.chatPresentationInterfaceState.inputMode, expanded != nil { displayTopDimNode = true expandTopDimNode = true } if displayTopDimNode { var topInset = listInsets.bottom + UIScreenPixel if let titleAccessoryPanelHeight = titleAccessoryPanelHeight { if expandTopDimNode { topInset -= titleAccessoryPanelHeight } else { topInset -= UIScreenPixel } } let topFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: max(0.0, topInset))) let messageActionSheetTopDimNode: ASDisplayNode if let current = self.messageActionSheetTopDimNode { messageActionSheetTopDimNode = current transition.updateFrame(node: messageActionSheetTopDimNode, frame: topFrame) } else { messageActionSheetTopDimNode = ASDisplayNode() messageActionSheetTopDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) messageActionSheetTopDimNode.alpha = 0.0 self.messageActionSheetTopDimNode = messageActionSheetTopDimNode self.addSubnode(messageActionSheetTopDimNode) transition.updateAlpha(node: messageActionSheetTopDimNode, alpha: 1.0) messageActionSheetTopDimNode.frame = topFrame messageActionSheetTopDimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.topDimNodeTapGesture(_:)))) } let inputPanelOrigin = layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight if expandTopDimNode { let exandedFrame = CGRect(origin: CGPoint(x: 0.0, y: inputPanelOrigin - layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height)) let expandedInputDimNode: ASDisplayNode if let current = self.expandedInputDimNode { expandedInputDimNode = current transition.updateFrame(node: expandedInputDimNode, frame: exandedFrame) } else { expandedInputDimNode = ASDisplayNode() expandedInputDimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5) expandedInputDimNode.alpha = 0.0 self.expandedInputDimNode = expandedInputDimNode if let inputNode = self.inputNode, inputNode.supernode != nil { self.insertSubnode(expandedInputDimNode, belowSubnode: inputNode) } else { self.addSubnode(expandedInputDimNode) } transition.updateAlpha(node: expandedInputDimNode, alpha: 1.0) expandedInputDimNode.frame = exandedFrame transition.animatePositionAdditive(node: expandedInputDimNode, offset: CGPoint(x: 0.0, y: previousInputPanelOrigin.y - inputPanelOrigin)) } } else { if let expandedInputDimNode = self.expandedInputDimNode { self.expandedInputDimNode = nil transition.animatePositionAdditive(node: expandedInputDimNode, offset: CGPoint(x: 0.0, y: previousInputPanelOrigin.y - inputPanelOrigin)) transition.updateAlpha(node: expandedInputDimNode, alpha: 0.0, completion: { [weak expandedInputDimNode] _ in expandedInputDimNode?.removeFromSupernode() }) } } } else { if let messageActionSheetTopDimNode = self.messageActionSheetTopDimNode { self.messageActionSheetTopDimNode = nil transition.updateAlpha(node: messageActionSheetTopDimNode, alpha: 0.0, completion: { [weak messageActionSheetTopDimNode] _ in messageActionSheetTopDimNode?.removeFromSupernode() }) } if let expandedInputDimNode = self.expandedInputDimNode { self.expandedInputDimNode = nil let inputPanelOrigin = layout.size.height - insets.bottom - bottomOverflowOffset - inputPanelsHeight let exandedFrame = CGRect(origin: CGPoint(x: 0.0, y: inputPanelOrigin - layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height)) transition.updateFrame(node: expandedInputDimNode, frame: exandedFrame) transition.updateAlpha(node: expandedInputDimNode, alpha: 0.0, completion: { [weak expandedInputDimNode] _ in expandedInputDimNode?.removeFromSupernode() }) } } if let messageActionSheetControllerAdditionalInset = self.messageActionSheetControllerAdditionalInset { listInsets.top = listInsets.top + messageActionSheetControllerAdditionalInset } listViewTransaction(ListViewUpdateSizeAndInsets(size: contentBounds.size, insets: listInsets, scrollIndicatorInsets: listScrollIndicatorInsets, duration: duration, curve: listViewCurve, ensureTopInsetForOverlayHighlightedItems: ensureTopInsetForOverlayHighlightedItems), additionalScrollDistance, scrollToTop) let navigateButtonsSize = self.navigateButtons.updateLayout(transition: transition) var navigateButtonsFrame = CGRect(origin: CGPoint(x: layout.size.width - layout.safeInsets.right - navigateButtonsSize.width - 6.0, y: layout.size.height - containerInsets.bottom - inputPanelsHeight - navigateButtonsSize.height - 6.0 - bottomOverflowOffset), size: navigateButtonsSize) if case .overlay = self.chatPresentationInterfaceState.mode { navigateButtonsFrame = navigateButtonsFrame.offsetBy(dx: -8.0, dy: -8.0) } var apparentInputPanelFrame = inputPanelFrame var apparentInputBackgroundFrame = inputBackgroundFrame var apparentNavigateButtonsFrame = navigateButtonsFrame if case let .media(_, maybeExpanded) = self.chatPresentationInterfaceState.inputMode, let expanded = maybeExpanded, case .search = expanded, let inputPanelFrame = inputPanelFrame { let verticalOffset = -inputPanelFrame.height - 41.0 apparentInputPanelFrame = inputPanelFrame.offsetBy(dx: 0.0, dy: verticalOffset) apparentInputBackgroundFrame.size.height -= verticalOffset apparentInputBackgroundFrame.origin.y += verticalOffset apparentNavigateButtonsFrame.origin.y += verticalOffset } transition.updateFrame(node: self.inputPanelBackgroundNode, frame: apparentInputBackgroundFrame) transition.updateFrame(node: self.inputPanelBackgroundSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: apparentInputBackgroundFrame.origin.y - UIScreenPixel), size: CGSize(width: apparentInputBackgroundFrame.size.width, height: UIScreenPixel))) transition.updateFrame(node: self.navigateButtons, frame: apparentNavigateButtonsFrame) if let titleAccessoryPanelNode = self.titleAccessoryPanelNode, let titleAccessoryPanelFrame = titleAccessoryPanelFrame, !titleAccessoryPanelNode.frame.equalTo(titleAccessoryPanelFrame) { if immediatelyLayoutTitleAccessoryPanelNodeAndAnimateAppearance { titleAccessoryPanelNode.frame = titleAccessoryPanelFrame.offsetBy(dx: 0.0, dy: -titleAccessoryPanelFrame.size.height) } transition.updateFrame(node: titleAccessoryPanelNode, frame: titleAccessoryPanelFrame) } if let inputPanelNode = self.inputPanelNode, let apparentInputPanelFrame = apparentInputPanelFrame, !inputPanelNode.frame.equalTo(apparentInputPanelFrame) { if immediatelyLayoutInputPanelAndAnimateAppearance { inputPanelNode.frame = apparentInputPanelFrame.offsetBy(dx: 0.0, dy: apparentInputPanelFrame.height) inputPanelNode.alpha = 0.0 } transition.updateFrame(node: inputPanelNode, frame: apparentInputPanelFrame) transition.updateAlpha(node: inputPanelNode, alpha: 1.0) } if let accessoryPanelNode = self.accessoryPanelNode, let accessoryPanelFrame = accessoryPanelFrame, !accessoryPanelNode.frame.equalTo(accessoryPanelFrame) { if immediatelyLayoutAccessoryPanelAndAnimateAppearance { var startAccessoryPanelFrame = accessoryPanelFrame startAccessoryPanelFrame.origin.y = previousInputPanelOrigin.y accessoryPanelNode.frame = startAccessoryPanelFrame accessoryPanelNode.alpha = 0.0 } transition.updateFrame(node: accessoryPanelNode, frame: accessoryPanelFrame) transition.updateAlpha(node: accessoryPanelNode, alpha: 1.0) } let inputContextPanelsFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - insets.bottom - inputPanelsHeight - insets.top - UIScreenPixel))) let inputContextPanelsOverMainPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: insets.top), size: CGSize(width: layout.size.width, height: max(0.0, layout.size.height - insets.bottom - (inputPanelSize == nil ? CGFloat(0.0) : inputPanelSize!.height) - insets.top - UIScreenPixel))) if let inputContextPanelNode = self.inputContextPanelNode { let panelFrame = inputContextPanelNode.placement == .overTextInput ? inputContextPanelsOverMainPanelFrame : inputContextPanelsFrame if immediatelyLayoutInputContextPanelAndAnimateAppearance { var startPanelFrame = panelFrame if let derivedLayoutState = self.derivedLayoutState { let referenceFrame = inputContextPanelNode.placement == .overTextInput ? derivedLayoutState.inputContextPanelsOverMainPanelFrame : derivedLayoutState.inputContextPanelsFrame startPanelFrame.origin.y = referenceFrame.maxY - panelFrame.height } inputContextPanelNode.frame = startPanelFrame inputContextPanelNode.updateLayout(size: startPanelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } if !inputContextPanelNode.frame.equalTo(panelFrame) || inputContextPanelNode.theme !== self.chatPresentationInterfaceState.theme { transition.updateFrame(node: inputContextPanelNode, frame: panelFrame) inputContextPanelNode.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition, interfaceState: self.chatPresentationInterfaceState) } } if let overlayContextPanelNode = self.overlayContextPanelNode { let panelFrame = overlayContextPanelNode.placement == .overTextInput ? inputContextPanelsOverMainPanelFrame : inputContextPanelsFrame if immediatelyLayoutOverlayContextPanelAndAnimateAppearance { overlayContextPanelNode.frame = panelFrame overlayContextPanelNode.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } else if !overlayContextPanelNode.frame.equalTo(panelFrame) { transition.updateFrame(node: overlayContextPanelNode, frame: panelFrame) overlayContextPanelNode.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition, interfaceState: self.chatPresentationInterfaceState) } } if let inputNode = self.inputNode, let effectiveInputNodeHeight = effectiveInputNodeHeight, let inputNodeHeightAndOverflow = inputNodeHeightAndOverflow { let inputNodeHeight = effectiveInputNodeHeight + inputNodeHeightAndOverflow.1 let inputNodeFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - inputNodeHeight), size: CGSize(width: layout.size.width, height: inputNodeHeight)) if immediatelyLayoutInputNodeAndAnimateAppearance { var adjustedForPreviousInputHeightFrame = inputNodeFrame var heightDifference = inputNodeHeight - previousInputHeight if previousInputHeight.isLessThanOrEqualTo(cleanInsets.bottom) { heightDifference = inputNodeHeight } adjustedForPreviousInputHeightFrame.origin.y += heightDifference inputNode.frame = adjustedForPreviousInputHeightFrame transition.updateFrame(node: inputNode, frame: inputNodeFrame) } else { transition.updateFrame(node: inputNode, frame: inputNodeFrame) } } if let dismissedTitleAccessoryPanelNode = dismissedTitleAccessoryPanelNode { var dismissedPanelFrame = dismissedTitleAccessoryPanelNode.frame dismissedPanelFrame.origin.y = -dismissedPanelFrame.size.height transition.updateFrame(node: dismissedTitleAccessoryPanelNode, frame: dismissedPanelFrame, completion: { [weak dismissedTitleAccessoryPanelNode] _ in dismissedTitleAccessoryPanelNode?.removeFromSupernode() }) } if let dismissedInputPanelNode = dismissedInputPanelNode { var frameCompleted = false var alphaCompleted = false let completed = { [weak self, weak dismissedInputPanelNode] in if let strongSelf = self, let dismissedInputPanelNode = dismissedInputPanelNode, strongSelf.inputPanelNode === dismissedInputPanelNode { return } if frameCompleted && alphaCompleted { dismissedInputPanelNode?.removeFromSupernode() } } let transitionTargetY = layout.size.height - insets.bottom transition.updateFrame(node: dismissedInputPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: transitionTargetY), size: dismissedInputPanelNode.frame.size), completion: { _ in frameCompleted = true completed() }) transition.updateAlpha(node: dismissedInputPanelNode, alpha: 0.0, completion: { _ in alphaCompleted = true completed() }) } if let dismissedAccessoryPanelNode = dismissedAccessoryPanelNode { var frameCompleted = false var alphaCompleted = false let completed = { [weak dismissedAccessoryPanelNode] in if frameCompleted && alphaCompleted { dismissedAccessoryPanelNode?.removeFromSupernode() } } var transitionTargetY = layout.size.height - insets.bottom if let inputPanelFrame = inputPanelFrame { transitionTargetY = inputPanelFrame.minY } transition.updateFrame(node: dismissedAccessoryPanelNode, frame: CGRect(origin: CGPoint(x: 0.0, y: transitionTargetY), size: dismissedAccessoryPanelNode.frame.size), completion: { _ in frameCompleted = true completed() }) transition.updateAlpha(node: dismissedAccessoryPanelNode, alpha: 0.0, completion: { _ in alphaCompleted = true completed() }) } if let dismissedInputContextPanelNode = dismissedInputContextPanelNode { var frameCompleted = false var animationCompleted = false let completed = { [weak dismissedInputContextPanelNode] in if let dismissedInputContextPanelNode = dismissedInputContextPanelNode, frameCompleted, animationCompleted { dismissedInputContextPanelNode.removeFromSupernode() } } let panelFrame = dismissedInputContextPanelNode.placement == .overTextInput ? inputContextPanelsOverMainPanelFrame : inputContextPanelsFrame if !dismissedInputContextPanelNode.frame.equalTo(panelFrame) { dismissedInputContextPanelNode.updateLayout(size: panelFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, transition: transition, interfaceState: self.chatPresentationInterfaceState) transition.updateFrame(node: dismissedInputContextPanelNode, frame: panelFrame, completion: { _ in frameCompleted = true completed() }) } else { frameCompleted = true } dismissedInputContextPanelNode.animateOut(completion: { animationCompleted = true completed() }) } if let dismissedOverlayContextPanelNode = dismissedOverlayContextPanelNode { var frameCompleted = false var animationCompleted = false let completed = { [weak dismissedOverlayContextPanelNode] in if let dismissedOverlayContextPanelNode = dismissedOverlayContextPanelNode, frameCompleted, animationCompleted { dismissedOverlayContextPanelNode.removeFromSupernode() } } let panelFrame = inputContextPanelsFrame if false && !dismissedOverlayContextPanelNode.frame.equalTo(panelFrame) { transition.updateFrame(node: dismissedOverlayContextPanelNode, frame: panelFrame, completion: { _ in frameCompleted = true completed() }) } else { frameCompleted = true } dismissedOverlayContextPanelNode.animateOut(completion: { animationCompleted = true completed() }) } if let dismissedInputNode = dismissedInputNode { let targetY: CGFloat if cleanInsets.bottom.isLess(than: insets.bottom) { targetY = layout.size.height - insets.bottom } else { targetY = layout.size.height } transition.updateFrame(node: dismissedInputNode, frame: CGRect(origin: CGPoint(x: 0.0, y: targetY), size: CGSize(width: layout.size.width, height: max(insets.bottom, dismissedInputNode.bounds.size.height))), force: true, completion: { [weak self, weak dismissedInputNode] completed in if completed, let dismissedInputNode = dismissedInputNode { if let strongSelf = self { if strongSelf.inputNode !== dismissedInputNode { dismissedInputNode.alpha = 0.0 dismissedInputNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, completion: { [weak dismissedInputNode] completed in if completed, let strongSelf = self, let dismissedInputNode = dismissedInputNode { if strongSelf.inputNode !== dismissedInputNode { dismissedInputNode.removeFromSupernode() } } }) } } else { dismissedInputNode.removeFromSupernode() } } }) } if let dismissAsOverlayCompletion = self.dismissAsOverlayCompletion { self.dismissAsOverlayCompletion = nil transition.updateBounds(node: self.navigateButtons, bounds: self.navigateButtons.bounds, force: true, completion: { _ in dismissAsOverlayCompletion() }) } if let scheduledAnimateInAsOverlayFromNode = self.scheduledAnimateInAsOverlayFromNode { self.scheduledAnimateInAsOverlayFromNode = nil self.bounds = CGRect(origin: CGPoint(), size: self.bounds.size) let animatedTransition: ContainedViewLayoutTransition if case .animated = protoTransition { animatedTransition = protoTransition } else { animatedTransition = .animated(duration: 0.4, curve: .spring) } self.performAnimateInAsOverlay(from: scheduledAnimateInAsOverlayFromNode, transition: animatedTransition) } self.derivedLayoutState = ChatControllerNodeDerivedLayoutState(inputContextPanelsFrame: inputContextPanelsFrame, inputContextPanelsOverMainPanelFrame: inputContextPanelsOverMainPanelFrame, inputNodeHeight: inputNodeHeightAndOverflow?.0, upperInputPositionBound: inputNodeHeightAndOverflow?.0 != nil ? self.upperInputPositionBound : nil) } private func chatPresentationInterfaceStateRequiresInputFocus(_ state: ChatPresentationInterfaceState) -> Bool { switch state.inputMode { case .text: if state.interfaceState.selectionState != nil { return false } else { return true } default: return false } } func updateChatPresentationInterfaceState(_ chatPresentationInterfaceState: ChatPresentationInterfaceState, transition: ContainedViewLayoutTransition, interactive: Bool) { self.selectedMessages = chatPresentationInterfaceState.interfaceState.selectionState?.selectedIds if let textInputPanelNode = self.textInputPanelNode { self.chatPresentationInterfaceState = self.chatPresentationInterfaceState.updatedInterfaceState { $0.withUpdatedEffectiveInputState(textInputPanelNode.inputTextState) } } if self.chatPresentationInterfaceState != chatPresentationInterfaceState { let themeUpdated = self.chatPresentationInterfaceState.theme !== chatPresentationInterfaceState.theme if self.chatPresentationInterfaceState.chatWallpaper != chatPresentationInterfaceState.chatWallpaper { self.backgroundNode.contents = chatControllerBackgroundImage(wallpaper: chatPresentationInterfaceState.chatWallpaper, postbox: account.postbox)?.cgImage } self.historyNode.verticalScrollIndicatorColor = UIColor(white: 0.5, alpha: 0.8) let updatedInputFocus = self.chatPresentationInterfaceStateRequiresInputFocus(self.chatPresentationInterfaceState) != self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) let updateInputTextState = self.chatPresentationInterfaceState.interfaceState.effectiveInputState != chatPresentationInterfaceState.interfaceState.effectiveInputState self.chatPresentationInterfaceState = chatPresentationInterfaceState self.navigateButtons.updateTheme(theme: chatPresentationInterfaceState.theme) if themeUpdated { self.inputPanelBackgroundNode.backgroundColor = chatPresentationInterfaceState.theme.chat.inputPanel.panelBackgroundColor self.inputPanelBackgroundSeparatorNode.backgroundColor = self.chatPresentationInterfaceState.theme.chat.inputPanel.panelStrokeColor } let keepSendButtonEnabled = chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil || chatPresentationInterfaceState.interfaceState.editMessage != nil var extendedSearchLayout = false loop: for (_, result) in chatPresentationInterfaceState.inputQueryResults { if case let .contextRequestResult(peer, _) = result, peer != nil { extendedSearchLayout = true break loop } } if let textInputPanelNode = self.textInputPanelNode, updateInputTextState { textInputPanelNode.updateInputTextState(chatPresentationInterfaceState.interfaceState.effectiveInputState, keepSendButtonEnabled: keepSendButtonEnabled, extendedSearchLayout: extendedSearchLayout, animated: transition.isAnimated) } else { textInputPanelNode?.updateKeepSendButtonEnabled(keepSendButtonEnabled: keepSendButtonEnabled, extendedSearchLayout: extendedSearchLayout, animated: transition.isAnimated) } var restrictionText: String? if chatPresentationInterfaceState.isNotAccessible { restrictionText = chatPresentationInterfaceState.strings.Channel_ErrorAccessDenied } else if let peer = chatPresentationInterfaceState.renderedPeer?.peer { restrictionText = peer.restrictionText } if let restrictionText = restrictionText { if self.restrictedNode == nil { let restrictedNode = ChatRecentActionsEmptyNode(theme: chatPresentationInterfaceState.theme, chatWallpaper: chatPresentationInterfaceState.chatWallpaper) self.historyNodeContainer.supernode?.insertSubnode(restrictedNode, aboveSubnode: self.historyNodeContainer) self.restrictedNode = restrictedNode } self.restrictedNode?.setup(title: "", text: processedPeerRestrictionText(restrictionText)) self.historyNodeContainer.isHidden = true self.navigateButtons.isHidden = true self.loadingNode.isHidden = true self.emptyNode?.isHidden = true } else if let restrictedNode = self.restrictedNode { self.restrictedNode = nil restrictedNode.removeFromSupernode() self.historyNodeContainer.isHidden = false self.navigateButtons.isHidden = false self.loadingNode.isHidden = false self.emptyNode?.isHidden = false } let layoutTransition: ContainedViewLayoutTransition = transition if updatedInputFocus { if !self.ignoreUpdateHeight { self.scheduleLayoutTransitionRequest(layoutTransition) } if self.chatPresentationInterfaceStateRequiresInputFocus(chatPresentationInterfaceState) { self.ensureInputViewFocused() } else { if let inputTextPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { inputTextPanelNode.ensureUnfocused() } } } else { if !self.ignoreUpdateHeight { if interactive { if let scheduledLayoutTransitionRequest = self.scheduledLayoutTransitionRequest { switch scheduledLayoutTransitionRequest.1 { case .immediate: self.scheduleLayoutTransitionRequest(layoutTransition) default: break } } else { self.scheduleLayoutTransitionRequest(layoutTransition) } } else { if let scheduledLayoutTransitionRequest = self.scheduledLayoutTransitionRequest { switch scheduledLayoutTransitionRequest.1 { case .immediate: self.requestLayout(layoutTransition) case .animated: self.scheduleLayoutTransitionRequest(scheduledLayoutTransitionRequest.1) } } else { self.requestLayout(layoutTransition) } } } } } } func updateAutomaticMediaDownloadSettings() { self.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { itemNode.updateAutomaticMediaDownloadSettings() } } } var isInputViewFocused: Bool { if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { return inputPanelNode.isFocused } else { return false } } func ensureInputViewFocused() { if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode { inputPanelNode.ensureFocused() } } @objc func tapGesture(_ recognizer: UITapGestureRecognizer) { if recognizer.state == .ended { self.dismissInput() } } func dismissInput() { if let _ = self.chatPresentationInterfaceState.inputTextPanelState.mediaRecordingState { return } switch self.chatPresentationInterfaceState.inputMode { case .none: break default: self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId({ state in return (.none, state.interfaceState.messageActionsState.closedButtonKeyboardMessageId) }) } self.searchNavigationNode?.deactivate() } private func scheduleLayoutTransitionRequest(_ transition: ContainedViewLayoutTransition) { let requestId = self.scheduledLayoutTransitionRequestId self.scheduledLayoutTransitionRequestId += 1 self.scheduledLayoutTransitionRequest = (requestId, transition) (self.view as? UITracingLayerView)?.schedule(layout: { [weak self] in if let strongSelf = self { if let (currentRequestId, currentRequestTransition) = strongSelf.scheduledLayoutTransitionRequest, currentRequestId == requestId { strongSelf.scheduledLayoutTransitionRequest = nil strongSelf.requestLayout(currentRequestTransition) } } }) self.setNeedsLayout() } func loadInputPanels(theme: PresentationTheme, strings: PresentationStrings) { if self.inputMediaNode == nil { var peerId: PeerId? if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation { peerId = id } let inputNode = ChatMediaInputNode(account: self.account, peerId: peerId, controllerInteraction: self.controllerInteraction, theme: theme, strings: strings, gifPaneIsActiveUpdated: { [weak self] value in if let strongSelf = self, let interfaceInteraction = strongSelf.interfaceInteraction { interfaceInteraction.updateInputModeAndDismissedButtonKeyboardMessageId { state in if case let .media(_, expanded) = state.inputMode { if value { return (.media(mode: .gif, expanded: expanded), nil) } else { return (.media(mode: .other, expanded: expanded), nil) } } else { return (state.inputMode, nil) } } } }) inputNode.interfaceInteraction = interfaceInteraction self.inputMediaNode = inputNode if let (validLayout, _) = self.validLayout { let _ = inputNode.updateLayout(width: validLayout.size.width, leftInset: validLayout.safeInsets.left, rightInset: validLayout.safeInsets.right, bottomInset: validLayout.intrinsicInsets.bottom, standardInputHeight: validLayout.standardInputHeight, inputHeight: validLayout.inputHeight ?? 0.0, maximumHeight: validLayout.standardInputHeight, inputPanelHeight: 44.0, transition: .immediate, interfaceState: self.chatPresentationInterfaceState) } } } func currentInputPanelFrame() -> CGRect? { return self.inputPanelNode?.frame } func frameForInputPanelAccessoryButton(_ item: ChatTextInputAccessoryItem) -> CGRect? { if let textInputPanelNode = self.textInputPanelNode, self.inputPanelNode === textInputPanelNode { return textInputPanelNode.frameForAccessoryButton(item).flatMap { return $0.offsetBy(dx: textInputPanelNode.frame.minX, dy: textInputPanelNode.frame.minY) } } return nil } func frameForInputActionButton() -> CGRect? { if let textInputPanelNode = self.textInputPanelNode, self.inputPanelNode === textInputPanelNode { return textInputPanelNode.frameForInputActionButton().flatMap { return $0.offsetBy(dx: textInputPanelNode.frame.minX, dy: textInputPanelNode.frame.minY) } } return nil } func frameForStickersButton() -> CGRect? { if let textInputPanelNode = self.textInputPanelNode, self.inputPanelNode === textInputPanelNode { return textInputPanelNode.frameForStickersButton().flatMap { return $0.offsetBy(dx: textInputPanelNode.frame.minX, dy: textInputPanelNode.frame.minY) } } return nil } var isTextInputPanelActive: Bool { return self.inputPanelNode is ChatTextInputPanelNode } func getWindowInputAccessoryHeight() -> CGFloat { var height = self.inputPanelBackgroundNode.bounds.size.height if case .overlay = self.chatPresentationInterfaceState.mode { height += 8.0 } return height } func animateInAsOverlay(from fromNode: ASDisplayNode?, completion: @escaping () -> Void) { if let inputPanelNode = self.inputPanelNode as? ChatTextInputPanelNode, let fromNode = fromNode { if inputPanelNode.isFocused { self.performAnimateInAsOverlay(from: fromNode, transition: .animated(duration: 0.4, curve: .spring)) completion() } else { self.animateInAsOverlayCompletion = completion self.bounds = CGRect(origin: CGPoint(x: -self.bounds.size.width * 2.0, y: 0.0), size: self.bounds.size) self.scheduledAnimateInAsOverlayFromNode = fromNode self.scheduleLayoutTransitionRequest(.immediate) inputPanelNode.ensureFocused() } } else { self.performAnimateInAsOverlay(from: fromNode, transition: .animated(duration: 0.4, curve: .spring)) completion() } } private func performAnimateInAsOverlay(from fromNode: ASDisplayNode?, transition: ContainedViewLayoutTransition) { if let containerBackgroundNode = self.containerBackgroundNode, let fromNode = fromNode { let fromFrame = fromNode.view.convert(fromNode.bounds, to: self.view) containerBackgroundNode.supernode?.insertSubnode(fromNode, aboveSubnode: containerBackgroundNode) fromNode.frame = fromFrame fromNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak fromNode] _ in fromNode?.removeFromSupernode() }) transition.animateFrame(node: containerBackgroundNode, from: CGRect(origin: fromFrame.origin.offsetBy(dx: -8.0, dy: -8.0), size: CGSize(width: fromFrame.size.width + 8.0 * 2.0, height: fromFrame.size.height + 8.0 + 20.0))) containerBackgroundNode.layer.animateSpring(from: 0.99 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.5, initialVelocity: 1.0, damping: 10.0, removeOnCompletion: true, additive: false, completion: nil) if let containerNode = self.containerNode { containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) transition.animateFrame(node: containerNode, from: fromFrame) transition.animatePositionAdditive(node: self.backgroundNode, offset: CGPoint(x: 0.0, y: -containerNode.bounds.size.height)) transition.animatePositionAdditive(node: self.historyNodeContainer, offset: CGPoint(x: 0.0, y: -containerNode.bounds.size.height)) transition.updateFrame(node: fromNode, frame: CGRect(origin: containerNode.frame.origin, size: fromNode.frame.size)) } self.backgroundEffectNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) let inputPanelsOffset = self.bounds.size.height - self.inputPanelBackgroundNode.frame.minY transition.animateFrame(node: self.inputPanelBackgroundNode, from: self.inputPanelBackgroundNode.frame.offsetBy(dx: 0.0, dy: inputPanelsOffset)) transition.animateFrame(node: self.inputPanelBackgroundSeparatorNode, from: self.inputPanelBackgroundSeparatorNode.frame.offsetBy(dx: 0.0, dy: inputPanelsOffset)) if let inputPanelNode = self.inputPanelNode { transition.animateFrame(node: inputPanelNode, from: inputPanelNode.frame.offsetBy(dx: 0.0, dy: inputPanelsOffset)) } if let accessoryPanelNode = self.accessoryPanelNode { transition.animateFrame(node: accessoryPanelNode, from: accessoryPanelNode.frame.offsetBy(dx: 0.0, dy: inputPanelsOffset)) } if let _ = self.scrollContainerNode { containerBackgroundNode.layer.animateSpring(from: 0.99 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.8, initialVelocity: 100.0, damping: 80.0, removeOnCompletion: true, additive: false, completion: nil) self.containerNode?.layer.animateSpring(from: 0.99 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.8, initialVelocity: 100.0, damping: 80.0, removeOnCompletion: true, additive: false, completion: nil) } self.navigateButtons.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) } else { self.backgroundEffectNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) if let containerNode = self.containerNode { containerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) } } if let animateInAsOverlayCompletion = self.animateInAsOverlayCompletion { self.animateInAsOverlayCompletion = nil animateInAsOverlayCompletion() } } func animateDismissAsOverlay(completion: @escaping () -> Void) { if let containerNode = self.containerNode { self.dismissedAsOverlay = true self.dismissAsOverlayLayout = self.validLayout?.0 self.backgroundEffectNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.27, removeOnCompletion: false) self.containerBackgroundNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.27, removeOnCompletion: false) self.containerBackgroundNode?.layer.animateScale(from: 1.0, to: 0.6, duration: 0.29, removeOnCompletion: false) containerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.27, removeOnCompletion: false) containerNode.layer.animateScale(from: 1.0, to: 0.6, duration: 0.29, removeOnCompletion: false) self.navigateButtons.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false) self.dismissAsOverlayCompletion = completion self.scheduleLayoutTransitionRequest(.animated(duration: 0.4, curve: .spring)) self.dismissInput() } else { completion() } } func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { if let scrollContainerNode = self.scrollContainerNode, scrollView === scrollContainerNode.view { if abs(scrollView.contentOffset.y) > 50.0 { scrollView.isScrollEnabled = false self.dismissAsOverlay() } } } func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { if let scrollContainerNode = self.scrollContainerNode, scrollView === scrollContainerNode.view { if self.hapticFeedback == nil { self.hapticFeedback = HapticFeedback() } self.hapticFeedback?.prepareImpact() } } func scrollViewDidScroll(_ scrollView: UIScrollView) { if let scrollContainerNode = self.scrollContainerNode, scrollView === scrollContainerNode.view { let dismissStatus = abs(scrollView.contentOffset.y) > 50.0 if dismissStatus != self.scrollViewDismissStatus { self.scrollViewDismissStatus = dismissStatus if !self.dismissedAsOverlay { self.hapticFeedback?.impact() } } } } func displayMessageActionSheet(stableId: UInt32?, sheetActions: [ChatMessageContextMenuSheetAction]?, displayContextMenuController: (ContextMenuController, ASDisplayNode, CGRect)?) { self.controllerInteraction.contextHighlightedState = stableId.flatMap { ChatInterfaceHighlightedState(messageStableId: $0) } self.updateItemNodesContextHighlightedStates(animated: true, sheetActions: sheetActions, displayContextMenuController: displayContextMenuController) } private func updateItemNodesContextHighlightedStates(animated: Bool, sheetActions: [ChatMessageContextMenuSheetAction]?, displayContextMenuController: (ContextMenuController, ASDisplayNode, CGRect)?) { self.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView { itemNode.updateHighlightedState(animated: animated) } } let transition: ContainedViewLayoutTransition = .animated(duration: 0.35, curve: .spring) var animateIn = false self.historyNode.updateNodeHighlightsAnimated(animated) if self.messageActionSheetController?.1 != self.controllerInteraction.contextHighlightedState?.messageStableId { if let (controller, _) = self.messageActionSheetController { controller.controllerNode.animateOut(transition: transition, completion: { [weak controller] in controller?.dismiss() }) self.messageActionSheetController = nil self.messageActionSheetControllerAdditionalInset = nil } if let stableId = self.controllerInteraction.contextHighlightedState?.messageStableId { let contextMenuController = displayContextMenuController?.0 let controller = ChatMessageActionSheetController(theme: self.chatPresentationInterfaceState.theme, actions: sheetActions ?? [], dismissed: { [weak self, weak contextMenuController] in self?.displayMessageActionSheet(stableId: nil, sheetActions: nil, displayContextMenuController: nil) contextMenuController?.dismiss() }, associatedController: contextMenuController) self.messageActionSheetController = (controller, stableId) if let sheetActions = sheetActions, !sheetActions.isEmpty { self.controllerInteraction.presentGlobalOverlayController(controller, nil) } animateIn = true } } if let (layout, navigationBarHeight) = self.validLayout { let globalSelfOrigin = self.view.convert(CGPoint(), to: nil) let menuHeight = self.messageActionSheetController?.0.controllerNode.updateLayout(layout: layout, horizontalOrigin: globalSelfOrigin.x, transition: .immediate) if let stableId = self.messageActionSheetController?.1 { var resultItemNode: ListViewItemNode? var resultItemSubnode: ASDisplayNode? self.historyNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item { switch item.content { case let .message(message, _, _, _): if message.stableId == stableId { resultItemNode = itemNode } case let .group(messages): for (message, _, _, _) in messages { if message.stableId == stableId { resultItemNode = itemNode if let media = message.media.first { resultItemSubnode = itemNode.transitionNode(id: message.id, media: media)?.0 } break } } } } } if let resultItemNode = resultItemNode, let menuHeight = menuHeight { var resultItemFrame = resultItemNode.frame if let resultItemSubnode = resultItemSubnode { resultItemFrame = resultItemSubnode.view.convert(resultItemSubnode.bounds, to: resultItemNode.view.superview) } if resultItemFrame.size.height < self.historyNode.bounds.size.height - self.historyNode.insets.top - self.historyNode.insets.bottom { if resultItemFrame.minY < menuHeight { messageActionSheetControllerAdditionalInset = menuHeight - resultItemFrame.minY } } } } self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop in self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop) }) if animateIn, let controller = self.messageActionSheetController?.0 { controller.controllerNode.animateIn(transition: transition) } if let menuHeight = menuHeight { if let _ = self.controllerInteraction.contextHighlightedState?.messageStableId, let (menuController, node, frame) = displayContextMenuController { self.controllerInteraction.presentController(menuController, ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak self] in if let strongSelf = self { var bounds = strongSelf.bounds bounds.size.height -= menuHeight return (node, frame, strongSelf, bounds) } else { return nil } })) } } } } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let _ = self.messageActionSheetController { self.displayMessageActionSheet(stableId: nil, sheetActions: nil, displayContextMenuController: nil) return self.navigationBar?.view } return nil } @objc func topDimNodeTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { state in if case let .media(mode, expanded) = state.inputMode, expanded != nil { return (.media(mode: mode, expanded: nil), nil) } else { return (state.inputMode, nil) } } } } func scrollToTop() { if case let .media(_, maybeExpanded) = self.chatPresentationInterfaceState.inputMode, maybeExpanded != nil { self.interfaceInteraction?.updateInputModeAndDismissedButtonKeyboardMessageId { state in if case let .media(mode, expanded) = state.inputMode, expanded != nil { return (.media(mode: mode, expanded: expanded), nil) } else { return (state.inputMode, nil) } } } else { self.historyNode.scrollScreenToTop() } } @objc func backgroundEffectTap(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.dismissAsOverlay() } } func updateDropInteraction(isActive: Bool) { if isActive { if self.dropDimNode == nil { let dropDimNode = ASDisplayNode() dropDimNode.backgroundColor = self.chatPresentationInterfaceState.theme.chatList.backgroundColor.withAlphaComponent(0.35) self.dropDimNode = dropDimNode self.addSubnode(dropDimNode) if let (layout, _) = self.validLayout { dropDimNode.frame = CGRect(origin: CGPoint(), size: layout.size) dropDimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } } } else if let dropDimNode = self.dropDimNode { self.dropDimNode = nil dropDimNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak dropDimNode] _ in dropDimNode?.removeFromSupernode() }) } } private func updateLayoutInternal(transition: ContainedViewLayoutTransition) { if let (layout, navigationHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationHeight, transition: transition, listViewTransaction: { updateSizeAndInsets, additionalScrollDistance, scrollToTop in self.historyNode.updateLayout(transition: transition, updateSizeAndInsets: updateSizeAndInsets, additionalScrollDistance: additionalScrollDistance, scrollToTop: scrollToTop) }) } } private func panGestureBegan(location: CGPoint) { guard let derivedLayoutState = self.derivedLayoutState, let (validLayout, _) = self.validLayout else { return } if self.upperInputPositionBound != nil { return } if let inputHeight = validLayout.inputHeight { if !inputHeight.isZero { return } } let keyboardGestureBeginLocation = location let accessoryHeight = self.getWindowInputAccessoryHeight() if let inputHeight = derivedLayoutState.inputNodeHeight, !inputHeight.isZero, keyboardGestureBeginLocation.y < validLayout.size.height - inputHeight - accessoryHeight { var enableGesture = true if let view = self.view.hitTest(location, with: nil) { if doesViewTreeDisableInteractiveTransitionGestureRecognizer(view) { enableGesture = false } } if enableGesture { self.keyboardGestureBeginLocation = keyboardGestureBeginLocation self.keyboardGestureAccessoryHeight = accessoryHeight } } } private func panGestureMoved(location: CGPoint) { if let keyboardGestureBeginLocation = self.keyboardGestureBeginLocation { let currentLocation = location let deltaY = keyboardGestureBeginLocation.y - location.y if deltaY * deltaY >= 3.0 * 3.0 || self.upperInputPositionBound != nil { self.upperInputPositionBound = currentLocation.y + (self.keyboardGestureAccessoryHeight ?? 0.0) self.updateLayoutInternal(transition: .immediate) } } } private func panGestureEnded(location: CGPoint, velocity: CGPoint?) { guard let derivedLayoutState = self.derivedLayoutState, let (validLayout, _) = self.validLayout else { return } if self.keyboardGestureBeginLocation == nil { return } self.keyboardGestureBeginLocation = nil let currentLocation = location let accessoryHeight = (self.keyboardGestureAccessoryHeight ?? 0.0) var canDismiss = false if let upperInputPositionBound = self.upperInputPositionBound, upperInputPositionBound >= validLayout.size.height - accessoryHeight { canDismiss = true } else if let velocity = velocity, velocity.y > 100.0 { canDismiss = true } if canDismiss, let inputHeight = derivedLayoutState.inputNodeHeight, currentLocation.y + (self.keyboardGestureAccessoryHeight ?? 0.0) > validLayout.size.height - inputHeight { self.upperInputPositionBound = nil self.requestUpdateInterfaceState(.animated(duration: 0.25, curve: .spring), true, { state in if case .none = state.inputMode { return state } return state.updatedInputMode { _ in return .none } }) } else { self.upperInputPositionBound = nil self.updateLayoutInternal(transition: .animated(duration: 0.25, curve: .spring)) } } func cancelInteractiveKeyboardGestures() { self.panRecognizer?.isEnabled = false self.panRecognizer?.isEnabled = true if self.upperInputPositionBound != nil { self.updateLayoutInternal(transition: .animated(duration: 0.25, curve: .spring)) } if self.keyboardGestureBeginLocation != nil { self.keyboardGestureBeginLocation = nil } } }