mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
776 lines
39 KiB
Swift
776 lines
39 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
import Display
|
|
import TelegramCore
|
|
import LegacyComponents
|
|
import TelegramPresentationData
|
|
import TelegramUIPreferences
|
|
import MergeLists
|
|
import AccountContext
|
|
import GalleryUI
|
|
import ChatListSearchItemHeader
|
|
import SegmentedControlNode
|
|
import AppBundle
|
|
|
|
private struct WebSearchContextResultStableId: Hashable {
|
|
let result: ChatContextResult
|
|
|
|
func hash(into hasher: inout Hasher) {
|
|
hasher.combine(result.id)
|
|
}
|
|
|
|
static func ==(lhs: WebSearchContextResultStableId, rhs: WebSearchContextResultStableId) -> Bool {
|
|
return lhs.result == rhs.result
|
|
}
|
|
}
|
|
|
|
private struct WebSearchEntry: Comparable, Identifiable {
|
|
let index: Int
|
|
let result: ChatContextResult
|
|
|
|
var stableId: WebSearchContextResultStableId {
|
|
return WebSearchContextResultStableId(result: self.result)
|
|
}
|
|
|
|
static func ==(lhs: WebSearchEntry, rhs: WebSearchEntry) -> Bool {
|
|
return lhs.index == rhs.index && lhs.result == rhs.result
|
|
}
|
|
|
|
static func <(lhs: WebSearchEntry, rhs: WebSearchEntry) -> Bool {
|
|
return lhs.index < rhs.index
|
|
}
|
|
|
|
func item(account: Account, theme: PresentationTheme, interfaceState: WebSearchInterfaceState, controllerInteraction: WebSearchControllerInteraction) -> GridItem {
|
|
return WebSearchItem(account: account, theme: theme, interfaceState: interfaceState, result: self.result, controllerInteraction: controllerInteraction)
|
|
}
|
|
}
|
|
|
|
private struct WebSearchTransition {
|
|
let deleteItems: [Int]
|
|
let insertItems: [GridNodeInsertItem]
|
|
let updateItems: [GridNodeUpdateItem]
|
|
let entryCount: Int
|
|
let hasMore: Bool
|
|
}
|
|
|
|
private func preparedTransition(from fromEntries: [WebSearchEntry], to toEntries: [WebSearchEntry], hasMore: Bool, account: Account, theme: PresentationTheme, interfaceState: WebSearchInterfaceState, controllerInteraction: WebSearchControllerInteraction) -> WebSearchTransition {
|
|
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
|
|
|
|
let insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(account: account, theme: theme, interfaceState: interfaceState, controllerInteraction: controllerInteraction), previousIndex: $0.2) }
|
|
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, interfaceState: interfaceState, controllerInteraction: controllerInteraction)) }
|
|
|
|
return WebSearchTransition(deleteItems: deleteIndices, insertItems: insertions, updateItems: updates, entryCount: toEntries.count, hasMore: hasMore)
|
|
}
|
|
|
|
private func gridNodeLayoutForContainerLayout(size: CGSize) -> GridNodeLayoutType {
|
|
let side = floorToScreenPixels((size.width - 3.0) / 4.0)
|
|
return .fixed(itemSize: CGSize(width: side, height: side), fillWidth: true, lineSpacing: 1.0, itemSpacing: 1.0)
|
|
}
|
|
|
|
|
|
private struct WebSearchRecentQueryStableId: Hashable {
|
|
let query: String
|
|
|
|
var hashValue: Int {
|
|
return query.hashValue
|
|
}
|
|
|
|
static func ==(lhs: WebSearchRecentQueryStableId, rhs: WebSearchRecentQueryStableId) -> Bool {
|
|
return lhs.query == rhs.query
|
|
}
|
|
}
|
|
|
|
private struct WebSearchRecentQueryEntry: Comparable, Identifiable {
|
|
let index: Int
|
|
let query: String
|
|
|
|
var stableId: WebSearchRecentQueryStableId {
|
|
return WebSearchRecentQueryStableId(query: self.query)
|
|
}
|
|
|
|
static func ==(lhs: WebSearchRecentQueryEntry, rhs: WebSearchRecentQueryEntry) -> Bool {
|
|
return lhs.index == rhs.index && lhs.query == rhs.query
|
|
}
|
|
|
|
static func <(lhs: WebSearchRecentQueryEntry, rhs: WebSearchRecentQueryEntry) -> Bool {
|
|
return lhs.index < rhs.index
|
|
}
|
|
|
|
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: WebSearchControllerInteraction, header: ListViewItemHeader) -> ListViewItem {
|
|
return WebSearchRecentQueryItem(account: account, theme: theme, strings: strings, query: self.query, tapped: { query in
|
|
controllerInteraction.setSearchQuery(query)
|
|
}, deleted: { query in
|
|
controllerInteraction.deleteRecentQuery(query)
|
|
}, header: header)
|
|
}
|
|
}
|
|
|
|
private struct WebSearchRecentTransition {
|
|
let deletions: [ListViewDeleteItem]
|
|
let insertions: [ListViewInsertItem]
|
|
let updates: [ListViewUpdateItem]
|
|
}
|
|
|
|
private func preparedWebSearchRecentTransition(from fromEntries: [WebSearchRecentQueryEntry], to toEntries: [WebSearchRecentQueryEntry], account: Account, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: WebSearchControllerInteraction, header: ListViewItemHeader) -> WebSearchRecentTransition {
|
|
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, controllerInteraction: controllerInteraction, 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, controllerInteraction: controllerInteraction, header: header), directionHint: nil) }
|
|
|
|
return WebSearchRecentTransition(deletions: deletions, insertions: insertions, updates: updates)
|
|
}
|
|
|
|
class WebSearchControllerNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
private let peer: Peer?
|
|
private var theme: PresentationTheme
|
|
private var strings: PresentationStrings
|
|
private var presentationData: PresentationData
|
|
private let mode: WebSearchMode
|
|
|
|
private let controllerInteraction: WebSearchControllerInteraction
|
|
private var webSearchInterfaceState: WebSearchInterfaceState
|
|
private let webSearchInterfaceStatePromise: ValuePromise<WebSearchInterfaceState>
|
|
|
|
private let segmentedBackgroundNode: ASDisplayNode
|
|
private let segmentedSeparatorNode: ASDisplayNode
|
|
private let segmentedControlNode: SegmentedControlNode
|
|
|
|
private let toolbarBackgroundNode: ASDisplayNode
|
|
private let toolbarSeparatorNode: ASDisplayNode
|
|
private let cancelButton: HighlightableButtonNode
|
|
private let sendButton: HighlightableButtonNode
|
|
private let badgeNode: WebSearchBadgeNode
|
|
|
|
private let attributionNode: ASImageNode
|
|
|
|
private let recentQueriesPlaceholder: ImmediateTextNode
|
|
private let recentQueriesNode: ListView
|
|
private var enqueuedRecentTransitions: [(WebSearchRecentTransition, Bool)] = []
|
|
|
|
private var gridNode: GridNode
|
|
private var enqueuedTransitions: [(WebSearchTransition, Bool)] = []
|
|
private var dequeuedInitialTransitionOnLayout = false
|
|
|
|
private var currentExternalResults: ChatContextResultCollection?
|
|
private var currentProcessedResults: ChatContextResultCollection?
|
|
private var currentEntries: [WebSearchEntry]?
|
|
private var hasMore = false
|
|
private var isLoadingMore = false
|
|
|
|
private let hiddenMediaId = Promise<String?>(nil)
|
|
private var hiddenMediaDisposable: Disposable?
|
|
|
|
private let results = ValuePromise<ChatContextResultCollection?>(nil, ignoreRepeated: true)
|
|
|
|
private let disposable = MetaDisposable()
|
|
private let loadMoreDisposable = MetaDisposable()
|
|
|
|
private var recentDisposable: Disposable?
|
|
|
|
private var containerLayout: (ContainerViewLayout, CGFloat)?
|
|
|
|
var requestUpdateInterfaceState: (Bool, (WebSearchInterfaceState) -> WebSearchInterfaceState) -> Void = { _, _ in }
|
|
var cancel: (() -> Void)?
|
|
var dismissInput: (() -> Void)?
|
|
|
|
init(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchControllerInteraction, peer: Peer?, mode: WebSearchMode) {
|
|
self.context = context
|
|
self.theme = presentationData.theme
|
|
self.strings = presentationData.strings
|
|
self.presentationData = presentationData
|
|
self.controllerInteraction = controllerInteraction
|
|
self.peer = peer
|
|
self.mode = mode
|
|
|
|
self.webSearchInterfaceState = WebSearchInterfaceState(presentationData: context.sharedContext.currentPresentationData.with { $0 })
|
|
self.webSearchInterfaceStatePromise = ValuePromise(self.webSearchInterfaceState, ignoreRepeated: true)
|
|
|
|
self.segmentedBackgroundNode = ASDisplayNode()
|
|
self.segmentedSeparatorNode = ASDisplayNode()
|
|
|
|
let items = [
|
|
strings.WebSearch_Images,
|
|
strings.WebSearch_GIFs
|
|
]
|
|
self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: items.map { SegmentedControlItem(title: $0) }, selectedIndex: 0)
|
|
|
|
self.toolbarBackgroundNode = ASDisplayNode()
|
|
self.toolbarSeparatorNode = ASDisplayNode()
|
|
|
|
self.attributionNode = ASImageNode()
|
|
|
|
self.cancelButton = HighlightableButtonNode()
|
|
self.sendButton = HighlightableButtonNode()
|
|
|
|
self.badgeNode = WebSearchBadgeNode(theme: theme)
|
|
|
|
self.gridNode = GridNode()
|
|
self.gridNode.backgroundColor = theme.list.plainBackgroundColor
|
|
|
|
self.recentQueriesNode = ListView()
|
|
self.recentQueriesNode.backgroundColor = theme.list.plainBackgroundColor
|
|
|
|
self.recentQueriesPlaceholder = ImmediateTextNode()
|
|
|
|
super.init()
|
|
|
|
self.setViewBlock({
|
|
return UITracingLayerView()
|
|
})
|
|
|
|
self.addSubnode(self.gridNode)
|
|
self.addSubnode(self.recentQueriesNode)
|
|
self.addSubnode(self.segmentedBackgroundNode)
|
|
self.addSubnode(self.segmentedSeparatorNode)
|
|
if case .media = mode {
|
|
self.addSubnode(self.segmentedControlNode)
|
|
}
|
|
self.addSubnode(self.toolbarBackgroundNode)
|
|
self.addSubnode(self.toolbarSeparatorNode)
|
|
self.addSubnode(self.cancelButton)
|
|
self.addSubnode(self.sendButton)
|
|
self.addSubnode(self.attributionNode)
|
|
self.addSubnode(self.badgeNode)
|
|
|
|
self.segmentedControlNode.selectedIndexChanged = { [weak self] index in
|
|
if let strongSelf = self, let scope = WebSearchScope(rawValue: Int32(index)) {
|
|
let _ = updateWebSearchSettingsInteractively(accountManager: strongSelf.context.sharedContext.accountManager) { _ -> WebSearchSettings in
|
|
return WebSearchSettings(scope: scope)
|
|
}.start()
|
|
strongSelf.requestUpdateInterfaceState(true) { current in
|
|
return current.withUpdatedScope(scope)
|
|
}
|
|
}
|
|
}
|
|
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
|
|
self.sendButton.addTarget(self, action: #selector(self.sendPressed), forControlEvents: .touchUpInside)
|
|
|
|
self.applyPresentationData()
|
|
|
|
self.disposable.set((combineLatest(self.results.get(), self.webSearchInterfaceStatePromise.get())
|
|
|> deliverOnMainQueue).start(next: { [weak self] results, interfaceState in
|
|
if let strongSelf = self {
|
|
strongSelf.updateInternalResults(results, interfaceState: interfaceState)
|
|
}
|
|
}))
|
|
|
|
let previousRecentItems = Atomic<[WebSearchRecentQueryEntry]?>(value: nil)
|
|
self.recentDisposable = (combineLatest(webSearchRecentQueries(postbox: self.context.account.postbox), self.webSearchInterfaceStatePromise.get())
|
|
|> deliverOnMainQueue).start(next: { [weak self] queries, interfaceState in
|
|
if let strongSelf = self {
|
|
var entries: [WebSearchRecentQueryEntry] = []
|
|
for i in 0 ..< queries.count {
|
|
entries.append(WebSearchRecentQueryEntry(index: i, query: queries[i]))
|
|
}
|
|
|
|
let header = ChatListSearchItemHeader(type: .recentPeers, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, actionTitle: interfaceState.presentationData.strings.WebSearch_RecentSectionClear, action: {
|
|
_ = clearRecentWebSearchQueries(postbox: strongSelf.context.account.postbox).start()
|
|
})
|
|
|
|
let previousEntries = previousRecentItems.swap(entries)
|
|
|
|
let transition = preparedWebSearchRecentTransition(from: previousEntries ?? [], to: entries, account: strongSelf.context.account, theme: interfaceState.presentationData.theme, strings: interfaceState.presentationData.strings, controllerInteraction: strongSelf.controllerInteraction, header: header)
|
|
strongSelf.enqueueRecentTransition(transition, firstTime: previousEntries == nil)
|
|
}
|
|
})
|
|
|
|
self.recentQueriesNode.beganInteractiveDragging = { [weak self] in
|
|
self?.dismissInput?()
|
|
}
|
|
|
|
|
|
self.gridNode.visibleItemsUpdated = { [weak self] visibleItems in
|
|
if let strongSelf = self, let bottom = visibleItems.bottom, let entries = strongSelf.currentEntries {
|
|
if bottom.0 >= entries.count - 8 {
|
|
strongSelf.loadMore()
|
|
}
|
|
}
|
|
}
|
|
self.gridNode.scrollingInitiated = { [weak self] in
|
|
self?.dismissInput?()
|
|
}
|
|
|
|
self.sendButton.highligthedChanged = { [weak self] highlighted in
|
|
if let strongSelf = self, strongSelf.badgeNode.alpha > 0.0 {
|
|
if highlighted {
|
|
strongSelf.badgeNode.layer.removeAnimation(forKey: "opacity")
|
|
strongSelf.badgeNode.alpha = 0.4
|
|
} else {
|
|
strongSelf.badgeNode.alpha = 1.0
|
|
strongSelf.badgeNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
}
|
|
|
|
self.hiddenMediaDisposable = (self.hiddenMediaId.get()
|
|
|> deliverOnMainQueue).start(next: { [weak self] id in
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction.hiddenMediaId = id
|
|
|
|
strongSelf.gridNode.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? WebSearchItemNode {
|
|
itemNode.updateHiddenMedia()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.disposable.dispose()
|
|
self.recentDisposable?.dispose()
|
|
self.loadMoreDisposable.dispose()
|
|
self.hiddenMediaDisposable?.dispose()
|
|
}
|
|
|
|
func updatePresentationData(theme: PresentationTheme, strings: PresentationStrings) {
|
|
let themeUpdated = theme !== self.theme
|
|
self.theme = theme
|
|
self.strings = strings
|
|
|
|
self.applyPresentationData(themeUpdated: themeUpdated)
|
|
}
|
|
|
|
func applyPresentationData(themeUpdated: Bool = true) {
|
|
self.cancelButton.setTitle(self.strings.Common_Cancel, with: Font.regular(17.0), with: self.theme.rootController.navigationBar.accentTextColor, for: .normal)
|
|
|
|
if let selectionState = self.controllerInteraction.selectionState {
|
|
let sendEnabled = selectionState.count() > 0
|
|
let color = sendEnabled ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.disabledButtonColor
|
|
self.sendButton.setTitle(self.strings.MediaPicker_Send, with: Font.medium(17.0), with: color, for: .normal)
|
|
}
|
|
|
|
if themeUpdated {
|
|
self.segmentedBackgroundNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor
|
|
self.segmentedSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor
|
|
self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme))
|
|
self.toolbarBackgroundNode.backgroundColor = self.theme.rootController.navigationBar.backgroundColor
|
|
self.toolbarSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor
|
|
|
|
self.attributionNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Grid/Giphy"), color: self.theme.list.itemSecondaryTextColor)
|
|
}
|
|
}
|
|
|
|
func animateIn(completion: (() -> Void)? = nil) {
|
|
self.layer.animatePosition(from: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), to: self.layer.position, duration: 0.5, timingFunction: kCAMediaTimingFunctionSpring)
|
|
}
|
|
|
|
func animateOut(completion: (() -> Void)? = nil) {
|
|
self.layer.animatePosition(from: self.layer.position, to: CGPoint(x: self.layer.position.x, y: self.layer.position.y + self.layer.bounds.size.height), duration: 0.2, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, removeOnCompletion: false, completion: { _ in
|
|
completion?()
|
|
})
|
|
}
|
|
|
|
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
|
self.containerLayout = (layout, navigationBarHeight)
|
|
|
|
var insets = layout.insets(options: [.input])
|
|
insets.top += navigationBarHeight
|
|
|
|
let segmentedHeight: CGFloat = self.segmentedControlNode.supernode != nil ? 44.0 : 5.0
|
|
let panelY: CGFloat = insets.top - UIScreenPixel - 4.0
|
|
|
|
transition.updateFrame(node: self.segmentedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: layout.size.width, height: segmentedHeight)))
|
|
transition.updateFrame(node: self.segmentedSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY + segmentedHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
|
|
|
let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 10.0 * 2.0), transition: transition)
|
|
transition.updateFrame(node: self.segmentedControlNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - controlSize.width) / 2.0), y: panelY + 5.0), size: controlSize))
|
|
|
|
insets.top -= 4.0
|
|
|
|
let toolbarHeight: CGFloat = 44.0
|
|
let toolbarY = layout.size.height - toolbarHeight - insets.bottom
|
|
transition.updateFrame(node: self.toolbarBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: toolbarY), size: CGSize(width: layout.size.width, height: toolbarHeight + insets.bottom)))
|
|
transition.updateFrame(node: self.toolbarSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: toolbarY), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
|
|
|
|
if let image = self.attributionNode.image {
|
|
transition.updateFrame(node: self.attributionNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - image.size.width) / 2.0), y: toolbarY + floor((toolbarHeight - image.size.height) / 2.0)), size: image.size))
|
|
transition.updateAlpha(node: self.attributionNode, alpha: self.webSearchInterfaceState.state?.scope == .gifs ? 1.0 : 0.0)
|
|
}
|
|
|
|
let toolbarPadding: CGFloat = 10.0
|
|
let cancelSize = self.cancelButton.measure(CGSize(width: layout.size.width, height: toolbarHeight))
|
|
transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: toolbarPadding + layout.safeInsets.left, y: toolbarY), size: CGSize(width: cancelSize.width, height: toolbarHeight)))
|
|
|
|
let sendSize = self.sendButton.measure(CGSize(width: layout.size.width, height: toolbarHeight))
|
|
let sendFrame = CGRect(origin: CGPoint(x: layout.size.width - toolbarPadding - layout.safeInsets.right - sendSize.width, y: toolbarY), size: CGSize(width: sendSize.width, height: toolbarHeight))
|
|
transition.updateFrame(node: self.sendButton, frame: sendFrame)
|
|
|
|
if let selectionState = self.controllerInteraction.selectionState {
|
|
self.sendButton.isHidden = false
|
|
|
|
let previousSendEnabled = self.sendButton.isEnabled
|
|
let sendEnabled = selectionState.count() > 0
|
|
self.sendButton.isEnabled = sendEnabled
|
|
if sendEnabled != previousSendEnabled {
|
|
let color = sendEnabled ? self.theme.rootController.navigationBar.accentTextColor : self.theme.rootController.navigationBar.disabledButtonColor
|
|
self.sendButton.setTitle(self.strings.MediaPicker_Send, with: Font.medium(17.0), with: color, for: .normal)
|
|
}
|
|
|
|
let selectedCount = selectionState.count()
|
|
let badgeText = String(selectedCount)
|
|
if selectedCount > 0 && (self.badgeNode.text != badgeText || self.badgeNode.alpha < 1.0) {
|
|
if transition.isAnimated {
|
|
var incremented = true
|
|
if let previousCount = Int(self.badgeNode.text) {
|
|
incremented = selectedCount > previousCount || self.badgeNode.alpha < 1.0
|
|
}
|
|
self.badgeNode.animateBump(incremented: incremented)
|
|
}
|
|
self.badgeNode.text = badgeText
|
|
|
|
let badgeSize = self.badgeNode.measure(layout.size)
|
|
transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: CGPoint(x: sendFrame.minX - badgeSize.width - 6.0, y: toolbarY + 11.0), size: badgeSize))
|
|
transition.updateAlpha(node: self.badgeNode, alpha: 1.0)
|
|
} else if selectedCount == 0 {
|
|
if transition.isAnimated {
|
|
self.badgeNode.animateOut()
|
|
}
|
|
let badgeSize = CGSize(width: 22.0, height: 22.0)
|
|
transition.updateFrame(node: self.badgeNode, frame: CGRect(origin: CGPoint(x: sendFrame.minX - badgeSize.width - 6.0, y: toolbarY + 11.0), size: badgeSize))
|
|
transition.updateAlpha(node: self.badgeNode, alpha: 0.0)
|
|
}
|
|
} else {
|
|
self.sendButton.isHidden = true
|
|
}
|
|
|
|
let previousBounds = self.gridNode.bounds
|
|
self.gridNode.bounds = CGRect(x: previousBounds.origin.x, y: previousBounds.origin.y, width: layout.size.width, height: layout.size.height)
|
|
self.gridNode.position = CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)
|
|
|
|
insets.top += segmentedHeight
|
|
insets.bottom += toolbarHeight
|
|
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, preloadSize: 400.0, type: gridNodeLayoutForContainerLayout(size: layout.size)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
|
|
|
var duration: Double = 0.0
|
|
var curve: UInt = 0
|
|
switch transition {
|
|
case .immediate:
|
|
break
|
|
case let .animated(animationDuration, animationCurve):
|
|
duration = animationDuration
|
|
switch animationCurve {
|
|
case .easeInOut, .custom:
|
|
break
|
|
case .spring:
|
|
curve = 7
|
|
}
|
|
}
|
|
|
|
let listViewCurve: ListViewAnimationCurve
|
|
if curve == 7 {
|
|
listViewCurve = .Spring(duration: duration)
|
|
} else {
|
|
listViewCurve = .Default(duration: duration)
|
|
}
|
|
|
|
self.recentQueriesNode.frame = CGRect(origin: CGPoint(), size: layout.size)
|
|
self.recentQueriesNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: layout.size, insets: insets, duration: duration, curve: listViewCurve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
|
|
|
|
if !self.dequeuedInitialTransitionOnLayout {
|
|
self.dequeuedInitialTransitionOnLayout = true
|
|
self.dequeueTransition()
|
|
}
|
|
}
|
|
|
|
func updateInterfaceState(_ interfaceState: WebSearchInterfaceState, animated: Bool) {
|
|
self.webSearchInterfaceState = interfaceState
|
|
self.webSearchInterfaceStatePromise.set(self.webSearchInterfaceState)
|
|
|
|
if let state = interfaceState.state {
|
|
self.segmentedControlNode.selectedIndex = Int(state.scope.rawValue)
|
|
}
|
|
|
|
if let validLayout = self.containerLayout {
|
|
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate)
|
|
}
|
|
}
|
|
|
|
func updateSelectionState(animated: Bool) {
|
|
self.gridNode.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? WebSearchItemNode {
|
|
itemNode.updateSelectionState(animated: animated)
|
|
}
|
|
}
|
|
|
|
if let validLayout = self.containerLayout {
|
|
self.containerLayoutUpdated(validLayout.0, navigationBarHeight: validLayout.1, transition: animated ? .animated(duration: 0.4, curve: .spring) : .immediate)
|
|
}
|
|
}
|
|
|
|
func updateResults(_ results: ChatContextResultCollection?, immediate: Bool = false) {
|
|
if self.currentExternalResults == results {
|
|
return
|
|
}
|
|
let previousResults = self.currentExternalResults
|
|
self.currentExternalResults = results
|
|
self.currentProcessedResults = results
|
|
|
|
self.isLoadingMore = false
|
|
self.loadMoreDisposable.set(nil)
|
|
|
|
if immediate && previousResults?.query == results?.query && previousResults?.botId != results?.botId {
|
|
let previousNode = self.gridNode
|
|
|
|
let gridNode = GridNode()
|
|
gridNode.backgroundColor = theme.list.plainBackgroundColor
|
|
gridNode.frame = previousNode.frame
|
|
|
|
gridNode.visibleItemsUpdated = { [weak self] visibleItems in
|
|
if let strongSelf = self, let bottom = visibleItems.bottom, let entries = strongSelf.currentEntries {
|
|
if bottom.0 >= entries.count - 8 {
|
|
strongSelf.loadMore()
|
|
}
|
|
}
|
|
}
|
|
gridNode.scrollingInitiated = { [weak self] in
|
|
self?.dismissInput?()
|
|
}
|
|
|
|
self.insertSubnode(gridNode, belowSubnode: self.recentQueriesNode)
|
|
self.gridNode = gridNode
|
|
self.currentEntries = nil
|
|
let directionMultiplier: CGFloat
|
|
if let state = self.webSearchInterfaceState.state {
|
|
switch state.scope {
|
|
case .images:
|
|
directionMultiplier = 1.0
|
|
case .gifs:
|
|
directionMultiplier = -1.0
|
|
}
|
|
} else {
|
|
directionMultiplier = 1.0
|
|
}
|
|
|
|
previousNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -directionMultiplier * self.bounds.width, y: 0.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { [weak previousNode] _ in
|
|
previousNode?.removeFromSupernode()
|
|
})
|
|
gridNode.layer.animatePosition(from: CGPoint(x: directionMultiplier * bounds.width, y: 0.0), to: CGPoint(), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
|
} else if previousResults?.botId != results?.botId || previousResults?.query != results?.query {
|
|
self.scrollToTop()
|
|
}
|
|
|
|
self.results.set(results)
|
|
}
|
|
|
|
func clearResults() {
|
|
self.results.set(nil)
|
|
}
|
|
|
|
private func loadMore() {
|
|
guard !self.isLoadingMore, let currentProcessedResults = self.currentProcessedResults, currentProcessedResults.results.count > 55, let nextOffset = currentProcessedResults.nextOffset else {
|
|
return
|
|
}
|
|
self.isLoadingMore = true
|
|
self.loadMoreDisposable.set((requestChatContextResults(account: self.context.account, botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, location: .single(currentProcessedResults.geoPoint), offset: nextOffset)
|
|
|> deliverOnMainQueue).start(next: { [weak self] nextResults in
|
|
guard let strongSelf = self, let nextResults = nextResults else {
|
|
return
|
|
}
|
|
strongSelf.isLoadingMore = false
|
|
var results: [ChatContextResult] = []
|
|
var existingIds = Set<String>()
|
|
for result in currentProcessedResults.results {
|
|
results.append(result)
|
|
existingIds.insert(result.id)
|
|
}
|
|
for result in nextResults.results {
|
|
if !existingIds.contains(result.id) {
|
|
results.append(result)
|
|
existingIds.insert(result.id)
|
|
}
|
|
}
|
|
let mergedResults = ChatContextResultCollection(botId: currentProcessedResults.botId, peerId: currentProcessedResults.peerId, query: currentProcessedResults.query, geoPoint: currentProcessedResults.geoPoint, queryId: nextResults.queryId, nextOffset: nextResults.nextOffset, presentation: currentProcessedResults.presentation, switchPeer: currentProcessedResults.switchPeer, results: results, cacheTimeout: currentProcessedResults.cacheTimeout)
|
|
strongSelf.currentProcessedResults = mergedResults
|
|
strongSelf.results.set(mergedResults)
|
|
}))
|
|
}
|
|
|
|
private func updateInternalResults(_ results: ChatContextResultCollection?, interfaceState: WebSearchInterfaceState) {
|
|
var entries: [WebSearchEntry] = []
|
|
var hasMore = false
|
|
if let state = interfaceState.state, state.query.isEmpty {
|
|
} else if let results = results {
|
|
hasMore = results.nextOffset != nil
|
|
|
|
var index = 0
|
|
var resultIds = Set<WebSearchContextResultStableId>()
|
|
for result in results.results {
|
|
let entry = WebSearchEntry(index: index, result: result)
|
|
if resultIds.contains(entry.stableId) {
|
|
continue
|
|
} else {
|
|
resultIds.insert(entry.stableId)
|
|
}
|
|
entries.append(entry)
|
|
index += 1
|
|
}
|
|
}
|
|
|
|
let firstTime = self.currentEntries == nil
|
|
let transition = preparedTransition(from: self.currentEntries ?? [], to: entries, hasMore: hasMore, account: self.context.account, theme: interfaceState.presentationData.theme, interfaceState: interfaceState, controllerInteraction: self.controllerInteraction)
|
|
self.currentEntries = entries
|
|
|
|
self.enqueueTransition(transition, firstTime: firstTime)
|
|
}
|
|
|
|
private func enqueueTransition(_ transition: WebSearchTransition, firstTime: Bool) {
|
|
self.enqueuedTransitions.append((transition, firstTime))
|
|
|
|
if self.containerLayout != nil {
|
|
while !self.enqueuedTransitions.isEmpty {
|
|
self.dequeueTransition()
|
|
}
|
|
}
|
|
}
|
|
|
|
private func dequeueTransition() {
|
|
if let (transition, _) = self.enqueuedTransitions.first {
|
|
self.enqueuedTransitions.remove(at: 0)
|
|
|
|
if let state = self.webSearchInterfaceState.state {
|
|
self.recentQueriesNode.isHidden = !state.query.isEmpty
|
|
}
|
|
|
|
self.hasMore = transition.hasMore
|
|
self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deleteItems, insertItems: transition.insertItems, updateItems: transition.updateItems, scrollToItem: nil, updateLayout: nil, itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, synchronousLoads: true), completion: { _ in })
|
|
}
|
|
}
|
|
|
|
private func enqueueRecentTransition(_ transition: WebSearchRecentTransition, firstTime: Bool) {
|
|
enqueuedRecentTransitions.append((transition, firstTime))
|
|
|
|
if self.containerLayout != 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.recentQueriesNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
|
|
})
|
|
}
|
|
}
|
|
|
|
@objc private func cancelPressed() {
|
|
self.cancel?()
|
|
}
|
|
|
|
@objc private func sendPressed() {
|
|
if let results = self.currentExternalResults {
|
|
self.controllerInteraction.sendSelected(results, nil)
|
|
}
|
|
self.cancel?()
|
|
}
|
|
|
|
func scrollToTop(animated: Bool = false) {
|
|
self.gridNode.scrollView.setContentOffset(CGPoint(x: 0.0, y: -self.gridNode.scrollView.contentInset.top), animated: animated)
|
|
}
|
|
|
|
func openResult(currentResult: ChatContextResult, present: @escaping (ViewController, Any?) -> Void) {
|
|
self.view.endEditing(true)
|
|
|
|
if self.controllerInteraction.selectionState != nil {
|
|
if let state = self.webSearchInterfaceState.state, state.scope == .images {
|
|
if let results = self.currentProcessedResults?.results {
|
|
presentLegacyWebSearchGallery(context: self.context, peer: self.peer, presentationData: self.presentationData, results: results, current: currentResult, selectionContext: self.controllerInteraction.selectionState, editingContext: self.controllerInteraction.editingState, updateHiddenMedia: { [weak self] id in
|
|
self?.hiddenMediaId.set(.single(id))
|
|
}, initialLayout: self.containerLayout?.0, transitionHostView: { [weak self] in
|
|
return self?.gridNode.view
|
|
}, transitionView: { [weak self] result in
|
|
return self?.transitionNode(for: result)?.transitionView()
|
|
}, completed: { [weak self] result in
|
|
if let strongSelf = self, let results = strongSelf.currentExternalResults {
|
|
strongSelf.controllerInteraction.sendSelected(results, result)
|
|
strongSelf.cancel?()
|
|
}
|
|
}, present: present)
|
|
}
|
|
} else {
|
|
if let results = self.currentProcessedResults?.results {
|
|
var entries: [WebSearchGalleryEntry] = []
|
|
var centralIndex: Int = 0
|
|
for i in 0 ..< results.count {
|
|
entries.append(WebSearchGalleryEntry(result: results[i]))
|
|
if results[i] == currentResult {
|
|
centralIndex = i
|
|
}
|
|
}
|
|
|
|
let controller = WebSearchGalleryController(context: self.context, peer: self.peer, selectionState: self.controllerInteraction.selectionState, editingState: self.controllerInteraction.editingState, entries: entries, centralIndex: centralIndex, replaceRootController: { (controller, _) in
|
|
|
|
}, baseNavigationController: nil, sendCurrent: { [weak self] result in
|
|
if let strongSelf = self, let results = strongSelf.currentExternalResults {
|
|
strongSelf.controllerInteraction.sendSelected(results, result)
|
|
strongSelf.cancel?()
|
|
}
|
|
})
|
|
self.hiddenMediaId.set((controller.hiddenMedia |> deliverOnMainQueue)
|
|
|> map { entry in
|
|
return entry?.result.id
|
|
})
|
|
present(controller, WebSearchGalleryControllerPresentationArguments(transitionArguments: { [weak self] entry -> GalleryTransitionArguments? in
|
|
if let strongSelf = self {
|
|
var transitionNode: WebSearchItemNode?
|
|
strongSelf.gridNode.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == entry.result.id {
|
|
transitionNode = itemNode
|
|
}
|
|
}
|
|
if let transitionNode = transitionNode {
|
|
return GalleryTransitionArguments(transitionNode: (transitionNode, { [weak transitionNode] in
|
|
return (transitionNode?.transitionView().snapshotContentTree(unhide: true), nil)
|
|
}), addToTransitionSurface: { view in
|
|
if let strongSelf = self {
|
|
strongSelf.gridNode.view.superview?.insertSubview(view, aboveSubview: strongSelf.gridNode.view)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
return nil
|
|
}))
|
|
}
|
|
}
|
|
} else {
|
|
presentLegacyWebSearchEditor(context: self.context, theme: self.theme, result: currentResult, initialLayout: self.containerLayout?.0, updateHiddenMedia: { [weak self] id in
|
|
self?.hiddenMediaId.set(.single(id))
|
|
}, transitionHostView: { [weak self] in
|
|
return self?.gridNode.view
|
|
}, transitionView: { [weak self] result in
|
|
return self?.transitionNode(for: result)?.transitionView()
|
|
}, completed: { [weak self] result in
|
|
if let strongSelf = self {
|
|
strongSelf.controllerInteraction.avatarCompleted(result)
|
|
strongSelf.cancel?()
|
|
}
|
|
}, present: present)
|
|
}
|
|
}
|
|
|
|
private func transitionNode(for result: ChatContextResult) -> WebSearchItemNode? {
|
|
var transitionNode: WebSearchItemNode?
|
|
self.gridNode.forEachItemNode { itemNode in
|
|
if let itemNode = itemNode as? WebSearchItemNode, itemNode.item?.result.id == result.id {
|
|
transitionNode = itemNode
|
|
}
|
|
}
|
|
return transitionNode
|
|
}
|
|
}
|