import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore import SyncCore import TelegramPresentationData import MergeLists import AccountContext import SearchUI import ChatListSearchItemHeader import WebSearchUI import SearchBarNode enum WallpaperSearchColor: CaseIterable { case blue case red case orange case yellow case green case teal case purple case pink case brown case black case gray case white var string: String { switch self { case .blue: return "Blue" case .red: return "Red" case .orange: return "Orange" case .yellow: return "Yellow" case .green: return "Green" case .teal: return "Teal" case .purple: return "Purple" case .pink: return "Pink" case .brown: return "Brown" case .black: return "Black" case .gray: return "Gray" case .white: return "White" } } var displayColor: UIColor { switch self { case .blue: return UIColor(rgb: 0x0076ff) case .red: return UIColor(rgb: 0xff0000) case .orange: return UIColor(rgb: 0xff8a00) case .yellow: return UIColor(rgb: 0xffca00) case .green: return UIColor(rgb: 0x00e432) case .teal: return UIColor(rgb: 0x1fa9ab) case .purple: return UIColor(rgb: 0x7300aa) case .pink: return UIColor(rgb: 0xf9bec5) case .brown: return UIColor(rgb: 0x734021) case .black: return UIColor(rgb: 0x000000) case .gray: return UIColor(rgb: 0x5c585f) case .white: return UIColor(rgb: 0xffffff) } } func localizedString(strings: PresentationStrings) -> String { switch self { case .blue: return strings.WallpaperSearch_ColorBlue case .red: return strings.WallpaperSearch_ColorRed case .orange: return strings.WallpaperSearch_ColorOrange case .yellow: return strings.WallpaperSearch_ColorYellow case .green: return strings.WallpaperSearch_ColorGreen case .teal: return strings.WallpaperSearch_ColorTeal case .purple: return strings.WallpaperSearch_ColorPurple case .pink: return strings.WallpaperSearch_ColorPink case .brown: return strings.WallpaperSearch_ColorBrown case .black: return strings.WallpaperSearch_ColorBlack case .gray: return strings.WallpaperSearch_ColorGray case .white: return strings.WallpaperSearch_ColorWhite } } } enum WallpaperSearchQuery: Equatable { case generic(String) case color(WallpaperSearchColor, String) var botQuery: String { switch self { case let .generic(query): return query case let .color(color, query): return "#color\(color.string) \(query)" } } var query: String { switch self { case let .generic(query), let .color(_, query): return query } } func updatedWithText(_ text: String) -> WallpaperSearchQuery { switch self { case .generic: return .generic(text) case let .color(color, _): return .color(color, text) } } func updatedWithColor(_ color: WallpaperSearchColor?) -> WallpaperSearchQuery { if let color = color { switch self { case let .generic(text): return .color(color, text) case let .color(_, text): return .color(color, text) } } else { switch self { case .generic: return self case let .color(_, text): return .generic(text) } } } } final class ThemeGridSearchInteraction { let openResult: (ChatContextResult) -> Void let selectColor: (WallpaperSearchColor) -> Void let setSearchQuery: (WallpaperSearchQuery) -> Void let deleteRecentQuery: (String) -> Void init(openResult: @escaping (ChatContextResult) -> Void, selectColor: @escaping (WallpaperSearchColor) -> Void, setSearchQuery: @escaping (WallpaperSearchQuery) -> Void, deleteRecentQuery: @escaping (String) -> Void) { self.openResult = openResult self.selectColor = selectColor self.setSearchQuery = setSearchQuery self.deleteRecentQuery = deleteRecentQuery } } private enum ThemeGridRecentEntryStableId: Hashable { case colors case query(String) static func ==(lhs: ThemeGridRecentEntryStableId, rhs: ThemeGridRecentEntryStableId) -> Bool { switch lhs { case .colors: if case .colors = rhs { return true } else { return false } case let .query(query): if case .query(query) = rhs { return true } else { return false } } } var hashValue: Int { switch self { case .colors: return 0 case let .query(query): return query.hashValue } } } private enum ThemeGridRecentEntry: Comparable, Identifiable { case colors(PresentationTheme, PresentationStrings) case query(Int, String) var stableId: ThemeGridRecentEntryStableId { switch self { case .colors: return .colors case let .query(_, query): return .query(query) } } static func ==(lhs: ThemeGridRecentEntry, rhs: ThemeGridRecentEntry) -> Bool { switch lhs { case let .colors(lhsTheme, lhsStrings): if case let .colors(rhsTheme, rhsStrings) = rhs { if lhsTheme !== rhsTheme { return false } if lhsStrings !== rhsStrings { return false } return true } else { return false } case let .query(lhsIndex, lhsQuery): if case .query(lhsIndex, lhsQuery) = rhs { return true } else { return false } } } static func <(lhs: ThemeGridRecentEntry, rhs: ThemeGridRecentEntry) -> Bool { switch lhs { case .colors: return true case let .query(lhsIndex, _): switch rhs { case .colors: return false case let .query(rhsIndex, _): return lhsIndex <= rhsIndex } } } func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ThemeGridSearchInteraction, header: ListViewItemHeader) -> ListViewItem { switch self { case let .colors(theme, strings): return ThemeGridSearchColorsItem(account: account, theme: theme, strings: strings, colorSelected: { color in interaction.selectColor(color) }) case let .query(_, query): return WebSearchRecentQueryItem(account: account, theme: theme, strings: strings, query: query, tapped: { query in interaction.setSearchQuery(.generic(query)) }, deleted: { query in interaction.deleteRecentQuery(query) }, header: header) } } } private struct ThemeGridSearchContainerRecentTransition { let deletions: [ListViewDeleteItem] let insertions: [ListViewInsertItem] let updates: [ListViewUpdateItem] } private struct ThemeGridSearchEntry: Comparable, Identifiable { let index: Int let result: ChatContextResult static func ==(lhs: ThemeGridSearchEntry, rhs: ThemeGridSearchEntry) -> Bool { return lhs.index == rhs.index && lhs.result == rhs.result } static func <(lhs: ThemeGridSearchEntry, rhs: ThemeGridSearchEntry) -> Bool { return lhs.index < rhs.index } var stableId: Int { return self.index } func item(account: Account, theme: PresentationTheme, interaction: ThemeGridSearchInteraction) -> ThemeGridSearchItem { return ThemeGridSearchItem(account: account, theme: theme, result: self.result, interaction: interaction) } } struct ThemeGridSearchContainerTransition { let deletions: [Int] let insertions: [GridNodeInsertItem] let updates: [GridNodeUpdateItem] let displayingResults: Bool let isEmpty: Bool let query: String } private func themeGridSearchContainerPreparedRecentTransition(from fromEntries: [ThemeGridRecentEntry], to toEntries: [ThemeGridRecentEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: ThemeGridSearchInteraction, header: ListViewItemHeader) -> ThemeGridSearchContainerRecentTransition { 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(account: account, theme: theme, strings: strings, interaction: interaction, header: header), directionHint: nil) } let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, header: header), directionHint: nil) } return ThemeGridSearchContainerRecentTransition(deletions: deletions, insertions: insertions, updates: updates) } private func themeGridSearchContainerPreparedTransition(from fromEntries: [ThemeGridSearchEntry], to toEntries: [ThemeGridSearchEntry], displayingResults: Bool, account: Account, theme: PresentationTheme, isEmpty: Bool, query: String, interaction: ThemeGridSearchInteraction) -> ThemeGridSearchContainerTransition { 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, theme: theme, interaction: interaction), previousIndex: $0.2) } let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, interaction: interaction)) } return ThemeGridSearchContainerTransition(deletions: deletions, insertions: insertions, updates: updates, displayingResults: displayingResults, isEmpty: isEmpty, query: query) } private struct ThemeGridSearchResult { let query: String let collection: ChatContextResultCollection let items: [ChatContextResult] let nextOffset: String? } private struct ThemeGridSearchContext { let result: ThemeGridSearchResult let loadMoreIndex: String? } final class ThemeGridSearchContentNode: SearchDisplayControllerContentNode { private let context: AccountContext private let recentListNode: ListView private let gridNode: GridNode private let dimNode: ASDisplayNode private let emptyResultsTitleNode: ImmediateTextNode private let emptyResultsTextNode: ImmediateTextNode private var enqueuedRecentTransitions: [(ThemeGridSearchContainerRecentTransition, Bool)] = [] private var enqueuedTransitions: [(ThemeGridSearchContainerTransition, Bool)] = [] private var validLayout: (ContainerViewLayout, CGFloat)? private var queryValue: WallpaperSearchQuery = .generic("") private let queryPromise: Promise private let searchDisposable = MetaDisposable() private var recentDisposable: Disposable? private var presentationData: PresentationData private var presentationDataDisposable: Disposable? private let presentationDataPromise: Promise private let _isSearching = ValuePromise(false, ignoreRepeated: true) override var isSearching: Signal { return self._isSearching.get() } init(context: AccountContext, openResult: @escaping (ChatContextResult) -> Void) { self.context = context self.queryPromise = Promise(self.queryValue) let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.presentationData = presentationData self.presentationDataPromise = Promise(self.presentationData) self.dimNode = ASDisplayNode() self.recentListNode = ListView() self.recentListNode.verticalScrollIndicatorColor = self.presentationData.theme.list.scrollIndicatorColor self.recentListNode.accessibilityPageScrolledString = { row, count in return presentationData.strings.VoiceOver_ScrollStatus(row, count).0 } self.gridNode = GridNode() self.emptyResultsTitleNode = ImmediateTextNode() self.emptyResultsTitleNode.attributedText = NSAttributedString(string: self.presentationData.strings.SharedMedia_SearchNoResults, font: Font.semibold(17.0), textColor: self.presentationData.theme.list.freeTextColor) self.emptyResultsTitleNode.textAlignment = .center self.emptyResultsTitleNode.isHidden = true self.emptyResultsTextNode = ImmediateTextNode() self.emptyResultsTextNode.maximumNumberOfLines = 0 self.emptyResultsTextNode.textAlignment = .center self.emptyResultsTextNode.isHidden = true super.init() self.dimNode.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.addSubnode(self.dimNode) self.addSubnode(self.recentListNode) self.addSubnode(self.gridNode) self.addSubnode(self.emptyResultsTitleNode) self.addSubnode(self.emptyResultsTextNode) let searchContext = Promise(nil) let searchContextValue = Atomic(value: nil) let updateSearchContext: ((ThemeGridSearchContext?) -> (ThemeGridSearchContext?, Bool)) -> Void = { f in var shouldUpdate = false let updated = searchContextValue.modify { current in let (u, s) = f(current) shouldUpdate = s if s { return u } else { return current } } if shouldUpdate { searchContext.set(.single(updated)) } } self.gridNode.isHidden = true self.gridNode.visibleItemsUpdated = { visibleItems in if let bottom = visibleItems.bottom { if let context = searchContextValue.with({ $0 }), bottom.0 >= context.result.items.count - 8 { updateSearchContext { previous in guard let previous = previous else { return (nil, false) } if previous.loadMoreIndex != nil { return (previous, false) } guard let _ = previous.result.items.last else { return (previous, false) } return (ThemeGridSearchContext(result: previous.result, loadMoreIndex: previous.result.nextOffset), true) } } } } self.recentListNode.isHidden = false let previousSearchItems = Atomic<[ThemeGridSearchEntry]?>(value: nil) let interaction = ThemeGridSearchInteraction(openResult: { [weak self] result in openResult(result) if let strongSelf = self { strongSelf.dismissInput?() let query = strongSelf.queryValue.query if !query.isEmpty { let _ = addRecentWallpaperSearchQuery(postbox: strongSelf.context.account.postbox, string: query).start() } } }, selectColor: { [weak self] color in self?.updateQuery({ $0.updatedWithColor(color) }, updateInterface: true) }, setSearchQuery: { [weak self] query in self?.dismissInput?() self?.updateQuery({ _ in return query }, updateInterface: true) }, deleteRecentQuery: { query in let _ = removeRecentWallpaperSearchQuery(postbox: context.account.postbox, string: query).start() }) let configuration = self.context.account.postbox.transaction { transaction -> SearchBotsConfiguration in return currentSearchBotsConfiguration(transaction: transaction) } let foundItems = self.queryPromise.get() |> mapToSignal { query -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in let query = query.botQuery guard !query.isEmpty else { return .single(nil) } let wallpaperQuery = "#wallpaper \(query)" updateSearchContext { _ in return (nil, true) } return .single(([], true)) |> then( configuration |> mapToSignal { configuration -> Signal in guard let name = configuration.imageBotUsername else { return .single(nil) } return context.engine.peers.resolvePeerByName(name: name) |> mapToSignal { peerId -> Signal in if let peerId = peerId { return context.account.postbox.loadedPeerWithId(peerId) |> map { peer -> Peer? in return peer } |> take(1) } else { return .single(nil) } } } |> mapToSignal { peer -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in if let user = peer as? TelegramUser, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { let loadMore = searchContext.get() |> mapToSignal { searchContext -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in if let searchContext = searchContext { if let _ = searchContext.loadMoreIndex, let nextOffset = searchContext.result.nextOffset { let collection = searchContext.result.collection let geoPoint = collection.geoPoint.flatMap { geoPoint -> (Double, Double) in return (geoPoint.latitude, geoPoint.longitude) } return requestChatContextResults(account: self.context.account, botId: collection.botId, peerId: collection.peerId, query: searchContext.result.query, location: .single(geoPoint), offset: nextOffset) |> map { results -> ChatContextResultCollection? in return results?.results } |> `catch` { error -> Signal in return .single(nil) } |> map { nextResults -> (ChatContextResultCollection, String?) in var results: [ChatContextResult] = [] var existingIds = Set() for result in searchContext.result.items { results.append(result) existingIds.insert(result.id) } var nextOffset: String? if let nextResults = nextResults { for result in nextResults.results { if !existingIds.contains(result.id) { results.append(result) existingIds.insert(result.id) } } if let newNextOffset = nextResults.nextOffset, !newNextOffset.isEmpty { nextOffset = newNextOffset } } let merged = ChatContextResultCollection(botId: collection.botId, peerId: collection.peerId, query: collection.query, geoPoint: collection.geoPoint, queryId: nextResults?.queryId ?? collection.queryId, nextOffset: nextOffset ?? "", presentation: collection.presentation, switchPeer: collection.switchPeer, results: results, cacheTimeout: collection.cacheTimeout) return (merged, nextOffset) } |> mapToSignal { newCollection, nextOffset -> Signal<([ThemeGridSearchEntry], Bool)?, NoError> in updateSearchContext { previous in return (ThemeGridSearchContext(result: ThemeGridSearchResult(query: searchContext.result.query, collection: newCollection, items: newCollection.results, nextOffset: nextOffset), loadMoreIndex: nil), true) } return .complete() } } else { var entries: [ThemeGridSearchEntry] = [] var i = 0 for result in searchContext.result.items { entries.append(ThemeGridSearchEntry(index: i, result: result)) i += 1 } return .single((entries, false)) } } else { return .complete() } } return (.complete() |> delay(0.1, queue: Queue.concurrentDefaultQueue())) |> then( requestContextResults(account: context.account, botId: user.id, query: wallpaperQuery, peerId: context.account.peerId, limit: 16) |> map { results -> ChatContextResultCollection? in return results?.results } |> map { collection -> ([ThemeGridSearchEntry], Bool)? in guard let collection = collection else { return nil } var entries: [ThemeGridSearchEntry] = [] var i = 0 for result in collection.results { entries.append(ThemeGridSearchEntry(index: i, result: result)) i += 1 } updateSearchContext { _ in return (ThemeGridSearchContext(result: ThemeGridSearchResult(query: wallpaperQuery, collection: collection, items: collection.results, nextOffset: collection.nextOffset), loadMoreIndex: nil), true) } return (entries, false) } |> delay(0.2, queue: Queue.concurrentDefaultQueue()) |> then(loadMore) ) } else { return .single(nil) } } ) } let previousRecentItems = Atomic<[ThemeGridRecentEntry]?>(value: nil) self.recentDisposable = (combineLatest(wallpaperSearchRecentQueries(postbox: self.context.account.postbox), self.presentationDataPromise.get()) |> deliverOnMainQueue).start(next: { [weak self] queries, presentationData in if let strongSelf = self { var entries: [ThemeGridRecentEntry] = [] entries.append(.colors(presentationData.theme, presentationData.strings)) for i in 0 ..< queries.count { entries.append(.query(i, queries[i])) } let header = ChatListSearchItemHeader(type: .recentPeers, theme: presentationData.theme, strings: presentationData.strings, actionTitle: presentationData.strings.WebSearch_RecentSectionClear, action: { _ = clearRecentWallpaperSearchQueries(postbox: strongSelf.context.account.postbox).start() }) let previousEntries = previousRecentItems.swap(entries) let transition = themeGridSearchContainerPreparedRecentTransition(from: previousEntries ?? [], to: entries, account: context.account, theme: presentationData.theme, strings: presentationData.strings, interaction: interaction, header: header) strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil) } }) self.searchDisposable.set((combineLatest(foundItems, self.presentationDataPromise.get(), self.queryPromise.get()) |> deliverOnMainQueue).start(next: { [weak self] entriesAndFlags, presentationData, query in if let strongSelf = self { strongSelf._isSearching.set(entriesAndFlags?.1 ?? false) let previousEntries = previousSearchItems.swap(entriesAndFlags?.0) var isEmpty = false if let entriesAndFlags = entriesAndFlags { isEmpty = entriesAndFlags.0.isEmpty && !entriesAndFlags.1 } let firstTime = previousEntries == nil let transition = themeGridSearchContainerPreparedTransition(from: previousEntries ?? [], to: entriesAndFlags?.0 ?? [], displayingResults: entriesAndFlags?.0 != nil, account: context.account, theme: presentationData.theme, isEmpty: isEmpty, query: query.query, interaction: interaction) strongSelf.enqueueTransition(transition, firstTime: firstTime) } })) self.presentationDataDisposable = (context.sharedContext.presentationData |> deliverOnMainQueue).start(next: { [weak self] presentationData in if let strongSelf = self { let previousTheme = strongSelf.presentationData.theme strongSelf.presentationData = presentationData strongSelf.presentationDataPromise.set(.single(presentationData)) if previousTheme !== presentationData.theme { strongSelf.updateTheme(theme: presentationData.theme) } } }) self.recentListNode.beganInteractiveDragging = { [weak self] _ in self?.dismissInput?() } self.gridNode.scrollingInitiated = { [weak self] in self?.dismissInput?() } } override func didLoad() { super.didLoad() self.dimNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.dimTapGesture(_:)))) } @objc func dimTapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state { self.cancel?() } } deinit { self.searchDisposable.dispose() self.recentDisposable?.dispose() self.presentationDataDisposable?.dispose() } private func updateTheme(theme: PresentationTheme) { self.backgroundColor = theme.chatList.backgroundColor self.dimNode.backgroundColor = theme.chatList.backgroundColor self.recentListNode.verticalScrollIndicatorColor = theme.list.scrollIndicatorColor } private func updateQuery(_ f: (WallpaperSearchQuery) -> (WallpaperSearchQuery), updateInterface: Bool = false) { let query = f(self.queryValue) if query != self.queryValue { self.queryValue = query self.queryPromise.set(.single(query)) if updateInterface { let tokens: [SearchBarToken] let text: String let placeholder: String switch query { case let .generic(query): tokens = [] text = query placeholder = self.presentationData.strings.Wallpaper_Search case let .color(color, query): let backgroundColor = color.displayColor let foregroundColor: UIColor let strokeColor: UIColor if color == .white { foregroundColor = .black strokeColor = self.presentationData.theme.rootController.navigationSearchBar.inputClearButtonColor } else { foregroundColor = .white strokeColor = color.displayColor } tokens = [SearchBarToken(id: 0, icon: UIImage(bundleImageName: "Settings/WallpaperSearchColorIcon"), title: color.localizedString(strings: self.presentationData.strings), style: SearchBarToken.Style(backgroundColor: backgroundColor, foregroundColor: foregroundColor, strokeColor: strokeColor))] text = query placeholder = self.presentationData.strings.Wallpaper_SearchShort } self.setQuery?(nil, tokens, text) self.setPlaceholder?(placeholder) } } } override func searchTextUpdated(text: String) { self.updateQuery({ $0.updatedWithText(text) }) } override func searchTextClearPrefix() { self.updateQuery({ $0.updatedWithColor(nil) }, updateInterface: true) } override func searchTextClearTokens() { self.updateQuery({ $0.updatedWithColor(nil) }, updateInterface: true) } private func enqueueRecentTransition(_ transition: ThemeGridSearchContainerRecentTransition, firstTime: Bool) { self.enqueuedRecentTransitions.append((transition, firstTime)) if self.validLayout != nil { while !self.enqueuedRecentTransitions.isEmpty { self.dequeueRecentTransition() } } } private func dequeueRecentTransition() { if let (transition, firstTime) = self.enqueuedRecentTransitions.first { self.enqueuedRecentTransitions.remove(at: 0) var options = ListViewDeleteAndInsertOptions() if firstTime { options.insert(.PreferSynchronousDrawing) } else { options.insert(.AnimateInsertion) } self.recentListNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in }) } } private func enqueueTransition(_ transition: ThemeGridSearchContainerTransition, firstTime: Bool) { self.enqueuedTransitions.append((transition, firstTime)) if self.validLayout != nil { while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } } } private func dequeueTransition() { if let (transition, _) = self.enqueuedTransitions.first { self.enqueuedTransitions.remove(at: 0) let displayingResults = transition.displayingResults self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { [weak self] _ in if let strongSelf = self { strongSelf.gridNode.isHidden = !displayingResults strongSelf.recentListNode.isHidden = displayingResults strongSelf.dimNode.isHidden = displayingResults strongSelf.backgroundColor = strongSelf.presentationData.theme.chatList.backgroundColor strongSelf.emptyResultsTextNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.WebSearch_SearchNoResultsDescription(transition.query).0, font: Font.regular(15.0), textColor: strongSelf.presentationData.theme.list.freeTextColor) let emptyResults = displayingResults && transition.isEmpty strongSelf.emptyResultsTitleNode.isHidden = !emptyResults strongSelf.emptyResultsTextNode.isHidden = !emptyResults if let (layout, navigationBarHeight) = strongSelf.validLayout { strongSelf.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: .immediate) } } }) } } override func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition) let hadValidLayout = self.validLayout != nil self.validLayout = (layout, navigationBarHeight) let minSpacing: CGFloat = 8.0 let referenceImageSize: CGSize let screenWidth = min(layout.size.width, layout.size.height) if screenWidth >= 375.0 { referenceImageSize = CGSize(width: 108.0, height: 230.0) } else { referenceImageSize = CGSize(width: 91.0, height: 161.0) } let imageCount = Int((layout.size.width - minSpacing * 2.0) / (referenceImageSize.width + minSpacing)) let imageSize = referenceImageSize.aspectFilled(CGSize(width: floor((layout.size.width - CGFloat(imageCount + 1) * minSpacing) / CGFloat(imageCount)), height: referenceImageSize.height)) let spacing = floor((layout.size.width - CGFloat(imageCount) * imageSize.width) / CGFloat(imageCount + 1)) let topInset = navigationBarHeight transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: CGSize(width: layout.size.width, height: layout.size.height - topInset))) let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition) self.recentListNode.frame = CGRect(origin: CGPoint(), size: layout.size) self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) self.gridNode.frame = CGRect(x: 0.0, y: 0.0, width: layout.size.width, height: layout.size.height) self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: UIEdgeInsets(top: navigationBarHeight + spacing, left: layout.safeInsets.left, bottom: layout.insets(options: [.input]).bottom, right: layout.safeInsets.right), preloadSize: 300.0, type: .fixed(itemSize: imageSize, fillWidth: nil, lineSpacing: spacing, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) let padding: CGFloat = 16.0 let emptyTitleSize = self.emptyResultsTitleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) let emptyTextSize = self.emptyResultsTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0, height: CGFloat.greatestFiniteMagnitude)) let insets = layout.insets(options: [.input]) let emptyTextSpacing: CGFloat = 8.0 let emptyTotalHeight = emptyTitleSize.height + emptyTextSize.height + emptyTextSpacing let emptyTitleY = navigationBarHeight + floorToScreenPixels((layout.size.height - navigationBarHeight - max(insets.bottom, layout.intrinsicInsets.bottom) - emptyTotalHeight) / 2.0) transition.updateFrame(node: self.emptyResultsTitleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTitleSize.width) / 2.0, y: emptyTitleY), size: emptyTitleSize)) transition.updateFrame(node: self.emptyResultsTextNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + padding + (layout.size.width - layout.safeInsets.left - layout.safeInsets.right - padding * 2.0 - emptyTextSize.width) / 2.0, y: emptyTitleY + emptyTitleSize.height + emptyTextSpacing), size: emptyTextSize)) if !hadValidLayout { while !self.enqueuedRecentTransitions.isEmpty { self.dequeueRecentTransition() } while !self.enqueuedTransitions.isEmpty { self.dequeueTransition() } } } private func clearRecentSearch() { let _ = (clearRecentlySearchedPeers(postbox: self.context.account.postbox) |> deliverOnMainQueue).start() } override func scrollToTop() { if !self.gridNode.isHidden { self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: GridNodeScrollToItem(index: 0, position: .top(0.0), transition: .animated(duration: 0.25, curve: .easeInOut), directionHint: .up, adjustForSection: true, adjustForTopInset: true), updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in }) } else { self.recentListNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in }) } } }