import Foundation import UIKit import AccountContext import TelegramCore import Postbox import SwiftSignalKit import AnimationCache import MultiAnimationRenderer import TelegramNotices public extension EmojiPagerContentComponent { private static func hasPremium(context: AccountContext, chatPeerId: EnginePeer.Id?, premiumIfSavedMessages: Bool) -> Signal { let hasPremium: Signal if premiumIfSavedMessages, let chatPeerId = chatPeerId, chatPeerId == context.account.peerId { hasPremium = .single(true) } else { 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 } return hasPremium } enum Subject: Equatable { case generic case status case channelStatus case reaction(onlyTop: Bool) case messageTag case emoji case topicIcon case quickReaction case profilePhoto case groupPhoto case backgroundIcon case reactionList case stickerAlt } static func emojiInputData( context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, isStandalone: Bool, subject: Subject, hasTrending: Bool, topReactionItems: [EmojiComponentReactionItem], topEmojiItems: [String] = [], areUnicodeEmojiEnabled: Bool, areCustomEmojiEnabled: Bool, chatPeerId: EnginePeer.Id?, selectedItems: Set = Set(), topStatusTitle: String? = nil, topicTitle: String? = nil, topicColor: Int32? = nil, backgroundIconColor: UIColor? = nil, hasSearch: Bool = true, hasRecent: Bool = true, forceHasPremium: Bool = false, premiumIfSavedMessages: Bool = true, hideBackground: Bool = false ) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings struct PeerSpecificPackData: Equatable { var info: StickerPackCollectionInfo var items: [StickerPackItem] var peer: EnginePeer static func ==(lhs: PeerSpecificPackData, rhs: PeerSpecificPackData) -> Bool { if lhs.info.id != rhs.info.id { return false } if lhs.items != rhs.items { return false } if lhs.peer != rhs.peer { return false } return true } } let peerSpecificPack: Signal if let chatPeerId = chatPeerId { peerSpecificPack = combineLatest( context.engine.peers.peerSpecificEmojiPack(peerId: chatPeerId), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId)) ) |> map { packData, peer -> PeerSpecificPackData? in guard let peer = peer else { return nil } guard let (info, items) = packData.packInfo else { return nil } return PeerSpecificPackData(info: info, items: items.compactMap { $0 as? StickerPackItem }, peer: peer) } |> distinctUntilChanged } else { peerSpecificPack = .single(nil) } var orderedItemListCollectionIds: [Int32] = [] switch subject { case .backgroundIcon, .reactionList: break default: orderedItemListCollectionIds.append(Namespaces.OrderedItemList.LocalRecentEmoji) } var iconStatusEmoji: Signal<[TelegramMediaFile], NoError> = .single([]) if case .status = subject { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedStatusEmoji) orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentStatusEmoji) iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconStatusEmoji, forceActualized: false) |> map { result -> [TelegramMediaFile] in switch result { case let .result(_, items, _): return items.map(\.file) default: return [] } } |> take(1) } else if case .channelStatus = subject { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedChannelStatusEmoji) orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudDisabledChannelStatusEmoji) iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconChannelStatusEmoji, forceActualized: false) |> map { result -> [TelegramMediaFile] in switch result { case let .result(_, items, _): return items.map(\.file) default: return [] } } |> take(1) } else if [.reaction(onlyTop: false), .quickReaction].contains(subject) { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudTopReactions) orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudRecentReactions) } else if case .messageTag = subject { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudDefaultTagReactions) } else if case .topicIcon = subject { iconStatusEmoji = context.engine.stickers.loadedStickerPack(reference: .iconTopicEmoji, forceActualized: false) |> map { result -> [TelegramMediaFile] in switch result { case let .result(_, items, _): return items.map(\.file) default: return [] } } |> take(1) } else if case .profilePhoto = subject { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedProfilePhotoEmoji) } else if case .groupPhoto = subject { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedGroupPhotoEmoji) } else if case .backgroundIcon = subject { orderedItemListCollectionIds.append(Namespaces.OrderedItemList.CloudFeaturedBackgroundIconEmoji) } let availableReactions: Signal if [.reaction(onlyTop: false), .quickReaction, .reactionList].contains(subject) { availableReactions = context.engine.stickers.availableReactions() } else { availableReactions = .single(nil) } let searchCategories: Signal if [.emoji, .reaction(onlyTop: false), .reactionList, .messageTag].contains(subject) { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .emoji) } else if case .status = subject { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .status) } else if case .channelStatus = subject { searchCategories = .single(nil) } else if [.profilePhoto, .groupPhoto].contains(subject) { searchCategories = context.engine.stickers.emojiSearchCategories(kind: .avatar) } else { searchCategories = .single(nil) } let emojiItems: Signal = combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000), forceHasPremium ? .single(true) : hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: premiumIfSavedMessages), context.account.viewTracker.featuredEmojiPacks(), availableReactions, searchCategories, iconStatusEmoji, peerSpecificPack, ApplicationSpecificNotice.dismissedTrendingEmojiPacks(accountManager: context.sharedContext.accountManager) ) |> map { view, hasPremium, featuredEmojiPacks, availableReactions, searchCategories, iconStatusEmoji, peerSpecificPack, dismissedTrendingEmojiPacks -> EmojiPagerContentComponent in struct ItemGroup { var supergroupId: AnyHashable var id: AnyHashable var title: String? var subtitle: String? var badge: String? var isPremiumLocked: Bool var isFeatured: Bool var collapsedLineCount: Int? var isClearable: Bool var headerItem: EntityKeyboardAnimationData? var items: [EmojiPagerContentComponent.Item] } var itemGroups: [ItemGroup] = [] var itemGroupIndexById: [AnyHashable: Int] = [:] let maybeAppendUnicodeEmoji = { let groupId: AnyHashable = "static" if itemGroupIndexById[groupId] != nil { return } if areUnicodeEmojiEnabled { for (subgroupId, list) in staticEmojiMapping { for emojiString in list { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .staticEmoji(emojiString), itemFile: nil, subgroupId: subgroupId.rawValue, icon: .none, tintMode: .none ) if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count let title: String? if case .stickerAlt = subject { title = nil } else { title = strings.EmojiInput_SectionTitleEmoji } itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } } } } var installedCollectionIds = Set() for (id, _, _) in view.collectionInfos { installedCollectionIds.insert(id) } let dismissedTrendingEmojiPacksSet = Set(dismissedTrendingEmojiPacks ?? []) let featuredEmojiPacksSet = Set(featuredEmojiPacks.map(\.info.id.id)) if dismissedTrendingEmojiPacksSet != featuredEmojiPacksSet && hasTrending { for featuredEmojiPack in featuredEmojiPacks { if installedCollectionIds.contains(featuredEmojiPack.info.id) { continue } guard let item = featuredEmojiPack.topItems.first else { continue } let animationData: EntityKeyboardAnimationData if let thumbnail = featuredEmojiPack.info.thumbnail { let type: EntityKeyboardAnimationData.ItemType if item.file.isAnimatedSticker { type = .lottie } else if item.file.isVideoEmoji || item.file.isVideoSticker { type = .video } else { type = .still } animationData = EntityKeyboardAnimationData( id: .stickerPackThumbnail(featuredEmojiPack.info.id), type: type, resource: .stickerPackThumbnail(stickerPack: .id(id: featuredEmojiPack.info.id.id, accessHash: featuredEmojiPack.info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: featuredEmojiPack.info.immediateThumbnailData, isReaction: false, isTemplate: false ) } else { animationData = EntityKeyboardAnimationData(file: item.file) } var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { tintMode = .primary } let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, subgroupId: nil, icon: .none, tintMode: tintMode ) let supergroupId = "featuredTop" let groupId: AnyHashable = supergroupId let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium if isPremiumLocked && isPremiumDisabled { continue } if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count let title = context.sharedContext.currentPresentationData.with({ $0 }).strings.EmojiInput_TrendingEmoji itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 0, isClearable: false, headerItem: nil, items: [resultItem])) } } } var recentEmoji: OrderedItemListView? var featuredStatusEmoji: OrderedItemListView? var featuredChannelStatusEmoji: OrderedItemListView? var disabledChannelStatusEmoji: OrderedItemListView? var recentStatusEmoji: OrderedItemListView? var topReactions: OrderedItemListView? var recentReactions: OrderedItemListView? var featuredAvatarEmoji: OrderedItemListView? var featuredBackgroundIconEmoji: OrderedItemListView? var defaultTagReactions: OrderedItemListView? for orderedView in view.orderedItemListsViews { if orderedView.collectionId == Namespaces.OrderedItemList.LocalRecentEmoji { recentEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedStatusEmoji { featuredStatusEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedChannelStatusEmoji { featuredChannelStatusEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudDisabledChannelStatusEmoji { disabledChannelStatusEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStatusEmoji { recentStatusEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentReactions { recentReactions = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudTopReactions { topReactions = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedProfilePhotoEmoji { featuredAvatarEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedGroupPhotoEmoji { featuredAvatarEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudFeaturedBackgroundIconEmoji { featuredBackgroundIconEmoji = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudDefaultTagReactions { defaultTagReactions = orderedView } } if case .stickerAlt = subject { for emoji in topEmojiItems { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .staticEmoji(emoji), itemFile: nil, subgroupId: nil, icon: .none, tintMode: .none ) let groupId = "recent" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) } } } else if case .topicIcon = subject { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .icon(.topic(String((topicTitle ?? "").prefix(1)), topicColor ?? 0)), itemFile: nil, subgroupId: nil, icon: .none, tintMode: .none ) let groupId = "recent" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) } var existingIds = Set() for file in iconStatusEmoji { if existingIds.contains(file.fileId) { continue } existingIds.insert(file.fileId) var tintMode: Item.TintMode = .none if file.isCustomTemplateEmoji { tintMode = .accent } for attribute in file.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute { switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { tintMode = .accent } default: break } } } let resultItem: EmojiPagerContentComponent.Item let animationData = EntityKeyboardAnimationData(file: file) resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: file, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } } } else if case .status = subject { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .icon(.premiumStar), itemFile: nil, subgroupId: nil, icon: .none, tintMode: .none ) let groupId = "recent" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) } var existingIds = Set() for file in iconStatusEmoji.prefix(7) { if existingIds.contains(file.fileId) { continue } existingIds.insert(file.fileId) var tintMode: Item.TintMode = .none if file.isCustomTemplateEmoji { tintMode = .accent } for attribute in file.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute { switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { tintMode = .accent } default: break } } } let resultItem: EmojiPagerContentComponent.Item let animationData = EntityKeyboardAnimationData(file: file) resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: file, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } } if let recentStatusEmoji = recentStatusEmoji { for item in recentStatusEmoji.items { guard let item = item.contents.get(RecentMediaItem.self) else { continue } let file = item.media if existingIds.contains(file.fileId) { continue } existingIds.insert(file.fileId) var tintMode: Item.TintMode = .none if file.isCustomTemplateEmoji { tintMode = .accent } for attribute in file.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute { switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { tintMode = .accent } default: break } } } let resultItem: EmojiPagerContentComponent.Item let animationData = EntityKeyboardAnimationData(file: file) resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: file, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { break } itemGroups[groupIndex].items.append(resultItem) } } } if let featuredStatusEmoji = featuredStatusEmoji { for item in featuredStatusEmoji.items { guard let item = item.contents.get(RecentMediaItem.self) else { continue } let file = item.media if existingIds.contains(file.fileId) { continue } existingIds.insert(file.fileId) let resultItem: EmojiPagerContentComponent.Item var tintMode: Item.TintMode = .none if file.isCustomTemplateEmoji { tintMode = .accent } for attribute in file.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute { switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { tintMode = .accent } default: break } } } let animationData = EntityKeyboardAnimationData(file: file) resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: file, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { break } itemGroups[groupIndex].items.append(resultItem) } } } } else if case .channelStatus = subject { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .icon(.stop), itemFile: nil, subgroupId: nil, icon: .none, tintMode: .accent ) let groupId = "recent" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) } var existingIds = Set() if let disabledChannelStatusEmoji { for item in disabledChannelStatusEmoji.items { guard let item = item.contents.get(RecentMediaItem.self) else { continue } let file = item.media existingIds.insert(file.fileId) } } for file in iconStatusEmoji.prefix(7) { if existingIds.contains(file.fileId) { continue } existingIds.insert(file.fileId) var tintMode: Item.TintMode = .none if file.isCustomTemplateEmoji { tintMode = .accent } for attribute in file.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute { switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { tintMode = .accent } default: break } } } let resultItem: EmojiPagerContentComponent.Item let animationData = EntityKeyboardAnimationData(file: file) resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: file, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } } if let featuredChannelStatusEmoji { for item in featuredChannelStatusEmoji.items { guard let item = item.contents.get(RecentMediaItem.self) else { continue } let file = item.media if existingIds.contains(file.fileId) { continue } existingIds.insert(file.fileId) let resultItem: EmojiPagerContentComponent.Item var tintMode: Item.TintMode = .none if file.isCustomTemplateEmoji { tintMode = .accent } for attribute in file.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute { switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { tintMode = .accent } default: break } } } let animationData = EntityKeyboardAnimationData(file: file) resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: file, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { /*if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { break }*/ itemGroups[groupIndex].items.append(resultItem) } } } } else if case .reactionList = subject { var existingIds = Set() if let availableReactions = availableReactions { for reactionItem in availableReactions.reactions { if !reactionItem.isEnabled { continue } if existingIds.contains(reactionItem.value) { continue } existingIds.insert(reactionItem.value) let icon: EmojiPagerContentComponent.Item.Icon if !hasPremium, case .custom = reactionItem.value { icon = .locked } else { icon = .none } var tintMode: Item.TintMode = .none if reactionItem.selectAnimation.isCustomTemplateEmoji { tintMode = .primary } let animationFile = reactionItem.selectAnimation let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: animationFile, subgroupId: nil, icon: icon, tintMode: tintMode ) let groupId = "liked" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } } } else if [.reaction(onlyTop: true), .reaction(onlyTop: false), .quickReaction].contains(subject) { var existingIds = Set() var topReactionItems = topReactionItems if topReactionItems.isEmpty { if let topReactions = topReactions { for item in topReactions.items { guard let topReaction = item.contents.get(RecentReactionItem.self) else { continue } switch topReaction.content { case let .builtin(value): if let reaction = availableReactions?.reactions.first(where: { $0.value == .builtin(value) }) { topReactionItems.append(EmojiComponentReactionItem(reaction: .builtin(value), file: reaction.selectAnimation)) } else { continue } case let .custom(file): topReactionItems.append(EmojiComponentReactionItem(reaction: .custom(file.fileId.id), file: file)) } } } } let maxTopLineCount: Int if case .reaction(onlyTop: true) = subject { maxTopLineCount = 1000 } else if hasPremium { maxTopLineCount = 2 } else { maxTopLineCount = 6 } for reactionItem in topReactionItems { if existingIds.contains(reactionItem.reaction) { continue } existingIds.insert(reactionItem.reaction) let icon: EmojiPagerContentComponent.Item.Icon if case .reaction(onlyTop: true) = subject { icon = .none } else if !hasPremium, case .custom = reactionItem.reaction { icon = .locked } else { icon = .none } var tintMode: Item.TintMode = .none if reactionItem.file.isCustomTemplateEmoji { tintMode = .primary } let animationFile = reactionItem.file let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: animationFile, subgroupId: nil, icon: icon, tintMode: tintMode ) let groupId = "recent" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) if itemGroups[groupIndex].items.count >= 8 * maxTopLineCount { break } } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } if case .reaction(onlyTop: false) = subject { var hasRecent = false if let recentReactions = recentReactions, !recentReactions.items.isEmpty { hasRecent = true } let maxRecentLineCount: Int if hasPremium { maxRecentLineCount = 10 } else { maxRecentLineCount = 10 } let popularTitle = hasRecent ? strings.Chat_ReactionSection_Recent : strings.Chat_ReactionSection_Popular if let availableReactions = availableReactions { for reactionItem in availableReactions.reactions { if !reactionItem.isEnabled { continue } if existingIds.contains(reactionItem.value) { continue } existingIds.insert(reactionItem.value) let icon: EmojiPagerContentComponent.Item.Icon if !hasPremium, case .custom = reactionItem.value { icon = .locked } else { icon = .none } var tintMode: Item.TintMode = .none if reactionItem.selectAnimation.isCustomTemplateEmoji { tintMode = .primary } let animationFile = reactionItem.selectAnimation let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: animationFile, subgroupId: nil, icon: icon, tintMode: tintMode ) if hasPremium { let groupId = "popular" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && subject != .quickReaction, headerItem: nil, items: [resultItem])) } } else { let groupId = "recent" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) if itemGroups[groupIndex].items.count >= maxRecentLineCount * 8 { break } } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } } } if let recentReactions = recentReactions { var popularInsertIndex = 0 for item in recentReactions.items { guard let item = item.contents.get(RecentReactionItem.self) else { continue } let animationFile: TelegramMediaFile let icon: EmojiPagerContentComponent.Item.Icon switch item.content { case let .builtin(value): if existingIds.contains(.builtin(value)) { continue } existingIds.insert(.builtin(value)) if let availableReactions = availableReactions, let availableReaction = availableReactions.reactions.first(where: { $0.value == .builtin(value) }) { if let centerAnimation = availableReaction.centerAnimation { animationFile = centerAnimation } else { continue } } else { continue } icon = .none case let .custom(file): if existingIds.contains(.custom(file.fileId.id)) { continue } existingIds.insert(.custom(file.fileId.id)) animationFile = file if !hasPremium { icon = .locked } else { icon = .none } } var tintMode: Item.TintMode = .none if animationFile.isCustomTemplateEmoji { tintMode = .primary } let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: animationFile, subgroupId: nil, icon: icon, tintMode: tintMode ) let groupId = "popular" if let groupIndex = itemGroupIndexById[groupId] { if itemGroups[groupIndex].items.count + 1 >= maxRecentLineCount * 8 { break } itemGroups[groupIndex].items.insert(resultItem, at: popularInsertIndex) popularInsertIndex += 1 } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: popularTitle, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: hasRecent && subject != .quickReaction, headerItem: nil, items: [resultItem])) } } } } } else if case .messageTag = subject { var existingIds = Set() var topReactionItems = topReactionItems if topReactionItems.isEmpty { if let defaultTagReactions { for item in defaultTagReactions.items { guard let topReaction = item.contents.get(RecentReactionItem.self) else { continue } switch topReaction.content { case let .builtin(value): if let reaction = availableReactions?.reactions.first(where: { $0.value == .builtin(value) }) { topReactionItems.append(EmojiComponentReactionItem(reaction: .builtin(value), file: reaction.selectAnimation)) } else { continue } case let .custom(file): topReactionItems.append(EmojiComponentReactionItem(reaction: .custom(file.fileId.id), file: file)) } } } } let maxTopLineCount: Int = 1000 for reactionItem in topReactionItems { if existingIds.contains(reactionItem.reaction) { continue } existingIds.insert(reactionItem.reaction) let icon: EmojiPagerContentComponent.Item.Icon = .none var tintMode: Item.TintMode = .none if reactionItem.file.isCustomTemplateEmoji { tintMode = .primary } let animationFile = reactionItem.file let animationData = EntityKeyboardAnimationData(file: animationFile, isReaction: true) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: animationFile, subgroupId: nil, icon: icon, tintMode: tintMode ) let groupId = "recent" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) if itemGroups[groupIndex].items.count >= 8 * maxTopLineCount { break } } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } } else if [.profilePhoto, .groupPhoto].contains(subject) { var existingIds = Set() let groupId = "recent" itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: topStatusTitle?.uppercased(), subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [])) if let featuredAvatarEmoji = featuredAvatarEmoji { for item in featuredAvatarEmoji.items { guard let item = item.contents.get(RecentMediaItem.self) else { continue } let file = item.media if existingIds.contains(file.fileId) { continue } existingIds.insert(file.fileId) let resultItem: EmojiPagerContentComponent.Item var tintMode: Item.TintMode = .none if file.isCustomTemplateEmoji { tintMode = .accent } for attribute in file.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute { switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { tintMode = .accent } default: break } } } let animationData = EntityKeyboardAnimationData(file: file) resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: file, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { break } itemGroups[groupIndex].items.append(resultItem) } } } } else if case .backgroundIcon = subject { var existingIds = Set() let resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .icon(.stop), itemFile: nil, subgroupId: nil, icon: .none, tintMode: .accent ) let groupId = "recent" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: nil, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: 5, isClearable: false, headerItem: nil, items: [resultItem])) } if let featuredBackgroundIconEmoji { for item in featuredBackgroundIconEmoji.items { guard let item = item.contents.get(RecentMediaItem.self) else { continue } let file = item.media if existingIds.contains(file.fileId) { continue } existingIds.insert(file.fileId) let resultItem: EmojiPagerContentComponent.Item var tintMode: Item.TintMode = .none if file.isCustomTemplateEmoji { if let backgroundIconColor { tintMode = .custom(backgroundIconColor) } else { tintMode = .accent } } for attribute in file.attributes { if case let .CustomEmoji(_, _, _, packReference) = attribute { switch packReference { case let .id(id, _): if id == 773947703670341676 || id == 2964141614563343 { tintMode = .accent } default: break } } } let animationData = EntityKeyboardAnimationData(file: file) resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: file, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { if itemGroups[groupIndex].items.count >= (5 + 8) * 8 { break } itemGroups[groupIndex].items.append(resultItem) } } } } let hasRecentEmoji = ![.reaction(onlyTop: true), .reaction(onlyTop: false), .quickReaction, .status, .profilePhoto, .groupPhoto, .topicIcon, .backgroundIcon, .reactionList, .messageTag, .stickerAlt].contains(subject) if let recentEmoji = recentEmoji, hasRecentEmoji { for item in recentEmoji.items { guard let item = item.contents.get(RecentEmojiItem.self) else { continue } if case let .file(file) = item.content, isPremiumDisabled, file.isPremiumEmoji { continue } if !areCustomEmojiEnabled, case .file = item.content { continue } let resultItem: EmojiPagerContentComponent.Item switch item.content { case let .file(file): var tintMode: Item.TintMode = .none if file.isCustomTemplateEmoji { tintMode = .primary } let animationData = EntityKeyboardAnimationData(file: file) resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: file, subgroupId: nil, icon: .none, tintMode: tintMode ) case let .text(text): resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .staticEmoji(text), itemFile: nil, subgroupId: nil, icon: .none, tintMode: .none ) } let groupId = "recent" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Emoji_FrequentlyUsed, subtitle: nil, badge: nil, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: true, headerItem: nil, items: [resultItem])) } } } var itemCollectionMapping: [ItemCollectionId: StickerPackCollectionInfo] = [:] for (id, info, _) in view.collectionInfos { if let info = info as? StickerPackCollectionInfo { itemCollectionMapping[id] = info } } var skippedCollectionIds = Set() var avatarPeer: EnginePeer? if let peerSpecificPack = peerSpecificPack { avatarPeer = peerSpecificPack.peer var processedIds = Set() for item in peerSpecificPack.items { if isPremiumDisabled && item.file.isPremiumSticker { continue } if processedIds.contains(item.file.fileId) { continue } processedIds.insert(item.file.fileId) var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { tintMode = .primary } let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, subgroupId: nil, icon: .none, tintMode: tintMode ) let groupId = "peerSpecific" if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.info.title, subtitle: nil, badge: strings.Emoji_GroupEmoji, isPremiumLocked: false, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: nil, items: [resultItem])) } } let supergroupId: AnyHashable = peerSpecificPack.info.id skippedCollectionIds.insert(supergroupId) } if !hasPremium { maybeAppendUnicodeEmoji() } if areCustomEmojiEnabled { for entry in view.entries { guard let item = entry.item as? StickerPackItem else { continue } var icon: EmojiPagerContentComponent.Item.Icon = .none if [.reaction(onlyTop: false), .quickReaction].contains(subject), !hasPremium { icon = .locked } let supergroupId = entry.index.collectionId let groupId: AnyHashable = supergroupId if skippedCollectionIds.contains(groupId) { continue } if case .channelStatus = subject { guard let collection = itemCollectionMapping[entry.index.collectionId] else { continue } if !collection.flags.contains(.isAvailableAsChannelStatus) { continue } } var isTemplate = false var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { if [.status, .channelStatus, .backgroundIcon].contains(subject) { if let backgroundIconColor { tintMode = .custom(backgroundIconColor) } else { tintMode = .accent } } else { tintMode = .primary } if case .backgroundIcon = subject { isTemplate = true } } else if case .backgroundIcon = subject { skippedCollectionIds.insert(groupId) continue } let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, subgroupId: nil, icon: icon, tintMode: tintMode ) let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium if isPremiumLocked && isPremiumDisabled { continue } if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count var title = "" var headerItem: EntityKeyboardAnimationData? inner: for (id, info, _) in view.collectionInfos { if id == entry.index.collectionId, let info = info as? StickerPackCollectionInfo { title = info.title if let thumbnail = info.thumbnail { let type: EntityKeyboardAnimationData.ItemType if item.file.isAnimatedSticker { type = .lottie } else if item.file.isVideoEmoji || item.file.isVideoSticker { type = .video } else { type = .still } headerItem = EntityKeyboardAnimationData( id: .stickerPackThumbnail(info.id), type: type, resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: info.immediateThumbnailData, isReaction: false, isTemplate: isTemplate ) } break inner } } itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: title, subtitle: nil, badge: nil, isPremiumLocked: isPremiumLocked, isFeatured: false, collapsedLineCount: nil, isClearable: false, headerItem: headerItem, items: [resultItem])) } } if !isStandalone { for featuredEmojiPack in featuredEmojiPacks { if installedCollectionIds.contains(featuredEmojiPack.info.id) { continue } let supergroupId = featuredEmojiPack.info.id let groupId: AnyHashable = supergroupId if skippedCollectionIds.contains(groupId) { continue } if case .channelStatus = subject { if !featuredEmojiPack.info.flags.contains(.isAvailableAsChannelStatus) { continue } } for item in featuredEmojiPack.topItems { var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { if [.status, .channelStatus, .backgroundIcon].contains(subject) { if let backgroundIconColor { tintMode = .custom(backgroundIconColor) } else { tintMode = .accent } } else { tintMode = .primary } } else if case .backgroundIcon = subject { skippedCollectionIds.insert(groupId) continue } let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, subgroupId: nil, icon: .none, tintMode: tintMode ) let isPremiumLocked: Bool = item.file.isPremiumEmoji && !hasPremium if isPremiumLocked && isPremiumDisabled { continue } if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count var headerItem: EntityKeyboardAnimationData? if let thumbnailFileId = featuredEmojiPack.info.thumbnailFileId, let file = featuredEmojiPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) { headerItem = EntityKeyboardAnimationData(file: file.file) } else if let thumbnail = featuredEmojiPack.info.thumbnail { let info = featuredEmojiPack.info let type: EntityKeyboardAnimationData.ItemType if item.file.isAnimatedSticker { type = .lottie } else if item.file.isVideoEmoji || item.file.isVideoSticker { type = .video } else { type = .still } headerItem = EntityKeyboardAnimationData( id: .stickerPackThumbnail(info.id), type: type, resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: info.immediateThumbnailData, isReaction: false, isTemplate: false ) } var isFeatured = true if case .reactionList = subject { isFeatured = false } itemGroups.append(ItemGroup(supergroupId: supergroupId, id: groupId, title: featuredEmojiPack.info.title, subtitle: nil, badge: nil, isPremiumLocked: isPremiumLocked, isFeatured: isFeatured, collapsedLineCount: 3, isClearable: false, headerItem: headerItem, items: [resultItem])) } } } } } if hasPremium { maybeAppendUnicodeEmoji() } var displaySearchWithPlaceholder: String? let searchInitiallyHidden = true if hasSearch { if [.reaction(onlyTop: false), .quickReaction, .messageTag].contains(subject) { displaySearchWithPlaceholder = strings.EmojiSearch_SearchReactionsPlaceholder } else if case .status = subject { displaySearchWithPlaceholder = strings.EmojiSearch_SearchStatusesPlaceholder } else if case .emoji = subject { displaySearchWithPlaceholder = strings.EmojiSearch_SearchEmojiPlaceholder } else if [.profilePhoto, .groupPhoto].contains(subject) { displaySearchWithPlaceholder = strings.Common_Search } else if case .stickerAlt = subject { displaySearchWithPlaceholder = strings.Common_Search } } let allItemGroups = itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in var hasClear = group.isClearable var isEmbedded = false if group.id == AnyHashable("featuredTop") { hasClear = true isEmbedded = true } var headerItem = group.headerItem if let groupId = group.id.base as? ItemCollectionId { outer: for (id, info, _) in view.collectionInfos { if id == groupId, let info = info as? StickerPackCollectionInfo { if let thumbnailFileId = info.thumbnailFileId { for item in group.items { if let itemFile = item.itemFile, itemFile.fileId.id == thumbnailFileId { headerItem = EntityKeyboardAnimationData(file: itemFile) break outer } } } } } } return EmojiPagerContentComponent.ItemGroup( supergroupId: group.supergroupId, groupId: group.id, title: group.title, subtitle: group.subtitle, badge: group.badge, actionButtonTitle: nil, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, isEmbedded: isEmbedded, hasClear: hasClear, hasEdit: false, collapsedLineCount: group.collapsedLineCount, displayPremiumBadges: false, headerItem: headerItem, fillWithLoadingPlaceholders: false, customTintColor: backgroundIconColor, items: group.items ) } let warpContentsOnEdges = [.reaction(onlyTop: true), .reaction(onlyTop: false), .quickReaction, .status, .channelStatus, .profilePhoto, .groupPhoto, .backgroundIcon, .messageTag].contains(subject) let enableLongPress = [.reaction(onlyTop: true), .reaction(onlyTop: false), .status, .channelStatus].contains(subject) return EmojiPagerContentComponent( id: "emoji", context: context, avatarPeer: avatarPeer, animationCache: animationCache, animationRenderer: animationRenderer, inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), panelItemGroups: allItemGroups, contentItemGroups: allItemGroups, itemLayoutType: .compact, itemContentUniqueId: nil, searchState: .empty(hasResults: false), warpContentsOnEdges: warpContentsOnEdges, hideBackground: hideBackground, displaySearchWithPlaceholder: displaySearchWithPlaceholder, searchCategories: searchCategories, searchInitiallyHidden: searchInitiallyHidden, searchAlwaysActive: false, searchIsPlaceholderOnly: false, searchUnicodeEmojiOnly: subject == .stickerAlt, emptySearchResults: nil, enableLongPress: enableLongPress, selectedItems: selectedItems, customTintColor: backgroundIconColor ) } return emojiItems } enum StickersSubject { case profilePhotoEmojiSelection case groupPhotoEmojiSelection case chatStickers case greetingStickers } static func stickerInputData( context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, stickerNamespaces: [ItemCollectionId.Namespace], stickerOrderedItemListCollectionIds: [Int32], chatPeerId: EnginePeer.Id?, hasSearch: Bool, hasTrending: Bool, forceHasPremium: Bool, hasEdit: Bool = false, hasAdd: Bool = false, searchIsPlaceholderOnly: Bool = true, subject: StickersSubject = .chatStickers, hideBackground: Bool = false ) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled struct PeerSpecificPackData: Equatable { var info: StickerPackCollectionInfo var items: [StickerPackItem] var peer: EnginePeer static func ==(lhs: PeerSpecificPackData, rhs: PeerSpecificPackData) -> Bool { if lhs.info.id != rhs.info.id { return false } if lhs.items != rhs.items { return false } if lhs.peer != rhs.peer { return false } return true } } let peerSpecificPack: Signal if let chatPeerId = chatPeerId { peerSpecificPack = combineLatest( context.engine.peers.peerSpecificStickerPack(peerId: chatPeerId), context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: chatPeerId)) ) |> map { packData, peer -> PeerSpecificPackData? in guard let peer = peer else { return nil } guard let (info, items) = packData.packInfo else { return nil } return PeerSpecificPackData(info: info, items: items.compactMap { $0 as? StickerPackItem }, peer: peer) } |> distinctUntilChanged } else { peerSpecificPack = .single(nil) } let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings let searchCategories: Signal switch subject { case .groupPhotoEmojiSelection, .profilePhotoEmojiSelection: searchCategories = context.engine.stickers.emojiSearchCategories(kind: .avatar) case .chatStickers, .greetingStickers: searchCategories = context.engine.stickers.emojiSearchCategories(kind: .combinedChatStickers) |> map { result -> EmojiSearchCategories? in guard let result else { return nil } var groups: [EmojiSearchCategories.Group] = [] groups = result.groups if case .greetingStickers = subject { if let index = groups.firstIndex(where: { group in return group.kind == .greeting }) { let group = groups.remove(at: index) groups.insert(group, at: 0) } } else if case .chatStickers = subject { if let index = groups.firstIndex(where: { group in return group.kind == .premium }) { let group = groups.remove(at: index) groups.append(group) } } return EmojiSearchCategories( hash: result.hash, groups: groups ) } } return combineLatest( context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: stickerOrderedItemListCollectionIds, namespaces: stickerNamespaces, aroundIndex: nil, count: 10000000), hasPremium(context: context, chatPeerId: chatPeerId, premiumIfSavedMessages: false), hasTrending ? context.account.viewTracker.featuredStickerPacks() : .single([]), context.engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: Namespaces.CachedItemCollection.featuredStickersConfiguration, id: ValueBoxKey(length: 0))), ApplicationSpecificNotice.dismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager), peerSpecificPack, searchCategories ) |> map { view, hasPremium, featuredStickerPacks, featuredStickersConfiguration, dismissedTrendingStickerPacks, peerSpecificPack, searchCategories -> EmojiPagerContentComponent in let hasPremium = forceHasPremium || hasPremium struct ItemGroup { var supergroupId: AnyHashable var id: AnyHashable var title: String var subtitle: String? var actionButtonTitle: String? var isPremiumLocked: Bool var isFeatured: Bool var displayPremiumBadges: Bool var hasEdit: Bool var headerItem: EntityKeyboardAnimationData? var items: [EmojiPagerContentComponent.Item] } var itemGroups: [ItemGroup] = [] var itemGroupIndexById: [AnyHashable: Int] = [:] var savedStickers: OrderedItemListView? var recentStickers: OrderedItemListView? for orderedView in view.orderedItemListsViews { if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers { recentStickers = orderedView } else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers { savedStickers = orderedView } } var installedCollectionIds = Set() for (id, _, _) in view.collectionInfos { installedCollectionIds.insert(id) } let dismissedTrendingStickerPacksSet = Set(dismissedTrendingStickerPacks ?? []) let featuredStickerPacksSet = Set(featuredStickerPacks.map(\.info.id.id)) if dismissedTrendingStickerPacksSet != featuredStickerPacksSet { let featuredStickersConfiguration = featuredStickersConfiguration?.get(FeaturedStickersConfiguration.self) for featuredStickerPack in featuredStickerPacks { if installedCollectionIds.contains(featuredStickerPack.info.id) { continue } guard let item = featuredStickerPack.topItems.first else { continue } let animationData: EntityKeyboardAnimationData if let thumbnail = featuredStickerPack.info.thumbnail { let type: EntityKeyboardAnimationData.ItemType if item.file.isAnimatedSticker { type = .lottie } else if item.file.isVideoEmoji || item.file.isVideoSticker { type = .video } else { type = .still } animationData = EntityKeyboardAnimationData( id: .stickerPackThumbnail(featuredStickerPack.info.id), type: type, resource: .stickerPackThumbnail(stickerPack: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: featuredStickerPack.info.immediateThumbnailData, isReaction: false, isTemplate: false ) } else { animationData = EntityKeyboardAnimationData(file: item.file) } var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { tintMode = .primary } let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, subgroupId: nil, icon: .none, tintMode: tintMode ) let supergroupId = "featuredTop" let groupId: AnyHashable = supergroupId let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium if isPremiumLocked && isPremiumDisabled { continue } if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count let trendingIsPremium = featuredStickersConfiguration?.isPremium ?? false let title = trendingIsPremium ? strings.Stickers_TrendingPremiumStickers : strings.StickerPacksSettings_FeaturedPacks itemGroups.append( ItemGroup( supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem] ) ) } } } if let savedStickers = savedStickers { let groupId = "saved" for item in savedStickers.items { guard let item = item.contents.get(SavedStickerItem.self) else { continue } if isPremiumDisabled && item.file.isPremiumSticker { continue } var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { tintMode = .primary } let animationData = EntityKeyboardAnimationData(file: item.file, partialReference: .savedSticker) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.EmojiInput_SectionTitleFavoriteStickers, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } } var addedCreateStickerButton = false if let recentStickers = recentStickers { let groupId = "recent" for item in recentStickers.items { guard let item = item.contents.get(RecentMediaItem.self) else { continue } if isPremiumDisabled && item.media.isPremiumSticker { continue } var tintMode: Item.TintMode = .none if item.media.isCustomTemplateEmoji { tintMode = .primary } let animationData = EntityKeyboardAnimationData(file: item.media, partialReference: .recentSticker) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.media, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: strings.Stickers_FrequentlyUsed, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } if hasAdd && !addedCreateStickerButton, let groupIndex = itemGroupIndexById[groupId] { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .icon(.add), itemFile: nil, subgroupId: nil, icon: .none, tintMode: .none ) itemGroups[groupIndex].items.insert(resultItem, at: 0) addedCreateStickerButton = true } } var avatarPeer: EnginePeer? if let peerSpecificPack = peerSpecificPack { avatarPeer = peerSpecificPack.peer var processedIds = Set() let groupId = "peerSpecific" for item in peerSpecificPack.items { if isPremiumDisabled && item.file.isPremiumSticker { continue } if processedIds.contains(item.file.fileId) { continue } processedIds.insert(item.file.fileId) var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { tintMode = .primary } let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: peerSpecificPack.peer.compactDisplayTitle, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } if hasEdit && !addedCreateStickerButton, let groupIndex = itemGroupIndexById[groupId] { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .icon(.add), itemFile: nil, subgroupId: nil, icon: .none, tintMode: .none ) itemGroups[groupIndex].items.insert(resultItem, at: 0) addedCreateStickerButton = true } } for entry in view.entries { guard let item = entry.item as? StickerPackItem else { continue } var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { tintMode = .primary } let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, subgroupId: nil, icon: .none, tintMode: tintMode ) let groupId = entry.index.collectionId if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count var title = "" var headerItem: EntityKeyboardAnimationData? var groupHasEdit = false inner: for (id, info, _) in view.collectionInfos { if id == groupId, let info = info as? StickerPackCollectionInfo { title = info.title groupHasEdit = info.flags.contains(.isCreator) if let thumbnail = info.thumbnail { let type: EntityKeyboardAnimationData.ItemType if item.file.isAnimatedSticker { type = .lottie } else if item.file.isVideoEmoji || item.file.isVideoSticker { type = .video } else { type = .still } headerItem = EntityKeyboardAnimationData( id: .stickerPackThumbnail(info.id), type: type, resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: info.immediateThumbnailData, isReaction: false, isTemplate: false ) } break inner } } itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: title, subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: true, hasEdit: hasEdit && groupHasEdit, headerItem: headerItem, items: [resultItem])) if hasEdit && !addedCreateStickerButton, let groupIndex = itemGroupIndexById[groupId] { let resultItem = EmojiPagerContentComponent.Item( animationData: nil, content: .icon(.add), itemFile: nil, subgroupId: nil, icon: .none, tintMode: .none ) itemGroups[groupIndex].items.insert(resultItem, at: 0) addedCreateStickerButton = true } } } for featuredStickerPack in featuredStickerPacks { if installedCollectionIds.contains(featuredStickerPack.info.id) { continue } for item in featuredStickerPack.topItems { var tintMode: Item.TintMode = .none if item.file.isCustomTemplateEmoji { tintMode = .primary } let animationData = EntityKeyboardAnimationData(file: item.file) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: item.file, subgroupId: nil, icon: .none, tintMode: tintMode ) let supergroupId = featuredStickerPack.info.id let groupId: AnyHashable = supergroupId let isPremiumLocked: Bool = item.file.isPremiumSticker && !hasPremium if isPremiumLocked && isPremiumDisabled { continue } if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count let subtitle: String = strings.StickerPack_StickerCount(Int32(featuredStickerPack.info.count)) var headerItem: EntityKeyboardAnimationData? if let thumbnailFileId = featuredStickerPack.info.thumbnailFileId, let file = featuredStickerPack.topItems.first(where: { $0.file.fileId.id == thumbnailFileId }) { headerItem = EntityKeyboardAnimationData(file: file.file) } else if let thumbnail = featuredStickerPack.info.thumbnail { let info = featuredStickerPack.info let type: EntityKeyboardAnimationData.ItemType if item.file.isAnimatedSticker { type = .lottie } else if item.file.isVideoEmoji || item.file.isVideoSticker { type = .video } else { type = .still } headerItem = EntityKeyboardAnimationData( id: .stickerPackThumbnail(info.id), type: type, resource: .stickerPackThumbnail(stickerPack: .id(id: info.id.id, accessHash: info.accessHash), resource: thumbnail.resource), dimensions: thumbnail.dimensions.cgSize, immediateThumbnailData: info.immediateThumbnailData, isReaction: false, isTemplate: false ) } itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: featuredStickerPack.info.title, subtitle: subtitle, actionButtonTitle: strings.Stickers_Install, isPremiumLocked: isPremiumLocked, isFeatured: true, displayPremiumBadges: false, hasEdit: false, headerItem: headerItem, items: [resultItem])) } } } let isMasks = stickerNamespaces.contains(Namespaces.ItemCollection.CloudMaskPacks) let allItemGroups = itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in var hasClear = false var isEmbedded = false if group.id == AnyHashable("recent") { hasClear = true } else if group.id == AnyHashable("featuredTop") { hasClear = true isEmbedded = true } else if group.id == AnyHashable("saved") { } return EmojiPagerContentComponent.ItemGroup( supergroupId: group.supergroupId, groupId: group.id, title: group.title, subtitle: group.subtitle, badge: nil, actionButtonTitle: group.actionButtonTitle, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, isEmbedded: isEmbedded, hasClear: hasClear, hasEdit: group.hasEdit, collapsedLineCount: nil, displayPremiumBadges: group.displayPremiumBadges, headerItem: group.headerItem, fillWithLoadingPlaceholders: false, items: group.items ) } let warpContentsOnEdges: Bool switch subject { case .profilePhotoEmojiSelection, .groupPhotoEmojiSelection: warpContentsOnEdges = true default: warpContentsOnEdges = false } return EmojiPagerContentComponent( id: isMasks ? "masks" : "stickers", context: context, avatarPeer: avatarPeer, animationCache: animationCache, animationRenderer: animationRenderer, inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), panelItemGroups: allItemGroups, contentItemGroups: allItemGroups, itemLayoutType: .detailed, itemContentUniqueId: nil, searchState: .empty(hasResults: false), warpContentsOnEdges: warpContentsOnEdges, hideBackground: hideBackground, displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil, searchCategories: searchCategories, searchInitiallyHidden: true, searchAlwaysActive: false, searchIsPlaceholderOnly: searchIsPlaceholderOnly, searchUnicodeEmojiOnly: false, emptySearchResults: nil, enableLongPress: false, selectedItems: Set(), customTintColor: nil ) } } static func messageEffectsInputData( context: AccountContext, animationCache: AnimationCache, animationRenderer: MultiAnimationRenderer, hasSearch: Bool, hideBackground: Bool = false ) -> Signal { let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 }) let isPremiumDisabled = premiumConfiguration.isPremiumDisabled let strings = context.sharedContext.currentPresentationData.with({ $0 }).strings let searchCategories: Signal = .single(nil) return combineLatest( hasPremium(context: context, chatPeerId: nil, premiumIfSavedMessages: false), context.engine.stickers.availableMessageEffects(), searchCategories ) |> map { hasPremium, availableMessageEffects, searchCategories -> EmojiPagerContentComponent in struct ItemGroup { var supergroupId: AnyHashable var id: AnyHashable var title: String? var subtitle: String? var actionButtonTitle: String? var isPremiumLocked: Bool var isFeatured: Bool var displayPremiumBadges: Bool var hasEdit: Bool var headerItem: EntityKeyboardAnimationData? var items: [EmojiPagerContentComponent.Item] } var itemGroups: [ItemGroup] = [] var itemGroupIndexById: [AnyHashable: Int] = [:] if let availableMessageEffects { var reactionEffects: [AvailableMessageEffects.MessageEffect] = [] var stickerEffects: [AvailableMessageEffects.MessageEffect] = [] for messageEffect in availableMessageEffects.messageEffects { if messageEffect.effectAnimation != nil { reactionEffects.append(messageEffect) } else { stickerEffects.append(messageEffect) } } for i in 0 ..< 2 { let groupId = i == 0 ? "reactions" : "stickers" for item in i == 0 ? reactionEffects : stickerEffects { if item.isPremium && isPremiumDisabled { continue } let itemFile: TelegramMediaFile = item.effectSticker var tintMode: Item.TintMode = .none if itemFile.isCustomTemplateEmoji { tintMode = .primary } let animationData = EntityKeyboardAnimationData(file: itemFile, partialReference: .none) let resultItem = EmojiPagerContentComponent.Item( animationData: animationData, content: .animation(animationData), itemFile: itemFile, subgroupId: nil, icon: .none, tintMode: tintMode ) if let groupIndex = itemGroupIndexById[groupId] { itemGroups[groupIndex].items.append(resultItem) } else { itemGroupIndexById[groupId] = itemGroups.count //TODO:localize itemGroups.append(ItemGroup(supergroupId: groupId, id: groupId, title: i == 0 ? nil : "Message Effects", subtitle: nil, actionButtonTitle: nil, isPremiumLocked: false, isFeatured: false, displayPremiumBadges: false, hasEdit: false, headerItem: nil, items: [resultItem])) } } } } let warpContentsOnEdges: Bool = true let allItemGroups = itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in let hasClear = false let isEmbedded = false return EmojiPagerContentComponent.ItemGroup( supergroupId: group.supergroupId, groupId: group.id, title: group.title, subtitle: group.subtitle, badge: nil, actionButtonTitle: group.actionButtonTitle, isFeatured: group.isFeatured, isPremiumLocked: group.isPremiumLocked, isEmbedded: isEmbedded, hasClear: hasClear, hasEdit: group.hasEdit, collapsedLineCount: nil, displayPremiumBadges: group.displayPremiumBadges, headerItem: group.headerItem, fillWithLoadingPlaceholders: false, items: group.items ) } return EmojiPagerContentComponent( id: "stickers", context: context, avatarPeer: nil, animationCache: animationCache, animationRenderer: animationRenderer, inputInteractionHolder: EmojiPagerContentComponent.InputInteractionHolder(), panelItemGroups: [], contentItemGroups: allItemGroups, itemLayoutType: .detailed, itemContentUniqueId: nil, searchState: .empty(hasResults: false), warpContentsOnEdges: warpContentsOnEdges, hideBackground: hideBackground, displaySearchWithPlaceholder: hasSearch ? strings.StickersSearch_SearchStickersPlaceholder : nil, searchCategories: searchCategories, searchInitiallyHidden: true, searchAlwaysActive: false, searchIsPlaceholderOnly: false, searchUnicodeEmojiOnly: false, emptySearchResults: nil, enableLongPress: false, selectedItems: Set(), customTintColor: nil ) } } }