import Foundation import UIKit import AsyncDisplayKit import Display import SwiftSignalKit import Postbox import TelegramCore import TelegramPresentationData import AccountContext import AppBundle import ChatControllerInteraction import MultiplexedVideoNode import ChatPresentationInterfaceState import EntityKeyboardGifContent final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode { private let context: AccountContext private let interaction: ChatEntityKeyboardInputNode.Interaction private let inputNodeInteraction: ChatMediaInputNodeInteraction private var theme: PresentationTheme private var strings: PresentationStrings private var multiplexedNode: MultiplexedVideoNode? private let notFoundNode: ASImageNode private let notFoundLabel: ImmediateTextNode private var nextOffset: (String, String)? private var isLoadingNextResults: Bool = false private var validLayout: CGSize? private let trendingPromise: Promise private let searchDisposable = MetaDisposable() private let _ready = Promise() var ready: Signal { return self._ready.get() } var deactivateSearchBar: (() -> Void)? var updateActivity: ((Bool) -> Void)? var requestUpdateQuery: ((String) -> Void)? var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)? private var hasInitialText = false init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, interaction: ChatEntityKeyboardInputNode.Interaction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise) { self.context = context self.interaction = interaction self.inputNodeInteraction = inputNodeInteraction self.trendingPromise = trendingPromise self.theme = theme self.strings = strings self.notFoundNode = ASImageNode() self.notFoundNode.displayWithoutProcessing = true self.notFoundNode.displaysAsynchronously = false self.notFoundNode.clipsToBounds = false self.notFoundLabel = ImmediateTextNode() self.notFoundLabel.displaysAsynchronously = false self.notFoundLabel.isUserInteractionEnabled = false self.notFoundNode.addSubnode(self.notFoundLabel) super.init() self.notFoundNode.isHidden = true self._ready.set(.single(Void())) self.addSubnode(self.notFoundNode) self.updateThemeAndStrings(theme: theme, strings: strings) } deinit { self.searchDisposable.dispose() } func updateText(_ text: String, languageCode: String?) { self.hasInitialText = true self.isLoadingNextResults = true let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError> if !text.isEmpty { signal = paneGifSearchForQuery(context: self.context, query: text, offset: "", updateActivity: self.updateActivity) |> map { result -> ([MultiplexedVideoNodeFile], String?)? in if let result = result { return (result.files, result.nextOffset) } else { return nil } } self.updateActivity?(true) } else { signal = self.trendingPromise.get() |> map { items -> ([MultiplexedVideoNodeFile], String?)? in if let items = items { return (items.files, nil) } else { return nil } } self.updateActivity?(false) } self.searchDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self, let (result, nextOffset) = result else { return } strongSelf.isLoadingNextResults = false if let nextOffset = nextOffset { strongSelf.nextOffset = (text, nextOffset) } else { strongSelf.nextOffset = nil } strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: result, isSearch: true, canLoadMore: false, isStale: false), synchronous: true, resetScrollingToOffset: nil) strongSelf.updateActivity?(false) strongSelf.notFoundNode.isHidden = text.isEmpty || !result.isEmpty })) } private func loadMore() { if self.isLoadingNextResults { return } guard let (text, nextOffsetValue) = self.nextOffset else { return } self.isLoadingNextResults = true let signal: Signal<([MultiplexedVideoNodeFile], String?)?, NoError> signal = paneGifSearchForQuery(context: self.context, query: text, offset: nextOffsetValue, updateActivity: self.updateActivity) |> map { result -> ([MultiplexedVideoNodeFile], String?)? in if let result = result { return (result.files, result.nextOffset) } else { return nil } } self.searchDisposable.set((signal |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self, let (result, nextOffset) = result else { return } var files = strongSelf.multiplexedNode?.files.trending ?? [] var currentIds = Set(files.map { $0.file.media.fileId }) for item in result { if currentIds.contains(item.file.media.fileId) { continue } currentIds.insert(item.file.media.fileId) files.append(item) } strongSelf.isLoadingNextResults = false if let nextOffset = nextOffset { strongSelf.nextOffset = (text, nextOffset) } else { strongSelf.nextOffset = nil } strongSelf.multiplexedNode?.setFiles(files: MultiplexedVideoNodeFiles(saved: [], trending: files, isSearch: true, canLoadMore: false, isStale: false), synchronous: true, resetScrollingToOffset: nil) strongSelf.notFoundNode.isHidden = text.isEmpty || !files.isEmpty })) } func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) { self.notFoundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/GifsNotFoundIcon"), color: theme.list.freeMonoIconColor) self.notFoundLabel.attributedText = NSAttributedString(string: strings.Gif_NoGifsFound, font: Font.medium(14.0), textColor: theme.list.freeTextColor) } func updatePreviewing(animated: Bool) { } func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? { if let multiplexedNode = self.multiplexedNode, let file = multiplexedNode.fileAt(point: point.offsetBy(dx: -multiplexedNode.frame.minX, dy: -multiplexedNode.frame.minY)) { return (self, file) } else { return nil } } func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) { let firstLayout = self.validLayout == nil self.validLayout = size if let image = self.notFoundNode.image { let areaHeight = size.height - inputHeight let labelSize = self.notFoundLabel.updateLayout(CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude)) transition.updateFrame(node: self.notFoundNode, frame: CGRect(origin: CGPoint(x: floor((size.width - image.size.width) / 2.0), y: floor((areaHeight - image.size.height - labelSize.height) / 2.0)), size: image.size)) transition.updateFrame(node: self.notFoundLabel, frame: CGRect(origin: CGPoint(x: floor((image.size.width - labelSize.width) / 2.0), y: image.size.height + 8.0), size: labelSize)) } if let multiplexedNode = self.multiplexedNode { multiplexedNode.topInset = 0.0 multiplexedNode.bottomInset = 0.0 let nodeFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: size.width, height: size.height)) transition.updateFrame(layer: multiplexedNode.layer, frame: nodeFrame) multiplexedNode.updateLayout(theme: self.theme, strings: self.strings, size: nodeFrame.size, transition: transition) } if firstLayout && !self.hasInitialText { self.updateText("", languageCode: nil) } } override func willEnterHierarchy() { super.willEnterHierarchy() if self.multiplexedNode == nil { let multiplexedNode = MultiplexedVideoNode(account: self.context.account, theme: self.theme, strings: self.strings) self.multiplexedNode = multiplexedNode if let layout = self.validLayout { multiplexedNode.frame = CGRect(origin: CGPoint(), size: layout) } self.addSubnode(multiplexedNode) multiplexedNode.fileSelected = { [weak self] file, sourceNode, sourceRect in if let (collection, result) = file.contextResult { let _ = self?.interaction.sendBotContextResultAsGif(collection, result, sourceNode.view, sourceRect, false, false) } else { let _ = self?.interaction.sendGif(file.file, sourceNode.view, sourceRect, false, false) } } multiplexedNode.fileContextMenu = { [weak self] fileReference, sourceNode, sourceRect, gesture, isSaved in self?.openGifContextMenu?(fileReference, sourceNode, sourceRect, gesture, isSaved) } multiplexedNode.didScroll = { [weak self] offset, height in guard let strongSelf = self, let multiplexedNode = strongSelf.multiplexedNode else { return } strongSelf.deactivateSearchBar?() if offset >= height - multiplexedNode.bounds.height - 200.0 { strongSelf.loadMore() } } multiplexedNode.reactionSelected = { [weak self] reaction in self?.requestUpdateQuery?(reaction) } } } func animateIn(additivePosition: CGFloat, transition: ContainedViewLayoutTransition) { guard let multiplexedNode = self.multiplexedNode else { return } multiplexedNode.alpha = 0.0 transition.updateAlpha(layer: multiplexedNode.layer, alpha: 1.0, completion: { _ in }) if case let .animated(duration, curve) = transition { multiplexedNode.layer.animatePosition(from: CGPoint(x: 0.0, y: additivePosition), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, additive: true) } } func animateOut(transition: ContainedViewLayoutTransition) { guard let multiplexedNode = self.multiplexedNode else { return } transition.updateAlpha(layer: multiplexedNode.layer, alpha: 0.0, completion: { _ in }) } }