Search progress

This commit is contained in:
Ali 2023-01-26 13:10:05 +01:00
parent 4609546c49
commit 16048d4d77
10 changed files with 918 additions and 53 deletions

View File

@ -1282,7 +1282,7 @@ public func openPostbox(basePath: String, seedConfiguration: SeedConfiguration,
#if DEBUG
//debugSaveState(basePath: basePath + "/db", name: "previous2")
debugRestoreState(basePath: basePath + "/db", name: "previous2")
//debugRestoreState(basePath: basePath + "/db", name: "previous2")
#endif
let startTime = CFAbsoluteTimeGetCurrent()

View File

@ -177,7 +177,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
loadMoreToken: nil,
displaySearchWithPlaceholder: nil,
searchCategories: nil,
searchInitiallyHidden: true
searchInitiallyHidden: true,
searchState: .empty
)
))
@ -244,11 +245,38 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
private var inputDataDisposable: Disposable?
private var hasRecentGifsDisposable: Disposable?
private struct EmojiSearchResult {
var groups: [EmojiPagerContentComponent.ItemGroup]
var id: AnyHashable
var version: Int
var isPreset: Bool
}
private struct EmojiSearchState {
var result: EmojiSearchResult?
var isSearching: Bool
init(result: EmojiSearchResult?, isSearching: Bool) {
self.result = result
self.isSearching = isSearching
}
}
private let emojiSearchDisposable = MetaDisposable()
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable, version: Int)?>(nil)
private let emojiSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) {
didSet {
self.emojiSearchState.set(.single(self.emojiSearchStateValue))
}
}
private let stickerSearchDisposable = MetaDisposable()
private let stickerSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable, version: Int)?>(nil)
private let stickerSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
private var stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) {
didSet {
self.stickerSearchState.set(.single(self.stickerSearchStateValue))
}
}
private let controllerInteraction: ChatControllerInteraction?
@ -356,7 +384,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
loadMoreToken: nil,
displaySearchWithPlaceholder: presentationData.strings.Common_Search,
searchCategories: searchCategories,
searchInitiallyHidden: true
searchInitiallyHidden: true,
searchState: .empty
)
)
}
@ -388,7 +417,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
loadMoreToken: nil,
displaySearchWithPlaceholder: presentationData.strings.Common_Search,
searchCategories: searchCategories,
searchInitiallyHidden: true
searchInitiallyHidden: true,
searchState: .empty
)
)
}
@ -426,7 +456,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
loadMoreToken: loadMoreToken,
displaySearchWithPlaceholder: presentationData.strings.Common_Search,
searchCategories: searchCategories,
searchInitiallyHidden: true
searchInitiallyHidden: true,
searchState: .active
)
)
}
@ -511,7 +542,8 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
loadMoreToken: loadMoreToken,
displaySearchWithPlaceholder: presentationData.strings.Common_Search,
searchCategories: searchCategories,
searchInitiallyHidden: true
searchInitiallyHidden: true,
searchState: .active
)
)
}
@ -950,13 +982,13 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
switch query {
case .none:
strongSelf.emojiSearchDisposable.set(nil)
strongSelf.emojiSearchResult.set(.single(nil))
strongSelf.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
case let .text(rawQuery, languageCode):
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isEmpty {
strongSelf.emojiSearchDisposable.set(nil)
strongSelf.emojiSearchResult.set(.single(nil))
strongSelf.emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
} else {
let context = strongSelf.context
@ -1080,13 +1112,15 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
}
var version = 0
strongSelf.emojiSearchStateValue.isSearching = true
strongSelf.emojiSearchDisposable.set((resultSignal
|> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query), version)))
strongSelf.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false)
version += 1
}))
}
@ -1128,14 +1162,23 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
items: items
)])
}
let delayValue: Double
/*#if DEBUG
delayValue = 2.3
#else*/
delayValue = 0.0
//#endif
var version = 0
strongSelf.emojiSearchStateValue.isSearching = true
strongSelf.emojiSearchDisposable.set((resultSignal
|> delay(delayValue, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
strongSelf.emojiSearchResult.set(.single((result, AnyHashable(value), version)))
strongSelf.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
version += 1
}))
}
@ -1352,10 +1395,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
switch query {
case .none:
strongSelf.stickerSearchDisposable.set(nil)
strongSelf.stickerSearchResult.set(.single(nil))
strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
case .text:
strongSelf.stickerSearchDisposable.set(nil)
strongSelf.stickerSearchResult.set(.single(nil))
strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
case let .category(value):
let resultSignal = strongSelf.context.engine.stickers.searchStickers(query: value, scope: [.installed, .remote])
|> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
@ -1406,9 +1449,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
return
}
if group.items.isEmpty && !result.isFinalResult {
strongSelf.stickerSearchStateValue.isSearching = true
return
}
strongSelf.stickerSearchResult.set(.single((result.items, AnyHashable(value), version)))
strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false)
version += 1
}))
}
@ -1428,10 +1472,10 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
updatedInputData,
self.gifComponent.get(),
self.emojiSearchResult.get(),
self.stickerSearchResult.get()
self.emojiSearchState.get(),
self.stickerSearchState.get()
)
|> deliverOnMainQueue).start(next: { [weak self] inputData, gifs, emojiSearchResult, stickerSearchResult in
|> deliverOnMainQueue).start(next: { [weak self] inputData, gifs, emojiSearchState, stickerSearchState in
guard let strongSelf = self else {
return
}
@ -1440,7 +1484,7 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if let emojiSearchResult = emojiSearchResult {
if let emojiSearchResult = emojiSearchState.result {
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) {
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
@ -1449,11 +1493,16 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
)
}
if let emoji = inputData.emoji {
inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: .active)
let defaultSearchState: EmojiPagerContentComponent.SearchState = emojiSearchResult.isPreset ? .active : .empty
inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState)
}
} else if emojiSearchState.isSearching {
if let emoji = inputData.emoji {
inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emoji.contentItemGroups, itemContentUniqueId: emoji.itemContentUniqueId, emptySearchResults: emoji.emptySearchResults, searchState: .searching)
}
}
if let stickerSearchResult = stickerSearchResult {
if let stickerSearchResult = stickerSearchState.result {
var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty }) {
//TODO:localize
@ -1463,7 +1512,12 @@ public final class ChatEntityKeyboardInputNode: ChatInputNode {
)
}
if let stickers = inputData.stickers {
inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: .active)
let defaultSearchState: EmojiPagerContentComponent.SearchState = stickerSearchResult.isPreset ? .active : .empty
inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: stickerSearchState.isSearching ? .searching : defaultSearchState)
}
} else if stickerSearchState.isSearching {
if let stickers = inputData.stickers {
inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickers.contentItemGroups, itemContentUniqueId: stickers.itemContentUniqueId, emptySearchResults: stickers.emptySearchResults, searchState: .searching)
}
}

View File

@ -45,6 +45,7 @@ swift_library(
"//submodules/TelegramNotices:TelegramNotices",
"//submodules/GZip",
"//submodules/rlottie:RLottieBinding",
"//submodules/lottie-ios:Lottie",
],
visibility = [
"//visibility:public",

View File

@ -1532,6 +1532,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
var isActive: Bool
var hasPresetSearch: Bool
var textInputState: EmojiSearchSearchBarComponent.TextInputState
var searchState: EmojiPagerContentComponent.SearchState
var size: CGSize
var canFocus: Bool
var searchCategories: EmojiSearchCategories?
@ -1561,6 +1562,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
if lhs.textInputState != rhs.textInputState {
return false
}
if lhs.searchState != rhs.searchState {
return false
}
if lhs.size != rhs.size {
return false
}
@ -1587,11 +1591,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
private let backgroundLayer: SimpleLayer
private let tintBackgroundLayer: SimpleLayer
private let searchIconView: UIImageView
private let searchIconTintView: UIImageView
private let backIconView: UIImageView
private let backIconTintView: UIImageView
private let statusIcon = ComponentView<Empty>()
private let clearIconView: UIImageView
private let clearIconTintView: UIImageView
@ -1625,12 +1625,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
self.backgroundLayer = SimpleLayer()
self.tintBackgroundLayer = SimpleLayer()
self.searchIconView = UIImageView()
self.searchIconTintView = UIImageView()
self.backIconView = UIImageView()
self.backIconTintView = UIImageView()
self.clearIconView = UIImageView()
self.clearIconTintView = UIImageView()
self.clearIconButton = HighlightableButton()
@ -1647,12 +1641,6 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
self.layer.addSublayer(self.backgroundLayer)
self.tintContainerView.layer.addSublayer(self.tintBackgroundLayer)
self.addSubview(self.searchIconView)
self.tintContainerView.addSubview(self.searchIconTintView)
self.addSubview(self.backIconView)
self.tintContainerView.addSubview(self.backIconTintView)
self.addSubview(self.clearIconView)
self.tintContainerView.addSubview(self.clearIconTintView)
self.addSubview(self.clearIconButton)
@ -1716,7 +1704,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let location = recognizer.location(in: self)
if self.backIconView.frame.contains(location) {
if let view = self.statusIcon.view, view.frame.contains(location), self.currentPresetSearchTerm != nil {
self.clearCategorySearch()
} else {
self.activateTextInput()
@ -1838,10 +1826,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
return
}
self.params = nil
self.update(context: params.context, theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, transition: transition)
self.update(context: params.context, theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, canFocus: params.canFocus, searchCategories: params.searchCategories, searchState: params.searchState, transition: transition)
}
public func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, transition: Transition) {
public func update(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, canFocus: Bool, searchCategories: EmojiSearchCategories?, searchState: EmojiPagerContentComponent.SearchState, transition: Transition) {
let textInputState: EmojiSearchSearchBarComponent.TextInputState
if let textField = self.textField {
textInputState = .active(hasText: !(textField.text ?? "").isEmpty)
@ -1858,6 +1846,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
isActive: isActive,
hasPresetSearch: self.currentPresetSearchTerm == nil,
textInputState: textInputState,
searchState: searchState,
size: size,
canFocus: canFocus,
searchCategories: searchCategories
@ -1870,7 +1859,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
let isActiveWithText = isActive && self.currentPresetSearchTerm == nil
if self.params?.theme !== theme {
self.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)?.withRenderingMode(.alwaysTemplate)
/*self.searchIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.searchIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
self.searchIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Loupe"), color: .white)
@ -1878,7 +1867,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
self.backIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.backIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
self.backIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white)
self.backIconTintView.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: .white)*/
self.clearIconView.image = generateTintedImage(image: UIImage(bundleImageName: "Components/Search Bar/Clear"), color: .white)?.withRenderingMode(.alwaysTemplate)
self.clearIconView.tintColor = useOpaqueTheme ? theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
@ -1888,11 +1877,11 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
self.params = params
let sideInset: CGFloat = 8.0
let sideInset: CGFloat = 12.0
let topInset: CGFloat = 8.0
let inputHeight: CGFloat = 36.0
let sideTextInset: CGFloat = 8.0 + 4.0 + 24.0
let sideTextInset: CGFloat = sideInset + 4.0 + 24.0
if useOpaqueTheme {
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor
@ -1941,7 +1930,40 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
let textFrame = CGRect(origin: CGPoint(x: textX, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - textX, height: backgroundFrame.height))
self.textFrame = textFrame
if let image = self.searchIconView.image {
let statusContent: EmojiSearchStatusComponent.Content
switch searchState {
case .empty:
statusContent = .search
case .searching:
statusContent = .progress
case .active:
statusContent = .results
}
let statusSize = CGSize(width: 24.0, height: 24.0)
let _ = self.statusIcon.update(
transition: transition,
component: AnyComponent(EmojiSearchStatusComponent(
theme: theme,
strings: strings,
useOpaqueTheme: useOpaqueTheme,
content: statusContent
)),
environment: {},
containerSize: statusSize
)
let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - statusSize.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - statusSize.height) / 2.0)), size: statusSize)
if let statusIconView = self.statusIcon.view as? EmojiSearchStatusComponent.View {
if statusIconView.superview == nil {
self.addSubview(statusIconView)
self.tintContainerView.addSubview(statusIconView.tintContainerView)
}
transition.setFrame(view: statusIconView, frame: iconFrame)
transition.setFrame(view: statusIconView.tintContainerView, frame: iconFrame)
}
/*if let image = self.searchIconView.image {
let iconFrame = CGRect(origin: CGPoint(x: textFrame.minX - image.size.width - 4.0, y: backgroundFrame.minY + floor((backgroundFrame.height - image.size.height) / 2.0)), size: image.size)
transition.setBounds(view: self.searchIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size))
transition.setPosition(view: self.searchIconView, position: iconFrame.center)
@ -1963,9 +1985,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
transition.setAlpha(view: self.backIconView, alpha: self.currentPresetSearchTerm != nil ? 1.0 : 0.0)
transition.setScale(view: self.backIconTintView, scale: self.currentPresetSearchTerm != nil ? 1.0 : 0.001)
transition.setAlpha(view: self.backIconTintView, alpha: self.currentPresetSearchTerm != nil ? 1.0 : 0.0)
}
}*/
let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX - 6.0, y: backgroundFrame.minX), size: CGSize(width: backgroundFrame.maxX - (textFrame.minX - 6.0), height: backgroundFrame.height))
let placeholderContentFrame = CGRect(origin: CGPoint(x: textFrame.minX - 6.0, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.maxX - (textFrame.minX - 6.0), height: backgroundFrame.height))
let _ = self.placeholderContent.update(
transition: transition,
component: AnyComponent(EmojiSearchSearchBarComponent(
@ -6545,7 +6567,7 @@ public final class EmojiPagerContentComponent: Component {
}
let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight))
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, searchCategories: component.searchCategories, transition: transition)
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, canFocus: !component.searchIsPlaceholderOnly, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition)
if !useOpaqueTheme {
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame)
transition.attachAnimation(view: visibleSearchHeader, id: "search_transition", completion: { [weak self] completed in

View File

@ -162,7 +162,7 @@ final class EmojiSearchSearchBarComponent: Component {
self.containerSize = containerSize
self.itemCount = itemCount
self.itemSpacing = 11.0
self.leftInset = 6.0
self.leftInset = 8.0
self.rightInset = 8.0
self.itemSize = CGSize(width: 24.0, height: 24.0)
self.textSpacing = 11.0

View File

@ -0,0 +1,781 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import PagerComponent
import TelegramPresentationData
import TelegramCore
import Postbox
import AnimationCache
import MultiAnimationRenderer
import AccountContext
import AsyncDisplayKit
import ComponentDisplayAdapters
import LottieAnimationComponent
import EmojiStatusComponent
import LottieComponent
import AudioToolbox
import SwiftSignalKit
import GZip
import RLottieBinding
import AppBundle
import Lottie
private final class LottieDirectContent: LottieComponent.Content {
let path: String
init(path: String) {
self.path = path
}
override func isEqual(to other: LottieComponent.Content) -> Bool {
guard let other = other as? LottieDirectContent else {
return false
}
if self.path != other.path {
return false
}
return true
}
override func load(_ f: @escaping (Data) -> Void) -> Disposable {
if let data = try? Data(contentsOf: URL(fileURLWithPath: self.path)) {
let result = TGGUnzipData(data, 2 * 1024 * 1024) ?? data
f(result)
}
return EmptyDisposable
}
}
private protocol EmojiSearchStatusAnimationState {
var content: EmojiSearchStatusComponent.ContentState { get }
var image: UIImage? { get }
var isCompleted: Bool { get }
func advanceIfNeeded()
func updateImage()
}
final class EmojiSearchStatusComponent: Component {
enum Content: Equatable {
case search
case progress
case results
}
let theme: PresentationTheme
let strings: PresentationStrings
let useOpaqueTheme: Bool
let content: Content
init(
theme: PresentationTheme,
strings: PresentationStrings,
useOpaqueTheme: Bool,
content: Content
) {
self.theme = theme
self.strings = strings
self.useOpaqueTheme = useOpaqueTheme
self.content = content
}
static func ==(lhs: EmojiSearchStatusComponent, rhs: EmojiSearchStatusComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.useOpaqueTheme != rhs.useOpaqueTheme {
return false
}
if lhs.content != rhs.content {
return false
}
return true
}
fileprivate enum ContentState {
case search
case searchToProgress
case progress
case results
init(content: Content) {
switch content {
case .search:
self = .search
case .progress:
self = .progress
case .results:
self = .results
}
}
var content: Content {
switch self {
case .search:
return .search
case .searchToProgress, .progress:
return .progress
case .results:
return .results
}
}
var automaticNextState: ContentState? {
switch self {
case .searchToProgress:
return .progress
default:
return nil
}
}
}
private final class LottieAnimationState: EmojiSearchStatusAnimationState {
let content: ContentState
private let animationInstance: LottieInstance
private var currentFrameStartTime: Double?
private var currentFrame: Int = 0
private let frameRange: ClosedRange<Int>?
private(set) var image: UIImage?
private(set) var previousAnimationState: EmojiSearchStatusAnimationState?
private(set) var isCompleted: Bool = false
var displaySize: CGSize {
didSet {
if self.displaySize != oldValue {
self.image = nil
}
}
}
init?(content: ContentState, data: Data, displaySize: CGSize, frameRange: ClosedRange<Int>?, previousAnimationState: EmojiSearchStatusAnimationState?) {
guard let animationInstance = LottieInstance(data: data, fitzModifier: .none, colorReplacements: nil, cacheKey: "") else {
return nil
}
self.content = content
self.animationInstance = animationInstance
self.displaySize = displaySize
self.frameRange = frameRange
self.previousAnimationState = previousAnimationState
if let frameRange {
self.currentFrame = frameRange.lowerBound
}
}
func advanceIfNeeded() {
if let previousAnimationState = self.previousAnimationState {
previousAnimationState.advanceIfNeeded()
if previousAnimationState.isCompleted {
self.previousAnimationState = nil
}
if previousAnimationState.image == nil {
self.image = nil
}
}
if self.isCompleted {
return
}
if let frameRange = self.frameRange {
if frameRange.lowerBound == frameRange.upperBound {
self.isCompleted = true
return
}
}
let timestamp = CACurrentMediaTime()
guard let currentFrameStartTime = self.currentFrameStartTime else {
currentFrameStartTime = timestamp
return
}
let secondsPerFrame: Double
if animationInstance.frameRate == 0 {
secondsPerFrame = 1.0 / 60.0
} else {
secondsPerFrame = 1.0 / Double(animationInstance.frameRate)
}
if currentFrameStartTime + secondsPerFrame * 0.9 <= timestamp {
self.currentFrame += 1
let maxFrame: Int
if let frameRange = self.frameRange {
maxFrame = frameRange.upperBound
} else {
maxFrame = Int(animationInstance.frameCount) - 1
}
if self.currentFrame >= maxFrame {
self.currentFrame = maxFrame
self.isCompleted = true
} else {
self.currentFrameStartTime = timestamp
self.image = nil
}
}
}
func updateImage() {
guard let frameContext = DrawingContext(size: self.displaySize, scale: 1.0, opaque: false, clear: true) else {
return
}
self.animationInstance.renderFrame(with: Int32(self.currentFrame % Int(self.animationInstance.frameCount)), into: frameContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.displaySize.width), height: Int32(self.displaySize.height), bytesPerRow: Int32(frameContext.bytesPerRow))
if let previousAnimationState = self.previousAnimationState as? ProgressAnimationState {
guard let context = DrawingContext(size: self.displaySize, scale: 1.0, opaque: false, clear: true) else {
return
}
if previousAnimationState.image == nil {
previousAnimationState.updateImage()
}
if let frameImage = frameContext.generateImage()?.cgImage, let cgImage = previousAnimationState.image?.cgImage {
context.withFlippedContext { c in
c.draw(cgImage, in: CGRect(origin: CGPoint(), size: context.size))
c.translateBy(x: self.displaySize.width * 0.5, y: self.displaySize.height * 0.5)
c.rotate(by: previousAnimationState.currentRotationAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0))
c.translateBy(x: -self.displaySize.width * 0.5, y: -self.displaySize.height * 0.5)
c.draw(frameImage, in: CGRect(origin: CGPoint(), size: context.size))
}
}
self.image = context.generateImage()?.withRenderingMode(.alwaysTemplate)
} else {
self.image = frameContext.generateImage()?.withRenderingMode(.alwaysTemplate)
}
}
}
private final class ProgressAnimationState: EmojiSearchStatusAnimationState {
let content: ContentState
private var currentFrameStartTime: Double?
private var currentOffset: CGFloat
private(set) var currentRotationAngle: CGFloat
private var lastStageStartOffset: CGFloat?
private var lastStageRotationAngle: CGFloat?
private(set) var image: UIImage?
var shouldComplete: Bool = false {
didSet {
if self.shouldComplete != oldValue && self.shouldComplete {
self.lastStageStartOffset = self.currentOffset
self.currentRotationAngle = self.currentRotationAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0)
self.lastStageRotationAngle = self.currentRotationAngle
}
}
}
private(set) var isCompleted: Bool = false
var displaySize: CGSize {
didSet {
if self.displaySize != oldValue {
self.image = nil
}
}
}
init(content: ContentState, displaySize: CGSize) {
self.content = content
self.displaySize = displaySize
self.currentOffset = 0.0
self.currentRotationAngle = 0.0
}
func advanceIfNeeded() {
if self.isCompleted {
return
}
let timestamp = CACurrentMediaTime()
guard let currentFrameStartTime = self.currentFrameStartTime else {
currentFrameStartTime = timestamp
return
}
let secondsPerFrame: Double = 1.0 / 60.0
let offsetVelocity: CGFloat = CGFloat.pi * 3.0
let maxOffset: CGFloat = CGFloat.pi * 2.0 - CGFloat.pi * 1.0 / 1.4
let rotationVelocity: CGFloat = CGFloat.pi * 3.0 * 1.0
if currentFrameStartTime + secondsPerFrame * 0.9 <= timestamp {
if let lastStageStartOffset = self.lastStageStartOffset {
let lastStageRemainingOffset: CGFloat = CGFloat.pi * 2.0 - lastStageStartOffset
let lastStageRemainingVelocity: CGFloat = lastStageRemainingOffset / 9.0 * 60.0
self.currentOffset = min(CGFloat.pi * 2.0, self.currentOffset + lastStageRemainingVelocity * secondsPerFrame)
} else if self.shouldComplete {
self.currentOffset = min(CGFloat.pi * 2.0, self.currentOffset + offsetVelocity * secondsPerFrame)
if self.currentOffset == CGFloat.pi * 2.0 {
self.isCompleted = true
}
} else {
self.currentOffset = min(maxOffset, self.currentOffset + offsetVelocity * secondsPerFrame)
}
if let lastStageRotationAngle = self.lastStageRotationAngle {
let _ = lastStageRotationAngle
/*let lastStageRemainingAngle: CGFloat = CGFloat.pi * 2.0 + lastStageRotationAngle
let lastStageRemainingAngleVelocity: CGFloat = lastStageRemainingAngle / 12.0 * 60.0
self.currentRotationAngle = max(-CGFloat.pi * 2.0, self.currentRotationAngle - lastStageRemainingAngleVelocity * secondsPerFrame)*/
self.currentRotationAngle = max(-CGFloat.pi * 2.0, self.currentRotationAngle - rotationVelocity * secondsPerFrame)
} else {
self.currentRotationAngle -= rotationVelocity * secondsPerFrame
}
if self.lastStageStartOffset != nil && self.lastStageRotationAngle != nil {
if self.currentOffset == CGFloat.pi * 2.0 && self.currentRotationAngle == -CGFloat.pi * 2.0 {
self.isCompleted = true
}
}
self.currentFrameStartTime = timestamp
self.image = nil
}
}
func updateImage() {
guard let context = DrawingContext(size: self.displaySize, scale: 1.0, opaque: false, clear: true) else {
return
}
context.withFlippedContext { c in
c.setStrokeColor(UIColor.white.cgColor)
c.setLineCap(.round)
let lineWidth: CGFloat = 1.33 * UIScreenScale
let fullDiameter = 20.0 * UIScreenScale
c.setLineWidth(lineWidth)
let startAngle: CGFloat = 0.0
let endAngle: CGFloat = startAngle + (CGFloat.pi * 2.0 - self.currentOffset.truncatingRemainder(dividingBy: CGFloat.pi * 2.0))
c.translateBy(x: self.displaySize.width * 0.5, y: self.displaySize.height * 0.5)
c.rotate(by: self.currentRotationAngle.truncatingRemainder(dividingBy: CGFloat.pi * 2.0))
c.translateBy(x: -self.displaySize.width * 0.5, y: -self.displaySize.height * 0.5)
if self.currentOffset != CGFloat.pi * 2.0 {
c.addArc(center: CGPoint(x: self.displaySize.width * 0.5, y: self.displaySize.height * 0.5), radius: fullDiameter * 0.5 - lineWidth, startAngle: startAngle, endAngle: endAngle, clockwise: false)
c.strokePath()
}
}
self.image = context.generateImage()?.withRenderingMode(.alwaysTemplate)
}
}
final class View: UIView {
private var component: EmojiSearchStatusComponent?
private var disappearingAnimationStates: [(UIImageView, UIImageView, EmojiSearchStatusAnimationState)] = []
private var currentAnimationState: EmojiSearchStatusAnimationState?
private var pendingContent: Content?
private var displaySize: CGSize?
private var displayLink: SharedDisplayLinkDriver.Link?
public let contentView: UIImageView
public let tintContainerView: UIView
public let tintContentView: UIImageView
override init(frame: CGRect) {
self.contentView = UIImageView()
self.tintContainerView = UIView()
self.tintContentView = UIImageView()
super.init(frame: frame)
self.addSubview(self.contentView)
self.tintContainerView.isUserInteractionEnabled = false
self.tintContainerView.addSubview(self.tintContentView)
//self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
}
}
func update(component: EmojiSearchStatusComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.component = component
let displaySize = CGSize(width: availableSize.width * UIScreenScale, height: availableSize.height * UIScreenScale)
self.displaySize = displaySize
let overlayColor = component.useOpaqueTheme ? component.theme.chat.inputMediaPanel.panelContentOpaqueSearchOverlayColor : component.theme.chat.inputMediaPanel.panelContentVibrantSearchOverlayColor
let baseColor: UIColor = .white
if self.contentView.tintColor != overlayColor {
self.contentView.tintColor = overlayColor
}
if self.tintContentView.tintColor != baseColor {
self.tintContentView.tintColor = baseColor
}
let currentTargetContent = self.pendingContent ?? self.currentAnimationState?.content.content
if component.content != currentTargetContent {
var canSwitchNow = false
if let currentAnimationState = self.currentAnimationState {
if currentAnimationState.isCompleted {
canSwitchNow = true
} else if let _ = currentAnimationState as? ProgressAnimationState {
canSwitchNow = true
}
} else {
canSwitchNow = true
}
if canSwitchNow {
/*if let currentAnimationState = self.currentAnimationState, case .search = currentAnimationState.content, case .progress = component.content {
self.switchToContent(content: .searchToProgress)
} else {*/
self.switchToContent(content: ContentState(content: component.content))
//}
} else {
self.pendingContent = component.content
}
}
self.updateAnimation()
transition.setFrame(view: self.contentView, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setFrame(view: self.tintContentView, frame: CGRect(origin: CGPoint(), size: availableSize))
return availableSize
}
private func switchToContent(content: ContentState) {
guard let displaySize = self.displaySize else {
return
}
enum FrameRangeValue {
case index(Int)
case marker(String)
case end
}
var name: String?
var isJson = false
var frameRange: (FrameRangeValue, FrameRangeValue)?
var manualTransition = false
var previousAnimationState: EmojiSearchStatusAnimationState?
previousAnimationState = nil
let manualPreviousState = self.currentAnimationState
if let currentAnimationState = self.currentAnimationState {
switch currentAnimationState.content {
case .search:
switch content {
case .search:
name = "emoji_search_to_arrow"
frameRange = (.index(0), .index(0))
case .searchToProgress:
name = "emoji_search_to_progress"
isJson = true
//frameRange = (.index(0), .marker("{\r\"name\":\"Search to Progress\"\r}"))
frameRange = (.index(0), .index(7))
case .progress:
manualTransition = true
break
case .results:
name = "emoji_search_to_arrow"
}
case .searchToProgress:
switch content {
case .search:
manualTransition = true
name = "emoji_search_to_arrow"
frameRange = (.index(0), .index(0))
case .searchToProgress:
break
case .progress:
break
case .results:
manualTransition = true
name = "emoji_arrow_to_search"
frameRange = (.index(0), .index(0))
}
case .progress:
switch content {
case .search:
manualTransition = true
name = "emoji_search_to_arrow"
frameRange = (.index(0), .index(0))
case .searchToProgress:
break
case .progress:
break
case .results:
manualTransition = true
name = "emoji_arrow_to_search"
frameRange = (.index(0), .index(0))
}
/*switch content {
case .search:
manualTransition = true
name = "emoji_search_to_arrow"
frameRange = (.index(0), .index(0))
case .searchToProgress:
name = "emoji_search_to_progress"
isJson = true
case .progress:
break
case .results:
name = "emoji_search_to_progress"
isJson = true
//frameRange = (.marker("{\n\"name\":\"Progress to Arrow\"\n}"), .end)
frameRange = (.index(87), .end)
previousAnimationState = currentAnimationState
(currentAnimationState as? ProgressAnimationState)?.shouldComplete = true
/*name = "emoji_arrow_to_search"
frameRange = (.index(0), .index(0))*/
}*/
case .results:
switch content {
case .search:
name = "emoji_arrow_to_search"
case .searchToProgress:
name = "emoji_search_to_progress"
isJson = true
case .progress:
manualTransition = true
case .results:
name = "emoji_arrow_to_search"
frameRange = (.index(0), .index(0))
}
}
} else {
switch content {
case .search:
name = "emoji_search_to_arrow"
frameRange = (.index(0), .index(0))
case .searchToProgress:
name = "emoji_search_to_progress"
isJson = true
case .progress:
break
case .results:
name = "emoji_arrow_to_search"
frameRange = (.index(0), .index(0))
}
}
if manualTransition, let manualPreviousState {
let tempImageView = UIImageView()
tempImageView.image = self.contentView.image
tempImageView.frame = self.contentView.frame
tempImageView.tintColor = self.contentView.tintColor
self.contentView.superview?.insertSubview(tempImageView, aboveSubview: self.contentView)
let tempTintImageView = UIImageView()
tempTintImageView.image = self.tintContentView.image
tempTintImageView.frame = self.tintContentView.frame
tempTintImageView.tintColor = self.tintContentView.tintColor
self.tintContentView.superview?.insertSubview(tempTintImageView, aboveSubview: self.tintContentView)
self.disappearingAnimationStates.append((tempImageView, tempTintImageView, manualPreviousState))
let minScale: CGFloat = 0.6
tempImageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak self, weak tempImageView] _ in
if let self, let tempImageView {
tempImageView.removeFromSuperview()
self.disappearingAnimationStates.removeAll(where: { $0.0 === tempImageView })
}
})
tempImageView.layer.animateScale(from: 1.0, to: minScale, duration: 0.18, removeOnCompletion: false)
tempTintImageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.18, removeOnCompletion: false, completion: { [weak self, weak tempTintImageView] _ in
if let self, let tempTintImageView {
tempImageView.removeFromSuperview()
self.disappearingAnimationStates.removeAll(where: { $0.1 === tempTintImageView })
}
})
tempTintImageView.layer.animateScale(from: 1.0, to: minScale, duration: 0.18, removeOnCompletion: false)
self.contentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
self.contentView.layer.animateScale(from: minScale, to: 1.0, duration: 0.18)
self.tintContentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18)
self.tintContentView.layer.animateScale(from: minScale, to: 1.0, duration: 0.18)
}
if case .progress = content {
self.currentAnimationState = ProgressAnimationState(content: content, displaySize: displaySize)
} else if let name, let data = getAppBundle().path(forResource: name, ofType: isJson ? "json" : "tgs").flatMap({
return try? Data(contentsOf: URL(fileURLWithPath: $0))
}).flatMap({ data -> Data in
if isJson {
return data
}
return TGGUnzipData(data, 2 * 1024 * 1024) ?? data
}) {
var resolvedFrameRange: ClosedRange<Int>?
if let frameRange {
var hasMarkers = false
if case .marker = frameRange.0 {
hasMarkers = true
}
if case .marker = frameRange.1 {
hasMarkers = true
}
if case .end = frameRange.0 {
hasMarkers = true
}
if case .end = frameRange.1 {
hasMarkers = true
}
var resolvedLowerBound: Int = 0
var resolvedUpperBound: Int = 0
if case let .index(index) = frameRange.0 {
resolvedLowerBound = index
}
if case let .index(index) = frameRange.1 {
resolvedUpperBound = index
}
if hasMarkers, let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any], let animation = try? Animation(dictionary: json) {
let numFrames = animation.endFrame - animation.startFrame
if case let .marker(markerName) = frameRange.0 {
if let value = animation.progressTime(forMarker: markerName) {
resolvedLowerBound = Int(value * numFrames)
}
}
if case .end = frameRange.0 {
resolvedLowerBound = Int(numFrames) - 1
}
if case let .marker(markerName) = frameRange.1 {
if let value = animation.progressTime(forMarker: markerName) {
resolvedUpperBound = Int(round(value * numFrames))
}
}
if case .end = frameRange.1 {
resolvedUpperBound = Int(numFrames) - 1
}
}
resolvedFrameRange = resolvedLowerBound ... max(resolvedLowerBound, resolvedUpperBound)
}
self.currentAnimationState = LottieAnimationState(content: content, data: data, displaySize: displaySize, frameRange: resolvedFrameRange, previousAnimationState: previousAnimationState)
} else {
self.currentAnimationState = nil
}
}
private func updateAnimation() {
var needsAnimation = false
for (tempView, tempTintView, animationState) in self.disappearingAnimationStates {
animationState.advanceIfNeeded()
if animationState.image == nil {
animationState.updateImage()
}
tempView.image = animationState.image
tempTintView.image = animationState.image
needsAnimation = true
}
while true {
if let currentAnimationState = self.currentAnimationState {
if self.pendingContent != nil, let currentAnimationState = currentAnimationState as? ProgressAnimationState {
currentAnimationState.shouldComplete = true
}
currentAnimationState.advanceIfNeeded()
if currentAnimationState.image == nil {
currentAnimationState.updateImage()
}
if let previousAnimationState = (currentAnimationState as? LottieAnimationState)?.previousAnimationState, !previousAnimationState.isCompleted {
needsAnimation = true
}
if currentAnimationState.isCompleted {
if self.pendingContent == nil, let automaticNextState = currentAnimationState.content.automaticNextState {
self.switchToContent(content: automaticNextState)
} else if let pendingContent = self.pendingContent {
self.pendingContent = nil
self.switchToContent(content: ContentState(content: pendingContent))
} else {
break
}
} else {
needsAnimation = true
break
}
} else {
break
}
}
if let currentAnimationState = self.currentAnimationState {
if currentAnimationState.image == nil {
currentAnimationState.updateImage()
}
if let image = currentAnimationState.image {
self.contentView.image = image
self.tintContentView.image = image
}
}
if needsAnimation {
if self.displayLink == nil {
var counter = 0
self.displayLink = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: false, { [weak self] in
counter += 1
if counter % 1 == 0 {
self?.updateAnimation()
}
})
}
} else {
if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.invalidate()
}
}
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -197,6 +197,7 @@ public final class GifPagerContentComponent: Component {
public let displaySearchWithPlaceholder: String?
public let searchCategories: EmojiSearchCategories?
public let searchInitiallyHidden: Bool
public let searchState: EmojiPagerContentComponent.SearchState
public init(
context: AccountContext,
@ -207,7 +208,8 @@ public final class GifPagerContentComponent: Component {
loadMoreToken: String?,
displaySearchWithPlaceholder: String?,
searchCategories: EmojiSearchCategories?,
searchInitiallyHidden: Bool
searchInitiallyHidden: Bool,
searchState: EmojiPagerContentComponent.SearchState
) {
self.context = context
self.inputInteraction = inputInteraction
@ -218,6 +220,7 @@ public final class GifPagerContentComponent: Component {
self.displaySearchWithPlaceholder = displaySearchWithPlaceholder
self.searchCategories = searchCategories
self.searchInitiallyHidden = searchInitiallyHidden
self.searchState = searchState
}
public static func ==(lhs: GifPagerContentComponent, rhs: GifPagerContentComponent) -> Bool {
@ -248,6 +251,9 @@ public final class GifPagerContentComponent: Component {
if lhs.searchInitiallyHidden != rhs.searchInitiallyHidden {
return false
}
if lhs.searchState != rhs.searchState {
return false
}
return true
}
@ -1066,7 +1072,7 @@ public final class GifPagerContentComponent: Component {
}
let searchHeaderFrame = CGRect(origin: CGPoint(x: itemLayout.searchInsets.left, y: itemLayout.searchInsets.top), size: CGSize(width: itemLayout.width - itemLayout.searchInsets.left - itemLayout.searchInsets.right, height: itemLayout.searchHeight))
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, searchCategories: component.searchCategories, transition: transition)
visibleSearchHeader.update(context: component.context, theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: false, isActive: false, size: searchHeaderFrame.size, canFocus: false, searchCategories: component.searchCategories, searchState: component.searchState, transition: transition)
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in
let _ = self
let _ = completed

File diff suppressed because one or more lines are too long