Files
Swiftgram/submodules/TelegramUI/Components/ComposePollScreen/Sources/StickerAttachmentScreen.swift
2026-03-22 13:44:04 +01:00

1256 lines
66 KiB
Swift

import Foundation
import UIKit
import Display
import AccountContext
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import ComponentFlow
import ViewControllerComponent
import AttachmentUI
import EntityKeyboard
import ChatEntityKeyboardInputNode
import ChatPresentationInterfaceState
import PagerComponent
import FeaturedStickersScreen
import TelegramNotices
import CounterControllerTitleView
import GlassBackgroundComponent
import GlassBarButtonComponent
import BundleIconComponent
final class StickerAttachmentScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let mode: StickerAttachmentScreen.Mode
let completion: (AnyMediaReference) -> Void
init(
context: AccountContext,
mode: StickerAttachmentScreen.Mode,
completion: @escaping (AnyMediaReference) -> Void
) {
self.context = context
self.mode = mode
self.completion = completion
}
static func ==(lhs: StickerAttachmentScreenComponent, rhs: StickerAttachmentScreenComponent) -> Bool {
return true
}
final class KeyboardClippingView: UIView {
var hitEdgeInsets: UIEdgeInsets = .zero
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
let bounds = self.bounds.inset(by: self.hitEdgeInsets)
return bounds.contains(point)
}
}
final class View: UIView, UIScrollViewDelegate {
fileprivate let keyboardView: ComponentView<Empty>
private let keyboardClippingView: KeyboardClippingView
private let panelBackgroundView: GlassBackgroundView
private let panelClippingView: UIView
private let panelHostView: PagerExternalTopPanelContainer
private let cancelButton: ComponentView<Empty>
private var component: StickerAttachmentScreenComponent?
private(set) weak var state: EmptyComponentState?
private var environment: EnvironmentType?
private var interaction: ChatEntityKeyboardInputNode.Interaction?
private var inputNodeInteraction: ChatMediaInputNodeInteraction?
private var searchVisible = false
private var forceUpdate = false
private var ignoreNextZeroScrollingOffset = false
private var topPanelScrollingOffset: CGFloat = 0.0
private var keyboardContentId: AnyHashable?
private let contentDisposable = MetaDisposable()
private var emojiContent: EmojiPagerContentComponent?
private var stickerContent: EmojiPagerContentComponent?
private var scheduledEmojiContentAnimationHint: EmojiPagerContentComponent.ContentAnimation?
private struct EmojiSearchResult {
var groups: [EmojiPagerContentComponent.ItemGroup]
var id: AnyHashable
var version: Int
var isPreset: Bool
}
private struct EmojiSearchState {
var result: EmojiSearchResult?
var isSearching: Bool
init(result: EmojiSearchResult?, isSearching: Bool) {
self.result = result
self.isSearching = isSearching
}
}
private let emojiSearchDisposable = MetaDisposable()
private let emojiSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) {
didSet {
self.emojiSearchState.set(.single(self.emojiSearchStateValue))
}
}
private let stickerSearchDisposable = MetaDisposable()
private let stickerSearchState = Promise<EmojiSearchState>(EmojiSearchState(result: nil, isSearching: false))
private var stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) {
didSet {
self.stickerSearchState.set(.single(self.stickerSearchStateValue))
}
}
override init(frame: CGRect) {
self.keyboardView = ComponentView<Empty>()
self.keyboardClippingView = KeyboardClippingView()
self.panelBackgroundView = GlassBackgroundView()
self.panelClippingView = UIView()
self.panelHostView = PagerExternalTopPanelContainer()
self.cancelButton = ComponentView<Empty>()
super.init(frame: frame)
self.addSubview(self.keyboardClippingView)
self.addSubview(self.panelBackgroundView)
self.panelBackgroundView.contentView.addSubview(self.panelClippingView)
self.panelClippingView.addSubview(self.panelHostView)
self.interaction = ChatEntityKeyboardInputNode.Interaction(
sendSticker: { [weak self] file, _, _, _, _, _, _, _, _ in
if let self {
self.complete(file.abstract)
}
return false
},
sendEmoji: { _, _, _ in
},
sendGif: { _, _, _, _, _ in
return false
},
sendBotContextResultAsGif: { _, _, _, _, _, _ in
return false
},
editGif: { _, _ in
},
updateChoosingSticker: { _ in },
switchToTextInput: {},
dismissTextInput: {},
insertText: { _ in
},
backwardsDeleteText: {},
openStickerEditor: {},
presentController: { [weak self] c, a in
if let self, let controller = self.environment?.controller() {
controller.present(c, in: .window(.root), with: a)
}
},
presentGlobalOverlayController: { [weak self] c, a in
if let self, let controller = self.environment?.controller() {
controller.presentInGlobalOverlay(c, with: a)
}
},
getNavigationController: {
return nil
},
requestLayout: { transition in
let _ = transition
}
)
self.inputNodeInteraction = ChatMediaInputNodeInteraction(
navigateToCollectionId: { _ in
},
navigateBackToStickers: {
},
setGifMode: { _ in
},
openSettings: {
},
openTrending: { _ in
},
dismissTrendingPacks: { _ in
},
toggleSearch: { _, _, _ in
},
openPeerSpecificSettings: {
},
dismissPeerSpecificSettings: {
},
clearRecentlyUsedStickers: {
}
)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.contentDisposable.dispose()
}
func complete(_ fileReference: AnyMediaReference) {
guard let component = self.component else {
return
}
component.completion(fileReference)
(self.environment?.controller() as? StickerAttachmentScreen)?.dismiss(animated: true)
}
func updateContent(component: StickerAttachmentScreenComponent) {
self.emojiContent?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] groupId, item, _, _, _, _ in
guard let self, let component = self.component else {
return
}
let context = component.context
if groupId == AnyHashable("featuredTop"), let file = item.itemFile {
let _ = (
combineLatest(
ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: context.account.peerId, premiumIfSavedMessages: true),
ChatEntityKeyboardInputNode.hasPremium(context: context, chatPeerId: context.account.peerId, premiumIfSavedMessages: false)
)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] hasPremium, hasGlobalPremium in
guard let self else {
return
}
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
let _ = (combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: [], namespaces: [Namespaces.ItemCollection.CloudEmojiPacks], aroundIndex: nil, count: 10000000),
context.account.postbox.combinedView(keys: [viewKey])
)
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] emojiPacksView, views in
guard let view = views.views[viewKey] as? OrderedItemListView else {
return
}
guard let self else {
return
}
var installedCollectionIds = Set<ItemCollectionId>()
for (id, _, _) in emojiPacksView.collectionInfos {
installedCollectionIds.insert(id)
}
let stickerPacks = view.items.map({ $0.contents.get(FeaturedStickerPackItem.self)! }).filter({
!installedCollectionIds.contains($0.info.id)
})
for featuredStickerPack in stickerPacks {
if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) {
if let pagerView = self.keyboardView.view as? EntityKeyboardComponent.View, let emojiInputInteraction = self.emojiContent?.inputInteractionHolder.inputInteraction {
pagerView.openCustomSearch(content: EmojiSearchContent(
context: context,
forceTheme: nil,
items: stickerPacks,
initialFocusId: featuredStickerPack.info.id,
hasPremiumForUse: hasPremium,
hasPremiumForInstallation: hasGlobalPremium,
parentInputInteraction: emojiInputInteraction
))
}
break
}
}
})
})
} else if let file = item.itemFile?._parse() {
self.complete(.standalone(media: file))
}
},
deleteBackwards: nil,
openStickerSettings: nil,
openFeatured: nil,
openSearch: {
},
addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
guard let self, let component = self.component, let collectionId = groupId.base as? ItemCollectionId else {
return
}
let context = component.context
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedEmojiPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { views in
guard let view = views.views[viewKey] as? OrderedItemListView else {
return
}
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredStickerPack.info.id == collectionId {
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false)
|> mapToSignal { result -> Signal<Void, NoError> in
switch result {
case let .result(info, items, installed):
if installed {
return .complete()
} else {
return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items)
}
case .fetching:
break
case .none:
break
}
return .complete()
}
|> deliverOnMainQueue).start(completed: {
})
break
}
}
})
},
clearGroup: { [weak self] groupId in
guard let self, let component = self.component else {
return
}
let context = component.context
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
if groupId == AnyHashable("recent") {
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
var items: [ActionSheetItem] = []
items.append(ActionSheetButtonItem(title: presentationData.strings.Emoji_ClearRecent, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = context.engine.stickers.clearRecentlyUsedEmoji().start()
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
} else if groupId == AnyHashable("popular") {
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
var items: [ActionSheetItem] = []
items.append(ActionSheetTextItem(title: presentationData.strings.Chat_ClearReactionsAlertText, parseMarkdown: true))
items.append(ActionSheetButtonItem(title: presentationData.strings.Chat_ClearReactionsAlertAction, color: .destructive, action: { [weak self, weak actionSheet] in
actionSheet?.dismissAnimated()
guard let self else {
return
}
self.scheduledEmojiContentAnimationHint = EmojiPagerContentComponent.ContentAnimation(type: .groupRemoved(id: "popular"))
let _ = context.engine.stickers.clearRecentlyUsedReactions().start()
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
}
},
editAction: { _ in },
pushController: { c in
},
presentController: { c in
},
presentGlobalOverlayController: { c in
},
navigationController: { [weak self] in
return self?.environment?.controller()?.navigationController as? NavigationController
},
requestUpdate: { [weak self] transition in
guard let self else {
return
}
if !transition.animation.isImmediate {
self.state?.updated(transition: transition)
}
},
updateSearchQuery: { [weak self] query in
guard let self, let component = self.component else {
return
}
let context = component.context
switch query {
case .none:
self.emojiSearchDisposable.set(nil)
self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
case let .text(rawQuery, languageCode):
let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines)
if query.isEmpty {
self.emojiSearchDisposable.set(nil)
self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false)))
} else {
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: Signal<Bool, NoError> = .single(true)
let resultSignal = combineLatest(
signal,
hasPremium
)
|> mapToSignal { keywords, hasPremium -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
var allEmoticons: [String: String] = [:]
for keyword in keywords {
for emoticon in keyword.emoticons {
allEmoticons[emoticon] = keyword.keyword
}
}
let remoteSignal: Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError>
if hasPremium {
remoteSignal = context.engine.stickers.searchEmoji(query: query, emoticon: Array(allEmoticons.keys), inputLanguageCode: languageCode)
} else {
remoteSignal = .single(([], true))
}
return remoteSignal
|> mapToSignal { foundEmoji -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in
if foundEmoji.items.isEmpty && !foundEmoji.isFinalResult {
return .complete()
}
var items: [EmojiPagerContentComponent.Item] = []
let appendUnicodeEmoji = {
for (_, list) in EmojiPagerContentComponent.staticEmojiMapping {
for emojiString in list {
if allEmoticons[emojiString] != nil {
let item = EmojiPagerContentComponent.Item(
animationData: nil,
content: .staticEmoji(emojiString),
itemFile: nil,
subgroupId: nil,
icon: .none,
tintMode: .none
)
items.append(item)
}
}
}
}
if !hasPremium {
appendUnicodeEmoji()
}
var existingIds = Set<MediaId>()
for itemFile in foundEmoji.items {
if existingIds.contains(itemFile.fileId) {
continue
}
existingIds.insert(itemFile.fileId)
if itemFile.isPremiumEmoji && !hasPremium {
continue
}
let animationData = EntityKeyboardAnimationData(file: TelegramMediaFile.Accessor(itemFile))
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: TelegramMediaFile.Accessor(itemFile),
subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
if hasPremium {
appendUnicodeEmoji()
}
return .single([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
)])
}
}
var version = 0
self.emojiSearchStateValue.isSearching = true
self.emojiSearchDisposable.set((resultSignal
|> delay(0.15, queue: .mainQueue())
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false)
version += 1
}))
}
case let .category(value):
let resultSignal = context.engine.stickers.searchEmoji(category: value)
|> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for itemFile in files {
if existingIds.contains(itemFile.fileId) {
continue
}
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: TelegramMediaFile.Accessor(itemFile))
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: TelegramMediaFile.Accessor(itemFile),
subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
)], isFinalResult))
}
var version = 0
self.emojiSearchDisposable.set((resultSignal
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
guard let group = result.items.first else {
return
}
if group.items.isEmpty && !result.isFinalResult {
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [
EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: true,
items: []
)
], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false)
return
}
self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false)
version += 1
}))
}
},
updateScrollingToItemGroup: { // [weak self] in
// if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View {
// componentView.scrolledToItemGroup()
// }
// self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
},
onScroll: {},
chatPeerId: nil,
peekBehavior: nil,
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
customContentView: nil,
useOpaqueTheme: true,
hideBackground: true,
stateContext: nil,
addImage: nil
)
var stickerPeekBehavior: EmojiContentPeekBehaviorImpl?
stickerPeekBehavior = EmojiContentPeekBehaviorImpl(
context: component.context,
forceTheme: nil,
interaction: nil,
chatPeerId: nil,
present: { [weak self] c, a in
self?.environment?.controller()?.presentInGlobalOverlay(c, with: a)
}
)
self.stickerContent?.inputInteractionHolder.inputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak self] groupId, item, _, _, _, _ in
guard let self, let component = self.component, let file = item.itemFile?._parse() else {
return
}
let context = component.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
if groupId == AnyHashable("featuredTop") {
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] views in
guard let self, let controller = self.environment?.controller(), let view = views.views[viewKey] as? OrderedItemListView else {
return
}
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredStickerPack.topItems.contains(where: { $0.file.fileId == file.fileId }) {
controller.push(FeaturedStickersScreen(
context: context,
highlightedPackId: featuredStickerPack.info.id,
forceTheme: nil,
stickerActionTitle: presentationData.strings.StickerPack_AddSticker,
sendSticker: { [weak self] fileReference, _, _ in
if let self {
self.complete(fileReference.abstract)
}
return true
}
))
break
}
}
})
} else {
let reference: FileMediaReference
if groupId == AnyHashable("saved") {
reference = .savedSticker(media: file)
} else if groupId == AnyHashable("recent") {
reference = .recentSticker(media: file)
} else {
reference = .standalone(media: file)
}
self.complete(reference.abstract)
}
},
deleteBackwards: nil,
openStickerSettings: nil,
openFeatured: nil,
openSearch: { [weak self] in
guard let self else {
return
}
if let pagerView = self.keyboardView.view as? EntityKeyboardComponent.View {
pagerView.openSearch()
}
},
addGroupAction: { [weak self] groupId, isPremiumLocked, _ in
guard let strongSelf = self, let component = strongSelf.component, let collectionId = groupId.base as? ItemCollectionId else {
return
}
let context = component.context
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { views in
guard let view = views.views[viewKey] as? OrderedItemListView else {
return
}
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
if featuredStickerPack.info.id == collectionId {
let _ = (context.engine.stickers.loadedStickerPack(reference: .id(id: featuredStickerPack.info.id.id, accessHash: featuredStickerPack.info.accessHash), forceActualized: false)
|> mapToSignal { result -> Signal<Void, NoError> in
switch result {
case let .result(info, items, installed):
if installed {
return .complete()
} else {
return context.engine.stickers.addStickerPackInteractively(info: info._parse(), items: items)
}
case .fetching:
break
case .none:
break
}
return .complete()
}
|> deliverOnMainQueue).start(completed: {
})
break
}
}
})
},
clearGroup: { [weak self] groupId in
guard let strongSelf = self, let component = strongSelf.component else {
return
}
let context = component.context
if groupId == AnyHashable("recent") {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let actionSheet = ActionSheetController(theme: ActionSheetControllerTheme(presentationTheme: presentationData.theme, fontSize: presentationData.listsFontSize))
var items: [ActionSheetItem] = []
items.append(ActionSheetButtonItem(title: presentationData.strings.Stickers_ClearRecent, color: .destructive, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
let _ = context.engine.stickers.clearRecentlyUsedStickers().start()
}))
actionSheet.setItemGroups([ActionSheetItemGroup(items: items), ActionSheetItemGroup(items: [
ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
actionSheet?.dismissAnimated()
})
])])
context.sharedContext.mainWindow?.presentInGlobalOverlay(actionSheet)
} else if groupId == AnyHashable("featuredTop") {
let viewKey = PostboxViewKey.orderedItemList(id: Namespaces.OrderedItemList.CloudFeaturedStickerPacks)
let _ = (context.account.postbox.combinedView(keys: [viewKey])
|> take(1)
|> deliverOnMainQueue).start(next: { views in
guard let view = views.views[viewKey] as? OrderedItemListView else {
return
}
var stickerPackIds: [Int64] = []
for featuredStickerPack in view.items.lazy.map({ $0.contents.get(FeaturedStickerPackItem.self)! }) {
stickerPackIds.append(featuredStickerPack.info.id.id)
}
let _ = ApplicationSpecificNotice.setDismissedTrendingStickerPacks(accountManager: context.sharedContext.accountManager, values: stickerPackIds).start()
})
} else if groupId == AnyHashable("peerSpecific") {
}
},
editAction: { _ in },
pushController: { c in
},
presentController: { c in
},
presentGlobalOverlayController: { c in
},
navigationController: { [weak self] in
return self?.environment?.controller()?.navigationController as? NavigationController
},
requestUpdate: { [weak self] transition in
guard let self else {
return
}
if !transition.animation.isImmediate {
self.state?.updated(transition: transition)
}
},
updateSearchQuery: { [weak self] query in
guard let self = self, let component = self.component else {
return
}
let context = component.context
switch query {
case .none:
self.stickerSearchDisposable.set(nil)
self.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
case .text:
self.stickerSearchDisposable.set(nil)
self.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false)
case let .category(value):
let resultSignal = context.engine.stickers.searchStickers(category: value, scope: [.installed, .remote])
|> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in
var items: [EmojiPagerContentComponent.Item] = []
var existingIds = Set<MediaId>()
for item in files.items {
let itemFile = item.file
if existingIds.contains(itemFile.fileId) {
continue
}
existingIds.insert(itemFile.fileId)
let animationData = EntityKeyboardAnimationData(file: TelegramMediaFile.Accessor(itemFile))
let item = EmojiPagerContentComponent.Item(
animationData: animationData,
content: .animation(animationData),
itemFile: TelegramMediaFile.Accessor(itemFile),
subgroupId: nil,
icon: .none,
tintMode: animationData.isTemplate ? .primary : .none
)
items.append(item)
}
return .single(([EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: false,
items: items
)], files.isFinalResult))
}
var version = 0
self.stickerSearchDisposable.set((resultSignal
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
guard let group = result.items.first else {
return
}
if group.items.isEmpty && !result.isFinalResult {
strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [
EmojiPagerContentComponent.ItemGroup(
supergroupId: "search",
groupId: "search",
title: nil,
subtitle: nil,
badge: nil,
actionButtonTitle: nil,
isFeatured: false,
isPremiumLocked: false,
isEmbedded: false,
hasClear: false,
hasEdit: false,
collapsedLineCount: nil,
displayPremiumBadges: false,
headerItem: nil,
fillWithLoadingPlaceholders: true,
items: []
)
], id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false)
return
}
strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value.id), version: version, isPreset: true), isSearching: false)
version += 1
}))
}
},
updateScrollingToItemGroup: {
// if let self, let componentView = self.hostView.componentView as? StickerSelectionComponent.View {
// componentView.scrolledToItemGroup()
// }
// self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
},
onScroll: {},
chatPeerId: nil,
peekBehavior: stickerPeekBehavior,
customLayout: nil,
externalBackground: nil,
externalExpansionView: nil,
customContentView: nil,
useOpaqueTheme: true,
hideBackground: true,
stateContext: nil,
addImage: nil
)
}
func update(component: StickerAttachmentScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
let environment = environment[EnvironmentType.self].value
self.environment = environment
self.backgroundColor = environment.theme.list.plainBackgroundColor
if self.component == nil {
let data = combineLatest(
queue: Queue.mainQueue(),
self.stickerSearchState.get(),
self.emojiSearchState.get()
)
self.contentDisposable.set(data.start(next: { [weak self] stickerSearchState, emojiSearchState in
guard let self else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
switch component.mode {
case var .emoji(emojiContent):
if let emojiSearchResult = emojiSearchState.result {
var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
emptySearchResults = EmojiPagerContentComponent.EmptySearchResults(
text: presentationData.strings.EmojiSearch_SearchEmojiEmptyResult,
iconFile: nil
)
}
let defaultSearchState: EmojiPagerContentComponent.SearchState = emojiSearchResult.isPreset ? .active : .empty(hasResults: true)
emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState)
} else if emojiSearchState.isSearching {
emojiContent = emojiContent.withUpdatedItemGroups(panelItemGroups: emojiContent.panelItemGroups, contentItemGroups: emojiContent.contentItemGroups, itemContentUniqueId: emojiContent.itemContentUniqueId, emptySearchResults: emojiContent.emptySearchResults, searchState: .searching)
}
self.emojiContent = emojiContent
case var .stickers(stickerContent):
if let stickerSearchResult = stickerSearchState.result {
var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults?
if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) {
stickerSearchResults = EmojiPagerContentComponent.EmptySearchResults(
text: presentationData.strings.EmojiSearch_SearchStickersEmptyResult,
iconFile: nil
)
}
let defaultSearchState: EmojiPagerContentComponent.SearchState = stickerSearchResult.isPreset ? .active : .empty(hasResults: true)
stickerContent = stickerContent.withUpdatedItemGroups(panelItemGroups: stickerContent.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: stickerSearchState.isSearching ? .searching : defaultSearchState)
} else if stickerSearchState.isSearching {
stickerContent = stickerContent.withUpdatedItemGroups(panelItemGroups: stickerContent.panelItemGroups, contentItemGroups: stickerContent.contentItemGroups, itemContentUniqueId: stickerContent.itemContentUniqueId, emptySearchResults: stickerContent.emptySearchResults, searchState: .searching)
}
self.stickerContent = stickerContent
}
self.updateContent(component: component)
}))
}
self.component = component
self.state = state
let topPanelHeight: CGFloat = 42.0
let topInset: CGFloat = 64.0 //component.topInset
let context = component.context
let stickerPeekBehavior = EmojiContentPeekBehaviorImpl(
context: context,
forceTheme: nil,
interaction: nil,
chatPeerId: nil,
present: { c, a in
}
)
let keyboardSize = self.keyboardView.update(
transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)),
component: AnyComponent(EntityKeyboardComponent(
theme: environment.theme,
strings: environment.strings,
isContentInFocus: true,
containerInsets: UIEdgeInsets(top: topPanelHeight + topInset - 11.0, left: 0.0, bottom: 0.0, right: 0.0),
topPanelInsets: UIEdgeInsets(top: 0.0, left: 12.0, bottom: 0.0, right: 12.0),
emojiContent: self.emojiContent,
stickerContent: self.stickerContent,
maskContent: nil,
gifContent: nil,
hasRecentGifs: false,
availableGifSearchEmojies: [],
defaultToEmojiTab: self.emojiContent != nil,
externalTopPanelContainer: self.panelHostView,
externalBottomPanelContainer: nil,
externalTintMaskContainer: nil,
displayTopPanelBackground: .blur,
topPanelExtensionUpdated: { _, _ in
},
topPanelScrollingOffset: { [weak self] offset, transition in
if let self {
if self.ignoreNextZeroScrollingOffset && offset == 0.0 {
} else {
self.ignoreNextZeroScrollingOffset = false
self.topPanelScrollingOffset = offset
}
}
},
hideInputUpdated: { [weak self] _, searchVisible, transition in
guard let self else {
return
}
self.forceUpdate = true
self.searchVisible = searchVisible
self.state?.updated(transition: transition)
let transition: ComponentTransition = .easeInOut(duration: 0.2)
if let controller = self.environment?.controller() as? StickerAttachmentScreen {
if let titleView = controller.navigationItem.titleView {
transition.setAlpha(view: titleView, alpha: searchVisible ? 0.0 : 1.0)
}
if searchVisible {
controller.requestAttachmentMenuExpansion()
}
}
},
hideTopPanelUpdated: { _, _ in
},
switchToTextInput: {},
switchToGifSubject: { _ in },
reorderItems: { _, _ in },
makeSearchContainerNode: { [weak self] content in
guard let self, let interaction = self.interaction, let inputNodeInteraction = self.inputNodeInteraction else {
return nil
}
let mappedMode: ChatMediaInputSearchMode
switch content {
case .stickers:
mappedMode = .sticker
case .gifs:
mappedMode = .gif
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let searchContainerNode = PaneSearchContainerNode(
context: context,
theme: presentationData.theme,
strings: presentationData.strings,
interaction: interaction,
inputNodeInteraction: inputNodeInteraction,
mode: mappedMode,
batchVideoRenderingContext: nil,
stickerActionTitle: presentationData.strings.StickerPack_AddSticker,
trendingGifsPromise: Promise(nil),
cancel: {
},
peekBehavior: stickerPeekBehavior
)
return searchContainerNode
},
contentIdUpdated: { [weak self] id in
guard let self else {
return
}
self.keyboardContentId = id
},
deviceMetrics: environment.deviceMetrics,
hiddenInputHeight: 0.0,
inputHeight: 0.0,
displayBottomPanel: false,
isExpanded: true,
clipContentToTopPanel: false,
useExternalSearchContainer: false
)),
environment: {},
forceUpdate: self.forceUpdate,
containerSize: availableSize
)
self.forceUpdate = false
if let keyboardComponentView = self.keyboardView.view {
if keyboardComponentView.superview == nil {
self.keyboardClippingView.addSubview(keyboardComponentView)
}
self.keyboardClippingView.clipsToBounds = false
transition.setFrame(view: self.keyboardClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelHeight + topInset), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelHeight - topInset)))
self.keyboardClippingView.hitEdgeInsets = UIEdgeInsets(top: -topPanelHeight - topInset, left: 0.0, bottom: 0.0, right: 0.0)
let panelBackgroundFrame = CGRect(origin: CGPoint(x: 12.0, y: topPanelHeight + topInset - 29.0), size: CGSize(width: availableSize.width - 24.0, height: 44.0))
self.panelClippingView.clipsToBounds = true
self.panelClippingView.layer.cornerRadius = panelBackgroundFrame.height * 0.5
transition.setFrame(view: self.panelClippingView, frame: CGRect(origin: .zero, size: panelBackgroundFrame.size))
self.panelBackgroundView.update(size: panelBackgroundFrame.size, cornerRadius: panelBackgroundFrame.size.height * 0.5, isDark: environment.theme.overallDarkAppearance, tintColor: .init(kind: .panel), isInteractive: true, isVisible: !self.searchVisible, transition: transition)
transition.setFrame(view: self.panelBackgroundView, frame: panelBackgroundFrame)
transition.setFrame(view: keyboardComponentView, frame: CGRect(origin: CGPoint(x: 0.0, y: -topPanelHeight - topInset), size: keyboardSize))
transition.setFrame(view: self.panelHostView, frame: CGRect(origin: CGPoint(x: -12.0, y: 8.0 - UIScreenPixel), size: CGSize(width: keyboardSize.width, height: 0.0)))
}
let barButtonSize = CGSize(width: 44.0, height: 44.0)
let cancelButtonSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(GlassBarButtonComponent(
size: barButtonSize,
backgroundColor: nil,
isDark: environment.theme.overallDarkAppearance,
state: .glass,
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
BundleIconComponent(
name: "Navigation/Close",
tintColor: environment.theme.chat.inputPanel.panelControlColor
)
)),
action: { [weak self] _ in
guard let self else {
return
}
(self.environment?.controller() as? StickerAttachmentScreen)?.dismiss(animated: true)
}
)),
environment: {},
containerSize: barButtonSize
)
let cancelButtonFrame = CGRect(origin: CGPoint(x: 16.0, y: 16.0), size: cancelButtonSize)
if let cancelButtonView = self.cancelButton.view {
if cancelButtonView.superview == nil {
self.addSubview(cancelButtonView)
}
transition.setBounds(view: cancelButtonView, bounds: CGRect(origin: .zero, size: cancelButtonFrame.size))
transition.setPosition(view: cancelButtonView, position: cancelButtonFrame.center)
transition.setAlpha(view: cancelButtonView, alpha: self.searchVisible ? 0.0 : 1.0)
transition.setScale(view: cancelButtonView, scale: self.searchVisible ? 0.001 : 1.0)
}
return availableSize
}
}
func makeView() -> View {
return View()
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class StickerAttachmentScreen: ViewControllerComponentContainer, AttachmentContainable {
enum Mode {
case stickers(EmojiPagerContentComponent)
case emoji(EmojiPagerContentComponent)
}
enum Source: Equatable {
enum PollMode: Equatable {
case description
case quizAnswer
case option
}
case poll(PollMode)
}
private let context: AccountContext
private let mode: Mode
private let completion: (AnyMediaReference) -> Void
init(context: AccountContext, mode: Mode, source: Source, completion: @escaping (AnyMediaReference) -> Void) {
self.context = context
self.mode = mode
self.completion = completion
super.init(context: context, component: StickerAttachmentScreenComponent(
context: context,
mode: mode,
completion: completion
), navigationBarAppearance: .transparent, theme: .default)
self._hasGlassStyle = true
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
switch source {
case let .poll(pollMode):
//TODO:localize
let title: String
let subtitle: String
switch mode {
case .stickers:
title = "Sticker"
switch pollMode {
case .description:
subtitle = "Add sticker to the poll description"
case .quizAnswer:
subtitle = "Add sticker to the quiz explanation"
case .option:
subtitle = "Add sticker to this option"
}
case .emoji:
title = "Emoji"
switch pollMode {
case .description:
subtitle = "Add emoji to the poll description"
case .quizAnswer:
subtitle = "Add emoji to the quiz explanation"
case .option:
subtitle = "Add emoji to this option"
}
}
let titleView = CounterControllerTitleView(theme: presentationData.theme, verticalOffset: -2.0)
titleView.title = CounterControllerTitle(title: title, counter: subtitle)
self.navigationItem.titleView = titleView
}
self.navigationItem.leftBarButtonItem = UIBarButtonItem(customView: UIView())
}
required init(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
public var isMinimized: Bool = false
public var requestAttachmentMenuExpansion: () -> Void = {
}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> ([AttachmentContainable], AttachmentMediaPickerContext?)) -> Void = { _ in
}
public var parentController: () -> ViewController? = {
return nil
}
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in
}
public var updateTabBarVisibility: (Bool, ContainedViewLayoutTransition) -> Void = { _, _ in
}
public var cancelPanGesture: () -> Void = {
}
public var isContainerPanning: () -> Bool = {
return false
}
public var isContainerExpanded: () -> Bool = {
return false
}
public var mediaPickerContext: AttachmentMediaPickerContext?
public var isPanGestureEnabled: (() -> Bool)? {
return {
return true
// guard let self, let componentView = self.node.hostView.componentView as? ComposePollScreenComponent.View else {
// return true
// }
// return componentView.isPanGestureEnabled()
}
}
public func isContainerPanningUpdated(_ panning: Bool) {
}
public func resetForReuse() {
}
public func prepareForReuse() {
}
public func requestDismiss(completion: @escaping () -> Void) {
completion()
}
public func shouldDismissImmediately() -> Bool {
return true
}
}