import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences import MergeLists import AccountContext import StickerPackPreviewUI import PeerInfoUI import SettingsUI import ContextUI import GalleryUI import OverlayStatusController import PresentationDataUtils import ChatInterfaceState struct PeerSpecificPackData { let peer: Peer let info: StickerPackCollectionInfo let items: [ItemCollectionItem] } enum CanInstallPeerSpecificPack { case none case available(peer: Peer, dismissed: Bool) } struct ChatMediaInputPanelTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let scrollToItem: ListViewScrollToItem? } struct ChatMediaInputGridTransition { let deletions: [Int] let insertions: [GridNodeInsertItem] let updates: [GridNodeUpdateItem] let updateFirstIndexInSectionOffset: Int? let stationaryItems: GridNodeStationaryItems let scrollToItem: GridNodeScrollToItem? let updateOpaqueState: ChatMediaInputStickerPaneOpaqueState? let animated: Bool } func preparedChatMediaInputPanelEntryTransition(context: AccountContext, from fromEntries: [ChatMediaInputPanelEntry], to toEntries: [ChatMediaInputPanelEntry], inputNodeInteraction: ChatMediaInputNodeInteraction, scrollToItem: ListViewScrollToItem?) -> ChatMediaInputPanelTransition { let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) } let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(context: context, inputNodeInteraction: inputNodeInteraction), directionHint: nil) } return ChatMediaInputPanelTransition(deletions: deletions, insertions: insertions, updates: updates, scrollToItem: scrollToItem) } func preparedChatMediaInputGridEntryTransition(account: Account, view: ItemCollectionsView, from fromEntries: [ChatMediaInputGridEntry], to toEntries: [ChatMediaInputGridEntry], update: StickerPacksCollectionUpdate, interfaceInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingInteraction: TrendingPaneInteraction) -> ChatMediaInputGridTransition { var stationaryItems: GridNodeStationaryItems = .none var scrollToItem: GridNodeScrollToItem? var animated = false switch update { case .initial: for i in (0 ..< toEntries.count).reversed() { switch toEntries[i] { case .search, .peerSpecificSetup, .trending: break case .sticker: scrollToItem = GridNodeScrollToItem(index: i, position: .top(0.0), transition: .immediate, directionHint: .down, adjustForSection: true, adjustForTopInset: true) } } case .generic: animated = true case .scroll: var fromStableIds = Set() for entry in fromEntries { fromStableIds.insert(entry.stableId) } var index = 0 var indices = Set() for entry in toEntries { if fromStableIds.contains(entry.stableId) { indices.insert(index) } index += 1 } stationaryItems = .indices(indices) case let .navigate(index, collectionId): if let index = index.flatMap({ ChatMediaInputGridEntryIndex.collectionIndex($0) }) { for i in 0 ..< toEntries.count { if toEntries[i].index >= index { var directionHint: GridNodePreviousItemsTransitionDirectionHint = .up if !fromEntries.isEmpty && fromEntries[0].index < toEntries[i].index { directionHint = .down } scrollToItem = GridNodeScrollToItem(index: i, position: .top(0.0), transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true, adjustForTopInset: true) break } } } else if !toEntries.isEmpty { if let collectionId = collectionId { for i in 0 ..< toEntries.count { var indexMatches = false switch toEntries[i].index { case let .collectionIndex(collectionIndex): if collectionIndex.collectionId == collectionId { indexMatches = true } case .peerSpecificSetup: if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue { indexMatches = true } default: break } if indexMatches { var directionHint: GridNodePreviousItemsTransitionDirectionHint = .up if !fromEntries.isEmpty && fromEntries[0].index < toEntries[i].index { directionHint = .down } scrollToItem = GridNodeScrollToItem(index: i, position: .top(0.0), transition: .animated(duration: 0.45, curve: .spring), directionHint: directionHint, adjustForSection: true, adjustForTopInset: true) break } } } if scrollToItem == nil { scrollToItem = GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .animated(duration: 0.45, curve: .spring), directionHint: .up, adjustForSection: true, adjustForTopInset: true) } } } let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries) let deletions = deleteIndices let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, trendingInteraction: trendingInteraction), previousIndex: $0.2) } let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, interfaceInteraction: interfaceInteraction, inputNodeInteraction: inputNodeInteraction, trendingInteraction: trendingInteraction)) } var firstIndexInSectionOffset = 0 if !toEntries.isEmpty { switch toEntries[0].index { case .search, .peerSpecificSetup, .trending: break case let .collectionIndex(index): firstIndexInSectionOffset = Int(index.itemIndex.index) } } let opaqueState = ChatMediaInputStickerPaneOpaqueState(hasLower: view.lower != nil) return ChatMediaInputGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, updateOpaqueState: opaqueState, animated: animated) } func chatMediaInputPanelEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasUnreadTrending: Bool?, theme: PresentationTheme, hasGifs: Bool = true, hasSettings: Bool = true, expanded: Bool = false) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] if hasGifs { entries.append(.recentGifs(theme, expanded)) } if let hasUnreadTrending = hasUnreadTrending { entries.append(.trending(hasUnreadTrending, theme, expanded)) } if let savedStickers = savedStickers, !savedStickers.items.isEmpty { entries.append(.savedStickers(theme, expanded)) } var savedStickerIds = Set() if let savedStickers = savedStickers, !savedStickers.items.isEmpty { for i in 0 ..< savedStickers.items.count { if let item = savedStickers.items[i].contents as? SavedStickerItem { savedStickerIds.insert(item.file.fileId.id) } } } if let recentStickers = recentStickers, !recentStickers.items.isEmpty { var found = false for item in recentStickers.items { if let item = item.contents as? RecentMediaItem, let _ = item.media as? TelegramMediaFile, let mediaId = item.media.id { if !savedStickerIds.contains(mediaId.id) { found = true break } } } if found { entries.append(.recentPacks(theme, expanded)) } } if let peerSpecificPack = peerSpecificPack { entries.append(.peerSpecific(theme: theme, peer: peerSpecificPack.peer, expanded: expanded)) } else if case let .available(peer, false) = canInstallPeerSpecificPack { entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded)) } var index = 0 for (_, info, item) in view.collectionInfos { if let info = info as? StickerPackCollectionInfo, item != nil { entries.append(.stickerPack(index: index, info: info, topItem: item as? StickerPackItem, theme: theme, expanded: expanded)) index += 1 } } if peerSpecificPack == nil, case let .available(peer, true) = canInstallPeerSpecificPack { entries.append(.peerSpecific(theme: theme, peer: peer, expanded: expanded)) } if hasSettings { entries.append(.settings(theme, expanded)) } return entries } func chatMediaInputPanelGifModeEntries(theme: PresentationTheme, reactions: [String], expanded: Bool) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] entries.append(.stickersMode(theme, expanded)) entries.append(.savedGifs(theme, expanded)) entries.append(.trendingGifs(theme, expanded)) for reaction in reactions { entries.append(.gifEmotion(entries.count, theme, reaction, expanded)) } return entries } func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, hasSearch: Bool = true, hasAccessories: Bool = true, strings: PresentationStrings, theme: PresentationTheme) -> [ChatMediaInputGridEntry] { var entries: [ChatMediaInputGridEntry] = [] if hasSearch && view.lower == nil { entries.append(.search(theme: theme, strings: strings)) } var stickerPackInfos: [ItemCollectionId: StickerPackCollectionInfo] = [:] for (id, info, _) in view.collectionInfos { if let info = info as? StickerPackCollectionInfo { stickerPackInfos[id] = info } } if view.lower == nil { var savedStickerIds = Set() if let savedStickers = savedStickers, !savedStickers.items.isEmpty { let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_FavoriteStickers.uppercased(), shortName: "", thumbnail: nil, immediateThumbnailData: nil, hash: 0, count: 0) for i in 0 ..< savedStickers.items.count { if let item = savedStickers.items[i].contents as? SavedStickerItem { savedStickerIds.insert(item.file.fileId.id) let index = ItemCollectionItemIndex(index: Int32(i), id: item.file.fileId.id) let stickerItem = StickerPackItem(index: index, file: item.file, indexKeys: []) entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -3, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, canManagePeerSpecificPack: nil, maybeManageable: hasAccessories, theme: theme)) } } } if let recentStickers = recentStickers, !recentStickers.items.isEmpty { let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_FrequentlyUsed.uppercased(), shortName: "", thumbnail: nil, immediateThumbnailData: nil, hash: 0, count: 0) var addedCount = 0 for i in 0 ..< recentStickers.items.count { if addedCount >= 20 { break } if let item = recentStickers.items[i].contents as? RecentMediaItem, let file = item.media as? TelegramMediaFile, let mediaId = item.media.id { if !savedStickerIds.contains(mediaId.id) { let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id) let stickerItem = StickerPackItem(index: index, file: file, indexKeys: []) entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -2, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, canManagePeerSpecificPack: nil, maybeManageable: hasAccessories, theme: theme)) addedCount += 1 } } } } var canManagePeerSpecificPack = false if case .available(_, false) = canInstallPeerSpecificPack { canManagePeerSpecificPack = true } if peerSpecificPack == nil && canManagePeerSpecificPack { entries.append(.peerSpecificSetup(theme: theme, strings: strings, dismissed: false)) } if let peerSpecificPack = peerSpecificPack { for i in 0 ..< peerSpecificPack.items.count { let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_GroupStickers, shortName: "", thumbnail: nil, immediateThumbnailData: nil, hash: 0, count: 0) if let item = peerSpecificPack.items[i] as? StickerPackItem { let index = ItemCollectionItemIndex(index: Int32(i), id: item.file.fileId.id) let stickerItem = StickerPackItem(index: index, file: item.file, indexKeys: []) entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, canManagePeerSpecificPack: canManagePeerSpecificPack, maybeManageable: hasAccessories, theme: theme)) } } } } for entry in view.entries { if let item = entry.item as? StickerPackItem { entries.append(.sticker(index: entry.index, stickerItem: item, stickerPackInfo: stickerPackInfos[entry.index.collectionId], canManagePeerSpecificPack: false, maybeManageable: hasAccessories, theme: theme)) } } if view.higher == nil { if peerSpecificPack == nil, case .available(_, true) = canInstallPeerSpecificPack { entries.append(.peerSpecificSetup(theme: theme, strings: strings, dismissed: true)) } } return entries } enum StickerPacksCollectionPosition: Equatable { case initial case scroll(aroundIndex: ItemCollectionViewEntryIndex?) case navigate(index: ItemCollectionViewEntryIndex?, collectionId: ItemCollectionId?) static func ==(lhs: StickerPacksCollectionPosition, rhs: StickerPacksCollectionPosition) -> Bool { switch lhs { case .initial: if case .initial = rhs { return true } else { return false } case let .scroll(lhsAroundIndex): if case let .scroll(rhsAroundIndex) = rhs, lhsAroundIndex == rhsAroundIndex { return true } else { return false } case .navigate: return false } } } enum StickerPacksCollectionUpdate { case initial case generic case scroll case navigate(ItemCollectionViewEntryIndex?, ItemCollectionId?) } enum ChatMediaInputGifMode: Equatable { case recent case trending case emojiSearch(String) } final class ChatMediaInputNodeInteraction { let navigateToCollectionId: (ItemCollectionId) -> Void let navigateBackToStickers: () -> Void let setGifMode: (ChatMediaInputGifMode) -> Void let openSettings: () -> Void let toggleSearch: (Bool, ChatMediaInputSearchMode?, String) -> Void let openPeerSpecificSettings: () -> Void let dismissPeerSpecificSettings: () -> Void let clearRecentlyUsedStickers: () -> Void var stickerSettings: ChatInterfaceStickerSettings? var highlightedStickerItemCollectionId: ItemCollectionId? var highlightedItemCollectionId: ItemCollectionId? var highlightedGifMode: ChatMediaInputGifMode = .recent var previewedStickerPackItem: StickerPreviewPeekItem? var appearanceTransition: CGFloat = 1.0 var displayStickerPlaceholder = true var displayStickerPackManageControls = true init(navigateToCollectionId: @escaping (ItemCollectionId) -> Void, navigateBackToStickers: @escaping () -> Void, setGifMode: @escaping (ChatMediaInputGifMode) -> Void, openSettings: @escaping () -> Void, toggleSearch: @escaping (Bool, ChatMediaInputSearchMode?, String) -> Void, openPeerSpecificSettings: @escaping () -> Void, dismissPeerSpecificSettings: @escaping () -> Void, clearRecentlyUsedStickers: @escaping () -> Void) { self.navigateToCollectionId = navigateToCollectionId self.navigateBackToStickers = navigateBackToStickers self.setGifMode = setGifMode self.openSettings = openSettings self.toggleSearch = toggleSearch self.openPeerSpecificSettings = openPeerSpecificSettings self.dismissPeerSpecificSettings = dismissPeerSpecificSettings self.clearRecentlyUsedStickers = clearRecentlyUsedStickers } } func clipScrollPosition(_ position: StickerPacksCollectionPosition) -> StickerPacksCollectionPosition { switch position { case let .scroll(index): if let index = index, index.collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue || index.collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue { return .scroll(aroundIndex: nil) } default: break } return position } enum ChatMediaInputPaneType { case gifs case stickers } struct ChatMediaInputPaneArrangement { let panes: [ChatMediaInputPaneType] let currentIndex: Int let indexTransition: CGFloat func withIndexTransition(_ indexTransition: CGFloat) -> ChatMediaInputPaneArrangement { return ChatMediaInputPaneArrangement(panes: self.panes, currentIndex: currentIndex, indexTransition: indexTransition) } func withCurrentIndex(_ currentIndex: Int) -> ChatMediaInputPaneArrangement { return ChatMediaInputPaneArrangement(panes: self.panes, currentIndex: currentIndex, indexTransition: self.indexTransition) } } final class CollectionListContainerNode: ASDisplayNode { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { for subview in self.view.subviews { if let result = subview.hitTest(point.offsetBy(dx: -subview.frame.minX, dy: -subview.frame.minY), with: event) { return result } } return nil } } final class ChatMediaInputNode: ChatInputNode { private let context: AccountContext private let peerId: PeerId? private let controllerInteraction: ChatControllerInteraction private let gifPaneIsActiveUpdated: (Bool) -> Void private var inputNodeInteraction: ChatMediaInputNodeInteraction! private var trendingInteraction: TrendingPaneInteraction? private let collectionListPanel: ASDisplayNode private let collectionListSeparator: ASDisplayNode private let collectionListContainer: CollectionListContainerNode private weak var peekController: PeekController? private let disposable = MetaDisposable() private let listView: ListView private let gifListView: ListView private var searchContainerNode: PaneSearchContainerNode? private let searchContainerNodeLoadedDisposable = MetaDisposable() private let paneClippingContainer: ASDisplayNode private let panesBackgroundNode: ASDisplayNode private let stickerPane: ChatMediaInputStickerPane private var animatingStickerPaneOut = false private let gifPane: ChatMediaInputGifPane private var animatingGifPaneOut = false private var animatingTrendingPaneOut = false private var panRecognizer: UIPanGestureRecognizer? private let itemCollectionsViewPosition = Promise() private var currentStickerPacksCollectionPosition: StickerPacksCollectionPosition? private var currentView: ItemCollectionsView? private let dismissedPeerSpecificStickerPack = Promise() private var panelCollapseScrollToIndex: Int? private let panelExpandedPromise = ValuePromise(false) private var panelExpanded: Bool = false { didSet { self.panelExpandedPromise.set(self.panelExpanded) } } private var panelCollapseTimer: SwiftSignalKit.Timer? var requestDisableStickerAnimations: ((Bool) -> Void)? private var validLayout: (CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, CGFloat, ChatPresentationInterfaceState, DeviceMetrics, Bool)? private var paneArrangement: ChatMediaInputPaneArrangement private var initializedArrangement = false private var theme: PresentationTheme private var strings: PresentationStrings private var fontSize: PresentationFontSize private let themeAndStringsPromise: Promise<(PresentationTheme, PresentationStrings)> private let _ready = Promise() private var didSetReady = false override var ready: Signal { return self._ready.get() } init(context: AccountContext, peerId: PeerId?, chatLocation: ChatLocation?, controllerInteraction: ChatControllerInteraction, chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings, fontSize: PresentationFontSize, gifPaneIsActiveUpdated: @escaping (Bool) -> Void) { self.context = context self.peerId = peerId self.controllerInteraction = controllerInteraction self.theme = theme self.strings = strings self.fontSize = fontSize self.gifPaneIsActiveUpdated = gifPaneIsActiveUpdated self.paneClippingContainer = ASDisplayNode() self.paneClippingContainer.clipsToBounds = true self.panesBackgroundNode = ASDisplayNode() self.themeAndStringsPromise = Promise((theme, strings)) self.collectionListPanel = ASDisplayNode() self.collectionListPanel.clipsToBounds = true self.collectionListSeparator = ASDisplayNode() self.collectionListSeparator.isLayerBacked = true self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor self.collectionListContainer = CollectionListContainerNode() self.collectionListContainer.clipsToBounds = true self.listView = ListView() // self.listView.clipsToBounds = false self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) self.listView.scroller.panGestureRecognizer.cancelsTouchesInView = false self.listView.accessibilityPageScrolledString = { row, count in return strings.VoiceOver_ScrollStatus(row, count).string } self.gifListView = ListView() // self.gifListView.clipsToBounds = false self.gifListView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) self.gifListView.scroller.panGestureRecognizer.cancelsTouchesInView = false self.gifListView.accessibilityPageScrolledString = { row, count in return strings.VoiceOver_ScrollStatus(row, count).string } var paneDidScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState, ContainedViewLayoutTransition) -> Void)? var fixPaneScrollImpl: ((ChatMediaInputPane, ChatMediaInputPaneScrollState) -> Void)? var openGifContextMenuImpl: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? self.stickerPane = ChatMediaInputStickerPane(theme: theme, strings: strings, paneDidScroll: { pane, state, transition in paneDidScrollImpl?(pane, state, transition) }, fixPaneScroll: { pane, state in fixPaneScrollImpl?(pane, state) }) self.gifPane = ChatMediaInputGifPane(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, paneDidScroll: { pane, state, transition in paneDidScrollImpl?(pane, state, transition) }, fixPaneScroll: { pane, state in fixPaneScrollImpl?(pane, state) }, openGifContextMenu: { file, sourceNode, sourceRect, gesture, isSaved in openGifContextMenuImpl?(file, sourceNode, sourceRect, gesture, isSaved) }) var getItemIsPreviewedImpl: ((StickerPackItem) -> Bool)? self.paneArrangement = ChatMediaInputPaneArrangement(panes: [.gifs, .stickers], currentIndex: 1, indexTransition: 0.0) super.init() self.inputNodeInteraction = ChatMediaInputNodeInteraction(navigateToCollectionId: { [weak self] collectionId in if let strongSelf = self, let currentView = strongSelf.currentView, (collectionId != strongSelf.inputNodeInteraction.highlightedItemCollectionId || true) { var index: Int32 = 0 if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue { strongSelf.setCurrentPane(.gifs, transition: .animated(duration: 0.25, curve: .spring)) } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.trending.rawValue { strongSelf.controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen( context: strongSelf.context, sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) } else { return false } } )) } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.savedStickers.rawValue { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring), collectionIdHint: collectionId.namespace) strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring), collectionIdHint: collectionId.namespace) strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) } else if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) strongSelf.currentStickerPacksCollectionPosition = .navigate(index: nil, collectionId: collectionId) strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: nil, collectionId: collectionId))) } else { strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) for (id, _, _) in currentView.collectionInfos { if id.namespace == collectionId.namespace { if id == collectionId { let itemIndex = ItemCollectionViewEntryIndex.lowerBound(collectionIndex: index, collectionId: id) strongSelf.currentStickerPacksCollectionPosition = .navigate(index: itemIndex, collectionId: nil) strongSelf.itemCollectionsViewPosition.set(.single(.navigate(index: itemIndex, collectionId: nil))) break } index += 1 } } } } }, navigateBackToStickers: { [weak self] in guard let strongSelf = self else { return } strongSelf.setCurrentPane(.stickers, transition: .animated(duration: 0.25, curve: .spring)) }, setGifMode: { [weak self] mode in guard let strongSelf = self else { return } strongSelf.gifPane.setMode(mode: mode) strongSelf.inputNodeInteraction.highlightedGifMode = strongSelf.gifPane.mode strongSelf.gifListView.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { itemNode.updateIsHighlighted() } } }, openSettings: { [weak self] in if let strongSelf = self { let controller = installedStickerPacksController(context: context, mode: .modal) controller.navigationPresentation = .modal strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) } }, toggleSearch: { [weak self] value, searchMode, query in if let strongSelf = self { if let searchMode = searchMode, value { var searchContainerNode: PaneSearchContainerNode? if let current = strongSelf.searchContainerNode { searchContainerNode = current } else { searchContainerNode = PaneSearchContainerNode(context: strongSelf.context, theme: strongSelf.theme, strings: strongSelf.strings, controllerInteraction: strongSelf.controllerInteraction, inputNodeInteraction: strongSelf.inputNodeInteraction, mode: searchMode, trendingGifsPromise: strongSelf.gifPane.trendingPromise, cancel: { self?.searchContainerNode?.deactivate() self?.inputNodeInteraction.toggleSearch(false, nil, "") }) searchContainerNode?.openGifContextMenu = { file, sourceNode, sourceRect, gesture, isSaved in self?.openGifContextMenu(file: file, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) } strongSelf.searchContainerNode = searchContainerNode if !query.isEmpty { DispatchQueue.main.async { searchContainerNode?.updateQuery(query) } } } if let searchContainerNode = searchContainerNode { strongSelf.searchContainerNodeLoadedDisposable.set((searchContainerNode.ready |> deliverOnMainQueue).start(next: { if let strongSelf = self { strongSelf.controllerInteraction.updateInputMode { current in switch current { case let .media(mode, _): return .media(mode: mode, expanded: .search(searchMode)) default: return current } } } })) } } else { strongSelf.controllerInteraction.updateInputMode { current in switch current { case let .media(mode, _): return .media(mode: mode, expanded: nil) default: return current } } } } }, openPeerSpecificSettings: { [weak self] in guard let peerId = peerId, peerId.namespace == Namespaces.Peer.CloudChannel else { return } let _ = (context.account.postbox.transaction { transaction -> StickerPackCollectionInfo? in return (transaction.getPeerCachedData(peerId: peerId) as? CachedChannelData)?.stickerPack } |> deliverOnMainQueue).start(next: { info in guard let strongSelf = self else { return } strongSelf.controllerInteraction.presentController(groupStickerPackSetupController(context: context, peerId: peerId, currentPackInfo: info), ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }) }, dismissPeerSpecificSettings: { [weak self] in self?.dismissPeerSpecificPackSetup() }, clearRecentlyUsedStickers: { [weak self] in if let strongSelf = self { let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: strongSelf.theme, fontSize: strongSelf.fontSize)) var items: [ActionSheetItem] = [] items.append(ActionSheetButtonItem(title: strongSelf.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in actionSheet?.dismissAnimated() let _ = (context.account.postbox.transaction { transaction in clearRecentlyUsedStickers(transaction: transaction) }).start() })) actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [ ActionSheetButtonItem(title: strongSelf.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in actionSheet?.dismissAnimated() }) ])]) strongSelf.controllerInteraction.presentController(actionSheet, nil) } }) getItemIsPreviewedImpl = { [weak self] item in if let strongSelf = self { return strongSelf.inputNodeInteraction.previewedStickerPackItem == .pack(item) } return false } self.panesBackgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0) self.addSubnode(self.paneClippingContainer) self.paneClippingContainer.addSubnode(panesBackgroundNode) self.collectionListPanel.addSubnode(self.listView) self.collectionListPanel.addSubnode(self.gifListView) self.gifListView.isHidden = true self.collectionListContainer.addSubnode(self.collectionListPanel) self.collectionListContainer.addSubnode(self.collectionListSeparator) self.addSubnode(self.collectionListContainer) let itemCollectionsView = self.itemCollectionsViewPosition.get() |> distinctUntilChanged |> mapToSignal { position -> Signal<(ItemCollectionsView, StickerPacksCollectionUpdate), NoError> in switch position { case .initial: var firstTime = true return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: nil, count: 50) |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in let update: StickerPacksCollectionUpdate if firstTime { firstTime = false update = .initial } else { update = .generic } return (view, update) } case let .scroll(aroundIndex): var firstTime = true return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: aroundIndex, count: 300) |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in let update: StickerPacksCollectionUpdate if firstTime { firstTime = false update = .scroll } else { update = .generic } return (view, update) } case let .navigate(index, collectionId): var firstTime = true return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers], namespaces: [Namespaces.ItemCollection.CloudStickerPacks], aroundIndex: index, count: 300) |> map { view -> (ItemCollectionsView, StickerPacksCollectionUpdate) in let update: StickerPacksCollectionUpdate if firstTime { firstTime = false update = .navigate(index, collectionId) } else { update = .generic } return (view, update) } } } self.inputNodeInteraction.stickerSettings = self.controllerInteraction.stickerSettings let previousEntries = Atomic<([ChatMediaInputPanelEntry], [ChatMediaInputPanelEntry], [ChatMediaInputGridEntry])>(value: ([], [], [])) let inputNodeInteraction = self.inputNodeInteraction! let peerSpecificPack: Signal<(PeerSpecificPackData?, CanInstallPeerSpecificPack), NoError> if let peerId = peerId { self.dismissedPeerSpecificStickerPack.set( context.engine.peers.getOpaqueChatInterfaceState(peerId: peerId, threadId: nil) |> map { opaqueState -> Bool in guard let opaqueState = opaqueState else { return false } let interfaceState = ChatInterfaceState.parse(opaqueState) if interfaceState.messageActionsState.closedPeerSpecificPackSetup { return true } return false } ) peerSpecificPack = combineLatest(context.engine.peers.peerSpecificStickerPack(peerId: peerId), context.account.postbox.multiplePeersView([peerId]), self.dismissedPeerSpecificStickerPack.get()) |> map { packData, peersView, dismissedPeerSpecificPack -> (PeerSpecificPackData?, CanInstallPeerSpecificPack) in if let peer = peersView.peers[peerId] { var canInstall: CanInstallPeerSpecificPack = .none if packData.canSetup { canInstall = .available(peer: peer, dismissed: dismissedPeerSpecificPack) } if let (info, items) = packData.packInfo { return (PeerSpecificPackData(peer: peer, info: info, items: items), canInstall) } else { return (nil, canInstall) } } return (nil, .none) } } else { peerSpecificPack = .single((nil, .none)) } let trendingInteraction = TrendingPaneInteraction(installPack: { [weak self] info in guard let info = info as? StickerPackCollectionInfo else { return } let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false) |> mapToSignal { result -> Signal in switch result { case let .result(info, items, installed): if installed { return .complete() } else { return context.engine.stickers.addStickerPackInteractively(info: info, items: items) } case .fetching: break case .none: break } return .complete() } |> deliverOnMainQueue).start(completed: { guard let strongSelf = self else { return } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } strongSelf.controllerInteraction.presentController(OverlayStatusController(theme: presentationData.theme, type: .success), nil) }) }, openPack: { [weak self] info in guard let strongSelf = self, let info = info as? StickerPackCollectionInfo else { return } strongSelf.view.window?.endEditing(true) let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash) let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) } else { return false } }) strongSelf.controllerInteraction.presentController(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) }, getItemIsPreviewed: { item in return getItemIsPreviewedImpl?(item) ?? false }, openSearch: { }) self.trendingInteraction = trendingInteraction let preferencesViewKey: PostboxViewKey = .preferences(keys: Set([PreferencesKeys.appConfiguration])) let reactions: Signal<[String], NoError> = context.account.postbox.combinedView(keys: [preferencesViewKey]) |> map { views -> [String] in let defaultReactions: [String] = ["👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"] guard let view = views.views[preferencesViewKey] as? PreferencesView else { return defaultReactions } guard let appConfiguration = view.values[PreferencesKeys.appConfiguration] as? AppConfiguration else { return defaultReactions } guard let data = appConfiguration.data, let emojis = data["gif_search_emojies"] as? [String] else { return defaultReactions } return emojis } |> distinctUntilChanged let previousView = Atomic(value: nil) let transitionQueue = Queue() let transitions = combineLatest(queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get(), reactions, self.panelExpandedPromise.get()) |> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings, reactions, panelExpanded -> (ItemCollectionsView, ChatMediaInputPanelTransition, ChatMediaInputPanelTransition, Bool, ChatMediaInputGridTransition, Bool) in let (view, viewUpdate) = viewAndUpdate let previous = previousView.swap(view) var update = viewUpdate if previous === view { update = .generic } let (theme, strings) = themeAndStrings var savedStickers: OrderedItemListView? var recentStickers: OrderedItemListView? for orderedView in view.orderedItemListsViews { if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { recentStickers = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers { savedStickers = orderedView } } var installedPacks = Set() for info in view.collectionInfos { installedPacks.insert(info.0) } var hasUnreadTrending: Bool? for pack in trendingPacks { if hasUnreadTrending == nil { hasUnreadTrending = false } if pack.unread { hasUnreadTrending = true break } } let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, hasUnreadTrending: hasUnreadTrending, theme: theme, expanded: panelExpanded) let gifPaneEntries = chatMediaInputPanelGifModeEntries(theme: theme, reactions: reactions, expanded: panelExpanded) var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, strings: strings, theme: theme) if view.higher == nil { var hasTopSeparator = true if gridEntries.count == 1, case .search = gridEntries[0] { hasTopSeparator = false } var index = 0 for item in trendingPacks { if !installedPacks.contains(item.info.id) { gridEntries.append(.trending(TrendingPanePackEntry(index: index, info: item.info, theme: theme, strings: strings, topItems: item.topItems, installed: installedPacks.contains(item.info.id), unread: item.unread, topSeparator: hasTopSeparator))) hasTopSeparator = true index += 1 } } } let (previousPanelEntries, previousGifPaneEntries, previousGridEntries) = previousEntries.swap((panelEntries, gifPaneEntries, gridEntries)) return (view, preparedChatMediaInputPanelEntryTransition(context: context, from: previousPanelEntries, to: panelEntries, inputNodeInteraction: inputNodeInteraction, scrollToItem: nil), preparedChatMediaInputPanelEntryTransition(context: context, from: previousGifPaneEntries, to: gifPaneEntries, inputNodeInteraction: inputNodeInteraction, scrollToItem: nil), previousPanelEntries.isEmpty, preparedChatMediaInputGridEntryTransition(account: context.account, view: view, from: previousGridEntries, to: gridEntries, update: update, interfaceInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction, trendingInteraction: trendingInteraction), previousGridEntries.isEmpty) } self.disposable.set((transitions |> deliverOnMainQueue).start(next: { [weak self] (view, panelTransition, gifPaneTransition, panelFirstTime, gridTransition, gridFirstTime) in if let strongSelf = self { strongSelf.currentView = view strongSelf.enqueuePanelTransition(panelTransition, firstTime: panelFirstTime, thenGridTransition: gridTransition, gridFirstTime: gridFirstTime) strongSelf.enqueueGifPanelTransition(gifPaneTransition, firstTime: false) if !strongSelf.initializedArrangement { strongSelf.initializedArrangement = true let currentPane = strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] if currentPane != strongSelf.paneArrangement.panes[strongSelf.paneArrangement.currentIndex] { strongSelf.setCurrentPane(currentPane, transition: .immediate) } } } })) self.stickerPane.gridNode.visibleItemsUpdated = { [weak self] visibleItems in if let strongSelf = self { var topVisibleCollectionId: ItemCollectionId? if let topVisibleSection = visibleItems.topSectionVisible as? ChatMediaInputStickerGridSection { topVisibleCollectionId = topVisibleSection.collectionId } else if let topVisible = visibleItems.topVisible { if let item = topVisible.1 as? ChatMediaInputStickerGridItem { topVisibleCollectionId = item.index.collectionId } else if let _ = topVisible.1 as? StickerPanePeerSpecificSetupGridItem { topVisibleCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, id: 0) } } if let collectionId = topVisibleCollectionId { if strongSelf.inputNodeInteraction.highlightedItemCollectionId != collectionId && strongSelf.inputNodeInteraction.highlightedItemCollectionId?.namespace != ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue { strongSelf.setHighlightedItemCollectionId(collectionId) } } if let currentView = strongSelf.currentView, let (topIndex, topItem) = visibleItems.top, let (bottomIndex, bottomItem) = visibleItems.bottom { if topIndex <= 10 && currentView.lower != nil { if let topItem = topItem as? ChatMediaInputStickerGridItem { let position: StickerPacksCollectionPosition = clipScrollPosition(.scroll(aroundIndex: topItem.index)) if strongSelf.currentStickerPacksCollectionPosition != position { strongSelf.currentStickerPacksCollectionPosition = position strongSelf.itemCollectionsViewPosition.set(.single(position)) } } } else if bottomIndex >= visibleItems.count - 10 && currentView.higher != nil { var position: StickerPacksCollectionPosition? if let bottomItem = bottomItem as? ChatMediaInputStickerGridItem { position = clipScrollPosition(.scroll(aroundIndex: bottomItem.index)) } if let position = position, strongSelf.currentStickerPacksCollectionPosition != position { strongSelf.currentStickerPacksCollectionPosition = position strongSelf.itemCollectionsViewPosition.set(.single(position)) } } } } } self.currentStickerPacksCollectionPosition = .initial self.itemCollectionsViewPosition.set(.single(.initial)) self.stickerPane.inputNodeInteraction = self.inputNodeInteraction self.gifPane.inputNodeInteraction = self.inputNodeInteraction paneDidScrollImpl = { [weak self] pane, state, transition in self?.updatePaneDidScroll(pane: pane, state: state, transition: transition) } fixPaneScrollImpl = { [weak self] pane, state in self?.fixPaneScroll(pane: pane, state: state) } openGifContextMenuImpl = { [weak self] file, sourceNode, sourceRect, gesture, isSaved in self?.openGifContextMenu(file: file, sourceNode: sourceNode, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) } self.listView.beganInteractiveDragging = { [weak self] position in if let strongSelf = self, false { if !strongSelf.panelExpanded, let index = strongSelf.listView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) { strongSelf.panelCollapseScrollToIndex = index } strongSelf.updateIsExpanded(true) } } self.listView.didEndScrolling = { [weak self] in if let strongSelf = self, false { strongSelf.setupCollapseTimer() } } self.gifListView.beganInteractiveDragging = { [weak self] position in if let strongSelf = self, false { if !strongSelf.panelExpanded, let index = strongSelf.gifListView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) { strongSelf.panelCollapseScrollToIndex = index } strongSelf.updateIsExpanded(true) } } self.gifListView.didEndScrolling = { [weak self] in if let strongSelf = self, false { strongSelf.setupCollapseTimer() } } } deinit { self.disposable.dispose() self.searchContainerNodeLoadedDisposable.dispose() self.panelCollapseTimer?.invalidate() } private func updateIsExpanded(_ isExpanded: Bool) { self.panelCollapseTimer?.invalidate() self.panelExpanded = isExpanded self.updatePaneClippingContainer(size: self.paneClippingContainer.bounds.size, offset: self.currentCollectionListPanelOffset(), transition: .animated(duration: 0.3, curve: .spring)) } private func setupCollapseTimer() { self.panelCollapseTimer?.invalidate() let timer = SwiftSignalKit.Timer(timeout: 1.5, repeat: false, completion: { [weak self] in self?.updateIsExpanded(false) }, queue: Queue.mainQueue()) self.panelCollapseTimer = timer timer.start() } private func openGifContextMenu(file: MultiplexedVideoNodeFile, sourceNode: ASDisplayNode, sourceRect: CGRect, gesture: ContextGesture, isSaved: Bool) { let canSaveGif: Bool if file.file.media.fileId.namespace == Namespaces.Media.CloudFile { canSaveGif = true } else { canSaveGif = false } let _ = (self.context.account.postbox.transaction { transaction -> Bool in if !canSaveGif { return false } return isGifSaved(transaction: transaction, mediaId: file.file.media.fileId) } |> deliverOnMainQueue).start(next: { [weak self] isGifSaved in guard let strongSelf = self else { return } let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: PeerId(0), namespace: Namespaces.Message.Local, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [file.file.media], peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: []) let gallery = GalleryController(context: strongSelf.context, source: .standaloneMessage(message), streamSingleVideo: true, replaceRootController: { _, _ in }, baseNavigationController: nil) gallery.setHintWillBePresentedInPreviewingContext(true) var items: [ContextMenuItem] = [] items.append(.action(ContextMenuActionItem(text: strongSelf.strings.MediaPicker_Send, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Resend"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) if isSaved { let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, false) } else if let (collection, result) = file.contextResult { let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, false) } }))) if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true } if !isScheduledMessages { if case let .peer(peerId) = interfaceState.chatLocation { if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) if isSaved { let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, true, false) } else if let (collection, result) = file.contextResult { let _ = self?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode, sourceRect, true) } }))) } if isSaved { items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) let _ = self?.controllerInteraction.sendGif(file.file, sourceNode, sourceRect, false, true) }))) } } } } if isSaved || isGifSaved { items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_ContextMenuDelete, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.actionSheet.destructiveActionTextColor) }, action: { _, f in f(.dismissWithoutContent) guard let strongSelf = self else { return } let _ = removeSavedGif(postbox: strongSelf.context.account.postbox, mediaId: file.file.media.fileId).start() }))) } else if canSaveGif && !isGifSaved { items.append(.action(ContextMenuActionItem(text: strongSelf.strings.Preview_SaveGif, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Save"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.dismissWithoutContent) guard let strongSelf = self else { return } let _ = addSavedGif(postbox: strongSelf.context.account.postbox, fileReference: file.file).start() }))) } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: sourceNode, sourceRect: sourceRect)), items: .single(items), reactionItems: [], gesture: gesture) strongSelf.controllerInteraction.presentGlobalOverlayController(contextController, nil) }) } private func updateThemeAndStrings(chatWallpaper: TelegramWallpaper, theme: PresentationTheme, strings: PresentationStrings) { if self.theme !== theme || self.strings !== strings { self.theme = theme self.strings = strings self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor self.panesBackgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0) self.searchContainerNode?.updateThemeAndStrings(theme: theme, strings: strings) self.stickerPane.updateThemeAndStrings(theme: theme, strings: strings) self.gifPane.updateThemeAndStrings(theme: theme, strings: strings) self.themeAndStringsPromise.set(.single((theme, strings))) } } override func didLoad() { super.didLoad() self.view.disablesInteractiveTransitionGestureRecognizer = true let peekRecognizer = PeekControllerGestureRecognizer(contentAtPoint: { [weak self] point in if let strongSelf = self { let panes: [ASDisplayNode] if let searchContainerNode = strongSelf.searchContainerNode { panes = [] if let (itemNode, item) = searchContainerNode.itemAt(point: point.offsetBy(dx: -searchContainerNode.frame.minX, dy: -searchContainerNode.frame.minY)) { if let item = item as? StickerPreviewPeekItem { return strongSelf.context.account.postbox.transaction { transaction -> Bool in return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId) } |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { var menuItems: [ContextMenuItem] = [] if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true } if !isScheduledMessages { if case let .peer(peerId) = interfaceState.chatLocation { if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode, imageNode.bounds) } } f(.default) }))) } menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode, imageNode.bounds) } } f(.default) }))) } } } menuItems.append( .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() } else { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } })) ) menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) if let strongSelf = self { loop: for attribute in item.file.attributes { switch attribute { case let .Sticker(_, packReference, _): if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self { return strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect) } else { return false } }) strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) strongSelf.controllerInteraction.presentController(controller, nil) } break loop default: break } } } }))) return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: item, menu: menuItems)) } else { return nil } } } else if let _ = item as? FileMediaReference { return nil } } } else { panes = [strongSelf.gifPane, strongSelf.stickerPane] } let panelPoint = strongSelf.view.convert(point, to: strongSelf.collectionListPanel.view) if panelPoint.y < strongSelf.collectionListPanel.frame.maxY { return .single(nil) } for pane in panes { if pane.supernode != nil, pane.frame.contains(point) { if let pane = pane as? ChatMediaInputGifPane { if let (_, _, _) = pane.fileAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) { return nil } } else if pane is ChatMediaInputStickerPane || pane is ChatMediaInputTrendingPane { var itemNodeAndItem: (ASDisplayNode, StickerPackItem)? if let pane = pane as? ChatMediaInputStickerPane { itemNodeAndItem = pane.itemAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) } else if let pane = pane as? ChatMediaInputTrendingPane { itemNodeAndItem = pane.itemAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) } if let (itemNode, item) = itemNodeAndItem { return strongSelf.context.account.postbox.transaction { transaction -> Bool in return getIsStickerSaved(transaction: transaction, fileId: item.file.fileId) } |> deliverOnMainQueue |> map { isStarred -> (ASDisplayNode, PeekControllerContent)? in if let strongSelf = self { var menuItems: [ContextMenuItem] = [] if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = strongSelf.validLayout { var isScheduledMessages = false if case .scheduledMessages = interfaceState.subject { isScheduledMessages = true } if !isScheduledMessages { if case let .peer(peerId) = interfaceState.chatLocation { if peerId != self?.context.account.peerId && peerId.namespace != Namespaces.Peer.SecretChat { menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_SendSilently, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, animationNode, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), true, false, nil, false, imageNode, imageNode.bounds) } } f(.default) }))) } menuItems.append(.action(ContextMenuActionItem(text: strongSelf.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in if let strongSelf = self, let peekController = strongSelf.peekController { if let animationNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.animationNode { let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, animationNode, animationNode.bounds) } else if let imageNode = (peekController.contentNode as? StickerPreviewPeekContentNode)?.imageNode { let _ = strongSelf.controllerInteraction.sendSticker(.standalone(media: item.file), false, true, nil, false, imageNode, imageNode.bounds) } } f(.default) }))) } } } menuItems.append( .action(ContextMenuActionItem(text: isStarred ? strongSelf.strings.Stickers_RemoveFromFavorites : strongSelf.strings.Stickers_AddToFavorites, icon: { theme in generateTintedImage(image: isStarred ? UIImage(bundleImageName: "Chat/Context Menu/Unstar") : UIImage(bundleImageName: "Chat/Context Menu/Rate"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) if let strongSelf = self { if isStarred { let _ = removeSavedSticker(postbox: strongSelf.context.account.postbox, mediaId: item.file.fileId).start() } else { let _ = addSavedSticker(postbox: strongSelf.context.account.postbox, network: strongSelf.context.account.network, file: item.file).start() } } })) ) menuItems.append( .action(ContextMenuActionItem(text: strongSelf.strings.StickerPack_ViewPack, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Sticker"), color: theme.actionSheet.primaryTextColor) }, action: { _, f in f(.default) if let strongSelf = self { loop: for attribute in item.file.attributes { switch attribute { case let .Sticker(_, packReference, _): if let packReference = packReference { let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { file, sourceNode, sourceRect in if let strongSelf = self { return strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceNode, sourceRect) } else { return false } }) strongSelf.controllerInteraction.navigationController()?.view.window?.endEditing(true) strongSelf.controllerInteraction.presentController(controller, nil) } break loop default: break } } } })) ) return (itemNode, StickerPreviewPeekContent(account: strongSelf.context.account, item: .pack(item), menu: menuItems)) } else { return nil } } } } } } } return nil }, present: { [weak self] content, sourceNode in if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let controller = PeekController(presentationData: presentationData, content: content, sourceNode: { return sourceNode }) controller.visibilityUpdated = { [weak self] visible in self?.requestDisableStickerAnimations?(visible) self?.simulateUpdateLayout(isVisible: !visible) } strongSelf.peekController = controller strongSelf.controllerInteraction.presentGlobalOverlayController(controller, nil) return controller } return nil }, updateContent: { [weak self] content in if let strongSelf = self { var item: StickerPreviewPeekItem? if let content = content as? StickerPreviewPeekContent { item = content.item } strongSelf.updatePreviewingItem(item: item, animated: true) } }) self.view.addGestureRecognizer(peekRecognizer) let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) self.panRecognizer = panRecognizer self.view.addGestureRecognizer(panRecognizer) } private func setCurrentPane(_ pane: ChatMediaInputPaneType, transition: ContainedViewLayoutTransition, collectionIdHint: Int32? = nil) { if let index = self.paneArrangement.panes.firstIndex(of: pane), index != self.paneArrangement.currentIndex { let previousGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs self.paneArrangement = self.paneArrangement.withIndexTransition(0.0).withCurrentIndex(index) let updatedGifPanelWasActive = self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.validLayout { let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: transition, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) self.updateAppearanceTransition(transition: transition) } if updatedGifPanelWasActive != previousGifPanelWasActive { self.gifPaneIsActiveUpdated(updatedGifPanelWasActive) } switch pane { case .gifs: self.setHighlightedItemCollectionId(ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue, id: 0)) case .stickers: if let highlightedStickerCollectionId = self.inputNodeInteraction.highlightedStickerItemCollectionId { self.setHighlightedItemCollectionId(highlightedStickerCollectionId) } else if let collectionIdHint = collectionIdHint { self.setHighlightedItemCollectionId(ItemCollectionId(namespace: collectionIdHint, id: 0)) } } } else { if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.validLayout { let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) } } } private func setHighlightedItemCollectionId(_ collectionId: ItemCollectionId) { if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue { if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .gifs { self.inputNodeInteraction.highlightedItemCollectionId = collectionId } } else { self.inputNodeInteraction.highlightedStickerItemCollectionId = collectionId if self.paneArrangement.panes[self.paneArrangement.currentIndex] == .stickers { self.inputNodeInteraction.highlightedItemCollectionId = collectionId } } if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.recentGifs.rawValue && self.gifListView.isHidden { self.listView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: self.bounds.width, y: 0.0), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak self] completed in guard let strongSelf = self, completed else { return } strongSelf.listView.isHidden = true strongSelf.listView.layer.removeAllAnimations() }) self.gifListView.layer.removeAllAnimations() self.gifListView.isHidden = false self.gifListView.layer.animatePosition(from: CGPoint(x: -self.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } else if !self.gifListView.isHidden { self.gifListView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -self.bounds.width, y: 0.0), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak self] completed in guard let strongSelf = self, completed else { return } strongSelf.gifListView.isHidden = true strongSelf.gifListView.layer.removeAllAnimations() }) self.listView.layer.removeAllAnimations() self.listView.isHidden = false self.listView.layer.animatePosition(from: CGPoint(x: self.bounds.width, y: 0.0), to: CGPoint(), duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, additive: true) } var ensuredNodeVisible = false var firstVisibleCollectionId: ItemCollectionId? self.listView.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode { if firstVisibleCollectionId == nil { firstVisibleCollectionId = itemNode.currentCollectionId } itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { self.panelCollapseScrollToIndex = targetIndex self.updateIsExpanded(false) } else { self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { self.panelCollapseScrollToIndex = targetIndex self.updateIsExpanded(false) } else { self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputRecentGifsItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { self.panelCollapseScrollToIndex = targetIndex self.updateIsExpanded(false) } else { self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { self.panelCollapseScrollToIndex = targetIndex self.updateIsExpanded(false) } else { self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { if self.panelExpanded, let targetIndex = self.listView.indexOf(itemNode: itemNode) { self.panelCollapseScrollToIndex = targetIndex self.updateIsExpanded(false) } else { self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true } } } if let currentView = self.currentView, let firstVisibleCollectionId = firstVisibleCollectionId, !ensuredNodeVisible { let targetIndex = currentView.collectionInfos.firstIndex(where: { id, _, _ in return id == collectionId }) let firstVisibleIndex = currentView.collectionInfos.firstIndex(where: { id, _, _ in return id == firstVisibleCollectionId }) if let targetIndex = targetIndex, let firstVisibleIndex = firstVisibleIndex { let toRight = targetIndex > firstVisibleIndex if self.panelExpanded { self.panelCollapseScrollToIndex = targetIndex self.updateIsExpanded(false) } else { self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [], scrollToItem: ListViewScrollToItem(index: targetIndex, position: toRight ? .bottom(0.0) : .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: toRight ? .Down : .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil) } } } } private func currentCollectionListPanelOffset() -> CGFloat { let paneOffsets = self.paneArrangement.panes.map { pane -> CGFloat in switch pane { case .stickers: return self.stickerPane.collectionListPanelOffset case .gifs: return self.gifPane.collectionListPanelOffset } } let mainOffset = paneOffsets[self.paneArrangement.currentIndex] if self.paneArrangement.indexTransition.isZero { return mainOffset } else { var sideOffset: CGFloat? if self.paneArrangement.indexTransition < 0.0 { if self.paneArrangement.currentIndex != 0 { sideOffset = paneOffsets[self.paneArrangement.currentIndex - 1] } } else { if self.paneArrangement.currentIndex != paneOffsets.count - 1 { sideOffset = paneOffsets[self.paneArrangement.currentIndex + 1] } } if let sideOffset = sideOffset { let interpolator = CGFloat.interpolator() let value = interpolator(mainOffset, sideOffset, abs(self.paneArrangement.indexTransition)) as! CGFloat return value } else { return mainOffset } } } private func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { var value: CGFloat = 1.0 - abs(self.currentCollectionListPanelOffset() / 41.0) value = min(1.0, max(0.0, value)) self.inputNodeInteraction.appearanceTransition = max(0.1, value) transition.updateAlpha(node: self.listView, alpha: value) transition.updateAlpha(node: self.gifListView, alpha: value) self.listView.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMediaInputStickerPackItemNode { itemNode.updateAppearanceTransition(transition: transition) } else if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { itemNode.updateAppearanceTransition(transition: transition) } else if let itemNode = itemNode as? ChatMediaInputRecentGifsItemNode { itemNode.updateAppearanceTransition(transition: transition) } else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode { itemNode.updateAppearanceTransition(transition: transition) } else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode { itemNode.updateAppearanceTransition(transition: transition) } else if let itemNode = itemNode as? ChatMediaInputSettingsItemNode { itemNode.updateAppearanceTransition(transition: transition) } } self.gifListView.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { itemNode.updateAppearanceTransition(transition: transition) } } } func simulateUpdateLayout(isVisible: Bool) { if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, _) = self.validLayout { let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) } } override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) { var searchMode: ChatMediaInputSearchMode? if let (_, _, _, _, _, _, _, _, interfaceState, _, _) = self.validLayout, case let .media(_, maybeExpanded) = interfaceState.inputMode, let expanded = maybeExpanded, case let .search(mode) = expanded { searchMode = mode } let wasVisible = self.validLayout?.10 ?? false self.validLayout = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) if self.theme !== interfaceState.theme || self.strings !== interfaceState.strings { self.updateThemeAndStrings(chatWallpaper: interfaceState.chatWallpaper, theme: interfaceState.theme, strings: interfaceState.strings) } var displaySearch = false let separatorHeight = UIScreenPixel let panelHeight: CGFloat var isExpanded: Bool = false if case let .media(_, maybeExpanded) = interfaceState.inputMode, let expanded = maybeExpanded { isExpanded = true switch expanded { case .content: panelHeight = maximumHeight case let .search(mode): panelHeight = maximumHeight displaySearch = true searchMode = mode } self.stickerPane.collectionListPanelOffset = 0.0 self.gifPane.collectionListPanelOffset = 0.0 self.updateAppearanceTransition(transition: transition) } else { panelHeight = standardInputHeight } if displaySearch { if let searchContainerNode = self.searchContainerNode { let containerFrame = CGRect(origin: CGPoint(x: 0.0, y: -inputPanelHeight), size: CGSize(width: width, height: panelHeight + inputPanelHeight)) if searchContainerNode.supernode != nil { transition.updateFrame(node: searchContainerNode, frame: containerFrame) searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: transition) } else { self.searchContainerNode = searchContainerNode self.insertSubnode(searchContainerNode, belowSubnode: self.collectionListContainer) searchContainerNode.frame = containerFrame searchContainerNode.updateLayout(size: containerFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: .immediate) var placeholderNode: PaneSearchBarPlaceholderNode? let anchorTop = CGPoint(x: 0.0, y: 0.0) let anchorTopView: UIView = self.view if let searchMode = searchMode { switch searchMode { case .gif: placeholderNode = self.gifPane.visibleSearchPlaceholderNode case .sticker: self.stickerPane.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { placeholderNode = itemNode } } case .trending: break } } searchContainerNode.animateIn(from: placeholderNode, anchorTop: anchorTop, anhorTopView: anchorTopView, transition: transition, completion: { [weak self] in self?.gifPane.removeFromSupernode() }) } } } let contentVerticalOffset: CGFloat = displaySearch ? -(inputPanelHeight + 41.0) : 0.0 let collectionListPanelOffset = self.currentCollectionListPanelOffset() transition.updateFrame(node: self.collectionListContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: contentVerticalOffset), size: CGSize(width: width, height: max(0.0, 41.0 + UIScreenPixel)))) transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: CGSize(width: width, height: 41.0))) transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: 41.0 + collectionListPanelOffset), size: CGSize(width: width, height: separatorHeight))) self.listView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0 + 31.0 + 20.0, height: width) transition.updatePosition(node: self.listView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0)) self.gifListView.bounds = CGRect(x: 0.0, y: 0.0, width: 41.0 + 31.0 + 20.0, height: width) transition.updatePosition(node: self.gifListView, position: CGPoint(x: width / 2.0, y: (41.0 - collectionListPanelOffset) / 2.0)) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) let updateSizeAndInsets = ListViewUpdateSizeAndInsets(size: CGSize(width: 41.0 + 31.0 + 20.0, height: width), insets: UIEdgeInsets(top: 4.0 + leftInset, left: 0.0, bottom: 4.0 + rightInset, right: 0.0), duration: duration, curve: curve) self.listView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.gifListView.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: updateSizeAndInsets, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) var visiblePanes: [(ChatMediaInputPaneType, CGFloat)] = [] var paneIndex = 0 for pane in self.paneArrangement.panes { let paneOrigin = CGFloat(paneIndex - self.paneArrangement.currentIndex) * width - self.paneArrangement.indexTransition * width if paneOrigin.isLess(than: width) && CGFloat(0.0).isLess(than: (paneOrigin + width)) { visiblePanes.append((pane, paneOrigin)) } paneIndex += 1 } for (pane, paneOrigin) in visiblePanes { let paneFrame = CGRect(origin: CGPoint(x: paneOrigin + leftInset, y: 0.0), size: CGSize(width: width - leftInset - rightInset, height: panelHeight)) switch pane { case .gifs: if self.gifPane.supernode == nil { if !displaySearch { self.paneClippingContainer.addSubnode(self.gifPane) if self.searchContainerNode == nil { self.gifPane.frame = CGRect(origin: CGPoint(x: -width, y: 0.0), size: CGSize(width: width, height: panelHeight)) } } } if self.gifPane.frame != paneFrame { self.gifPane.layer.removeAnimation(forKey: "position") transition.updateFrame(node: self.gifPane, frame: paneFrame) } case .stickers: if self.stickerPane.supernode == nil { self.paneClippingContainer.addSubnode(self.stickerPane) self.stickerPane.frame = CGRect(origin: CGPoint(x: width, y: 0.0), size: CGSize(width: width, height: panelHeight)) } if self.stickerPane.frame != paneFrame { self.stickerPane.layer.removeAnimation(forKey: "position") transition.updateFrame(node: self.stickerPane, frame: paneFrame) } } } self.gifPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight), topInset: 41.0, bottomInset: bottomInset, isExpanded: isExpanded, isVisible: isVisible, deviceMetrics: deviceMetrics, transition: transition) self.trendingInteraction?.itemContext.canPlayMedia = isVisible self.stickerPane.updateLayout(size: CGSize(width: width - leftInset - rightInset, height: panelHeight), topInset: 41.0, bottomInset: bottomInset, isExpanded: isExpanded, isVisible: isVisible && visiblePanes.contains(where: { $0.0 == .stickers }), deviceMetrics: deviceMetrics, transition: transition) if self.gifPane.supernode != nil { if !visiblePanes.contains(where: { $0.0 == .gifs }) { if case .animated = transition { if !self.animatingGifPaneOut { self.animatingGifPaneOut = true var toLeft = false if let index = self.paneArrangement.panes.firstIndex(of: .gifs), index < self.paneArrangement.currentIndex { toLeft = true } transition.animatePosition(node: self.gifPane, to: CGPoint(x: (toLeft ? -width : width) + width / 2.0, y: self.gifPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in if let strongSelf = self, value { strongSelf.animatingGifPaneOut = false strongSelf.gifPane.removeFromSupernode() } }) } } else { self.animatingGifPaneOut = false self.gifPane.removeFromSupernode() } } } else { self.animatingGifPaneOut = false } if self.stickerPane.supernode != nil { if !visiblePanes.contains(where: { $0.0 == .stickers }) { if case .animated = transition { if !self.animatingStickerPaneOut { self.animatingStickerPaneOut = true var toLeft = false if let index = self.paneArrangement.panes.firstIndex(of: .stickers), index < self.paneArrangement.currentIndex { toLeft = true } transition.animatePosition(node: self.stickerPane, to: CGPoint(x: (toLeft ? -width : width) + width / 2.0, y: self.stickerPane.layer.position.y), removeOnCompletion: false, completion: { [weak self] value in if let strongSelf = self, value { strongSelf.animatingStickerPaneOut = false strongSelf.stickerPane.removeFromSupernode() } }) } } else { self.animatingStickerPaneOut = false self.stickerPane.removeFromSupernode() } } } else { self.animatingStickerPaneOut = false } if !displaySearch, let searchContainerNode = self.searchContainerNode { self.searchContainerNode = nil self.searchContainerNodeLoadedDisposable.set(nil) var paneIsEmpty = false var placeholderNode: PaneSearchBarPlaceholderNode? if let searchMode = searchMode { switch searchMode { case .gif: placeholderNode = self.gifPane.visibleSearchPlaceholderNode paneIsEmpty = placeholderNode != nil case .sticker: paneIsEmpty = true self.stickerPane.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? PaneSearchBarPlaceholderNode { placeholderNode = itemNode } if let _ = itemNode as? ChatMediaInputStickerGridItemNode { paneIsEmpty = false } } case .trending: break } } if let placeholderNode = placeholderNode { placeholderNode.isHidden = false searchContainerNode.animateOut(to: placeholderNode, animateOutSearchBar: !paneIsEmpty, transition: transition, completion: { [weak searchContainerNode] in searchContainerNode?.removeFromSupernode() }) } else { transition.updateAlpha(node: searchContainerNode, alpha: 0.0, completion: { [weak searchContainerNode] _ in searchContainerNode?.removeFromSupernode() }) } } if let panRecognizer = self.panRecognizer, panRecognizer.isEnabled != !displaySearch { panRecognizer.isEnabled = !displaySearch } if isVisible && !wasVisible { transition.updateFrame(node: self.gifPane, frame: self.gifPane.frame, force: true, completion: { [weak self] _ in self?.gifPane.initializeIfNeeded() }) } self.updatePaneClippingContainer(size: CGSize(width: width, height: panelHeight), offset: self.currentCollectionListPanelOffset(), transition: transition) transition.updateFrame(node: self.panesBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: panelHeight))) return (standardInputHeight, max(0.0, panelHeight - standardInputHeight)) } private func enqueuePanelTransition(_ transition: ChatMediaInputPanelTransition, firstTime: Bool, thenGridTransition gridTransition: ChatMediaInputGridTransition, gridFirstTime: Bool) { var options = ListViewDeleteAndInsertOptions() if firstTime { options.insert(.Synchronous) options.insert(.LowLatency) } else { options.insert(.AnimateInsertion) } var scrollToItem: ListViewScrollToItem? if let targetIndex = self.panelCollapseScrollToIndex { var position: ListViewScrollPosition if self.panelExpanded { position = .center(.top) } else { position = .top(self.listView.frame.height / 2.0 + 96.0) } scrollToItem = ListViewScrollToItem(index: targetIndex, position: position, animated: true, curve: .Default(duration: nil), directionHint: .Down) self.panelCollapseScrollToIndex = nil } self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateOpaqueState: nil, completion: { [weak self] _ in if let strongSelf = self { strongSelf.enqueueGridTransition(gridTransition, firstTime: gridFirstTime) if !strongSelf.didSetReady { strongSelf.didSetReady = true strongSelf._ready.set(.single(Void())) } } }) } private func enqueueGifPanelTransition(_ transition: ChatMediaInputPanelTransition, firstTime: Bool) { var options = ListViewDeleteAndInsertOptions() options.insert(.Synchronous) options.insert(.LowLatency) self.gifListView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateOpaqueState: nil, completion: { _ in }) } private func enqueueGridTransition(_ transition: ChatMediaInputGridTransition, firstTime: Bool) { var itemTransition: ContainedViewLayoutTransition = .immediate if transition.animated { itemTransition = .animated(duration: 0.3, curve: .spring) } self.stickerPane.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: transition.stationaryItems, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset, updateOpaqueState: transition.updateOpaqueState), completion: { _ in }) } private func updatePreviewingItem(item: StickerPreviewPeekItem?, animated: Bool) { if self.inputNodeInteraction.previewedStickerPackItem != item { self.inputNodeInteraction.previewedStickerPackItem = item self.stickerPane.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? ChatMediaInputStickerGridItemNode { itemNode.updatePreviewing(animated: animated) } } self.searchContainerNode?.contentNode.updatePreviewing(animated: animated) } } @objc func panGesture(_ recognizer: UIPanGestureRecognizer) { switch recognizer.state { case .began: if self.animatingGifPaneOut { self.animatingGifPaneOut = false self.gifPane.removeFromSupernode() } self.gifPane.layer.removeAllAnimations() self.stickerPane.layer.removeAllAnimations() if self.animatingStickerPaneOut { self.animatingStickerPaneOut = false self.stickerPane.removeFromSupernode() } case .changed: if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.validLayout { let translationX = -recognizer.translation(in: self.view).x var indexTransition = translationX / width if self.paneArrangement.currentIndex == 0 { indexTransition = max(0.0, indexTransition) } else if self.paneArrangement.currentIndex == self.paneArrangement.panes.count - 1 { indexTransition = min(0.0, indexTransition) } self.paneArrangement = self.paneArrangement.withIndexTransition(indexTransition) let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) } case .ended: if let (width, _, _, _, _, _, _, _, _, _, _) = self.validLayout { var updatedIndex = self.paneArrangement.currentIndex if abs(self.paneArrangement.indexTransition * width) > 30.0 { if self.paneArrangement.indexTransition < 0.0 { updatedIndex = max(0, self.paneArrangement.currentIndex - 1) } else { updatedIndex = min(self.paneArrangement.panes.count - 1, self.paneArrangement.currentIndex + 1) } } self.paneArrangement = self.paneArrangement.withIndexTransition(0.0) self.setCurrentPane(self.paneArrangement.panes[updatedIndex], transition: .animated(duration: 0.25, curve: .spring)) } case .cancelled: if let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible) = self.validLayout { self.paneArrangement = self.paneArrangement.withIndexTransition(0.0) let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .animated(duration: 0.25, curve: .spring), interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible) } default: break } } private var isExpanded: Bool { var isExpanded: Bool = false if let validLayout = self.validLayout, case let .media(_, maybeExpanded) = validLayout.8.inputMode, maybeExpanded != nil { isExpanded = true } return isExpanded } private func updatePaneDidScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState, transition: ContainedViewLayoutTransition) { if self.isExpanded { pane.collectionListPanelOffset = 0.0 } else { var computedAbsoluteOffset: CGFloat if let absoluteOffset = state.absoluteOffset, absoluteOffset >= 0.0 { computedAbsoluteOffset = 0.0 } else { computedAbsoluteOffset = pane.collectionListPanelOffset + state.relativeChange } computedAbsoluteOffset = max(-41.0, min(computedAbsoluteOffset, 0.0)) pane.collectionListPanelOffset = computedAbsoluteOffset if transition.isAnimated { if pane.collectionListPanelOffset < -41.0 / 2.0 { pane.collectionListPanelOffset = -41.0 } else { pane.collectionListPanelOffset = 0.0 } } } var collectionListPanelOffset = self.currentCollectionListPanelOffset() if self.panelExpanded { collectionListPanelOffset = 0.0 } let listPanelOffset = collectionListPanelOffset * 2.0 self.updateAppearanceTransition(transition: transition) transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: listPanelOffset), size: self.collectionListPanel.bounds.size)) transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - listPanelOffset) / 2.0)) transition.updatePosition(node: self.gifListView, position: CGPoint(x: self.gifListView.position.x, y: (41.0 - listPanelOffset) / 2.0)) self.updatePaneClippingContainer(size: self.paneClippingContainer.bounds.size, offset: collectionListPanelOffset, transition: transition) } private func updatePaneClippingContainer(size: CGSize, offset: CGFloat, transition: ContainedViewLayoutTransition) { var offset = offset var additionalOffset: CGFloat = 0.0 if self.panelExpanded { offset = 0.0 additionalOffset = 31.0 } transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: offset + 41.0 + additionalOffset), size: self.collectionListSeparator.bounds.size)) transition.updateFrame(node: self.paneClippingContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: offset + 41.0 + additionalOffset), size: size)) transition.updateSublayerTransformOffset(layer: self.paneClippingContainer.layer, offset: CGPoint(x: 0.0, y: -offset - 41.0 - additionalOffset)) } private func fixPaneScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState) { if let absoluteOffset = state.absoluteOffset, absoluteOffset >= 0.0 { pane.collectionListPanelOffset = 0.0 } else { if pane.collectionListPanelOffset < -41.0 / 2.0 { pane.collectionListPanelOffset = -41.0 } else { pane.collectionListPanelOffset = 0.0 } } var collectionListPanelOffset = self.currentCollectionListPanelOffset() if self.panelExpanded { collectionListPanelOffset = 0.0 } let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .spring) self.updateAppearanceTransition(transition: transition) transition.updateFrame(node: self.collectionListPanel, frame: CGRect(origin: CGPoint(x: 0.0, y: collectionListPanelOffset), size: self.collectionListPanel.bounds.size)) transition.updatePosition(node: self.listView, position: CGPoint(x: self.listView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) transition.updatePosition(node: self.gifListView, position: CGPoint(x: self.gifListView.position.x, y: (41.0 - collectionListPanelOffset) / 2.0)) self.updatePaneClippingContainer(size: self.paneClippingContainer.bounds.size, offset: collectionListPanelOffset, transition: transition) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let searchContainerNode = self.searchContainerNode { if let result = searchContainerNode.hitTest(point.offsetBy(dx: -searchContainerNode.frame.minX, dy: -searchContainerNode.frame.minY), with: event) { return result } } let result = super.hitTest(point, with: event) return result } static func setupPanelIconInsets(item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> UIEdgeInsets { var insets = UIEdgeInsets() if previousItem != nil { insets.top += 3.0 } if nextItem != nil { insets.bottom += 3.0 } return insets } private func dismissPeerSpecificPackSetup() { guard let peerId = self.peerId else { return } self.dismissedPeerSpecificStickerPack.set(.single(true)) let _ = ChatInterfaceState.update(engine: self.context.engine, peerId: peerId, threadId: nil, { current in return current.withUpdatedMessageActionsState({ value in var value = value value.closedPeerSpecificPackSetup = true return value }) }).start() } } private final class ContextControllerContentSourceImpl: ContextControllerContentSource { let controller: ViewController weak var sourceNode: ASDisplayNode? let sourceRect: CGRect let navigationController: NavigationController? = nil let passthroughTouches: Bool = false init(controller: ViewController, sourceNode: ASDisplayNode?, sourceRect: CGRect) { self.controller = controller self.sourceNode = sourceNode self.sourceRect = sourceRect } func transitionInfo() -> ContextControllerTakeControllerInfo? { let sourceNode = self.sourceNode let sourceRect = self.sourceRect return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in if let sourceNode = sourceNode { return (sourceNode, sourceRect) } else { return nil } }) } func animatedIn() { if let controller = self.controller as? GalleryController { controller.viewDidAppear(false) } } }