import Foundation import UIKit import AsyncDisplayKit import Display import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData private func fixGridScrolling(_ gridNode: GridNode) { var searchItemNode: GridItemNode? var nextItemNode: GridItemNode? gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { searchItemNode = itemNode } else if searchItemNode != nil && nextItemNode == nil, let itemNode = itemNode as? GridItemNode { nextItemNode = itemNode } } if let searchItemNode = searchItemNode { let contentInset = gridNode.scrollView.contentInset.top let itemFrame = gridNode.convert(searchItemNode.frame, to: gridNode.supernode) if itemFrame.contains(CGPoint(x: 0.0, y: contentInset)) { let transition = ContainedViewLayoutTransition.animated(duration: 0.3, curve: .easeInOut) var scrollIndex: Int? if itemFrame.minY + itemFrame.height * 0.6 < contentInset { for i in 0 ..< gridNode.items.count { if let _ = gridNode.items[i] as? StickerPaneTrendingListGridItem { scrollIndex = i break } else if let _ = gridNode.items[i] as? ChatMediaInputStickerGridItem { scrollIndex = i break } } } else { for i in 0 ..< gridNode.items.count { if let _ = gridNode.items[i] as? PaneSearchBarPlaceholderItem { scrollIndex = i break } } } if let scrollIndex = scrollIndex { gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: GridNodeScrollToItem(index: scrollIndex, position: .top(0.0), transition: transition, directionHint: .up, adjustForSection: true, adjustForTopInset: true), updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { _ in }) } } } } final class ChatMediaInputStickerPaneOpaqueState { let hasLower: Bool init(hasLower: Bool) { self.hasLower = hasLower } } final class ChatMediaInputStickerPane: ChatMediaInputPane { private var isExpanded: Bool? private var isPaneVisible = false let gridNode: GridNode private let paneDidScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void private let fixPaneScroll: (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void private var didScrollPreviousOffset: CGFloat? private var didScrollPreviousState: ChatMediaInputPaneScrollState? var beganScrolling: (() -> Void)? var endedScrolling: (() -> Void)? init(theme: PresentationTheme, strings: PresentationStrings, paneDidScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void, fixPaneScroll: @escaping (ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void) { self.gridNode = GridNode() self.paneDidScroll = paneDidScroll self.fixPaneScroll = fixPaneScroll super.init() self.addSubnode(self.gridNode) self.gridNode.presentationLayoutUpdated = { [weak self] layout, transition in if let strongSelf = self, let opaqueState = strongSelf.gridNode.opaqueState as? ChatMediaInputStickerPaneOpaqueState { var offset: CGFloat if opaqueState.hasLower { offset = -(layout.contentOffset.y + 41.0) } else { offset = -(layout.contentOffset.y + 41.0) offset = min(0.0, offset + 56.0) } var relativeChange: CGFloat = 0.0 if let didScrollPreviousOffset = strongSelf.didScrollPreviousOffset { relativeChange = offset - didScrollPreviousOffset } strongSelf.didScrollPreviousOffset = offset let state = ChatMediaInputPaneScrollState(absoluteOffset: offset, relativeChange: relativeChange) strongSelf.didScrollPreviousState = state if !transition.isAnimated { strongSelf.paneDidScroll(strongSelf, state, transition) } } } self.gridNode.scrollingInitiated = { [weak self] in self?.beganScrolling?() } self.gridNode.scrollingCompleted = { [weak self] in if let strongSelf = self { if let didScrollPreviousState = strongSelf.didScrollPreviousState { strongSelf.fixPaneScroll(strongSelf, didScrollPreviousState) } fixGridScrolling(strongSelf.gridNode) strongSelf.endedScrolling?() } } self.gridNode.setupNode = { [weak self] itemNode in guard let strongSelf = self else { return } if let itemNode = itemNode as? ChatMediaInputStickerGridItemNode { itemNode.updateIsPanelVisible(strongSelf.isPaneVisible) } else if let itemNode = itemNode as? StickerPaneTrendingListGridItemNode { itemNode.updateIsPanelVisible(strongSelf.isPaneVisible) } } self.gridNode.scrollView.alwaysBounceVertical = true } override func updateLayout(size: CGSize, topInset: CGFloat, bottomInset: CGFloat, isExpanded: Bool, isVisible: Bool, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { var changedIsExpanded = false if let previousIsExpanded = self.isExpanded { if previousIsExpanded != isExpanded { changedIsExpanded = true } } self.isExpanded = isExpanded let maxItemSize: CGSize if case .tablet = deviceMetrics.type, size.width > 480.0 { maxItemSize = CGSize(width: 90.0, height: 96.0) } else { maxItemSize = CGSize(width: 75.0, height: 80.0) } let sideInset: CGFloat = 2.0 var itemSide: CGFloat = floor((size.width - sideInset * 2.0) / 5.0) itemSide = min(itemSide, maxItemSize.width) let itemSize = CGSize(width: itemSide, height: max(itemSide, maxItemSize.height)) var scrollToItem: GridNodeScrollToItem? if changedIsExpanded { if isExpanded { var scrollIndex: Int? for i in 0 ..< self.gridNode.items.count { if let _ = self.gridNode.items[i] as? PaneSearchBarPlaceholderItem { scrollIndex = i break } } if let scrollIndex = scrollIndex { scrollToItem = GridNodeScrollToItem(index: scrollIndex, position: .top(0.0), transition: transition, directionHint: .down, adjustForSection: true, adjustForTopInset: true) } } else { var scrollIndex: Int? for i in 0 ..< self.gridNode.items.count { if let _ = self.gridNode.items[i] as? ChatMediaInputStickerGridItem { scrollIndex = i break } } if let scrollIndex = scrollIndex { scrollToItem = GridNodeScrollToItem(index: scrollIndex, position: .top(0.0), transition: transition, directionHint: .down, adjustForSection: true, adjustForTopInset: true) } } } self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: scrollToItem, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: size, insets: UIEdgeInsets(top: topInset, left: sideInset, bottom: bottomInset, right: sideInset), preloadSize: isVisible ? 300.0 : 0.0, type: .fixed(itemSize: itemSize, fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) transition.updateFrame(node: self.gridNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: size.height))) if self.isPaneVisible != isVisible { self.isPaneVisible = isVisible self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMediaInputStickerGridItemNode { itemNode.updateIsPanelVisible(isVisible) } else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode { itemNode.updateCanPlayMedia() } else if let itemNode = itemNode as? StickerPaneTrendingListGridItemNode { itemNode.updateIsPanelVisible(isVisible) } } } } func itemAt(point: CGPoint) -> (ASDisplayNode, StickerPackItem)? { if let itemNode = self.gridNode.itemNodeAtPoint(self.view.convert(point, to: self.gridNode.view)) as? ChatMediaInputStickerGridItemNode, let stickerPackItem = itemNode.stickerPackItem { return (itemNode, stickerPackItem) } return nil } }