mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-04 21:41:45 +00:00
Implement status emoji search
This commit is contained in:
parent
5b1f01ce75
commit
4be4770776
@ -235,7 +235,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
private var emojiContentDisposable: Disposable?
|
||||
|
||||
private let emojiSearchDisposable = MetaDisposable()
|
||||
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable)?>(nil)
|
||||
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable, emptyResultEmoji: TelegramMediaFile?)?>(nil)
|
||||
private var stableEmptyResultEmoji: TelegramMediaFile?
|
||||
|
||||
private var horizontalExpandRecognizer: UIPanGestureRecognizer?
|
||||
private var horizontalExpandStartLocation: CGPoint?
|
||||
@ -419,7 +420,22 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|
||||
var emojiContent = emojiContent
|
||||
if let emojiSearchResult = emojiSearchResult {
|
||||
emojiContent = emojiContent.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id)
|
||||
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
|
||||
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) {
|
||||
if strongSelf.stableEmptyResultEmoji == nil {
|
||||
strongSelf.stableEmptyResultEmoji = emojiSearchResult.emptyResultEmoji
|
||||
}
|
||||
//TODO:localize
|
||||
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
|
||||
text: "No emoji found",
|
||||
iconFile: strongSelf.stableEmptyResultEmoji
|
||||
)
|
||||
} else {
|
||||
strongSelf.stableEmptyResultEmoji = nil
|
||||
}
|
||||
emojiContent = emojiContent.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults)
|
||||
} else {
|
||||
strongSelf.stableEmptyResultEmoji = nil
|
||||
}
|
||||
|
||||
strongSelf.emojiContent = emojiContent
|
||||
@ -1315,12 +1331,14 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
|> distinctUntilChanged
|
||||
|
||||
let resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
|
||||
|> mapToSignal { keywords -> Signal<(result: [EmojiPagerContentComponent.ItemGroup], emptyResultEmoji: TelegramMediaFile?), NoError> in
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||
context.engine.stickers.availableReactions(),
|
||||
hasPremium
|
||||
)
|
||||
|> map { view, hasPremium -> [EmojiPagerContentComponent.ItemGroup] in
|
||||
|> take(1)
|
||||
|> map { view, availableReactions, hasPremium -> (result: [EmojiPagerContentComponent.ItemGroup], emptyResultEmoji: TelegramMediaFile?) in
|
||||
var result: [(String, TelegramMediaFile?, String)] = []
|
||||
|
||||
var allEmoticons: [String: String] = [:]
|
||||
@ -1371,8 +1389,42 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
return [EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search", groupId: "search", title: nil, subtitle: nil, actionButtonTitle: nil, isFeatured: false, isPremiumLocked: false, isEmbedded: false, hasClear: false, collapsedLineCount: nil, displayPremiumBadges: false, headerItem: nil, items: items)]
|
||||
var emptyResultEmoji: TelegramMediaFile?
|
||||
if let availableReactions = availableReactions {
|
||||
//let reactionFilter: [String] = ["😖", "😫", "🫠", "😨", "❓"]
|
||||
let filteredReactions: [TelegramMediaFile] = availableReactions.reactions.compactMap { reaction -> TelegramMediaFile? in
|
||||
switch reaction.value {
|
||||
case let .builtin(value):
|
||||
let _ = value
|
||||
//if reactionFilter.contains(value) {
|
||||
return reaction.selectAnimation
|
||||
/*} else {
|
||||
return nil
|
||||
}*/
|
||||
case .custom:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
emptyResultEmoji = filteredReactions.randomElement()
|
||||
}
|
||||
|
||||
return (result: [EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
items: items
|
||||
)],
|
||||
emptyResultEmoji: emptyResultEmoji
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1382,7 +1434,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.emojiSearchResult.set(.single((result, AnyHashable(query))))
|
||||
strongSelf.emojiSearchResult.set(.single((result.result, AnyHashable(query), result.emptyResultEmoji)))
|
||||
}))
|
||||
}
|
||||
},
|
||||
|
||||
@ -210,6 +210,7 @@ public final class EmojiStatusSelectionComponent: Component {
|
||||
self.panelBackgroundView.update(size: self.panelBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
transition.setFrame(view: self.panelSeparatorView, frame: CGRect(origin: CGPoint(x: 0.0, y: component.hideTopPanel ? -UIScreenPixel : topPanelHeight), size: CGSize(width: keyboardSize.width, height: UIScreenPixel)))
|
||||
transition.setAlpha(view: self.panelSeparatorView, alpha: component.hideTopPanel ? 0.0 : 1.0)
|
||||
}
|
||||
|
||||
return availableSize
|
||||
@ -250,6 +251,10 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
private var freezeUpdates: Bool = false
|
||||
private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation?
|
||||
|
||||
private let emojiSearchDisposable = MetaDisposable()
|
||||
private let emojiSearchResult = Promise<(groups: [EmojiPagerContentComponent.ItemGroup], id: AnyHashable, emptyResultEmoji: TelegramMediaFile?)?>(nil)
|
||||
private var stableEmptyResultEmoji: TelegramMediaFile?
|
||||
|
||||
private var previewItem: (groupId: AnyHashable, item: EmojiPagerContentComponent.Item)?
|
||||
private var dismissedPreviewItem: (groupId: AnyHashable, item: EmojiPagerContentComponent.Item)?
|
||||
private var previewScreenView: ComponentView<Empty>?
|
||||
@ -265,6 +270,8 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
private var isAnimatingOut: Bool = false
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
private var isReactionSearchActive: Bool = false
|
||||
|
||||
init(controller: EmojiStatusSelectionController, context: AccountContext, sourceView: UIView?, emojiContent: Signal<EmojiPagerContentComponent, NoError>, currentSelection: Int64?) {
|
||||
self.controller = controller
|
||||
self.context = context
|
||||
@ -306,12 +313,36 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
self.layer.addSublayer(self.cloudLayer0)
|
||||
self.layer.addSublayer(self.cloudLayer1)
|
||||
|
||||
self.emojiContentDisposable = (emojiContent
|
||||
|> deliverOnMainQueue).start(next: { [weak self] emojiContent in
|
||||
self.emojiContentDisposable = (combineLatest(queue: .mainQueue(),
|
||||
emojiContent,
|
||||
self.emojiSearchResult.get()
|
||||
)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] emojiContent, emojiSearchResult in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.controller?._ready.set(.single(true))
|
||||
|
||||
var emojiContent = emojiContent
|
||||
if let emojiSearchResult = emojiSearchResult {
|
||||
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
|
||||
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty }) {
|
||||
if strongSelf.stableEmptyResultEmoji == nil {
|
||||
strongSelf.stableEmptyResultEmoji = emojiSearchResult.emptyResultEmoji
|
||||
}
|
||||
//TODO:localize
|
||||
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
|
||||
text: "No emoji found",
|
||||
iconFile: strongSelf.stableEmptyResultEmoji
|
||||
)
|
||||
} else {
|
||||
strongSelf.stableEmptyResultEmoji = nil
|
||||
}
|
||||
emojiContent = emojiContent.withUpdatedItemGroups(itemGroups: emojiSearchResult.groups, itemContentUniqueId: emojiSearchResult.id, emptySearchResults: emptySearchResults)
|
||||
} else {
|
||||
strongSelf.stableEmptyResultEmoji = nil
|
||||
}
|
||||
|
||||
if strongSelf.emojiContent == nil || !strongSelf.freezeUpdates {
|
||||
strongSelf.emojiContent = emojiContent
|
||||
}
|
||||
@ -366,7 +397,150 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
},
|
||||
requestUpdate: { _ in
|
||||
},
|
||||
updateSearchQuery: { _ in
|
||||
updateSearchQuery: { rawQuery in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
if query.isEmpty {
|
||||
strongSelf.emojiSearchDisposable.set(nil)
|
||||
strongSelf.emojiSearchResult.set(.single(nil))
|
||||
} else {
|
||||
let context = strongSelf.context
|
||||
|
||||
let languageCode = "en"
|
||||
var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false)
|
||||
if !languageCode.lowercased().hasPrefix("en") {
|
||||
signal = signal
|
||||
|> mapToSignal { keywords in
|
||||
return .single(keywords)
|
||||
|> then(
|
||||
context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3)
|
||||
|> map { englishKeywords in
|
||||
return keywords + englishKeywords
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|
||||
|> map { peer -> Bool in
|
||||
guard case let .user(user) = peer else {
|
||||
return false
|
||||
}
|
||||
return user.isPremium
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let resultSignal = signal
|
||||
|> mapToSignal { keywords -> Signal<(result: [EmojiPagerContentComponent.ItemGroup], emptyResultEmoji: TelegramMediaFile?), NoError> in
|
||||
return combineLatest(
|
||||
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
|
||||
context.engine.stickers.availableReactions(),
|
||||
hasPremium
|
||||
)
|
||||
|> take(1)
|
||||
|> map { view, availableReactions, hasPremium -> (result: [EmojiPagerContentComponent.ItemGroup], emptyResultEmoji: TelegramMediaFile?) in
|
||||
var result: [(String, TelegramMediaFile?, String)] = []
|
||||
|
||||
var allEmoticons: [String: String] = [:]
|
||||
for keyword in keywords {
|
||||
for emoticon in keyword.emoticons {
|
||||
allEmoticons[emoticon] = keyword.keyword
|
||||
}
|
||||
}
|
||||
|
||||
for entry in view.entries {
|
||||
guard let item = entry.item as? StickerPackItem else {
|
||||
continue
|
||||
}
|
||||
for attribute in item.file.attributes {
|
||||
switch attribute {
|
||||
case let .CustomEmoji(_, alt, _):
|
||||
if !item.file.isPremiumEmoji || hasPremium {
|
||||
if !alt.isEmpty, let keyword = allEmoticons[alt] {
|
||||
result.append((alt, item.file, keyword))
|
||||
} else if alt == query {
|
||||
result.append((alt, item.file, alt))
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var items: [EmojiPagerContentComponent.Item] = []
|
||||
|
||||
var existingIds = Set<MediaId>()
|
||||
for item in result {
|
||||
if let itemFile = item.1 {
|
||||
if existingIds.contains(itemFile.fileId) {
|
||||
continue
|
||||
}
|
||||
existingIds.insert(itemFile.fileId)
|
||||
let animationData = EntityKeyboardAnimationData(file: itemFile)
|
||||
let item = EmojiPagerContentComponent.Item(
|
||||
animationData: animationData,
|
||||
content: .animation(animationData),
|
||||
itemFile: itemFile, subgroupId: nil,
|
||||
icon: .none,
|
||||
accentTint: false
|
||||
)
|
||||
items.append(item)
|
||||
}
|
||||
}
|
||||
|
||||
var emptyResultEmoji: TelegramMediaFile?
|
||||
if let availableReactions = availableReactions {
|
||||
//let reactionFilter: [String] = ["😖", "😫", "🫠", "😨", "❓"]
|
||||
let filteredReactions: [TelegramMediaFile] = availableReactions.reactions.compactMap { reaction -> TelegramMediaFile? in
|
||||
switch reaction.value {
|
||||
case let .builtin(value):
|
||||
let _ = value
|
||||
//if reactionFilter.contains(value) {
|
||||
return reaction.selectAnimation
|
||||
/*} else {
|
||||
return nil
|
||||
}*/
|
||||
case .custom:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
emptyResultEmoji = filteredReactions.randomElement()
|
||||
}
|
||||
|
||||
return (result: [EmojiPagerContentComponent.ItemGroup(
|
||||
supergroupId: "search",
|
||||
groupId: "search",
|
||||
title: nil,
|
||||
subtitle: nil,
|
||||
actionButtonTitle: nil,
|
||||
isFeatured: false,
|
||||
isPremiumLocked: false,
|
||||
isEmbedded: false,
|
||||
hasClear: false,
|
||||
collapsedLineCount: nil,
|
||||
displayPremiumBadges: false,
|
||||
headerItem: nil,
|
||||
items: items
|
||||
)],
|
||||
emptyResultEmoji: emptyResultEmoji
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.emojiSearchDisposable.set((resultSignal
|
||||
|> delay(0.15, queue: .mainQueue())
|
||||
|> deliverOnMainQueue).start(next: { result in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.emojiSearchResult.set(.single((result.result, AnyHashable(query), result.emptyResultEmoji)))
|
||||
}))
|
||||
}
|
||||
},
|
||||
chatPeerId: nil,
|
||||
peekBehavior: nil,
|
||||
@ -398,6 +572,7 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
self.emojiContentDisposable?.dispose()
|
||||
self.availableReactionsDisposable?.dispose()
|
||||
self.genericReactionEffectDisposable?.dispose()
|
||||
self.emojiSearchDisposable.dispose()
|
||||
}
|
||||
|
||||
private func refreshLayout(transition: Transition) {
|
||||
@ -664,8 +839,14 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
emojiContent: emojiContent,
|
||||
backgroundColor: listBackgroundColor,
|
||||
separatorColor: separatorColor,
|
||||
hideTopPanel: false,
|
||||
hideTopPanelUpdated: { _, _ in }
|
||||
hideTopPanel: self.isReactionSearchActive,
|
||||
hideTopPanelUpdated: { [weak self] hideTopPanel, transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isReactionSearchActive = hideTopPanel
|
||||
strongSelf.refreshLayout(transition: transition)
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: componentWidth, height: min(308.0, layout.size.height))
|
||||
@ -1045,6 +1226,10 @@ public final class EmojiStatusSelectionController: ViewController {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
override public var overlayWantsToBeBelowKeyboard: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
public init(context: AccountContext, mode: Mode, sourceView: UIView, emojiContent: Signal<EmojiPagerContentComponent, NoError>, currentSelection: Int64?, destinationItemView: @escaping () -> UIView?) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
|
||||
@ -29,14 +29,12 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/VideoAnimationCache:VideoAnimationCache",
|
||||
"//submodules/TelegramUI/Components/MultiAnimationRenderer:MultiAnimationRenderer",
|
||||
"//submodules/TelegramUI/Components/EmojiTextAttachmentView:EmojiTextAttachmentView",
|
||||
"//submodules/TelegramUI/Components/EmojiStatusComponent:EmojiStatusComponent",
|
||||
"//submodules/SoftwareVideo:SoftwareVideo",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
"//submodules/StickerResources:StickerResources",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
#"//submodules/ContextUI:ContextUI",
|
||||
#"//submodules/PremiumUI:PremiumUI",
|
||||
#"//submodules/StickerPeekUI:StickerPeekUI",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||
"//submodules/Components/SolidRoundedButtonComponent:SolidRoundedButtonComponent",
|
||||
|
||||
@ -22,6 +22,7 @@ import UndoUI
|
||||
import AudioToolbox
|
||||
import SolidRoundedButtonComponent
|
||||
import EmojiTextAttachmentView
|
||||
import EmojiStatusComponent
|
||||
|
||||
private let premiumBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat List/PeerPremiumIcon"), color: .white)
|
||||
private let featuredBadgeIcon: UIImage? = generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Media/PanelBadgeAdd"), color: .white)
|
||||
@ -1512,6 +1513,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
private struct Params: Equatable {
|
||||
var theme: PresentationTheme
|
||||
var strings: PresentationStrings
|
||||
var text: String
|
||||
var useOpaqueTheme: Bool
|
||||
var isActive: Bool
|
||||
var size: CGSize
|
||||
@ -1523,6 +1525,9 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.useOpaqueTheme != rhs.useOpaqueTheme {
|
||||
return false
|
||||
}
|
||||
@ -1733,13 +1738,14 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
return
|
||||
}
|
||||
self.params = nil
|
||||
self.update(theme: params.theme, strings: params.strings, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, transition: transition)
|
||||
self.update(theme: params.theme, strings: params.strings, text: params.text, useOpaqueTheme: params.useOpaqueTheme, isActive: params.isActive, size: params.size, transition: transition)
|
||||
}
|
||||
|
||||
public func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, transition: Transition) {
|
||||
public func update(theme: PresentationTheme, strings: PresentationStrings, text: String, useOpaqueTheme: Bool, isActive: Bool, size: CGSize, transition: Transition) {
|
||||
let params = Params(
|
||||
theme: theme,
|
||||
strings: strings,
|
||||
text: text,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
isActive: isActive,
|
||||
size: size
|
||||
@ -1776,11 +1782,10 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
self.backgroundLayer.cornerRadius = inputHeight * 0.5
|
||||
self.tintBackgroundLayer.cornerRadius = inputHeight * 0.5
|
||||
|
||||
//TODO:localize
|
||||
let textSize = self.textView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: "Search Reactions",
|
||||
text: text,
|
||||
font: Font.regular(17.0),
|
||||
color: theme.chat.inputMediaPanel.panelContentVibrantOverlayColor
|
||||
)),
|
||||
@ -1790,7 +1795,7 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
let _ = self.tintTextView.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(
|
||||
text: "Search Reactions",
|
||||
text: text,
|
||||
font: Font.regular(17.0),
|
||||
color: .white
|
||||
)),
|
||||
@ -1892,6 +1897,99 @@ public final class EmojiSearchHeaderView: UIView, UITextFieldDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
private final class EmptySearchResultsView: UIView {
|
||||
override public static var layerClass: AnyClass {
|
||||
return PassthroughLayer.self
|
||||
}
|
||||
|
||||
let tintContainerView: UIView
|
||||
let titleLabel: ComponentView<Empty>
|
||||
let titleTintLabel: ComponentView<Empty>
|
||||
let icon: ComponentView<Empty>
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.tintContainerView = UIView()
|
||||
|
||||
self.titleLabel = ComponentView()
|
||||
self.titleTintLabel = ComponentView()
|
||||
self.icon = ComponentView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
(self.layer as? PassthroughLayer)?.mirrorLayer = self.tintContainerView.layer
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(context: AccountContext, theme: PresentationTheme, useOpaqueTheme: Bool, text: String, file: TelegramMediaFile?, size: CGSize, transition: Transition) {
|
||||
let titleColor: UIColor
|
||||
if useOpaqueTheme {
|
||||
titleColor = theme.chat.inputMediaPanel.panelContentControlOpaqueOverlayColor
|
||||
} else {
|
||||
titleColor = theme.chat.inputMediaPanel.panelContentControlVibrantOverlayColor
|
||||
}
|
||||
|
||||
let iconSize: CGSize
|
||||
if let file = file {
|
||||
iconSize = self.icon.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(EmojiStatusComponent(
|
||||
context: context,
|
||||
animationCache: context.animationCache,
|
||||
animationRenderer: context.animationRenderer,
|
||||
content: .animation(content: .file(file: file), size: CGSize(width: 32.0, height: 32.0), placeholderColor: titleColor, themeColor: nil, loopMode: .forever),
|
||||
isVisibleForAnimations: true,
|
||||
action: nil
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
} else {
|
||||
iconSize = CGSize()
|
||||
}
|
||||
|
||||
let titleSize = self.titleLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: text, font: Font.regular(15.0), color: titleColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 100.0)
|
||||
)
|
||||
let _ = self.titleTintLabel.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: text, font: Font.regular(15.0), color: .white)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: size.width, height: 100.0)
|
||||
)
|
||||
|
||||
let spacing: CGFloat = 4.0
|
||||
let contentHeight = iconSize.height + spacing + titleSize.height
|
||||
let contentOriginY = floor((size.height - contentHeight) / 2.0)
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floor((size.width - iconSize.width) / 2.0), y: contentOriginY), size: iconSize)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor((size.width - titleSize.width) / 2.0), y: iconFrame.maxY + spacing), size: titleSize)
|
||||
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
transition.setFrame(view: iconView, frame: iconFrame)
|
||||
}
|
||||
if let titleLabelView = self.titleLabel.view {
|
||||
if titleLabelView.superview == nil {
|
||||
self.addSubview(titleLabelView)
|
||||
}
|
||||
transition.setFrame(view: titleLabelView, frame: titleFrame)
|
||||
}
|
||||
if let titleTintLabelView = self.titleTintLabel.view {
|
||||
if titleTintLabelView.superview == nil {
|
||||
self.tintContainerView.addSubview(titleTintLabelView)
|
||||
}
|
||||
transition.setFrame(view: titleTintLabelView, frame: titleFrame)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public protocol EmojiContentPeekBehavior: AnyObject {
|
||||
func setGestureRecognizerEnabled(view: UIView, isEnabled: Bool, itemAtPoint: @escaping (CGPoint) -> (AnyHashable, EmojiPagerContentComponent.View.ItemLayer, TelegramMediaFile)?)
|
||||
}
|
||||
@ -2211,6 +2309,26 @@ public final class EmojiPagerContentComponent: Component {
|
||||
case detailed
|
||||
}
|
||||
|
||||
public final class EmptySearchResults: Equatable {
|
||||
public let text: String
|
||||
public let iconFile: TelegramMediaFile?
|
||||
|
||||
public init(text: String, iconFile: TelegramMediaFile?) {
|
||||
self.text = text
|
||||
self.iconFile = iconFile
|
||||
}
|
||||
|
||||
public static func ==(lhs: EmptySearchResults, rhs: EmptySearchResults) -> Bool {
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
if lhs.iconFile?.fileId != rhs.iconFile?.fileId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public let id: AnyHashable
|
||||
public let context: AccountContext
|
||||
public let avatarPeer: EnginePeer?
|
||||
@ -2221,7 +2339,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
public let itemLayoutType: ItemLayoutType
|
||||
public let itemContentUniqueId: AnyHashable?
|
||||
public let warpContentsOnEdges: Bool
|
||||
public let displaySearch: Bool
|
||||
public let displaySearchWithPlaceholder: String?
|
||||
public let emptySearchResults: EmptySearchResults?
|
||||
public let enableLongPress: Bool
|
||||
public let selectedItems: Set<MediaId>
|
||||
|
||||
@ -2236,7 +2355,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemLayoutType: ItemLayoutType,
|
||||
itemContentUniqueId: AnyHashable?,
|
||||
warpContentsOnEdges: Bool,
|
||||
displaySearch: Bool,
|
||||
displaySearchWithPlaceholder: String?,
|
||||
emptySearchResults: EmptySearchResults?,
|
||||
enableLongPress: Bool,
|
||||
selectedItems: Set<MediaId>
|
||||
) {
|
||||
@ -2250,12 +2370,13 @@ public final class EmojiPagerContentComponent: Component {
|
||||
self.itemLayoutType = itemLayoutType
|
||||
self.itemContentUniqueId = itemContentUniqueId
|
||||
self.warpContentsOnEdges = warpContentsOnEdges
|
||||
self.displaySearch = displaySearch
|
||||
self.displaySearchWithPlaceholder = displaySearchWithPlaceholder
|
||||
self.emptySearchResults = emptySearchResults
|
||||
self.enableLongPress = enableLongPress
|
||||
self.selectedItems = selectedItems
|
||||
}
|
||||
|
||||
public func withUpdatedItemGroups(itemGroups: [ItemGroup], itemContentUniqueId: AnyHashable?) -> EmojiPagerContentComponent {
|
||||
public func withUpdatedItemGroups(itemGroups: [ItemGroup], itemContentUniqueId: AnyHashable?, emptySearchResults: EmptySearchResults?) -> EmojiPagerContentComponent {
|
||||
return EmojiPagerContentComponent(
|
||||
id: self.id,
|
||||
context: self.context,
|
||||
@ -2267,7 +2388,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemLayoutType: self.itemLayoutType,
|
||||
itemContentUniqueId: itemContentUniqueId,
|
||||
warpContentsOnEdges: self.warpContentsOnEdges,
|
||||
displaySearch: self.displaySearch,
|
||||
displaySearchWithPlaceholder: self.displaySearchWithPlaceholder,
|
||||
emptySearchResults: emptySearchResults,
|
||||
enableLongPress: self.enableLongPress,
|
||||
selectedItems: self.selectedItems
|
||||
)
|
||||
@ -2304,7 +2426,10 @@ public final class EmojiPagerContentComponent: Component {
|
||||
if lhs.warpContentsOnEdges != rhs.warpContentsOnEdges {
|
||||
return false
|
||||
}
|
||||
if lhs.displaySearch != rhs.displaySearch {
|
||||
if lhs.displaySearchWithPlaceholder != rhs.displaySearchWithPlaceholder {
|
||||
return false
|
||||
}
|
||||
if lhs.emptySearchResults != rhs.emptySearchResults {
|
||||
return false
|
||||
}
|
||||
if lhs.enableLongPress != rhs.enableLongPress {
|
||||
@ -3079,6 +3204,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
|
||||
private let placeholdersContainerView: UIView
|
||||
private var visibleSearchHeader: EmojiSearchHeaderView?
|
||||
private var visibleEmptySearchResultsView: EmptySearchResultsView?
|
||||
private var visibleItemPlaceholderViews: [ItemLayer.Key: ItemPlaceholderView] = [:]
|
||||
private var visibleItemSelectionLayers: [ItemLayer.Key: ItemSelectionLayer] = [:]
|
||||
private var visibleItemLayers: [ItemLayer.Key: ItemLayer] = [:]
|
||||
@ -5703,7 +5829,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemGroups: itemGroups,
|
||||
expandedGroupIds: self.expandedGroupIds,
|
||||
curveNearBounds: component.warpContentsOnEdges,
|
||||
displaySearch: component.displaySearch,
|
||||
displaySearch: component.displaySearchWithPlaceholder != nil,
|
||||
isSearchActivated: self.isSearchActivated,
|
||||
customLayout: component.inputInteractionHolder.inputInteraction?.customLayout
|
||||
)
|
||||
@ -5730,7 +5856,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
transition.setPosition(view: self.scrollView, position: CGPoint(x: 0.0, y: scrollOriginY))
|
||||
let previousSize = self.scrollView.bounds.size
|
||||
var resetScrolling = false
|
||||
if self.scrollView.bounds.isEmpty && component.displaySearch {
|
||||
if self.scrollView.bounds.isEmpty && component.displaySearchWithPlaceholder != nil {
|
||||
resetScrolling = true
|
||||
}
|
||||
if previousComponent?.itemContentUniqueId != component.itemContentUniqueId {
|
||||
@ -5829,7 +5955,7 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
|
||||
if resetScrolling {
|
||||
if component.displaySearch && !self.isSearchActivated {
|
||||
if component.displaySearchWithPlaceholder != nil && !self.isSearchActivated {
|
||||
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 50.0), size: scrollSize)
|
||||
} else {
|
||||
self.scrollView.bounds = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: scrollSize)
|
||||
@ -5879,7 +6005,9 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if component.displaySearch {
|
||||
let useOpaqueTheme = component.inputInteractionHolder.inputInteraction?.useOpaqueTheme ?? false
|
||||
|
||||
if let displaySearchWithPlaceholder = component.displaySearchWithPlaceholder {
|
||||
let visibleSearchHeader: EmojiSearchHeaderView
|
||||
if let current = self.visibleSearchHeader {
|
||||
visibleSearchHeader = current
|
||||
@ -5924,12 +6052,10 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
let useOpaqueTheme = component.inputInteractionHolder.inputInteraction?.useOpaqueTheme ?? false
|
||||
|
||||
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(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, transition: transition)
|
||||
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] _ in
|
||||
guard let strongSelf = self, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
|
||||
visibleSearchHeader.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, text: displaySearchWithPlaceholder, useOpaqueTheme: useOpaqueTheme, isActive: self.isSearchActivated, size: searchHeaderFrame.size, transition: transition)
|
||||
transition.setFrame(view: visibleSearchHeader, frame: searchHeaderFrame, completion: { [weak self] completed in
|
||||
guard let strongSelf = self, completed, let visibleSearchHeader = strongSelf.visibleSearchHeader else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -5946,6 +6072,37 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
if let emptySearchResults = component.emptySearchResults {
|
||||
let visibleEmptySearchResultsView: EmptySearchResultsView
|
||||
var emptySearchResultsTransition = transition
|
||||
if let current = self.visibleEmptySearchResultsView {
|
||||
visibleEmptySearchResultsView = current
|
||||
} else {
|
||||
emptySearchResultsTransition = .immediate
|
||||
visibleEmptySearchResultsView = EmptySearchResultsView(frame: CGRect())
|
||||
self.visibleEmptySearchResultsView = visibleEmptySearchResultsView
|
||||
self.addSubview(visibleEmptySearchResultsView)
|
||||
self.mirrorContentClippingView?.addSubview(visibleEmptySearchResultsView.tintContainerView)
|
||||
}
|
||||
let emptySearchResultsSize = CGSize(width: availableSize.width, height: availableSize.height - itemLayout.searchInsets.top - itemLayout.searchHeight)
|
||||
visibleEmptySearchResultsView.update(
|
||||
context: component.context,
|
||||
theme: keyboardChildEnvironment.theme,
|
||||
useOpaqueTheme: useOpaqueTheme,
|
||||
text: emptySearchResults.text,
|
||||
file: emptySearchResults.iconFile,
|
||||
size: emptySearchResultsSize,
|
||||
transition: emptySearchResultsTransition
|
||||
)
|
||||
emptySearchResultsTransition.setFrame(view: visibleEmptySearchResultsView, frame: CGRect(origin: CGPoint(x: 0.0, y: itemLayout.searchInsets.top + itemLayout.searchHeight), size: emptySearchResultsSize))
|
||||
} else {
|
||||
if let visibleEmptySearchResultsView = self.visibleEmptySearchResultsView {
|
||||
self.visibleEmptySearchResultsView = nil
|
||||
visibleEmptySearchResultsView.removeFromSuperview()
|
||||
visibleEmptySearchResultsView.tintContainerView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
self.updateVisibleItems(transition: itemTransition, attemptSynchronousLoads: attemptSynchronousLoads, previousItemPositions: previousItemPositions, previousAbsoluteItemPositions: previousAbsoluteItemPositions, updatedItemPositions: updatedItemPositions, hintDisappearingGroupFrame: hintDisappearingGroupFrame)
|
||||
|
||||
return availableSize
|
||||
@ -6623,6 +6780,13 @@ public final class EmojiPagerContentComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
var displaySearchWithPlaceholder: String?
|
||||
if isReactionSelection {
|
||||
displaySearchWithPlaceholder = "Search Reactions"
|
||||
} else if isStatusSelection {
|
||||
displaySearchWithPlaceholder = "Search Statuses"
|
||||
}
|
||||
|
||||
return EmojiPagerContentComponent(
|
||||
id: "emoji",
|
||||
context: context,
|
||||
@ -6667,7 +6831,8 @@ public final class EmojiPagerContentComponent: Component {
|
||||
itemLayoutType: .compact,
|
||||
itemContentUniqueId: nil,
|
||||
warpContentsOnEdges: isReactionSelection || isStatusSelection,
|
||||
displaySearch: isReactionSelection,
|
||||
displaySearchWithPlaceholder: displaySearchWithPlaceholder,
|
||||
emptySearchResults: nil,
|
||||
enableLongPress: (isReactionSelection && !isQuickReactionSelection) || isStatusSelection,
|
||||
selectedItems: selectedItems
|
||||
)
|
||||
|
||||
@ -537,7 +537,8 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
itemLayoutType: .detailed,
|
||||
itemContentUniqueId: nil,
|
||||
warpContentsOnEdges: false,
|
||||
displaySearch: false,
|
||||
displaySearchWithPlaceholder: nil,
|
||||
emptySearchResults: nil,
|
||||
enableLongPress: false,
|
||||
selectedItems: Set()
|
||||
)
|
||||
@ -1714,9 +1715,9 @@ final class ChatEntityKeyboardInputNode: ChatInputNode {
|
||||
|
||||
private func processInputData(inputData: InputData) -> InputData {
|
||||
return InputData(
|
||||
emoji: inputData.emoji.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.itemGroups), itemContentUniqueId: nil),
|
||||
emoji: inputData.emoji.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .emoji, itemGroups: inputData.emoji.itemGroups), itemContentUniqueId: nil, emptySearchResults: nil),
|
||||
stickers: inputData.stickers.flatMap { stickers in
|
||||
return stickers.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.itemGroups), itemContentUniqueId: nil)
|
||||
return stickers.withUpdatedItemGroups(itemGroups: self.processStableItemGroupList(category: .stickers, itemGroups: stickers.itemGroups), itemContentUniqueId: nil, emptySearchResults: nil)
|
||||
},
|
||||
gifs: inputData.gifs,
|
||||
availableGifSearchEmojies: inputData.availableGifSearchEmojies
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user