Swiftgram/submodules/TelegramUI/Sources/ChatEntityKeyboardInputNode.swift
2022-06-21 21:47:01 +01:00

407 lines
18 KiB
Swift

import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import AccountContext
import ChatPresentationInterfaceState
import ComponentFlow
import EntityKeyboard
import AnimationCache
import MultiAnimationRenderer
import Postbox
import TelegramCore
import ComponentDisplayAdapters
import SettingsUI
final class ChatEntityKeyboardInputNode: ChatInputNode {
struct InputData: Equatable {
let emoji: EmojiPagerContentComponent
let stickers: EmojiPagerContentComponent
let gifs: GifPagerContentComponent
init(
emoji: EmojiPagerContentComponent,
stickers: EmojiPagerContentComponent,
gifs: GifPagerContentComponent
) {
self.emoji = emoji
self.stickers = stickers
self.gifs = gifs
}
}
static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction) -> Signal<InputData, NoError> {
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: context.currentAppConfiguration.with { $0 })
let isPremiumDisabled = premiumConfiguration.isPremiumDisabled
let emojiInputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak interfaceInteraction] item, _, _, _ in
guard let interfaceInteraction = interfaceInteraction else {
return
}
interfaceInteraction.insertText(item.emoji)
},
deleteBackwards: { [weak interfaceInteraction] in
guard let interfaceInteraction = interfaceInteraction else {
return
}
interfaceInteraction.backwardsDeleteText()
},
openStickerSettings: {
}
)
let stickerInputInteraction = EmojiPagerContentComponent.InputInteraction(
performItemAction: { [weak interfaceInteraction] item, view, rect, layer in
guard let interfaceInteraction = interfaceInteraction else {
return
}
let _ = interfaceInteraction.sendSticker(.standalone(media: item.file), false, view, rect, layer)
},
deleteBackwards: { [weak interfaceInteraction] in
guard let interfaceInteraction = interfaceInteraction else {
return
}
interfaceInteraction.backwardsDeleteText()
},
openStickerSettings: { [weak controllerInteraction] in
guard let controllerInteraction = controllerInteraction else {
return
}
let controller = installedStickerPacksController(context: context, mode: .modal)
controller.navigationPresentation = .modal
controllerInteraction.navigationController()?.pushViewController(controller)
}
)
let gifInputInteraction = GifPagerContentComponent.InputInteraction(
performItemAction: { [weak controllerInteraction] item, view, rect in
guard let controllerInteraction = controllerInteraction else {
return
}
let _ = controllerInteraction.sendGif(.savedGif(media: item.file), view, rect, false, false)
}
)
let animationCache = AnimationCacheImpl(basePath: context.account.postbox.mediaBox.basePath + "/animation-cache", allocateTempFile: {
return TempBox.shared.tempFile(fileName: "file").path
})
let animationRenderer = MultiAnimationRendererImpl()
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false),
context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudSavedStickers))
)
|> map { animatedEmoji, savedStickers -> EmojiPagerContentComponent in
var emojiItems: [EmojiPagerContentComponent.Item] = []
for item in savedStickers {
if let item = item.contents.get(SavedStickerItem.self) {
if item.file.isVideoSticker {
emojiItems.append(EmojiPagerContentComponent.Item(
emoji: "",
file: item.file
))
}
}
}
switch animatedEmoji {
case let .result(_, items, _):
for item in items {
if let emoji = item.getStringRepresentationsOfIndexKeys().first {
let strippedEmoji = emoji.basicEmoji.0.strippedEmoji
emojiItems.append(EmojiPagerContentComponent.Item(
emoji: strippedEmoji,
file: item.file
))
}
}
default:
break
}
var itemGroups: [EmojiPagerContentComponent.ItemGroup] = []
itemGroups.append(EmojiPagerContentComponent.ItemGroup(
id: "all",
title: nil,
items: emojiItems
))
return EmojiPagerContentComponent(
context: context,
animationCache: animationCache,
animationRenderer: animationRenderer,
inputInteraction: emojiInputInteraction,
itemGroups: itemGroups,
itemLayoutType: .compact
)
}
let hasPremium = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> map { peer -> Bool in
guard case let .user(user) = peer else {
return false
}
return user.isPremium
}
|> distinctUntilChanged
let orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
let stickerItems: Signal<EmojiPagerContentComponent, NoError> = combineLatest(
context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000),
hasPremium
)
|> map { view, hasPremium -> EmojiPagerContentComponent in
struct ItemGroup {
var id: AnyHashable
var items: [EmojiPagerContentComponent.Item]
}
var itemGroups: [ItemGroup] = []
var itemGroupIndexById: [AnyHashable: Int] = [:]
var savedStickers: OrderedItemListView?
var recentStickers: OrderedItemListView?
var premiumStickers: OrderedItemListView?
var cloudPremiumStickers: OrderedItemListView?
for orderedView in view.orderedItemListsViews {
if orderedView.collectionId == Namespaces.OrderedItemList.CloudRecentStickers {
recentStickers = orderedView
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudSavedStickers {
savedStickers = orderedView
} else if orderedView.collectionId == Namespaces.OrderedItemList.PremiumStickers {
premiumStickers = orderedView
} else if orderedView.collectionId == Namespaces.OrderedItemList.CloudPremiumStickers {
cloudPremiumStickers = orderedView
}
}
if let savedStickers = savedStickers {
for item in savedStickers.items {
guard let item = item.contents.get(SavedStickerItem.self) else {
continue
}
if isPremiumDisabled && item.file.isPremiumSticker {
continue
}
let resultItem = EmojiPagerContentComponent.Item(
emoji: "",
file: item.file
)
let groupId = "saved"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(id: groupId, items: [resultItem]))
}
}
}
if let recentStickers = recentStickers {
var count = 0
for item in recentStickers.items {
guard let item = item.contents.get(RecentMediaItem.self) else {
continue
}
if isPremiumDisabled && item.media.isPremiumSticker {
continue
}
let resultItem = EmojiPagerContentComponent.Item(
emoji: "",
file: item.media
)
let groupId = "recent"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(id: groupId, items: [resultItem]))
}
count += 1
if count >= 5 {
break
}
}
}
var hasPremiumStickers = false
if hasPremium {
if let premiumStickers = premiumStickers, !premiumStickers.items.isEmpty {
hasPremiumStickers = true
} else if let cloudPremiumStickers = cloudPremiumStickers, !cloudPremiumStickers.items.isEmpty {
hasPremiumStickers = true
}
}
if hasPremiumStickers {
var premiumStickers = premiumStickers?.items ?? []
if let cloudPremiumStickers = cloudPremiumStickers {
premiumStickers.append(contentsOf: cloudPremiumStickers.items)
}
var processedIds = Set<MediaId>()
for item in premiumStickers {
guard let item = item.contents.get(RecentMediaItem.self) else {
continue
}
if isPremiumDisabled && item.media.isPremiumSticker {
continue
}
if processedIds.contains(item.media.fileId) {
continue
}
processedIds.insert(item.media.fileId)
let resultItem = EmojiPagerContentComponent.Item(
emoji: "",
file: item.media
)
let groupId = "premium"
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(id: groupId, items: [resultItem]))
}
}
}
for entry in view.entries {
guard let item = entry.item as? StickerPackItem else {
continue
}
let resultItem = EmojiPagerContentComponent.Item(
emoji: "",
file: item.file
)
let groupId = entry.index.collectionId
if let groupIndex = itemGroupIndexById[groupId] {
itemGroups[groupIndex].items.append(resultItem)
} else {
itemGroupIndexById[groupId] = itemGroups.count
itemGroups.append(ItemGroup(id: groupId, items: [resultItem]))
}
}
return EmojiPagerContentComponent(
context: context,
animationCache: animationCache,
animationRenderer: animationRenderer,
inputInteraction: stickerInputInteraction,
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
var title: String?
if group.id == AnyHashable("saved") {
title = nil
} else if group.id == AnyHashable("recent") {
//TODO:localize
title = "Recently Used".uppercased()
} else if group.id == AnyHashable("premium") {
//TODO:localize
title = "Premium".uppercased()
} else {
for (id, info, _) in view.collectionInfos {
if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo {
title = info.title.uppercased()
break
}
}
}
return EmojiPagerContentComponent.ItemGroup(id: group.id, title: title, items: group.items)
},
itemLayoutType: .detailed
)
}
let gifItems: Signal<GifPagerContentComponent, NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs))
|> map { savedGifs -> GifPagerContentComponent in
var items: [GifPagerContentComponent.Item] = []
for gifItem in savedGifs {
items.append(GifPagerContentComponent.Item(
file: gifItem.contents.get(RecentMediaItem.self)!.media
))
}
return GifPagerContentComponent(
context: context,
inputInteraction: gifInputInteraction,
items: items
)
}
return combineLatest(queue: .mainQueue(),
emojiItems,
stickerItems,
gifItems
)
|> map { emoji, stickers, gifs -> InputData in
return InputData(
emoji: emoji,
stickers: stickers,
gifs: gifs
)
}
}
private let entityKeyboardView: ComponentHostView<Empty>
private var currentInputData: InputData
private var inputDataDisposable: Disposable?
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>) {
self.currentInputData = currentInputData
self.entityKeyboardView = ComponentHostView<Empty>()
super.init()
self.view.addSubview(self.entityKeyboardView)
self.externalTopPanelContainer = SparseContainerView()
self.inputDataDisposable = (updatedInputData
|> deliverOnMainQueue).start(next: { [weak self] inputData in
guard let strongSelf = self else {
return
}
strongSelf.currentInputData = inputData
})
}
deinit {
self.inputDataDisposable?.dispose()
}
override func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, transition: ContainedViewLayoutTransition, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool) -> (CGFloat, CGFloat) {
let entityKeyboardSize = self.entityKeyboardView.update(
transition: Transition(transition),
component: AnyComponent(EntityKeyboardComponent(
theme: interfaceState.theme,
bottomInset: bottomInset,
emojiContent: self.currentInputData.emoji,
stickerContent: self.currentInputData.stickers,
gifContent: self.currentInputData.gifs,
externalTopPanelContainer: self.externalTopPanelContainer,
topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in
guard let strongSelf = self else {
return
}
if strongSelf.topBackgroundExtension != topPanelExtension {
strongSelf.topBackgroundExtension = topPanelExtension
strongSelf.topBackgroundExtensionUpdated?(transition.containedViewLayoutTransition)
}
}
)),
environment: {},
containerSize: CGSize(width: width, height: standardInputHeight)
)
transition.updateFrame(view: self.entityKeyboardView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: entityKeyboardSize))
return (standardInputHeight, 0.0)
}
}