import Foundation import UIKit import Display import AsyncDisplayKit import Postbox import TelegramCore import SwiftSignalKit import TelegramPresentationData import TelegramUIPreferences import TelegramNotices import MergeLists import AccountContext import StickerPackPreviewUI import PeerInfoUI import SettingsUI import ContextUI import GalleryUI import OverlayStatusController import PresentationDataUtils import ChatInterfaceState import ChatPresentationInterfaceState import UndoUI import PremiumUI struct PeerSpecificPackData { let peer: Peer let info: StickerPackCollectionInfo let items: [ItemCollectionItem] } enum CanInstallPeerSpecificPack { case none case available(peer: Peer, dismissed: Bool) } final class ChatMediaInputPanelOpaqueState { let entries: [ChatMediaInputPanelEntry] init(entries: [ChatMediaInputPanelEntry]) { self.entries = entries } } struct ChatMediaInputPanelTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] let scrollToItem: ListViewScrollToItem? let updateOpaqueState: ChatMediaInputPanelOpaqueState? } 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, updateOpaqueState: ChatMediaInputPanelOpaqueState(entries: toEntries)) } 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 .trendingList, .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, .trendingList, .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?, temporaryPackOrder: [ItemCollectionId]? = nil, trendingIsDismissed: Bool = false, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, theme: PresentationTheme, strings: PresentationStrings, premiumStickers: OrderedItemListView? = nil, hasGifs: Bool = true, hasSettings: Bool = true, expanded: Bool = false, reorderable: Bool = false) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] if hasGifs { entries.append(.recentGifs(theme, strings, expanded)) } if trendingIsDismissed { entries.append(.trending(true, theme, strings, expanded)) } if let savedStickers = savedStickers, !savedStickers.items.isEmpty { entries.append(.savedStickers(theme, strings, 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.get(SavedStickerItem.self) { 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.get(RecentMediaItem.self), let mediaId = item.media.id { if !savedStickerIds.contains(mediaId.id) { found = true break } } } if found { entries.append(.recentPacks(theme, strings, 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)) } if let premiumStickers = premiumStickers, !premiumStickers.items.isEmpty { entries.append(.premium(theme, strings, expanded)) } var index = 0 var sortedPacks: [(ItemCollectionId, StickerPackCollectionInfo, StickerPackItem?)] = [] for (id, info, item) in view.collectionInfos { if let info = info as? StickerPackCollectionInfo, let item = item as? StickerPackItem { sortedPacks.append((id, info, item)) } } if let temporaryPackOrder = temporaryPackOrder { var packDict: [ItemCollectionId: Int] = [:] for i in 0 ..< sortedPacks.count { packDict[sortedPacks[i].0] = i } var tempSortedPacks: [(ItemCollectionId, StickerPackCollectionInfo, StickerPackItem?)] = [] var processedPacks = Set() for id in temporaryPackOrder { if let index = packDict[id] { tempSortedPacks.append(sortedPacks[index]) processedPacks.insert(id) } } let restPacks = sortedPacks.filter { !processedPacks.contains($0.0) } sortedPacks = restPacks + tempSortedPacks } for (_, info, topItem) in sortedPacks { entries.append(.stickerPack(index: index, info: info, topItem: topItem, theme: theme, expanded: expanded, reorderable: reorderable)) 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, strings, expanded)) } return entries } func chatMediaInputPanelGifModeEntries(theme: PresentationTheme, strings: PresentationStrings, reactions: [String], animatedEmojiStickers: [String: [StickerPackItem]], expanded: Bool) -> [ChatMediaInputPanelEntry] { var entries: [ChatMediaInputPanelEntry] = [] entries.append(.stickersMode(theme, strings, expanded)) entries.append(.savedGifs(theme, strings, expanded)) entries.append(.trendingGifs(theme, strings, expanded)) for reaction in reactions { entries.append(.gifEmotion(entries.count, theme, strings, reaction, animatedEmojiStickers[reaction]?.first?.file, expanded)) } return entries } func chatMediaInputGridEntries(view: ItemCollectionsView, savedStickers: OrderedItemListView?, recentStickers: OrderedItemListView?, peerSpecificPack: PeerSpecificPackData?, canInstallPeerSpecificPack: CanInstallPeerSpecificPack, trendingPacks: [FeaturedStickerPackItem], installedPacks: Set, premiumStickers: OrderedItemListView? = nil, trendingIsDismissed: Bool = false, hasSearch: Bool = true, hasAccessories: Bool = true, strings: PresentationStrings, theme: PresentationTheme, hasPremium: Bool, isPremiumDisabled: Bool, trendingIsPremium: Bool) -> [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.get(SavedStickerItem.self) { 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: []) if isPremiumDisabled && item.file.isPremiumSticker { } else { entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -3, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, canManagePeerSpecificPack: nil, maybeManageable: hasAccessories, theme: theme, isLocked: stickerItem.file.isPremiumSticker && !hasPremium)) } } } } let filteredTrending = trendingPacks.filter { !installedPacks.contains($0.info.id) } if !trendingIsDismissed && !filteredTrending.isEmpty { entries.append(.trendingList(theme: theme, strings: strings, packs: filteredTrending, isPremium: false)) } 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.get(RecentMediaItem.self), let mediaId = item.media.id { let file = item.media if !savedStickerIds.contains(mediaId.id) { let index = ItemCollectionItemIndex(index: Int32(i), id: mediaId.id) let stickerItem = StickerPackItem(index: index, file: file, indexKeys: []) if isPremiumDisabled && file.isPremiumSticker { } else { entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -2, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, canManagePeerSpecificPack: nil, maybeManageable: hasAccessories, theme: theme, isLocked: stickerItem.file.isPremiumSticker && !hasPremium)) 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 { 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) for i in 0 ..< peerSpecificPack.items.count { 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: []) if isPremiumDisabled && item.file.isPremiumSticker { } else { entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, canManagePeerSpecificPack: canManagePeerSpecificPack, maybeManageable: hasAccessories, theme: theme, isLocked: stickerItem.file.isPremiumSticker && !hasPremium)) } } } } if let premiumStickers = premiumStickers, !premiumStickers.items.isEmpty && hasPremium && !isPremiumDisabled { let packInfo = StickerPackCollectionInfo(id: ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.premium.rawValue, id: 0), flags: [], accessHash: 0, title: strings.Stickers_PremiumStickers.uppercased(), shortName: "", thumbnail: nil, immediateThumbnailData: nil, hash: 0, count: 0) for i in 0 ..< premiumStickers.items.count { if let item = premiumStickers.items[i].contents.get(RecentMediaItem.self) { let file = item.media let index = ItemCollectionItemIndex(index: Int32(i), id: file.fileId.id) let stickerItem = StickerPackItem(index: index, file: file, indexKeys: []) entries.append(.sticker(index: ItemCollectionViewEntryIndex(collectionIndex: -1, collectionId: packInfo.id, itemIndex: index), stickerItem: stickerItem, stickerPackInfo: packInfo, canManagePeerSpecificPack: nil, maybeManageable: hasAccessories, theme: theme, isLocked: stickerItem.file.isPremiumSticker && !hasPremium)) } } } } for entry in view.entries { if let item = entry.item as? StickerPackItem { if isPremiumDisabled && item.file.isPremiumSticker { } else { entries.append(.sticker(index: entry.index, stickerItem: item, stickerPackInfo: stickerPackInfos[entry.index.collectionId], canManagePeerSpecificPack: false, maybeManageable: hasAccessories, theme: theme, isLocked: item.file.isPremiumSticker && !hasPremium)) } } } 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 openTrending: (ItemCollectionId?) -> Void let dismissTrendingPacks: ([ItemCollectionId]) -> 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, openTrending: @escaping (ItemCollectionId?) -> Void, dismissTrendingPacks: @escaping ([ItemCollectionId]) -> 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.openTrending = openTrending self.dismissTrendingPacks = dismissTrendingPacks 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 scrollingStickerPacksListPromise = ValuePromise(false) private var scrollingStickersGridPromise = ValuePromise(false) private var previewingStickersPromise = ValuePromise(false) private var choosingSticker: Signal { return combineLatest(self.scrollingStickerPacksListPromise.get(), self.scrollingStickersGridPromise.get(), self.previewingStickersPromise.get()) |> map { scrollingStickerPacksList, scrollingStickersGrid, previewingStickers -> Bool in return scrollingStickerPacksList || scrollingStickersGrid || previewingStickers } |> distinctUntilChanged } private var choosingStickerDisposable: Disposable? private var panelFocusScrollToIndex: Int? private var panelFocusInitialPosition: CGPoint? private let panelIsFocusedPromise = ValuePromise(false) private var panelIsFocused: Bool = false { didSet { self.panelIsFocusedPromise.set(self.panelIsFocused) } } private var panelFocusTimer: SwiftSignalKit.Timer? private var lastReorderItemIndex: Int? 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.collectionListSeparator = ASDisplayNode() self.collectionListSeparator.isLayerBacked = true self.collectionListSeparator.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor self.collectionListContainer = CollectionListContainerNode() self.listView = ListView() self.listView.useSingleDimensionTouchPoint = true self.listView.reorderedItemHasShadow = false self.listView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) self.listView.scroller.panGestureRecognizer.cancelsTouchesInView = true self.listView.accessibilityPageScrolledString = { row, count in return strings.VoiceOver_ScrollStatus(row, count).string } self.gifListView = ListView() self.gifListView.useSingleDimensionTouchPoint = true self.gifListView.transform = CATransform3DMakeRotation(-CGFloat(Double.pi / 2.0), 0.0, 0.0, 1.0) self.gifListView.scroller.panGestureRecognizer.cancelsTouchesInView = true 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.stickerPane.beganScrolling = { [weak self] in self?.scrollingStickersGridPromise.set(true) } self.stickerPane.endedScrolling = { [weak self] in self?.scrollingStickersGridPromise.set(false) } let temporaryPackOrder = Promise<[ItemCollectionId]?>(nil) self.listView.willBeginReorder = { [weak self] point in self?.listView.beganInteractiveDragging(point) } self.listView.reorderBegan = { [weak self] in self?.stopCollapseTimer() } self.listView.reorderItem = { [weak self] fromIndex, toIndex, opaqueState in guard let entries = (opaqueState as? ChatMediaInputPanelOpaqueState)?.entries else { return .single(false) } self?.lastReorderItemIndex = toIndex let fromEntry = entries[fromIndex] guard case let .stickerPack(_, fromPackInfo, _, _, _, _) = fromEntry else { return .single(false) } var referenceId: ItemCollectionId? var beforeAll = false var afterAll = false if toIndex < entries.count { switch entries[toIndex] { case let .stickerPack(_, toPackInfo, _, _, _, _): referenceId = toPackInfo.id default: if entries[toIndex] < fromEntry { beforeAll = true } else { afterAll = true } } } else { afterAll = true } var currentIds: [ItemCollectionId] = [] for entry in entries { switch entry { case let .stickerPack(_, info, _, _, _, _): currentIds.append(info.id) default: break } } var previousIndex: Int? for i in 0 ..< currentIds.count { if currentIds[i] == fromPackInfo.id { previousIndex = i currentIds.remove(at: i) break } } var didReorder = false if let referenceId = referenceId { var inserted = false for i in 0 ..< currentIds.count { if currentIds[i] == referenceId { if fromIndex < toIndex { didReorder = previousIndex != i + 1 currentIds.insert(fromPackInfo.id, at: i + 1) } else { didReorder = previousIndex != i currentIds.insert(fromPackInfo.id, at: i) } inserted = true break } } if !inserted { didReorder = previousIndex != currentIds.count currentIds.append(fromPackInfo.id) } } else if beforeAll { didReorder = previousIndex != 0 currentIds.insert(fromPackInfo.id, at: 0) } else if afterAll { didReorder = previousIndex != currentIds.count currentIds.append(fromPackInfo.id) } temporaryPackOrder.set(.single(currentIds)) return .single(didReorder) } self.listView.reorderCompleted = { [weak self] opaqueState in guard let entries = (opaqueState as? ChatMediaInputPanelOpaqueState)?.entries else { return } var currentIds: [ItemCollectionId] = [] for entry in entries { switch entry { case let .stickerPack(_, info, _, _, _, _): currentIds.append(info.id) default: break } } let _ = (context.engine.stickers.reorderStickerPacks(namespace: Namespaces.ItemCollection.CloudStickerPacks, itemIds: currentIds) |> deliverOnMainQueue).start(completed: { [weak self] in temporaryPackOrder.set(.single(nil)) if let strongSelf = self { if let lastReorderItemIndex = strongSelf.lastReorderItemIndex { strongSelf.lastReorderItemIndex = nil if strongSelf.panelIsFocused { strongSelf.panelFocusScrollToIndex = lastReorderItemIndex } } } self?.startCollapseTimer(timeout: 2.0) }) } 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, highlightedPackId: nil, 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 if collectionId.namespace == ChatMediaInputPanelAuxiliaryNamespace.premium.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) } }, openTrending: { [weak self] packId in if let strongSelf = self { strongSelf.controllerInteraction.navigationController()?.pushViewController(FeaturedStickersScreen( context: strongSelf.context, highlightedPackId: packId, sendSticker: { fileReference, sourceNode, sourceRect in if let strongSelf = self { return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect) } else { return false } } )) } }, dismissTrendingPacks: { _ in let _ = (context.account.viewTracker.featuredStickerPacks() |> take(1) |> deliverOnMainQueue).start(next: { packs in let ids = packs.map { $0.info.id.id } let _ = ApplicationSpecificNotice.setDismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager, values: ids).start() }) }, 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, _, focused): return .media(mode: mode, expanded: .search(searchMode), focused: focused) default: return current } } } })) } } else { strongSelf.controllerInteraction.updateInputMode { current in switch current { case let .media(mode, _, focused): return .media(mode: mode, expanded: nil, focused: focused) default: return current } } } } }, openPeerSpecificSettings: { [weak self] in guard let peerId = peerId, peerId.namespace == Namespaces.Peer.CloudChannel else { return } let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.StickerPack(id: peerId)) |> 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.engine.stickers.clearRecentlyUsedStickers().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(self.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 let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers] let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks] switch position { case .initial: var firstTime = true return context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, 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: orderedItemListCollectionIds, namespaces: namespaces, 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: orderedItemListCollectionIds, namespaces: namespaces, 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, updatedPresentationData: strongSelf.controllerInteraction.updatedPresentationData, 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, nil) }, getItemIsPreviewed: { item in return getItemIsPreviewedImpl?(item) ?? false }, openSearch: { }) self.trendingInteraction = trendingInteraction let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App()) |> map { appConfiguration -> [String] in let defaultReactions: [String] = ["👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"] guard let data = appConfiguration.data, let emojis = data["gif_search_emojies"] as? [String] else { return defaultReactions } return emojis } |> distinctUntilChanged let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false) |> map { animatedEmoji -> [String: [StickerPackItem]] in var animatedEmojiStickers: [String: [StickerPackItem]] = [:] switch animatedEmoji { case let .result(_, items, _): for item in items { if let emoji = item.getStringRepresentationsOfIndexKeys().first { animatedEmojiStickers[emoji.basicEmoji.0] = [item] let strippedEmoji = emoji.basicEmoji.0.strippedEmoji if animatedEmojiStickers[strippedEmoji] == nil { animatedEmojiStickers[strippedEmoji] = [item] } } } default: break } return animatedEmojiStickers } let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let previousView = Atomic(value: nil) let transitionQueue = Queue() let transitions = combineLatest( queue: transitionQueue, itemCollectionsView, peerSpecificPack, context.account.viewTracker.featuredStickerPacks(), self.themeAndStringsPromise.get(), reactions, self.panelIsFocusedPromise.get(), ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager), temporaryPackOrder.get(), animatedEmojiStickers, context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId)), context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))) ) |> map { viewAndUpdate, peerSpecificPack, trendingPacks, themeAndStrings, reactions, panelExpanded, dismissedTrendingStickerPacks, temporaryPackOrder, animatedEmojiStickers, accountPeer, featuredStickersConfiguration -> (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? var premiumStickers: OrderedItemListView? // var cloudPremiumStickers: OrderedItemListView? for orderedView in view.orderedItemListsViews { if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { recentStickers = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers { savedStickers = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.PremiumStickers { premiumStickers = orderedView } // else if orderedView.collectionId == Namespaces.OrderedItemList.CloudPremiumStickers { // cloudPremiumStickers = orderedView // } } var installedPacks = Set() for info in view.collectionInfos { installedPacks.insert(info.0) } var trendingIsDismissed = false if let dismissedTrendingStickerPacks = dismissedTrendingStickerPacks, Set(trendingPacks.map({ $0.info.id.id })) == Set(dismissedTrendingStickerPacks) { trendingIsDismissed = true } let hasPremium = accountPeer?.isPremium ?? false let featuredStickersConfiguration = featuredStickersConfiguration?.get(FeaturedStickersConfiguration.self) let panelEntries = chatMediaInputPanelEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, temporaryPackOrder: temporaryPackOrder, trendingIsDismissed: trendingIsDismissed, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, theme: theme, strings: strings, premiumStickers: hasPremium ? premiumStickers : nil, expanded: panelExpanded, reorderable: true) let gifPaneEntries = chatMediaInputPanelGifModeEntries(theme: theme, strings: strings, reactions: reactions, animatedEmojiStickers: animatedEmojiStickers, expanded: panelExpanded) var gridEntries = chatMediaInputGridEntries(view: view, savedStickers: savedStickers, recentStickers: recentStickers, peerSpecificPack: peerSpecificPack.0, canInstallPeerSpecificPack: peerSpecificPack.1, trendingPacks: trendingPacks, installedPacks: installedPacks, premiumStickers: premiumStickers, trendingIsDismissed: trendingIsDismissed, strings: strings, theme: theme, hasPremium: hasPremium, isPremiumDisabled: premiumConfiguration.isPremiumDisabled, trendingIsPremium: featuredStickersConfiguration?.isPremium ?? false) 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 _ = topVisible.1 as? StickerPaneTrendingListGridItem { topVisibleCollectionId = ItemCollectionId(namespace: ChatMediaInputPanelAuxiliaryNamespace.recentStickers.rawValue, id: 0) } else 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 { strongSelf.stopCollapseTimer() strongSelf.scrollingStickerPacksListPromise.set(true) var position = position var index = strongSelf.listView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) if index == nil { position.y += 10.0 index = strongSelf.listView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) } if let index = index { strongSelf.panelFocusScrollToIndex = index strongSelf.panelFocusInitialPosition = position } strongSelf.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in if case let .media(mode, expanded, _) = inputMode { return (inputTextState, .media(mode: mode, expanded: expanded, focused: true)) } else { return (inputTextState, inputMode) } } } } self.listView.endedInteractiveDragging = { [weak self] position in if let strongSelf = self { strongSelf.panelFocusInitialPosition = position } } self.listView.didEndScrolling = { [weak self] decelerated in if let strongSelf = self { if decelerated { strongSelf.panelFocusScrollToIndex = nil strongSelf.panelFocusInitialPosition = nil } strongSelf.startCollapseTimer(timeout: decelerated ? 0.5 : 2.5) strongSelf.scrollingStickerPacksListPromise.set(false) } } self.gifListView.beganInteractiveDragging = { [weak self] position in if let strongSelf = self { strongSelf.stopCollapseTimer() var position = position var index = strongSelf.gifListView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) if index == nil { position.y += 10.0 index = strongSelf.gifListView.itemIndexAtPoint(CGPoint(x: 0.0, y: position.y)) } if let index = index { strongSelf.panelFocusScrollToIndex = index strongSelf.panelFocusInitialPosition = position } strongSelf.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in if case let .media(mode, expanded, _) = inputMode { return (inputTextState, .media(mode: mode, expanded: expanded, focused: true)) } else { return (inputTextState, inputMode) } } } } self.gifListView.endedInteractiveDragging = { [weak self] position in if let strongSelf = self { strongSelf.panelFocusInitialPosition = position } } self.gifListView.didEndScrolling = { [weak self] decelerated in if let strongSelf = self { if decelerated { strongSelf.panelFocusScrollToIndex = nil strongSelf.panelFocusInitialPosition = nil } strongSelf.startCollapseTimer(timeout: decelerated ? 0.5 : 2.5) } } self.choosingStickerDisposable = (self.choosingSticker |> deliverOnMainQueue).start(next: { [weak self] value in if let strongSelf = self { strongSelf.controllerInteraction.updateChoosingSticker(value) } }) } deinit { self.disposable.dispose() self.choosingStickerDisposable?.dispose() self.searchContainerNodeLoadedDisposable.dispose() self.panelFocusTimer?.invalidate() } private func updateIsFocused(_ isExpanded: Bool) { guard self.panelIsFocused != isExpanded else { return } self.panelIsFocused = isExpanded self.updatePaneClippingContainer(size: self.paneClippingContainer.bounds.size, offset: self.currentCollectionListPanelOffset(), transition: .animated(duration: 0.3, curve: .spring)) } private func startCollapseTimer(timeout: Double) { self.panelFocusTimer?.invalidate() let timer = SwiftSignalKit.Timer(timeout: timeout, repeat: false, completion: { [weak self] in self?.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in if case let .media(mode, expanded, _) = inputMode { return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) } else { return (inputTextState, inputMode) } } }, queue: Queue.mainQueue()) self.panelFocusTimer = timer timer.start() } private func stopCollapseTimer() { self.panelFocusTimer?.invalidate() self.panelFocusTimer = nil } 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.engine.stickers.isGifSaved(id: file.file.media.fileId) |> deliverOnMainQueue).start(next: { [weak self] isGifSaved in guard let strongSelf = self else { return } var isGifSaved = isGifSaved if !canSaveGif { isGifSaved = false } let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } 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 context = strongSelf.context let presentationData = context.sharedContext.currentPresentationData.with { $0 } let controllerInteraction = strongSelf.controllerInteraction let _ = (toggleGifSaved(account: context.account, fileReference: file.file, saved: true) |> deliverOnMainQueue).start(next: { result in switch result { case .generic: controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: nil, text: presentationData.strings.Gallery_GifSaved), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let text: String if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { text = presentationData.strings.Premium_MaxSavedGifsFinalText } else { text = presentationData.strings.Premium_MaxSavedGifsText("\(premiumLimit)").string } controllerInteraction.presentController(UndoOverlayController(presentationData: presentationData, content: .universal(animation: "anim_gif", scale: 0.075, colors: [:], title: presentationData.strings.Premium_MaxSavedGifsTitle("\(limit)").string, text: text), elevatedLayout: false, animateInAsReplacement: false, action: { action in if case .info = action { let controller = PremiumIntroScreen(context: context, source: .savedGifs) controllerInteraction.navigationController()?.pushViewController(controller) return true } return false }), nil) } }) }))) } let contextController = ContextController(account: strongSelf.context.account, presentationData: presentationData, source: .controller(ContextControllerContentSourceImpl(controller: gallery, sourceNode: sourceNode, sourceRect: sourceRect)), items: .single(ContextController.Items(content: .list(items))), 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.engine.stickers.isStickerSaved(id: 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/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let _ = (strongSelf.context.engine.stickers.toggleStickerSaved(file: item.file, saved: !isStarred) |> deliverOnMainQueue).start(next: { result in switch result { case .generic: strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) let text: String if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { text = strongSelf.strings.Premium_MaxFavedStickersFinalText } else { text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self { if case .info = action { let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers) strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) return true } } return false }), nil) } }) } })) ) 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, updatedPresentationData: strongSelf.controllerInteraction.updatedPresentationData, 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, theme: strongSelf.theme, strings: strongSelf.strings, item: item, menu: menuItems, openPremiumIntro: { [weak self] in guard let strongSelf = self else { return } let controller = PremiumIntroScreen(context: strongSelf.context, source: .stickers) strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) })) } 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 { let accountPeerId = strongSelf.context.account.peerId return combineLatest( strongSelf.context.engine.stickers.isStickerSaved(id: item.file.fileId), strongSelf.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: accountPeerId)) |> map { peer -> Bool in var hasPremium = false if case let .user(user) = peer, user.isPremium { hasPremium = true } return hasPremium } ) |> deliverOnMainQueue |> map { isStarred, hasPremium -> (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/Unfave") : UIImage(bundleImageName: "Chat/Context Menu/Fave"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) if let strongSelf = self { let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 } let _ = (strongSelf.context.engine.stickers.toggleStickerSaved(file: item.file, saved: !isStarred) |> deliverOnMainQueue).start(next: { result in switch result { case .generic: strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: nil, text: !isStarred ? strongSelf.strings.Conversation_StickerAddedToFavorites : strongSelf.strings.Conversation_StickerRemovedFromFavorites, undoText: nil), elevatedLayout: false, action: { _ in return false }), nil) case let .limitExceeded(limit, premiumLimit): let premiumConfiguration = PremiumConfiguration.with(appConfiguration: strongSelf.context.currentAppConfiguration.with { $0 }) let text: String if limit == premiumLimit || premiumConfiguration.isPremiumDisabled { text = strongSelf.strings.Premium_MaxFavedStickersFinalText } else { text = strongSelf.strings.Premium_MaxFavedStickersText("\(premiumLimit)").string } strongSelf.controllerInteraction.presentGlobalOverlayController(UndoOverlayController(presentationData: presentationData, content: .sticker(context: strongSelf.context, file: item.file, title: strongSelf.strings.Premium_MaxFavedStickersTitle("\(limit)").string, text: text, undoText: nil), elevatedLayout: false, action: { [weak self] action in if let strongSelf = self { if case .info = action { let controller = PremiumIntroScreen(context: strongSelf.context, source: .savedStickers) strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) return true } } return false }), nil) } }) } })) ) 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, updatedPresentationData: strongSelf.controllerInteraction.updatedPresentationData, 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, theme: strongSelf.theme, strings: strongSelf.strings, item: .pack(item), isLocked: item.file.isPremiumSticker && !hasPremium, menu: menuItems, openPremiumIntro: { [weak self] in guard let strongSelf = self else { return } let controller = PremiumIntroScreen(context: strongSelf.context, source: .stickers) strongSelf.controllerInteraction.navigationController()?.pushViewController(controller) })) } 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?.previewingStickersPromise.set(visible) 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) } }) peekRecognizer.checkSingleTapActivationAtPoint = { [weak self] point in guard let strongSelf = self else { return false } let pane = strongSelf.stickerPane let panelPoint = strongSelf.view.convert(point, to: strongSelf.collectionListPanel.view) if panelPoint.y < strongSelf.collectionListPanel.frame.maxY { return false } if pane.supernode != nil, pane.frame.contains(point) { let itemNodeAndItem = pane.itemAt(point: point.offsetBy(dx: -pane.frame.minX, dy: -pane.frame.minY)) if let item = itemNodeAndItem?.0 as? ChatMediaInputStickerGridItemNode, item.isLocked == true { return true } } return false } 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.panelIsFocused, let targetIndex = self.listView.indexOf(itemNode: itemNode) { self.panelFocusScrollToIndex = targetIndex self.panelFocusInitialPosition = nil self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in if case let .media(mode, expanded, _) = inputMode { return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) } else { return (inputTextState, inputMode) } } } else { self.panelFocusScrollToIndex = nil self.panelFocusInitialPosition = nil self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputMetaSectionItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { if self.panelIsFocused, let targetIndex = self.listView.indexOf(itemNode: itemNode) { self.panelFocusScrollToIndex = targetIndex self.panelFocusInitialPosition = nil self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in if case let .media(mode, expanded, _) = inputMode { return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) } else { return (inputTextState, inputMode) } } } else { self.panelFocusScrollToIndex = nil self.panelFocusInitialPosition = nil self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputRecentGifsItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { if self.panelIsFocused, let targetIndex = self.listView.indexOf(itemNode: itemNode) { self.panelFocusScrollToIndex = targetIndex self.panelFocusInitialPosition = nil self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in if case let .media(mode, expanded, _) = inputMode { return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) } else { return (inputTextState, inputMode) } } } else { self.panelFocusScrollToIndex = nil self.panelFocusInitialPosition = nil self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputTrendingItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { if self.panelIsFocused, let targetIndex = self.listView.indexOf(itemNode: itemNode) { self.panelFocusScrollToIndex = targetIndex self.panelFocusInitialPosition = nil self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in if case let .media(mode, expanded, _) = inputMode { return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) } else { return (inputTextState, inputMode) } } } else { self.panelFocusScrollToIndex = nil self.panelFocusInitialPosition = nil self.listView.ensureItemNodeVisible(itemNode) } ensuredNodeVisible = true } } else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode { itemNode.updateIsHighlighted() if itemNode.currentCollectionId == collectionId { if self.panelIsFocused, let targetIndex = self.listView.indexOf(itemNode: itemNode) { self.panelFocusScrollToIndex = targetIndex self.panelFocusInitialPosition = nil self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in if case let .media(mode, expanded, _) = inputMode { return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) } else { return (inputTextState, inputMode) } } } else { self.panelFocusScrollToIndex = nil self.panelFocusInitialPosition = nil 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.panelIsFocused { self.panelFocusScrollToIndex = targetIndex self.panelFocusInitialPosition = nil self.interfaceInteraction?.updateTextInputStateAndMode { inputTextState, inputMode in if case let .media(mode, expanded, _) = inputMode { return (inputTextState, .media(mode: mode, expanded: expanded, focused: false)) } else { return (inputTextState, inputMode) } } } else { self.panelFocusScrollToIndex = nil self.panelFocusInitialPosition = nil 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 isFocused = false var isExpanded: Bool = false if case let .media(_, _, focused) = interfaceState.inputMode { isFocused = focused } 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 } self.updateIsFocused(isFocused) 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 + 40.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 + 40.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 self.paneArrangement.currentIndex == 1 { if let targetIndex = self.panelFocusScrollToIndex, !self.listView.isReordering { var position: ListViewScrollPosition if self.panelIsFocused { if let initialPosition = self.panelFocusInitialPosition { position = .top(96.0 + (initialPosition.y - self.listView.frame.height / 2.0) * 0.5) } else { position = .top(96.0) } } else { if let initialPosition = self.panelFocusInitialPosition { position = .top(self.listView.frame.height / 2.0 + 96.0 + (initialPosition.y - self.listView.frame.height / 2.0)) } else { position = .top(self.listView.frame.height / 2.0 + 96.0) } self.panelFocusScrollToIndex = nil self.panelFocusInitialPosition = nil } scrollToItem = ListViewScrollToItem(index: targetIndex, position: position, animated: true, curve: .Spring(duration: 0.4), directionHint: .Down, displayLink: true) } } self.listView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, updateOpaqueState: transition.updateOpaqueState, 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() if firstTime { options.insert(.Synchronous) options.insert(.LowLatency) } else { options.insert(.AnimateInsertion) } var scrollToItem: ListViewScrollToItem? if self.paneArrangement.currentIndex == 0 { if let targetIndex = self.panelFocusScrollToIndex { var position: ListViewScrollPosition if self.panelIsFocused { if let initialPosition = self.panelFocusInitialPosition { position = .top(96.0 + (initialPosition.y - self.gifListView.frame.height / 2.0) * 0.5) } else { position = .top(96.0) } } else { if let initialPosition = self.panelFocusInitialPosition { position = .top(self.gifListView.frame.height / 2.0 + 96.0 + (initialPosition.y - self.gifListView.frame.height / 2.0)) } else { position = .top(self.gifListView.frame.height / 2.0 + 96.0) } self.panelFocusScrollToIndex = nil self.panelFocusInitialPosition = nil } scrollToItem = ListViewScrollToItem(index: targetIndex, position: position, animated: true, curve: .Spring(duration: 0.4), directionHint: .Down, displayLink: true) } } self.gifListView.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: scrollToItem, 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.panelIsFocused { 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 if self.panelIsFocused { offset = 0.0 } transition.updateFrame(node: self.collectionListSeparator, frame: CGRect(origin: CGPoint(x: 0.0, y: offset + 41.0), size: self.collectionListSeparator.bounds.size)) transition.updateFrame(node: self.paneClippingContainer, frame: CGRect(origin: CGPoint(x: 0.0, y: offset + 41.0), size: size)) transition.updateSublayerTransformOffset(layer: self.paneClippingContainer.layer, offset: CGPoint(x: 0.0, y: -offset - 41.0)) } 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.panelIsFocused { 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 self.panelIsFocused { if point.y > -41.0 && point.y < 38.0 { let convertedPoint = CGPoint(x: max(0.0, point.y), y: point.x) if let result = self.listView.hitTest(convertedPoint, with: event) { return result } if let result = self.gifListView.hitTest(convertedPoint, with: event) { return result } } } 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) } } }