mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00

Added ability to download music without streaming Added progress indicators for various blocking tasks Fixed image gallery swipe to dismiss after zooming Added online member count indication in supergroups Fixed contact statuses in contact search
1872 lines
101 KiB
Swift
1872 lines
101 KiB
Swift
import Foundation
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
import Display
|
|
import TelegramCore
|
|
|
|
private final class ChatControllerNodeView: UITracingLayerView, WindowInputAccessoryHeightProvider {
|
|
var inputAccessoryHeight: (() -> CGFloat)?
|
|
var hitTestImpl: ((CGPoint, UIEvent?) -> UIView?)?
|
|
|
|
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
|
|
|
|
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<Set<MessageId>?>(nil)
|
|
var selectedMessages: Set<MessageId>? {
|
|
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 displayPasteMenu: ([UIImage]) -> 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?) {
|
|
self.account = account
|
|
self.chatLocation = chatLocation
|
|
self.controllerInteraction = controllerInteraction
|
|
self.chatPresentationInterfaceState = chatPresentationInterfaceState
|
|
self.automaticMediaDownloadSettings = automaticMediaDownloadSettings
|
|
self.navigationBar = navigationBar
|
|
|
|
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.historyNodeContainer = ASDisplayNode()
|
|
self.historyNodeContainer.addSubnode(self.historyNode)
|
|
self.loadingNode = ChatLoadingNode(theme: chatPresentationInterfaceState.theme)
|
|
|
|
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.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))
|
|
}
|
|
}
|
|
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 {
|
|
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?.pasteImages = { [weak self] images in
|
|
self?.displayPasteMenu(images)
|
|
}
|
|
self.textInputPanelNode?.pasteData = { [weak self] data in
|
|
//self?.sendGifData(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
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
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?
|
|
|
|
var inputPanelSize: CGSize?
|
|
var immediatelyLayoutInputPanelAndAnimateAppearance = false
|
|
if let inputPanelNode = inputPanelForChatPresentationIntefaceState(self.chatPresentationInterfaceState, account: self.account, currentPanel: self.inputPanelNode, textInputPanelNode: self.textInputPanelNode, interfaceInteraction: self.interfaceInteraction) {
|
|
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 case .standard(true) = self.chatPresentationInterfaceState.mode {
|
|
self.inputPanelNode = nil
|
|
inputPanelSize = CGSize(width: layout.size.width, height: 0.0)
|
|
}
|
|
|
|
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))
|
|
|
|
self.loadingNode.updateLayout(size: contentBounds.size, insets: insets, transition: transition)
|
|
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
|
|
}
|
|
|
|
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])
|
|
}
|
|
}
|
|
|
|
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)
|
|
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, 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) {
|
|
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
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
if let peer = chatPresentationInterfaceState.renderedPeer?.peer, let restrictionText = peer.restrictionText {
|
|
if self.restrictedNode == nil {
|
|
let restrictedNode = ChatRecentActionsEmptyNode(theme: chatPresentationInterfaceState.theme)
|
|
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
|
|
} else if let restrictedNode = self.restrictedNode {
|
|
self.restrictedNode = nil
|
|
restrictedNode.removeFromSupernode()
|
|
self.historyNodeContainer.isHidden = false
|
|
self.navigateButtons.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
|
|
}
|
|
}
|
|
}
|