mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 22:25:57 +00:00
Various improvements
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "ChatEntityKeyboardInputNode",
|
||||
module_name = "ChatEntityKeyboardInputNode",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/Postbox:Postbox",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/ChatPresentationInterfaceState:ChatPresentationInterfaceState",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||
"//submodules/TelegramUI/Components/EntityKeyboard:EntityKeyboard",
|
||||
"//submodules/TelegramUI/Components/AnimationCache:AnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/TelegramUI/Components/ChatInputNode:ChatInputNode",
|
||||
"//submodules/Components/PagerComponent:PagerComponent",
|
||||
"//submodules/PremiumUI:PremiumUI",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/GalleryUI:GalleryUI",
|
||||
"//submodules/AttachmentTextInputPanelNode:AttachmentTextInputPanelNode",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
"//submodules/StickerPeekUI:StickerPeekUI",
|
||||
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
|
||||
"//submodules/TelegramUI/Components/MultiplexedVideoNode:MultiplexedVideoNode",
|
||||
"//submodules/TelegramUI/Components/ChatControllerInteraction:ChatControllerInteraction",
|
||||
"//submodules/FeaturedStickersScreen:FeaturedStickersScreen",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,279 @@
|
||||
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
|
||||
|
||||
final class GifPaneSearchContentNode: ASDisplayNode & PaneSearchContentNode {
|
||||
private let context: AccountContext
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
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<ChatMediaInputGifPaneTrendingState?>
|
||||
private let searchDisposable = MetaDisposable()
|
||||
|
||||
private let _ready = Promise<Void>()
|
||||
var ready: Signal<Void, NoError> {
|
||||
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, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, trendingPromise: Promise<ChatMediaInputGifPaneTrendingState?>) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
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?.controllerInteraction.sendBotContextResultAsGif(collection, result, sourceNode.view, sourceRect, false)
|
||||
} else {
|
||||
let _ = self?.controllerInteraction.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
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftSignalKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramPresentationData
|
||||
import ActivityIndicator
|
||||
import AppBundle
|
||||
import FeaturedStickersScreen
|
||||
|
||||
private func generateLoupeIcon(color: UIColor) -> UIImage? {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: color)
|
||||
}
|
||||
|
||||
private func generateClearIcon(color: UIColor) -> UIImage? {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: color)
|
||||
}
|
||||
|
||||
private func generateBackground(backgroundColor: UIColor, foregroundColor: UIColor) -> UIImage? {
|
||||
let diameter: CGFloat = 10.0
|
||||
return generateImage(CGSize(width: diameter, height: diameter), contextGenerator: { size, context in
|
||||
context.setFillColor(backgroundColor.cgColor)
|
||||
context.fill(CGRect(origin: CGPoint(), size: size))
|
||||
context.setFillColor(foregroundColor.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(), size: size))
|
||||
}, opaque: true)?.stretchableImage(withLeftCapWidth: Int(diameter / 2.0), topCapHeight: Int(diameter / 2.0))
|
||||
}
|
||||
|
||||
private class PaneSearchBarTextField: UITextField {
|
||||
public var didDeleteBackwardWhileEmpty: (() -> Void)?
|
||||
|
||||
let placeholderLabel: ImmediateTextNode
|
||||
var placeholderString: NSAttributedString? {
|
||||
didSet {
|
||||
self.placeholderLabel.attributedText = self.placeholderString
|
||||
}
|
||||
}
|
||||
|
||||
let prefixLabel: ASTextNode
|
||||
var prefixString: NSAttributedString? {
|
||||
didSet {
|
||||
self.prefixLabel.attributedText = self.prefixString
|
||||
}
|
||||
}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.placeholderLabel = ImmediateTextNode()
|
||||
self.placeholderLabel.isUserInteractionEnabled = false
|
||||
self.placeholderLabel.displaysAsynchronously = false
|
||||
self.placeholderLabel.maximumNumberOfLines = 1
|
||||
|
||||
self.prefixLabel = ASTextNode()
|
||||
self.prefixLabel.isUserInteractionEnabled = false
|
||||
self.prefixLabel.displaysAsynchronously = false
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubnode(self.placeholderLabel)
|
||||
self.addSubnode(self.prefixLabel)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override var keyboardAppearance: UIKeyboardAppearance {
|
||||
get {
|
||||
return super.keyboardAppearance
|
||||
}
|
||||
set {
|
||||
let resigning = self.isFirstResponder
|
||||
if resigning {
|
||||
self.resignFirstResponder()
|
||||
}
|
||||
super.keyboardAppearance = newValue
|
||||
if resigning {
|
||||
self.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
||||
if bounds.size.width.isZero {
|
||||
return CGRect(origin: CGPoint(), size: CGSize())
|
||||
}
|
||||
var rect = bounds.insetBy(dx: 4.0, dy: 4.0)
|
||||
|
||||
let prefixSize = self.prefixLabel.measure(bounds.size)
|
||||
if !prefixSize.width.isZero {
|
||||
let prefixOffset = prefixSize.width
|
||||
rect.origin.x += prefixOffset
|
||||
rect.size.width -= prefixOffset
|
||||
}
|
||||
return rect
|
||||
}
|
||||
|
||||
override func editingRect(forBounds bounds: CGRect) -> CGRect {
|
||||
return self.textRect(forBounds: bounds)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
let bounds = self.bounds
|
||||
if bounds.size.width.isZero {
|
||||
return
|
||||
}
|
||||
|
||||
let constrainedSize = self.textRect(forBounds: self.bounds).size
|
||||
let labelSize = self.placeholderLabel.updateLayout(constrainedSize)
|
||||
self.placeholderLabel.frame = CGRect(origin: CGPoint(x: self.textRect(forBounds: bounds).minX, y: self.textRect(forBounds: bounds).minY + 4.0), size: labelSize)
|
||||
|
||||
let prefixSize = self.prefixLabel.measure(constrainedSize)
|
||||
let prefixBounds = bounds.insetBy(dx: 4.0, dy: 4.0)
|
||||
self.prefixLabel.frame = CGRect(origin: CGPoint(x: prefixBounds.minX, y: prefixBounds.minY + 1.0), size: prefixSize)
|
||||
}
|
||||
|
||||
override func deleteBackward() {
|
||||
if self.text == nil || self.text!.isEmpty {
|
||||
self.didDeleteBackwardWhileEmpty?()
|
||||
}
|
||||
super.deleteBackward()
|
||||
}
|
||||
}
|
||||
|
||||
class PaneSearchBarNode: ASDisplayNode, UITextFieldDelegate {
|
||||
var cancel: (() -> Void)?
|
||||
var textUpdated: ((String, String) -> Void)?
|
||||
var clearPrefix: (() -> Void)?
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let separatorNode: ASDisplayNode
|
||||
private let textBackgroundNode: ASImageNode
|
||||
private var activityIndicator: ActivityIndicator?
|
||||
private let iconNode: ASImageNode
|
||||
private let textField: PaneSearchBarTextField
|
||||
private let clearButton: HighlightableButtonNode
|
||||
private let cancelButton: ASButtonNode
|
||||
|
||||
var placeholderString: NSAttributedString? {
|
||||
get {
|
||||
return self.textField.placeholderString
|
||||
} set(value) {
|
||||
self.textField.placeholderString = value
|
||||
}
|
||||
}
|
||||
|
||||
var prefixString: NSAttributedString? {
|
||||
get {
|
||||
return self.textField.prefixString
|
||||
} set(value) {
|
||||
let previous = self.prefixString
|
||||
let updated: Bool
|
||||
if let previous = previous, let value = value {
|
||||
updated = !previous.isEqual(to: value)
|
||||
} else {
|
||||
updated = (previous != nil) != (value != nil)
|
||||
}
|
||||
if updated {
|
||||
self.textField.prefixString = value
|
||||
self.textField.setNeedsLayout()
|
||||
self.updateIsEmpty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var text: String {
|
||||
get {
|
||||
return self.textField.text ?? ""
|
||||
} set(value) {
|
||||
if self.textField.text ?? "" != value {
|
||||
self.textField.text = value
|
||||
self.textFieldDidChange(self.textField)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var activity: Bool = false {
|
||||
didSet {
|
||||
if self.activity != oldValue {
|
||||
if self.activity {
|
||||
if self.activityIndicator == nil, let theme = self.theme {
|
||||
let activityIndicator = ActivityIndicator(type: .custom(theme.chat.inputMediaPanel.stickersSearchControlColor, 13.0, 1.0, false))
|
||||
self.activityIndicator = activityIndicator
|
||||
self.addSubnode(activityIndicator)
|
||||
if let (boundingSize, leftInset, rightInset) = self.validLayout {
|
||||
self.updateLayout(boundingSize: boundingSize, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
} else if let activityIndicator = self.activityIndicator {
|
||||
self.activityIndicator = nil
|
||||
activityIndicator.removeFromSupernode()
|
||||
}
|
||||
self.iconNode.isHidden = self.activity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var validLayout: (CGSize, CGFloat, CGFloat)?
|
||||
private var theme: PresentationTheme?
|
||||
|
||||
override init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
|
||||
self.textBackgroundNode = ASImageNode()
|
||||
self.textBackgroundNode.isLayerBacked = false
|
||||
self.textBackgroundNode.displaysAsynchronously = false
|
||||
self.textBackgroundNode.displayWithoutProcessing = true
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.isUserInteractionEnabled = false
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
|
||||
self.textField = PaneSearchBarTextField()
|
||||
self.textField.accessibilityTraits = .searchField
|
||||
self.textField.autocorrectionType = .no
|
||||
self.textField.returnKeyType = .search
|
||||
self.textField.font = Font.regular(17.0)
|
||||
|
||||
self.clearButton = HighlightableButtonNode()
|
||||
self.clearButton.imageNode.displaysAsynchronously = false
|
||||
self.clearButton.imageNode.displayWithoutProcessing = true
|
||||
self.clearButton.displaysAsynchronously = false
|
||||
self.clearButton.isHidden = true
|
||||
|
||||
self.cancelButton = ASButtonNode()
|
||||
self.cancelButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -8.0, bottom: -8.0, right: -8.0)
|
||||
self.cancelButton.displaysAsynchronously = false
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.separatorNode)
|
||||
|
||||
self.addSubnode(self.textBackgroundNode)
|
||||
self.view.addSubview(self.textField)
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.clearButton)
|
||||
self.addSubnode(self.cancelButton)
|
||||
|
||||
self.textField.delegate = self
|
||||
self.textField.addTarget(self, action: #selector(self.textFieldDidChange(_:)), for: .editingChanged)
|
||||
|
||||
self.textField.didDeleteBackwardWhileEmpty = { [weak self] in
|
||||
self?.clearPressed()
|
||||
}
|
||||
|
||||
self.cancelButton.addTarget(self, action: #selector(self.cancelPressed), forControlEvents: .touchUpInside)
|
||||
self.clearButton.addTarget(self, action: #selector(self.clearPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.theme = theme
|
||||
|
||||
if let activityIndicator = self.activityIndicator {
|
||||
activityIndicator.type = .custom(theme.chat.inputMediaPanel.stickersSearchControlColor, 13.0, 1.0, false)
|
||||
}
|
||||
self.separatorNode.backgroundColor = theme.chat.inputMediaPanel.panelSeparatorColor
|
||||
self.textBackgroundNode.image = generateStretchableFilledCircleImage(diameter: 36.0, color: theme.chat.inputMediaPanel.stickersSearchBackgroundColor)
|
||||
self.textField.textColor = theme.chat.inputMediaPanel.stickersSearchPrimaryColor
|
||||
self.iconNode.image = generateLoupeIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor)
|
||||
self.clearButton.setImage(generateClearIcon(color: theme.chat.inputMediaPanel.stickersSearchControlColor), for: [])
|
||||
self.cancelButton.setAttributedTitle(NSAttributedString(string: strings.Common_Cancel, font: Font.regular(17.0), textColor: theme.chat.inputPanel.panelControlAccentColor), for: [])
|
||||
self.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textField.tintColor = theme.list.itemAccentColor
|
||||
|
||||
if let (boundingSize, leftInset, rightInset) = self.validLayout {
|
||||
self.updateLayout(boundingSize: boundingSize, leftInset: leftInset, rightInset: rightInset, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(boundingSize: CGSize, leftInset: CGFloat, rightInset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = (boundingSize, leftInset, rightInset)
|
||||
|
||||
self.backgroundNode.frame = self.bounds
|
||||
transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: self.bounds.size.height), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel)))
|
||||
|
||||
let verticalOffset: CGFloat = -20.0
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: boundingSize.width - leftInset - rightInset, height: boundingSize.height))
|
||||
|
||||
let cancelButtonSize = self.cancelButton.measure(CGSize(width: 100.0, height: CGFloat.infinity))
|
||||
transition.updateFrame(node: self.cancelButton, frame: CGRect(origin: CGPoint(x: contentFrame.maxX - 8.0 - cancelButtonSize.width, y: verticalOffset + 34.0), size: cancelButtonSize))
|
||||
|
||||
let textBackgroundFrame = CGRect(origin: CGPoint(x: contentFrame.minX + 8.0, y: verticalOffset + 28.0), size: CGSize(width: contentFrame.width - 16.0 - cancelButtonSize.width - 11.0, height: 36.0))
|
||||
transition.updateFrame(node: self.textBackgroundNode, frame: textBackgroundFrame)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 27.0, y: textBackgroundFrame.minY), size: CGSize(width: max(1.0, textBackgroundFrame.size.width - 27.0 - 20.0), height: textBackgroundFrame.size.height))
|
||||
|
||||
if let iconImage = self.iconNode.image {
|
||||
let iconSize = iconImage.size
|
||||
transition.updateFrame(node: self.iconNode, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 5.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - iconSize.height) / 2.0)), size: iconSize))
|
||||
}
|
||||
|
||||
if let activityIndicator = self.activityIndicator {
|
||||
let indicatorSize = activityIndicator.measure(CGSize(width: 32.0, height: 32.0))
|
||||
transition.updateFrame(node: activityIndicator, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.minX + 11.0, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - indicatorSize.height) / 2.0)), size: indicatorSize))
|
||||
}
|
||||
|
||||
let clearSize = self.clearButton.measure(CGSize(width: 100.0, height: 100.0))
|
||||
transition.updateFrame(node: self.clearButton, frame: CGRect(origin: CGPoint(x: textBackgroundFrame.maxX - 8.0 - clearSize.width, y: textBackgroundFrame.minY + floor((textBackgroundFrame.size.height - clearSize.height) / 2.0)), size: clearSize))
|
||||
|
||||
self.textField.frame = textFrame
|
||||
self.textField.layoutSubviews()
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
if let cancel = self.cancel {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func activate() {
|
||||
self.textField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func animateIn(from node: PaneSearchBarPlaceholderNode, duration: Double, timingFunction: String, completion: @escaping () -> Void) {
|
||||
let initialTextBackgroundFrame = node.view.convert(node.backgroundNode.frame, to: self.view)
|
||||
|
||||
var backgroundCompleted = false
|
||||
var separatorCompleted = false
|
||||
var textBackgroundCompleted = false
|
||||
let intermediateCompletion: () -> Void = {
|
||||
if backgroundCompleted && separatorCompleted && textBackgroundCompleted {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, initialTextBackgroundFrame.maxY + 8.0)))
|
||||
if let fromBackgroundColor = node.backgroundColor, let toBackgroundColor = self.backgroundNode.backgroundColor {
|
||||
self.backgroundNode.layer.animate(from: fromBackgroundColor.cgColor, to: toBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration * 0.7)
|
||||
} else {
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
}
|
||||
self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: timingFunction, completion: { _ in
|
||||
backgroundCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
let initialSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, initialTextBackgroundFrame.maxY + 8.0)), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel))
|
||||
self.separatorNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration)
|
||||
self.separatorNode.layer.animateFrame(from: initialSeparatorFrame, to: self.separatorNode.frame, duration: duration, timingFunction: timingFunction, completion: { _ in
|
||||
separatorCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
self.textBackgroundNode.layer.animateFrame(from: initialTextBackgroundFrame, to: self.textBackgroundNode.frame, duration: duration, timingFunction: timingFunction, completion: { _ in
|
||||
textBackgroundCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
let labelFrame = self.textField.placeholderLabel.frame
|
||||
let initialLabelNodeFrame = CGRect(origin: node.labelNode.view.convert(node.labelNode.bounds, to: self.textField.superview).origin, size: labelFrame.size)
|
||||
self.textField.layer.animateFrame(from: CGRect(origin: initialLabelNodeFrame.origin.offsetBy(dx: -labelFrame.minX, dy: -labelFrame.minY), size: self.textField.frame.size), to: self.textField.frame, duration: duration, timingFunction: timingFunction)
|
||||
|
||||
let iconFrame = self.iconNode.frame
|
||||
let initialIconFrame = CGRect(origin: node.iconNode.view.convert(node.iconNode.bounds, to: self.iconNode.view.superview).origin, size: iconFrame.size)
|
||||
self.iconNode.layer.animateFrame(from: initialIconFrame, to: self.iconNode.frame, duration: duration, timingFunction: timingFunction)
|
||||
|
||||
let cancelButtonFrame = self.cancelButton.frame
|
||||
self.cancelButton.layer.animatePosition(from: CGPoint(x: self.bounds.size.width + cancelButtonFrame.size.width / 2.0, y: initialTextBackgroundFrame.minY + 2.0 + cancelButtonFrame.size.height / 2.0), to: self.cancelButton.layer.position, duration: duration, timingFunction: timingFunction)
|
||||
node.isHidden = true
|
||||
}
|
||||
|
||||
func deactivate(clear: Bool = true) {
|
||||
self.textField.resignFirstResponder()
|
||||
if clear {
|
||||
self.textField.text = nil
|
||||
self.textField.placeholderLabel.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
func transitionOut(to node: PaneSearchBarPlaceholderNode, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
let targetTextBackgroundFrame = node.view.convert(node.backgroundNode.view.frame, to: self.view)
|
||||
|
||||
let duration: Double = 0.5
|
||||
let timingFunction = kCAMediaTimingFunctionSpring
|
||||
|
||||
node.isHidden = true
|
||||
self.clearButton.isHidden = true
|
||||
self.textField.text = ""
|
||||
|
||||
var backgroundCompleted = false
|
||||
var separatorCompleted = false
|
||||
var textBackgroundCompleted = false
|
||||
let intermediateCompletion: () -> Void = { [weak node] in
|
||||
if backgroundCompleted && separatorCompleted && textBackgroundCompleted {
|
||||
completion()
|
||||
node?.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
let targetBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: self.bounds.size.width, height: max(0.0, targetTextBackgroundFrame.maxY + 8.0)))
|
||||
if let toBackgroundColor = node.backgroundColor, let fromBackgroundColor = self.backgroundNode.backgroundColor {
|
||||
self.backgroundNode.layer.animate(from: fromBackgroundColor.cgColor, to: toBackgroundColor.cgColor, keyPath: "backgroundColor", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: duration * 0.5, removeOnCompletion: false)
|
||||
} else {
|
||||
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false)
|
||||
}
|
||||
self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: targetBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in
|
||||
backgroundCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
let targetSeparatorFrame = CGRect(origin: CGPoint(x: 0.0, y: max(0.0, targetTextBackgroundFrame.maxY + 8.0)), size: CGSize(width: self.bounds.size.width, height: UIScreenPixel))
|
||||
self.separatorNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration / 2.0, removeOnCompletion: false)
|
||||
self.separatorNode.layer.animateFrame(from: self.separatorNode.frame, to: targetSeparatorFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in
|
||||
separatorCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
self.textBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false, completion: { _ in
|
||||
textBackgroundCompleted = true
|
||||
intermediateCompletion()
|
||||
})
|
||||
|
||||
let transitionBackgroundNode = ASImageNode()
|
||||
transitionBackgroundNode.isLayerBacked = true
|
||||
transitionBackgroundNode.displaysAsynchronously = false
|
||||
transitionBackgroundNode.displayWithoutProcessing = true
|
||||
transitionBackgroundNode.image = node.backgroundNode.image
|
||||
self.insertSubnode(transitionBackgroundNode, aboveSubnode: self.textBackgroundNode)
|
||||
transitionBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0, removeOnCompletion: false)
|
||||
transitionBackgroundNode.layer.animateFrame(from: self.textBackgroundNode.frame, to: targetTextBackgroundFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
|
||||
|
||||
let textFieldFrame = self.textField.frame
|
||||
let targetLabelNodeFrame = CGRect(origin: node.labelNode.view.convert(node.labelNode.bounds, to: self.textField.superview).origin, size: textFieldFrame.size)
|
||||
self.textField.layer.animateFrame(from: self.textField.frame, to: CGRect(origin: targetLabelNodeFrame.origin.offsetBy(dx: -self.textField.placeholderLabel.frame.minX, dy: -self.textField.placeholderLabel.frame.minY), size: self.textField.frame.size), duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
|
||||
if let snapshot = node.labelNode.layer.snapshotContentTree() {
|
||||
snapshot.frame = CGRect(origin: self.textField.placeholderLabel.frame.origin, size: node.labelNode.frame.size)
|
||||
self.textField.layer.addSublayer(snapshot)
|
||||
snapshot.animateAlpha(from: 0.0, to: 1.0, duration: duration * 2.0 / 3.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue)
|
||||
//self.textField.placeholderLabel.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 3.0 / 2.0, timingFunction: CAMediaTimingFunctionName.linear.rawValue, removeOnCompletion: false)
|
||||
|
||||
}
|
||||
|
||||
let iconFrame = self.iconNode.frame
|
||||
let targetIconFrame = CGRect(origin: node.iconNode.view.convert(node.iconNode.bounds, to: self.iconNode.view.superview).origin, size: iconFrame.size)
|
||||
self.iconNode.image = node.iconNode.image
|
||||
self.iconNode.layer.animateFrame(from: self.iconNode.frame, to: targetIconFrame, duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
|
||||
|
||||
let cancelButtonFrame = self.cancelButton.frame
|
||||
self.cancelButton.layer.animatePosition(from: self.cancelButton.layer.position, to: CGPoint(x: self.bounds.size.width + cancelButtonFrame.size.width / 2.0, y: targetTextBackgroundFrame.minY + 2.0 + cancelButtonFrame.size.height / 2.0), duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
if string.range(of: "\n") != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
self.textField.resignFirstResponder()
|
||||
return false
|
||||
}
|
||||
|
||||
@objc func textFieldDidChange(_ textField: UITextField) {
|
||||
self.updateIsEmpty()
|
||||
if let textUpdated = self.textUpdated {
|
||||
textUpdated(textField.text ?? "", self.textField.textInputMode?.primaryLanguage ?? "")
|
||||
}
|
||||
}
|
||||
|
||||
private func updateIsEmpty() {
|
||||
let isEmpty = !(textField.text?.isEmpty ?? true)
|
||||
if isEmpty != self.textField.placeholderLabel.isHidden {
|
||||
self.textField.placeholderLabel.isHidden = isEmpty
|
||||
}
|
||||
self.clearButton.isHidden = !isEmpty && self.prefixString == nil
|
||||
}
|
||||
|
||||
@objc func cancelPressed() {
|
||||
if let cancel = self.cancel {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func clearPressed() {
|
||||
if (self.textField.text?.isEmpty ?? true) {
|
||||
if self.prefixString != nil {
|
||||
self.clearPrefix?()
|
||||
}
|
||||
} else {
|
||||
self.textField.text = ""
|
||||
self.textFieldDidChange(self.textField)
|
||||
}
|
||||
}
|
||||
|
||||
func updateQuery(_ query: String) {
|
||||
self.textField.text = query
|
||||
self.textFieldDidChange(self.textField)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ChatPresentationInterfaceState
|
||||
import EntityKeyboard
|
||||
import ChatControllerInteraction
|
||||
import MultiplexedVideoNode
|
||||
import FeaturedStickersScreen
|
||||
|
||||
private let searchBarHeight: CGFloat = 52.0
|
||||
|
||||
public protocol PaneSearchContentNode {
|
||||
var ready: Signal<Void, NoError> { get }
|
||||
var deactivateSearchBar: (() -> Void)? { get set }
|
||||
var updateActivity: ((Bool) -> Void)? { get set }
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings)
|
||||
func updateText(_ text: String, languageCode: String?)
|
||||
func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition)
|
||||
|
||||
func animateIn(additivePosition: CGFloat, transition: ContainedViewLayoutTransition)
|
||||
func animateOut(transition: ContainedViewLayoutTransition)
|
||||
|
||||
func updatePreviewing(animated: Bool)
|
||||
func itemAt(point: CGPoint) -> (ASDisplayNode, Any)?
|
||||
}
|
||||
|
||||
public final class PaneSearchContainerNode: ASDisplayNode, EntitySearchContainerNode {
|
||||
private let context: AccountContext
|
||||
private let mode: ChatMediaInputSearchMode
|
||||
public private(set) var contentNode: PaneSearchContentNode & ASDisplayNode
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let searchBar: PaneSearchBarNode
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
public var onCancel: (() -> Void)?
|
||||
|
||||
public var openGifContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
|
||||
|
||||
public var ready: Signal<Void, NoError> {
|
||||
return self.contentNode.ready
|
||||
}
|
||||
|
||||
public init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction, mode: ChatMediaInputSearchMode, trendingGifsPromise: Promise<ChatMediaInputGifPaneTrendingState?>, cancel: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
switch mode {
|
||||
case .gif:
|
||||
self.contentNode = GifPaneSearchContentNode(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction, trendingPromise: trendingGifsPromise)
|
||||
case .sticker, .trending:
|
||||
self.contentNode = StickerPaneSearchContentNode(context: context, theme: theme, strings: strings, controllerInteraction: controllerInteraction, inputNodeInteraction: inputNodeInteraction)
|
||||
}
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
|
||||
self.searchBar = PaneSearchBarNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.clipsToBounds = true
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.contentNode)
|
||||
self.addSubnode(self.searchBar)
|
||||
|
||||
self.contentNode.deactivateSearchBar = { [weak self] in
|
||||
self?.searchBar.deactivate(clear: false)
|
||||
}
|
||||
self.contentNode.updateActivity = { [weak self] active in
|
||||
self?.searchBar.activity = active
|
||||
}
|
||||
|
||||
self.searchBar.cancel = { [weak self] in
|
||||
cancel()
|
||||
|
||||
self?.searchBar.view.endEditing(true)
|
||||
self?.onCancel?()
|
||||
}
|
||||
self.searchBar.activate()
|
||||
|
||||
self.searchBar.textUpdated = { [weak self] text, languageCode in
|
||||
self?.contentNode.updateText(text, languageCode: languageCode)
|
||||
}
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
|
||||
if let contentNode = self.contentNode as? GifPaneSearchContentNode {
|
||||
contentNode.requestUpdateQuery = { [weak self] query in
|
||||
self?.updateQuery(query)
|
||||
}
|
||||
contentNode.openGifContextMenu = { [weak self] file, node, rect, gesture, isSaved in
|
||||
self?.openGifContextMenu?(file, node, rect, gesture, isSaved)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.backgroundNode.backgroundColor = theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0)
|
||||
self.contentNode.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
self.searchBar.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
|
||||
let placeholder: String
|
||||
switch mode {
|
||||
case .gif:
|
||||
placeholder = strings.Gif_Search
|
||||
case .sticker, .trending:
|
||||
placeholder = strings.Stickers_Search
|
||||
}
|
||||
self.searchBar.placeholderString = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: theme.chat.inputMediaPanel.stickersSearchPlaceholderColor)
|
||||
}
|
||||
|
||||
public func updateQuery(_ query: String) {
|
||||
self.searchBar.updateQuery(query)
|
||||
}
|
||||
|
||||
public func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
|
||||
return self.contentNode.itemAt(point: CGPoint(x: point.x, y: point.y - searchBarHeight))
|
||||
}
|
||||
|
||||
public func updateLayout(size: CGSize, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, inputHeight: CGFloat, deviceMetrics: DeviceMetrics, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = size
|
||||
transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(), size: size))
|
||||
|
||||
transition.updateFrame(node: self.searchBar, frame: CGRect(origin: CGPoint(), size: CGSize(width: size.width, height: searchBarHeight)))
|
||||
self.searchBar.updateLayout(boundingSize: CGSize(width: size.width, height: searchBarHeight), leftInset: leftInset, rightInset: rightInset, transition: transition)
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: leftInset, y: searchBarHeight), size: CGSize(width: size.width - leftInset - rightInset, height: size.height - searchBarHeight))
|
||||
|
||||
transition.updateFrame(node: self.contentNode, frame: contentFrame)
|
||||
self.contentNode.updateLayout(size: contentFrame.size, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, inputHeight: inputHeight, deviceMetrics: deviceMetrics, transition: transition)
|
||||
}
|
||||
|
||||
public func deactivate() {
|
||||
self.searchBar.deactivate(clear: true)
|
||||
}
|
||||
|
||||
public func animateIn(from placeholder: PaneSearchBarPlaceholderNode?, anchorTop: CGPoint, anhorTopView: UIView, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
var verticalOrigin: CGFloat = anhorTopView.convert(anchorTop, to: self.view).y
|
||||
if let placeholder = placeholder {
|
||||
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
|
||||
verticalOrigin = placeholderFrame.minY - 4.0
|
||||
self.contentNode.animateIn(additivePosition: verticalOrigin, transition: transition)
|
||||
} else {
|
||||
self.contentNode.animateIn(additivePosition: 0.0, transition: transition)
|
||||
}
|
||||
|
||||
switch transition {
|
||||
case let .animated(duration, curve):
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration / 2.0)
|
||||
if let placeholder = placeholder {
|
||||
self.searchBar.animateIn(from: placeholder, duration: duration, timingFunction: curve.timingFunction, completion: completion)
|
||||
} else {
|
||||
self.searchBar.alpha = 0.0
|
||||
transition.updateAlpha(node: self.searchBar, alpha: 1.0)
|
||||
}
|
||||
if let size = self.validLayout {
|
||||
let initialBackgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin)))
|
||||
self.backgroundNode.layer.animateFrame(from: initialBackgroundFrame, to: self.backgroundNode.frame, duration: duration, timingFunction: curve.timingFunction)
|
||||
}
|
||||
case .immediate:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
public func animateOut(to placeholder: PaneSearchBarPlaceholderNode, animateOutSearchBar: Bool, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void) {
|
||||
if case let .animated(duration, curve) = transition {
|
||||
if let size = self.validLayout {
|
||||
let placeholderFrame = placeholder.view.convert(placeholder.bounds, to: self.view)
|
||||
let verticalOrigin = placeholderFrame.minY - 4.0
|
||||
self.backgroundNode.layer.animateFrame(from: self.backgroundNode.frame, to: CGRect(origin: CGPoint(x: 0.0, y: verticalOrigin), size: CGSize(width: size.width, height: max(0.0, size.height - verticalOrigin))), duration: duration, timingFunction: curve.timingFunction, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
self.searchBar.transitionOut(to: placeholder, transition: transition, completion: completion)
|
||||
transition.updateAlpha(node: self.backgroundNode, alpha: 0.0)
|
||||
if animateOutSearchBar {
|
||||
transition.updateAlpha(node: self.searchBar, alpha: 0.0)
|
||||
}
|
||||
self.contentNode.animateOut(transition: transition)
|
||||
self.deactivate()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,620 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import SwiftSignalKit
|
||||
import Postbox
|
||||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import LegacyComponents
|
||||
import MergeLists
|
||||
import AccountContext
|
||||
import StickerPackPreviewUI
|
||||
import StickerPeekUI
|
||||
import Emoji
|
||||
import AppBundle
|
||||
import OverlayStatusController
|
||||
import UndoUI
|
||||
import ChatControllerInteraction
|
||||
import FeaturedStickersScreen
|
||||
import ChatPresentationInterfaceState
|
||||
import FeaturedStickersScreen
|
||||
|
||||
private enum StickerSearchEntryId: Equatable, Hashable {
|
||||
case sticker(String?, Int64)
|
||||
case global(ItemCollectionId)
|
||||
}
|
||||
|
||||
private enum StickerSearchEntry: Identifiable, Comparable {
|
||||
case sticker(index: Int, code: String?, stickerItem: FoundStickerItem, theme: PresentationTheme)
|
||||
case global(index: Int, info: StickerPackCollectionInfo, topItems: [StickerPackItem], installed: Bool, topSeparator: Bool)
|
||||
|
||||
var stableId: StickerSearchEntryId {
|
||||
switch self {
|
||||
case let .sticker(_, code, stickerItem, _):
|
||||
return .sticker(code, stickerItem.file.fileId.id)
|
||||
case let .global(_, info, _, _, _):
|
||||
return .global(info.id)
|
||||
}
|
||||
}
|
||||
|
||||
static func ==(lhs: StickerSearchEntry, rhs: StickerSearchEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .sticker(lhsIndex, lhsCode, lhsStickerItem, lhsTheme):
|
||||
if case let .sticker(rhsIndex, rhsCode, rhsStickerItem, rhsTheme) = rhs {
|
||||
if lhsIndex != rhsIndex {
|
||||
return false
|
||||
}
|
||||
if lhsCode != rhsCode {
|
||||
return false
|
||||
}
|
||||
if lhsStickerItem != rhsStickerItem {
|
||||
return false
|
||||
}
|
||||
if lhsTheme !== rhsTheme {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
case let .global(index, info, topItems, installed, topSeparator):
|
||||
if case .global(index, info, topItems, installed, topSeparator) = rhs {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func <(lhs: StickerSearchEntry, rhs: StickerSearchEntry) -> Bool {
|
||||
switch lhs {
|
||||
case let .sticker(lhsIndex, _, _, _):
|
||||
switch rhs {
|
||||
case let .sticker(rhsIndex, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
default:
|
||||
return true
|
||||
}
|
||||
case let .global(lhsIndex, _, _, _, _):
|
||||
switch rhs {
|
||||
case .sticker:
|
||||
return false
|
||||
case let .global(rhsIndex, _, _, _, _):
|
||||
return lhsIndex < rhsIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func item(account: Account, theme: PresentationTheme, strings: PresentationStrings, interaction: StickerPaneSearchInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> GridItem {
|
||||
switch self {
|
||||
case let .sticker(_, code, stickerItem, theme):
|
||||
return StickerPaneSearchStickerItem(account: account, code: code, stickerItem: stickerItem, inputNodeInteraction: inputNodeInteraction, theme: theme, selected: { node, rect in
|
||||
interaction.sendSticker(.standalone(media: stickerItem.file), node.view, rect)
|
||||
})
|
||||
case let .global(_, info, topItems, installed, topSeparator):
|
||||
let itemContext = StickerPaneSearchGlobalItemContext()
|
||||
itemContext.canPlayMedia = true
|
||||
return StickerPaneSearchGlobalItem(account: account, theme: theme, strings: strings, listAppearance: false, info: info, topItems: topItems, topSeparator: topSeparator, regularInsets: false, installed: installed, unread: false, open: {
|
||||
interaction.open(info)
|
||||
}, install: {
|
||||
interaction.install(info, topItems, !installed)
|
||||
}, getItemIsPreviewed: { item in
|
||||
return interaction.getItemIsPreviewed(item)
|
||||
}, itemContext: itemContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct StickerPaneSearchGridTransition {
|
||||
let deletions: [Int]
|
||||
let insertions: [GridNodeInsertItem]
|
||||
let updates: [GridNodeUpdateItem]
|
||||
let updateFirstIndexInSectionOffset: Int?
|
||||
let stationaryItems: GridNodeStationaryItems
|
||||
let scrollToItem: GridNodeScrollToItem?
|
||||
let animated: Bool
|
||||
}
|
||||
|
||||
private func preparedChatMediaInputGridEntryTransition(account: Account, theme: PresentationTheme, strings: PresentationStrings, from fromEntries: [StickerSearchEntry], to toEntries: [StickerSearchEntry], interaction: StickerPaneSearchInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) -> StickerPaneSearchGridTransition {
|
||||
let stationaryItems: GridNodeStationaryItems = .none
|
||||
let scrollToItem: GridNodeScrollToItem? = nil
|
||||
var animated = false
|
||||
animated = 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, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction), previousIndex: $0.2) }
|
||||
let updates = updateIndices.map { GridNodeUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(account: account, theme: theme, strings: strings, interaction: interaction, inputNodeInteraction: inputNodeInteraction)) }
|
||||
|
||||
let firstIndexInSectionOffset = 0
|
||||
|
||||
return StickerPaneSearchGridTransition(deletions: deletions, insertions: insertions, updates: updates, updateFirstIndexInSectionOffset: firstIndexInSectionOffset, stationaryItems: stationaryItems, scrollToItem: scrollToItem, animated: animated)
|
||||
}
|
||||
|
||||
final class StickerPaneSearchContentNode: ASDisplayNode, PaneSearchContentNode {
|
||||
private let context: AccountContext
|
||||
private let controllerInteraction: ChatControllerInteraction
|
||||
private let inputNodeInteraction: ChatMediaInputNodeInteraction
|
||||
private var interaction: StickerPaneSearchInteraction?
|
||||
|
||||
private var theme: PresentationTheme
|
||||
private var strings: PresentationStrings
|
||||
|
||||
private let trendingPane: ChatMediaInputTrendingPane
|
||||
private let gridNode: GridNode
|
||||
private let notFoundNode: ASImageNode
|
||||
private let notFoundLabel: ImmediateTextNode
|
||||
|
||||
private var validLayout: CGSize?
|
||||
|
||||
private var enqueuedTransitions: [StickerPaneSearchGridTransition] = []
|
||||
|
||||
private let searchDisposable = MetaDisposable()
|
||||
|
||||
private let queue = Queue()
|
||||
private let currentEntries = Atomic<[StickerSearchEntry]?>(value: nil)
|
||||
private let currentRemotePacks = Atomic<FoundStickerSets?>(value: nil)
|
||||
|
||||
private let _ready = Promise<Void>()
|
||||
var ready: Signal<Void, NoError> {
|
||||
return self._ready.get()
|
||||
}
|
||||
|
||||
var deactivateSearchBar: (() -> Void)?
|
||||
var updateActivity: ((Bool) -> Void)?
|
||||
|
||||
private let installDisposable = MetaDisposable()
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, controllerInteraction: ChatControllerInteraction, inputNodeInteraction: ChatMediaInputNodeInteraction) {
|
||||
self.context = context
|
||||
self.controllerInteraction = controllerInteraction
|
||||
self.inputNodeInteraction = inputNodeInteraction
|
||||
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
|
||||
self.trendingPane = ChatMediaInputTrendingPane(context: context, controllerInteraction: controllerInteraction, getItemIsPreviewed: { [weak inputNodeInteraction] item in
|
||||
return inputNodeInteraction?.previewedStickerPackItem == .pack(item.file)
|
||||
}, isPane: false)
|
||||
|
||||
self.gridNode = GridNode()
|
||||
|
||||
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)
|
||||
|
||||
self.gridNode.isHidden = true
|
||||
self.trendingPane.isHidden = false
|
||||
self.notFoundNode.isHidden = true
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.trendingPane)
|
||||
self.addSubnode(self.gridNode)
|
||||
self.addSubnode(self.notFoundNode)
|
||||
|
||||
self.gridNode.scrollView.alwaysBounceVertical = true
|
||||
self.gridNode.scrollingInitiated = { [weak self] in
|
||||
self?.deactivateSearchBar?()
|
||||
}
|
||||
|
||||
self.trendingPane.scrollingInitiated = { [weak self] in
|
||||
self?.deactivateSearchBar?()
|
||||
}
|
||||
|
||||
self.interaction = StickerPaneSearchInteraction(open: { [weak self] info in
|
||||
if let strongSelf = self {
|
||||
strongSelf.view.window?.endEditing(true)
|
||||
let packReference: StickerPackReference = .id(id: info.id.id, accessHash: info.accessHash)
|
||||
let controller = StickerPackScreen(context: strongSelf.context, mainStickerPack: packReference, stickerPacks: [packReference], parentNavigationController: strongSelf.controllerInteraction.navigationController(), sendSticker: { [weak self] fileReference, sourceNode, sourceRect in
|
||||
if let strongSelf = self {
|
||||
return strongSelf.controllerInteraction.sendSticker(fileReference, false, false, nil, false, sourceNode, sourceRect, nil, [])
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
strongSelf.controllerInteraction.presentController(controller, nil)
|
||||
}
|
||||
}, install: { [weak self] info, items, install in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let context = strongSelf.context
|
||||
if install {
|
||||
var installSignal = strongSelf.context.engine.stickers.loadedStickerPack(reference: .id(id: info.id.id, accessHash: info.accessHash), forceActualized: false)
|
||||
|> mapToSignal { result -> Signal<(StickerPackCollectionInfo, [StickerPackItem]), NoError> in
|
||||
switch result {
|
||||
case let .result(info, items, installed):
|
||||
if installed {
|
||||
return .complete()
|
||||
} else {
|
||||
return preloadedStickerPackThumbnail(account: context.account, info: info, items: items)
|
||||
|> filter { $0 }
|
||||
|> ignoreValues
|
||||
|> then(
|
||||
context.engine.stickers.addStickerPackInteractively(info: info, items: items)
|
||||
|> ignoreValues
|
||||
)
|
||||
|> mapToSignal { _ -> Signal<(StickerPackCollectionInfo, [StickerPackItem]), NoError> in
|
||||
}
|
||||
|> then(.single((info, items)))
|
||||
}
|
||||
case .fetching:
|
||||
break
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
return .complete()
|
||||
}
|
||||
|> deliverOnMainQueue
|
||||
|
||||
let context = strongSelf.context
|
||||
var cancelImpl: (() -> Void)?
|
||||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
self?.controllerInteraction.presentController(controller, nil)
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.12, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
installSignal = installSignal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
cancelImpl = {
|
||||
self?.installDisposable.set(nil)
|
||||
}
|
||||
|
||||
strongSelf.installDisposable.set(installSignal.start(next: { info, items in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
var animateInAsReplacement = false
|
||||
if let navigationController = strongSelf.controllerInteraction.navigationController() {
|
||||
for controller in navigationController.overlayControllers {
|
||||
if let controller = controller as? UndoOverlayController {
|
||||
controller.dismissWithCommitActionAndReplacementAnimation()
|
||||
animateInAsReplacement = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.controllerInteraction.navigationController()?.presentOverlay(controller: UndoOverlayController(presentationData: presentationData, content: .stickersModified(title: presentationData.strings.StickerPackActionInfo_AddedTitle, text: presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: items.first, context: strongSelf.context), elevatedLayout: false, animateInAsReplacement: animateInAsReplacement, action: { _ in
|
||||
return true
|
||||
}))
|
||||
}))
|
||||
} else {
|
||||
let _ = (context.engine.stickers.removeStickerPackInteractively(id: info.id, option: .delete)
|
||||
|> deliverOnMainQueue).start(next: { _ in
|
||||
})
|
||||
}
|
||||
}, sendSticker: { [weak self] file, sourceView, sourceRect in
|
||||
if let strongSelf = self {
|
||||
let _ = strongSelf.controllerInteraction.sendSticker(file, false, false, nil, false, sourceView, sourceRect, nil, [])
|
||||
}
|
||||
}, getItemIsPreviewed: { item in
|
||||
return inputNodeInteraction.previewedStickerPackItem == .pack(item.file)
|
||||
})
|
||||
|
||||
self._ready.set(self.trendingPane.ready)
|
||||
self.trendingPane.activate()
|
||||
|
||||
self.updateThemeAndStrings(theme: theme, strings: strings)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.searchDisposable.dispose()
|
||||
self.installDisposable.dispose()
|
||||
}
|
||||
|
||||
func updateText(_ text: String, languageCode: String?) {
|
||||
let signal: Signal<([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)?, NoError>
|
||||
if !text.isEmpty {
|
||||
let context = self.context
|
||||
let stickers: Signal<[(String?, FoundStickerItem)], NoError> = Signal { subscriber in
|
||||
var signals: Signal<[Signal<(String?, [FoundStickerItem]), NoError>], NoError> = .single([])
|
||||
|
||||
let query = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if query.isSingleEmoji {
|
||||
signals = .single([context.engine.stickers.searchStickers(query: text.basicEmoji.0)
|
||||
|> map { (nil, $0) }])
|
||||
} else if query.count > 1, let languageCode = languageCode, !languageCode.isEmpty && languageCode != "emoji" {
|
||||
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query.lowercased(), completeMatch: query.count < 3)
|
||||
if !languageCode.lowercased().hasPrefix("en") {
|
||||
signal = signal
|
||||
|> mapToSignal { keywords in
|
||||
return .single(keywords)
|
||||
|> then(
|
||||
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query.lowercased(), completeMatch: query.count < 3)
|
||||
|> map { englishKeywords in
|
||||
return keywords + englishKeywords
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
signals = signal
|
||||
|> map { keywords -> [Signal<(String?, [FoundStickerItem]), NoError>] in
|
||||
var signals: [Signal<(String?, [FoundStickerItem]), NoError>] = []
|
||||
let emoticons = keywords.flatMap { $0.emoticons }
|
||||
for emoji in emoticons {
|
||||
signals.append(context.engine.stickers.searchStickers(query: emoji.basicEmoji.0)
|
||||
// |> take(1)
|
||||
|> map { (emoji, $0) })
|
||||
}
|
||||
return signals
|
||||
}
|
||||
}
|
||||
|
||||
return (signals
|
||||
|> mapToSignal { signals in
|
||||
return combineLatest(signals)
|
||||
}).start(next: { results in
|
||||
var result: [(String?, FoundStickerItem)] = []
|
||||
for (emoji, stickers) in results {
|
||||
for sticker in stickers {
|
||||
result.append((emoji, sticker))
|
||||
}
|
||||
}
|
||||
subscriber.putNext(result)
|
||||
}, completed: {
|
||||
// subscriber.putCompletion()
|
||||
})
|
||||
}
|
||||
|
||||
let local = context.engine.stickers.searchStickerSets(query: text)
|
||||
let remote = context.engine.stickers.searchStickerSetsRemotely(query: text)
|
||||
|> delay(0.2, queue: Queue.mainQueue())
|
||||
let rawPacks = local
|
||||
|> mapToSignal { result -> Signal<(FoundStickerSets, Bool, FoundStickerSets?), NoError> in
|
||||
var localResult = result
|
||||
if let currentRemote = self.currentRemotePacks.with ({ $0 }) {
|
||||
localResult = localResult.merge(with: currentRemote)
|
||||
}
|
||||
return .single((localResult, false, nil))
|
||||
|> then(
|
||||
remote
|
||||
|> map { remote -> (FoundStickerSets, Bool, FoundStickerSets?) in
|
||||
return (result.merge(with: remote), true, remote)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
let installedPackIds = context.account.postbox.combinedView(keys: [.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])])
|
||||
|> map { view -> Set<ItemCollectionId> in
|
||||
var installedPacks = Set<ItemCollectionId>()
|
||||
if let stickerPacksView = view.views[.itemCollectionInfos(namespaces: [Namespaces.ItemCollection.CloudStickerPacks])] as? ItemCollectionInfosView {
|
||||
if let packsEntries = stickerPacksView.entriesByNamespace[Namespaces.ItemCollection.CloudStickerPacks] {
|
||||
for entry in packsEntries {
|
||||
installedPacks.insert(entry.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return installedPacks
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
let packs = combineLatest(rawPacks, installedPackIds)
|
||||
|> map { packs, installedPackIds -> (FoundStickerSets, Bool, FoundStickerSets?) in
|
||||
var (localPacks, completed, remotePacks) = packs
|
||||
|
||||
for i in 0 ..< localPacks.infos.count {
|
||||
let installed = installedPackIds.contains(localPacks.infos[i].0)
|
||||
if installed != localPacks.infos[i].3 {
|
||||
localPacks.infos[i].3 = installed
|
||||
}
|
||||
}
|
||||
|
||||
if remotePacks != nil {
|
||||
for i in 0 ..< remotePacks!.infos.count {
|
||||
let installed = installedPackIds.contains(remotePacks!.infos[i].0)
|
||||
if installed != remotePacks!.infos[i].3 {
|
||||
remotePacks!.infos[i].3 = installed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (localPacks, completed, remotePacks)
|
||||
}
|
||||
|
||||
signal = combineLatest(stickers, packs)
|
||||
|> map { stickers, packs -> ([(String?, FoundStickerItem)], FoundStickerSets, Bool, FoundStickerSets?)? in
|
||||
return (stickers, packs.0, packs.1, packs.2)
|
||||
}
|
||||
self.updateActivity?(true)
|
||||
} else {
|
||||
signal = .single(nil)
|
||||
self.updateActivity?(false)
|
||||
}
|
||||
|
||||
self.searchDisposable.set((signal
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] result in
|
||||
Queue.mainQueue().async {
|
||||
guard let strongSelf = self, let interaction = strongSelf.interaction else {
|
||||
return
|
||||
}
|
||||
|
||||
var entries: [StickerSearchEntry] = []
|
||||
if let (stickers, packs, final, remote) = result {
|
||||
if let remote = remote {
|
||||
let _ = strongSelf.currentRemotePacks.swap(remote)
|
||||
}
|
||||
strongSelf.gridNode.isHidden = false
|
||||
strongSelf.trendingPane.isHidden = true
|
||||
|
||||
if final {
|
||||
strongSelf.updateActivity?(false)
|
||||
}
|
||||
|
||||
var index = 0
|
||||
var existingStickerIds = Set<MediaId>()
|
||||
var previousCode: String?
|
||||
for (code, sticker) in stickers {
|
||||
if let id = sticker.file.id, !existingStickerIds.contains(id) {
|
||||
entries.append(.sticker(index: index, code: code != previousCode ? code : nil, stickerItem: sticker, theme: strongSelf.theme))
|
||||
index += 1
|
||||
|
||||
previousCode = code
|
||||
existingStickerIds.insert(id)
|
||||
}
|
||||
}
|
||||
var isFirstGlobal = true
|
||||
for (collectionId, info, _, installed) in packs.infos {
|
||||
if let info = info as? StickerPackCollectionInfo {
|
||||
var topItems: [StickerPackItem] = []
|
||||
for e in packs.entries {
|
||||
if let item = e.item as? StickerPackItem {
|
||||
if e.index.collectionId == collectionId {
|
||||
topItems.append(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
entries.append(.global(index: index, info: info, topItems: topItems, installed: installed, topSeparator: !isFirstGlobal))
|
||||
isFirstGlobal = false
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
if final || !entries.isEmpty {
|
||||
strongSelf.notFoundNode.isHidden = !entries.isEmpty
|
||||
}
|
||||
} else {
|
||||
let _ = strongSelf.currentRemotePacks.swap(nil)
|
||||
strongSelf.updateActivity?(false)
|
||||
strongSelf.gridNode.isHidden = true
|
||||
strongSelf.notFoundNode.isHidden = true
|
||||
strongSelf.trendingPane.isHidden = false
|
||||
}
|
||||
|
||||
let previousEntries = strongSelf.currentEntries.swap(entries)
|
||||
let transition = preparedChatMediaInputGridEntryTransition(account: strongSelf.context.account, theme: strongSelf.theme, strings: strongSelf.strings, from: previousEntries ?? [], to: entries, interaction: interaction, inputNodeInteraction: strongSelf.inputNodeInteraction)
|
||||
strongSelf.enqueueTransition(transition)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.notFoundNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/StickersNotFoundIcon"), color: theme.list.freeMonoIconColor)
|
||||
self.notFoundLabel.attributedText = NSAttributedString(string: strings.Stickers_NoStickersFound, font: Font.medium(14.0), textColor: theme.list.freeTextColor)
|
||||
}
|
||||
|
||||
private func enqueueTransition(_ transition: StickerPaneSearchGridTransition) {
|
||||
self.enqueuedTransitions.append(transition)
|
||||
|
||||
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 itemTransition: ContainedViewLayoutTransition = .immediate
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: transition.deletions, insertItems: transition.insertions, updateItems: transition.updates, scrollToItem: transition.scrollToItem, updateLayout: nil, itemTransition: itemTransition, stationaryItems: .none, updateFirstIndexInSectionOffset: transition.updateFirstIndexInSectionOffset), completion: { _ in })
|
||||
}
|
||||
}
|
||||
|
||||
func updatePreviewing(animated: Bool) {
|
||||
self.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? StickerPaneSearchStickerItemNode {
|
||||
itemNode.updatePreviewing(animated: animated)
|
||||
} else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode {
|
||||
itemNode.updatePreviewing(animated: animated)
|
||||
}
|
||||
}
|
||||
self.trendingPane.updatePreviewing(animated: animated)
|
||||
}
|
||||
|
||||
func itemAt(point: CGPoint) -> (ASDisplayNode, Any)? {
|
||||
if !self.trendingPane.isHidden {
|
||||
if let (itemNode, item) = self.trendingPane.itemAt(point: self.view.convert(point, to: self.trendingPane.view)) {
|
||||
return (itemNode, StickerPreviewPeekItem.pack(item.file))
|
||||
}
|
||||
} else {
|
||||
if let itemNode = self.gridNode.itemNodeAtPoint(self.view.convert(point, to: self.gridNode.view)) {
|
||||
if let itemNode = itemNode as? StickerPaneSearchStickerItemNode, let stickerItem = itemNode.stickerItem {
|
||||
return (itemNode, StickerPreviewPeekItem.found(stickerItem))
|
||||
} else if let itemNode = itemNode as? StickerPaneSearchGlobalItemNode {
|
||||
if let (node, item) = itemNode.itemAt(point: self.view.convert(point, to: itemNode.view)) {
|
||||
return (node, StickerPreviewPeekItem.pack(item.file))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(), size: size)
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: contentFrame.size, insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0 + bottomInset, right: 0.0), preloadSize: 300.0, type: .fixed(itemSize: CGSize(width: 75.0, height: 75.0), fillWidth: nil, lineSpacing: 0.0, itemSpacing: nil)), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil), completion: { _ in })
|
||||
|
||||
transition.updateFrame(node: self.trendingPane, frame: contentFrame)
|
||||
self.trendingPane.updateLayout(size: contentFrame.size, topInset: 0.0, bottomInset: bottomInset, isExpanded: false, isVisible: true, deviceMetrics: deviceMetrics, transition: transition)
|
||||
|
||||
transition.updateFrame(node: self.gridNode, frame: contentFrame)
|
||||
if firstLayout {
|
||||
while !self.enqueuedTransitions.isEmpty {
|
||||
self.dequeueTransition()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(additivePosition: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
self.gridNode.alpha = 0.0
|
||||
transition.updateAlpha(node: self.gridNode, alpha: 1.0, completion: { _ in
|
||||
})
|
||||
self.trendingPane.alpha = 0.0
|
||||
transition.updateAlpha(node: self.trendingPane, alpha: 1.0, completion: { _ in
|
||||
})
|
||||
|
||||
if case let .animated(duration, curve) = transition {
|
||||
self.trendingPane.layer.animatePosition(from: CGPoint(x: 0.0, y: additivePosition), to: CGPoint(), duration: duration, timingFunction: curve.timingFunction, additive: true)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(transition: ContainedViewLayoutTransition) {
|
||||
transition.updateAlpha(node: self.gridNode, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
transition.updateAlpha(node: self.trendingPane, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
transition.updateAlpha(node: self.notFoundNode, alpha: 0.0, completion: { _ in
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user