diff --git a/submodules/Postbox/Sources/Postbox.swift b/submodules/Postbox/Sources/Postbox.swift index 0c47192e44..96b8cf988c 100644 --- a/submodules/Postbox/Sources/Postbox.swift +++ b/submodules/Postbox/Sources/Postbox.swift @@ -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() diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 2b16eded47..6d6c8efc13 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -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(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(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) } } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/BUILD b/submodules/TelegramUI/Components/EntityKeyboard/BUILD index b7e3f5c22f..07f7dfcf90 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/BUILD +++ b/submodules/TelegramUI/Components/EntityKeyboard/BUILD @@ -45,6 +45,7 @@ swift_library( "//submodules/TelegramNotices:TelegramNotices", "//submodules/GZip", "//submodules/rlottie:RLottieBinding", + "//submodules/lottie-ios:Lottie", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 9f6cfc1fe7..d0aeebd961 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -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() 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 diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift index 087d776de1..888b2c4851 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchSearchBarComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift new file mode 100644 index 0000000000..8f64a0fa07 --- /dev/null +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiSearchStatusComponent.swift @@ -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? + 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?, 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, 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? + 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, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift index a0fa067b71..0ae4ea731e 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/GifPagerContentComponent.swift @@ -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 diff --git a/submodules/TelegramUI/Resources/Animations/emoji_arrow_to_search.tgs b/submodules/TelegramUI/Resources/Animations/emoji_arrow_to_search.tgs new file mode 100644 index 0000000000..7c4bb0f75a Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/emoji_arrow_to_search.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/emoji_search_to_arrow.tgs b/submodules/TelegramUI/Resources/Animations/emoji_search_to_arrow.tgs new file mode 100644 index 0000000000..03b5709d32 Binary files /dev/null and b/submodules/TelegramUI/Resources/Animations/emoji_search_to_arrow.tgs differ diff --git a/submodules/TelegramUI/Resources/Animations/emoji_search_to_progress.json b/submodules/TelegramUI/Resources/Animations/emoji_search_to_progress.json new file mode 100644 index 0000000000..8f38ef87a8 --- /dev/null +++ b/submodules/TelegramUI/Resources/Animations/emoji_search_to_progress.json @@ -0,0 +1 @@ +{"v":"5.9.0","fr":60,"ip":0,"op":106,"w":24,"h":24,"nm":"carsearchprogress_24","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Ellipse 61","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[12,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[33.333,33.333,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[4.971,0],[0,-4.971]],"o":[[0,0],[0,-4.971],[-4.971,0],[0,4.971]],"v":[[-8,0],[9,0],[0,-9],[-9,0]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":80,"s":[69]},{"t":96,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":80,"s":[100]},{"t":96,"s":[35]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 61","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":80,"op":106,"st":-150,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Path 17","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[7,12,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[-33.333,33.333,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[8,4],[0,-4],[-8,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":90,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 17","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":96,"s":[50]},{"t":106,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":96,"s":[50]},{"t":106,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":80,"op":106,"st":-120,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Ellipse 62","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":80,"s":[720]}],"ix":10},"p":{"s":true,"x":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[9]},{"t":10,"s":[12]}],"ix":3},"y":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[9]},{"t":10,"s":[12]}],"ix":4}},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[33.333,33.333,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0,0],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":0,"s":[12,12]},{"t":10,"s":[18,18]}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":80,"s":[75]}],"ix":1},"e":{"a":0,"k":100,"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.33,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 59","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":80,"st":-20,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Path 39","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[19.6,19.6,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[2.2,2.2],[-2.2,-2.2]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":1.66,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[300,300],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Path 39","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[0]},{"t":10,"s":[34]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":10,"s":[34]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":80,"st":-20,"bm":0}],"markers":[{"tm":0,"cm":"{\r\"name\":\"Search to Progress\"\r}","dr":0},{"tm":80,"cm":"{\n\"name\":\"Progress to Arrow\"\n}","dr":0}]} \ No newline at end of file