mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
794 lines
36 KiB
Swift
794 lines
36 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
|
|
import TextFormat
|
|
import PagerComponent
|
|
|
|
final class ChatEntityKeyboardInputNode: ChatInputNode {
|
|
struct InputData: Equatable {
|
|
var emoji: EmojiPagerContentComponent
|
|
var stickers: EmojiPagerContentComponent
|
|
var gifs: GifPagerContentComponent
|
|
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
|
|
|
init(
|
|
emoji: EmojiPagerContentComponent,
|
|
stickers: EmojiPagerContentComponent,
|
|
gifs: GifPagerContentComponent,
|
|
availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji]
|
|
) {
|
|
self.emoji = emoji
|
|
self.stickers = stickers
|
|
self.gifs = gifs
|
|
self.availableGifSearchEmojies = availableGifSearchEmojies
|
|
}
|
|
}
|
|
|
|
static func inputData(context: AccountContext, interfaceInteraction: ChatPanelInterfaceInteraction, controllerInteraction: ChatControllerInteraction, chatPeerId: PeerId?) -> 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
|
|
}
|
|
var text = "."
|
|
var emojiAttribute: ChatTextInputTextCustomEmojiAttribute?
|
|
loop: for attribute in item.file.attributes {
|
|
switch attribute {
|
|
case let .Sticker(displayText, packReference, _):
|
|
text = displayText
|
|
if let packReference = packReference {
|
|
emojiAttribute = ChatTextInputTextCustomEmojiAttribute(stickerPack: packReference, fileId: item.file.fileId.id)
|
|
break loop
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if let emojiAttribute = emojiAttribute {
|
|
interfaceInteraction.insertText(NSAttributedString(string: text, attributes: [ChatTextInputAttributes.customEmoji: emojiAttribute]))
|
|
}
|
|
},
|
|
deleteBackwards: { [weak interfaceInteraction] in
|
|
guard let interfaceInteraction = interfaceInteraction else {
|
|
return
|
|
}
|
|
interfaceInteraction.backwardsDeleteText()
|
|
},
|
|
openStickerSettings: {
|
|
},
|
|
pushController: { [weak controllerInteraction] controller in
|
|
guard let controllerInteraction = controllerInteraction else {
|
|
return
|
|
}
|
|
controllerInteraction.navigationController()?.pushViewController(controller)
|
|
},
|
|
presentController: { [weak controllerInteraction] controller in
|
|
guard let controllerInteraction = controllerInteraction else {
|
|
return
|
|
}
|
|
controllerInteraction.presentController(controller, nil)
|
|
},
|
|
presentGlobalOverlayController: { [weak controllerInteraction] controller in
|
|
guard let controllerInteraction = controllerInteraction else {
|
|
return
|
|
}
|
|
controllerInteraction.presentGlobalOverlayController(controller, nil)
|
|
},
|
|
navigationController: { [weak controllerInteraction] in
|
|
return controllerInteraction?.navigationController()
|
|
},
|
|
sendSticker: { [weak controllerInteraction] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in
|
|
guard let controllerInteraction = controllerInteraction else {
|
|
return
|
|
}
|
|
let _ = controllerInteraction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer)
|
|
},
|
|
chatPeerId: chatPeerId
|
|
)
|
|
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)
|
|
},
|
|
pushController: { [weak controllerInteraction] controller in
|
|
guard let controllerInteraction = controllerInteraction else {
|
|
return
|
|
}
|
|
controllerInteraction.navigationController()?.pushViewController(controller)
|
|
},
|
|
presentController: { [weak controllerInteraction] controller in
|
|
guard let controllerInteraction = controllerInteraction else {
|
|
return
|
|
}
|
|
controllerInteraction.presentController(controller, nil)
|
|
},
|
|
presentGlobalOverlayController: { [weak controllerInteraction] controller in
|
|
guard let controllerInteraction = controllerInteraction else {
|
|
return
|
|
}
|
|
controllerInteraction.presentGlobalOverlayController(controller, nil)
|
|
},
|
|
navigationController: { [weak controllerInteraction] in
|
|
return controllerInteraction?.navigationController()
|
|
},
|
|
sendSticker: { [weak controllerInteraction] fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer in
|
|
guard let controllerInteraction = controllerInteraction else {
|
|
return
|
|
}
|
|
let _ = controllerInteraction.sendSticker(fileReference, silentPosting, schedule, query, clearInput, sourceView, sourceRect, sourceLayer)
|
|
},
|
|
chatPeerId: chatPeerId
|
|
)
|
|
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 orderedItemListCollectionIds: [Int32] = [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.PremiumStickers, Namespaces.OrderedItemList.CloudPremiumStickers]
|
|
let namespaces: [ItemCollectionId.Namespace] = [Namespaces.ItemCollection.CloudStickerPacks]
|
|
|
|
let emojiItems: Signal<EmojiPagerContentComponent, NoError> = context.account.postbox.itemCollectionsView(orderedItemListCollectionIds: orderedItemListCollectionIds, namespaces: namespaces, aroundIndex: nil, count: 10000000)
|
|
|> map { view -> EmojiPagerContentComponent in
|
|
struct ItemGroup {
|
|
var id: AnyHashable
|
|
var items: [EmojiPagerContentComponent.Item]
|
|
}
|
|
var itemGroups: [ItemGroup] = []
|
|
var itemGroupIndexById: [AnyHashable: Int] = [:]
|
|
|
|
var emojiCollectionIds = Set<ItemCollectionId>()
|
|
for (id, info, _) in view.collectionInfos {
|
|
if let info = info as? StickerPackCollectionInfo {
|
|
if info.shortName.lowercased().contains("emoji") {
|
|
emojiCollectionIds.insert(id)
|
|
}
|
|
}
|
|
}
|
|
|
|
for entry in view.entries {
|
|
guard let item = entry.item as? StickerPackItem else {
|
|
continue
|
|
}
|
|
if item.file.isAnimatedSticker || item.file.isVideoSticker {
|
|
if emojiCollectionIds.contains(entry.index.collectionId) {
|
|
let resultItem = EmojiPagerContentComponent.Item(
|
|
emoji: "",
|
|
file: item.file,
|
|
stickerPackItem: nil
|
|
)
|
|
|
|
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(
|
|
id: "emoji",
|
|
context: context,
|
|
animationCache: animationCache,
|
|
animationRenderer: animationRenderer,
|
|
inputInteraction: emojiInputInteraction,
|
|
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
|
|
var title: String?
|
|
if group.id == AnyHashable("recent") {
|
|
//TODO:localize
|
|
title = "Recently Used"
|
|
} else {
|
|
for (id, info, _) in view.collectionInfos {
|
|
if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo {
|
|
title = info.title
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return EmojiPagerContentComponent.ItemGroup(id: group.id, title: title, items: group.items)
|
|
},
|
|
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 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,
|
|
stickerPackItem: nil
|
|
)
|
|
|
|
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,
|
|
stickerPackItem: nil
|
|
)
|
|
|
|
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,
|
|
stickerPackItem: nil
|
|
)
|
|
|
|
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,
|
|
stickerPackItem: item
|
|
)
|
|
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(
|
|
id: "stickers",
|
|
context: context,
|
|
animationCache: animationCache,
|
|
animationRenderer: animationRenderer,
|
|
inputInteraction: stickerInputInteraction,
|
|
itemGroups: itemGroups.map { group -> EmojiPagerContentComponent.ItemGroup in
|
|
var title: String?
|
|
if group.id == AnyHashable("saved") {
|
|
//TODO:localize
|
|
title = "Saved"
|
|
} else if group.id == AnyHashable("recent") {
|
|
//TODO:localize
|
|
title = "Recently Used"
|
|
} else if group.id == AnyHashable("premium") {
|
|
//TODO:localize
|
|
title = "Premium"
|
|
} else {
|
|
for (id, info, _) in view.collectionInfos {
|
|
if AnyHashable(id) == group.id, let info = info as? StickerPackCollectionInfo {
|
|
title = info.title
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return EmojiPagerContentComponent.ItemGroup(id: group.id, title: title, items: group.items)
|
|
},
|
|
itemLayoutType: .detailed
|
|
)
|
|
}
|
|
|
|
let reactions: Signal<[String], NoError> = context.engine.data.subscribe(TelegramEngine.EngineData.Item.Configuration.App())
|
|
|> map { appConfiguration -> [String] in
|
|
let defaultReactions: [String] = ["👍", "👎", "😍", "😂", "😯", "😕", "😢", "😡", "💪", "👏", "🙈", "😒"]
|
|
|
|
guard let data = appConfiguration.data, let emojis = data["gif_search_emojies"] as? [String] else {
|
|
return defaultReactions
|
|
}
|
|
return emojis
|
|
}
|
|
|> distinctUntilChanged
|
|
|
|
let animatedEmojiStickers = context.engine.stickers.loadedStickerPack(reference: .animatedEmoji, forceActualized: false)
|
|
|> map { animatedEmoji -> [String: [StickerPackItem]] in
|
|
var animatedEmojiStickers: [String: [StickerPackItem]] = [:]
|
|
switch animatedEmoji {
|
|
case let .result(_, items, _):
|
|
for item in items {
|
|
if let emoji = item.getStringRepresentationsOfIndexKeys().first {
|
|
animatedEmojiStickers[emoji.basicEmoji.0] = [item]
|
|
let strippedEmoji = emoji.basicEmoji.0.strippedEmoji
|
|
if animatedEmojiStickers[strippedEmoji] == nil {
|
|
animatedEmojiStickers[strippedEmoji] = [item]
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
return animatedEmojiStickers
|
|
}
|
|
|
|
// We are intentionally not subscribing to the recent gif updates here
|
|
let gifItems: Signal<GifPagerContentComponent, NoError> = context.engine.data.get(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,
|
|
subject: .recent,
|
|
items: items
|
|
)
|
|
}
|
|
|
|
return combineLatest(queue: .mainQueue(),
|
|
emojiItems,
|
|
stickerItems,
|
|
gifItems,
|
|
reactions,
|
|
animatedEmojiStickers
|
|
)
|
|
|> map { emoji, stickers, gifs, reactions, animatedEmojiStickers -> InputData in
|
|
var availableGifSearchEmojies: [EntityKeyboardComponent.GifSearchEmoji] = []
|
|
for reaction in reactions {
|
|
if let file = animatedEmojiStickers[reaction]?.first?.file {
|
|
availableGifSearchEmojies.append(EntityKeyboardComponent.GifSearchEmoji(emoji: reaction, file: file, title: reaction))
|
|
}
|
|
}
|
|
|
|
return InputData(
|
|
emoji: emoji,
|
|
stickers: stickers,
|
|
gifs: gifs,
|
|
availableGifSearchEmojies: availableGifSearchEmojies
|
|
)
|
|
}
|
|
}
|
|
|
|
private let context: AccountContext
|
|
private let entityKeyboardView: ComponentHostView<Empty>
|
|
|
|
private let defaultToEmojiTab: Bool
|
|
private var currentInputData: InputData
|
|
private var inputDataDisposable: Disposable?
|
|
|
|
private let controllerInteraction: ChatControllerInteraction
|
|
|
|
private var inputNodeInteraction: ChatMediaInputNodeInteraction?
|
|
|
|
private let trendingGifsPromise = Promise<ChatMediaInputGifPaneTrendingState?>(nil)
|
|
|
|
private var isMarkInputCollapsed: Bool = false
|
|
|
|
var externalTopPanelContainerImpl: PagerExternalTopPanelContainer?
|
|
override var externalTopPanelContainer: UIView? {
|
|
return self.externalTopPanelContainerImpl
|
|
}
|
|
|
|
private var currentState: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, bottomInset: CGFloat, standardInputHeight: CGFloat, inputHeight: CGFloat, maximumHeight: CGFloat, inputPanelHeight: CGFloat, interfaceState: ChatPresentationInterfaceState, deviceMetrics: DeviceMetrics, isVisible: Bool, isExpanded: Bool)?
|
|
|
|
private var gifMode: GifPagerContentComponent.Subject = .recent {
|
|
didSet {
|
|
self.gifModeSubject.set(self.gifMode)
|
|
}
|
|
}
|
|
private let gifModeSubject: ValuePromise<GifPagerContentComponent.Subject>
|
|
|
|
init(context: AccountContext, currentInputData: InputData, updatedInputData: Signal<InputData, NoError>, defaultToEmojiTab: Bool, controllerInteraction: ChatControllerInteraction) {
|
|
self.context = context
|
|
self.currentInputData = currentInputData
|
|
self.defaultToEmojiTab = defaultToEmojiTab
|
|
|
|
self.controllerInteraction = controllerInteraction
|
|
|
|
self.entityKeyboardView = ComponentHostView<Empty>()
|
|
|
|
self.gifModeSubject = ValuePromise<GifPagerContentComponent.Subject>(self.gifMode, ignoreRepeated: true)
|
|
|
|
super.init()
|
|
|
|
self.view.addSubview(self.entityKeyboardView)
|
|
|
|
self.externalTopPanelContainerImpl = PagerExternalTopPanelContainer()
|
|
|
|
self.inputDataDisposable = (combineLatest(queue: .mainQueue(),
|
|
updatedInputData,
|
|
self.updatedGifs()
|
|
)
|
|
|> deliverOnMainQueue).start(next: { [weak self] inputData, gifs in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
var inputData = inputData
|
|
inputData.gifs = gifs
|
|
|
|
strongSelf.currentInputData = inputData
|
|
strongSelf.performLayout()
|
|
})
|
|
|
|
self.inputNodeInteraction = ChatMediaInputNodeInteraction(
|
|
navigateToCollectionId: { _ in
|
|
},
|
|
navigateBackToStickers: {
|
|
},
|
|
setGifMode: { _ in
|
|
},
|
|
openSettings: {
|
|
},
|
|
openTrending: { _ in
|
|
},
|
|
dismissTrendingPacks: { _ in
|
|
},
|
|
toggleSearch: { _, _, _ in
|
|
},
|
|
openPeerSpecificSettings: {
|
|
},
|
|
dismissPeerSpecificSettings: {
|
|
},
|
|
clearRecentlyUsedStickers: {
|
|
}
|
|
)
|
|
|
|
self.trendingGifsPromise.set(paneGifSearchForQuery(context: context, query: "", offset: nil, incompleteResults: true, delayRequest: false, updateActivity: nil)
|
|
|> map { items -> ChatMediaInputGifPaneTrendingState? in
|
|
if let items = items {
|
|
return ChatMediaInputGifPaneTrendingState(files: items.files, nextOffset: items.nextOffset)
|
|
} else {
|
|
return nil
|
|
}
|
|
})
|
|
}
|
|
|
|
deinit {
|
|
self.inputDataDisposable?.dispose()
|
|
}
|
|
|
|
private func updatedGifs() -> Signal<GifPagerContentComponent, NoError> {
|
|
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 context = self.context
|
|
let trendingGifs = self.trendingGifsPromise.get()
|
|
let updatedGifs = self.gifModeSubject.get()
|
|
|> mapToSignal { subject -> Signal<GifPagerContentComponent, NoError> in
|
|
switch subject {
|
|
case .recent:
|
|
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,
|
|
subject: subject,
|
|
items: items
|
|
)
|
|
}
|
|
return gifItems
|
|
case .trending:
|
|
return trendingGifs
|
|
|> map { trendingGifs -> GifPagerContentComponent in
|
|
var items: [GifPagerContentComponent.Item] = []
|
|
|
|
if let trendingGifs = trendingGifs {
|
|
for file in trendingGifs.files {
|
|
items.append(GifPagerContentComponent.Item(
|
|
file: file.file.media
|
|
))
|
|
}
|
|
}
|
|
|
|
return GifPagerContentComponent(
|
|
context: context,
|
|
inputInteraction: gifInputInteraction,
|
|
subject: subject,
|
|
items: items
|
|
)
|
|
}
|
|
case let .emojiSearch(query):
|
|
return paneGifSearchForQuery(context: context, query: query, offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil)
|
|
|> map { result -> GifPagerContentComponent in
|
|
var items: [GifPagerContentComponent.Item] = []
|
|
|
|
/*let canLoadMore: Bool
|
|
if let result = result {
|
|
canLoadMore = !result.isComplete
|
|
} else {
|
|
canLoadMore = true
|
|
}*/
|
|
|
|
if let result = result {
|
|
for file in result.files {
|
|
items.append(GifPagerContentComponent.Item(
|
|
file: file.file.media
|
|
))
|
|
}
|
|
}
|
|
|
|
return GifPagerContentComponent(
|
|
context: context,
|
|
inputInteraction: gifInputInteraction,
|
|
subject: subject,
|
|
items: items
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
return .single(self.currentInputData.gifs)
|
|
|> then(updatedGifs)
|
|
}
|
|
|
|
func markInputCollapsed() {
|
|
self.isMarkInputCollapsed = true
|
|
}
|
|
|
|
private func performLayout() {
|
|
guard let (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded) = self.currentState else {
|
|
return
|
|
}
|
|
let _ = self.updateLayout(width: width, leftInset: leftInset, rightInset: rightInset, bottomInset: bottomInset, standardInputHeight: standardInputHeight, inputHeight: inputHeight, maximumHeight: maximumHeight, inputPanelHeight: inputPanelHeight, transition: .immediate, interfaceState: interfaceState, deviceMetrics: deviceMetrics, isVisible: isVisible, isExpanded: isExpanded)
|
|
}
|
|
|
|
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, isExpanded: Bool) -> (CGFloat, CGFloat) {
|
|
self.currentState = (width, leftInset, rightInset, bottomInset, standardInputHeight, inputHeight, maximumHeight, inputPanelHeight, interfaceState, deviceMetrics, isVisible, isExpanded)
|
|
|
|
let wasMarkedInputCollapsed = self.isMarkInputCollapsed
|
|
self.isMarkInputCollapsed = false
|
|
|
|
let expandedHeight = standardInputHeight + self.expansionFraction * (maximumHeight - standardInputHeight)
|
|
|
|
var hiddenInputHeight: CGFloat = 0.0
|
|
if self.hideInput && !self.adjustLayoutForHiddenInput {
|
|
hiddenInputHeight = inputPanelHeight
|
|
}
|
|
|
|
let context = self.context
|
|
let controllerInteraction = self.controllerInteraction
|
|
let inputNodeInteraction = self.inputNodeInteraction!
|
|
let trendingGifsPromise = self.trendingGifsPromise
|
|
|
|
var mappedTransition = Transition(transition)
|
|
|
|
if wasMarkedInputCollapsed {
|
|
mappedTransition = mappedTransition.withUserData(EntityKeyboardComponent.MarkInputCollapsed())
|
|
}
|
|
|
|
let entityKeyboardSize = self.entityKeyboardView.update(
|
|
transition: mappedTransition,
|
|
component: AnyComponent(EntityKeyboardComponent(
|
|
theme: interfaceState.theme,
|
|
bottomInset: bottomInset,
|
|
emojiContent: self.currentInputData.emoji,
|
|
stickerContent: self.currentInputData.stickers,
|
|
gifContent: self.currentInputData.gifs,
|
|
availableGifSearchEmojies: self.currentInputData.availableGifSearchEmojies,
|
|
defaultToEmojiTab: self.defaultToEmojiTab,
|
|
externalTopPanelContainer: self.externalTopPanelContainerImpl,
|
|
topPanelExtensionUpdated: { [weak self] topPanelExtension, transition in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if strongSelf.topBackgroundExtension != topPanelExtension {
|
|
strongSelf.topBackgroundExtension = topPanelExtension
|
|
strongSelf.topBackgroundExtensionUpdated?(transition.containedViewLayoutTransition)
|
|
}
|
|
},
|
|
hideInputUpdated: { [weak self] hideInput, adjustLayout, transition in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
if strongSelf.hideInput != hideInput || strongSelf.adjustLayoutForHiddenInput != adjustLayout {
|
|
strongSelf.hideInput = hideInput
|
|
strongSelf.adjustLayoutForHiddenInput = adjustLayout
|
|
strongSelf.hideInputUpdated?(transition.containedViewLayoutTransition)
|
|
}
|
|
},
|
|
switchToTextInput: { [weak self] in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.controllerInteraction.updateInputMode { _ in
|
|
return .text
|
|
}
|
|
},
|
|
switchToGifSubject: { [weak self] subject in
|
|
guard let strongSelf = self else {
|
|
return
|
|
}
|
|
strongSelf.gifModeSubject.set(subject)
|
|
},
|
|
makeSearchContainerNode: { content in
|
|
let mappedMode: ChatMediaInputSearchMode
|
|
switch content {
|
|
case .stickers:
|
|
mappedMode = .sticker
|
|
case .gifs:
|
|
mappedMode = .gif
|
|
}
|
|
|
|
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
|
return PaneSearchContainerNode(
|
|
context: context,
|
|
theme: presentationData.theme,
|
|
strings: presentationData.strings,
|
|
controllerInteraction: controllerInteraction,
|
|
inputNodeInteraction: inputNodeInteraction,
|
|
mode: mappedMode,
|
|
trendingGifsPromise: trendingGifsPromise,
|
|
cancel: {
|
|
}
|
|
)
|
|
},
|
|
deviceMetrics: deviceMetrics,
|
|
hiddenInputHeight: hiddenInputHeight,
|
|
isExpanded: isExpanded
|
|
)),
|
|
environment: {},
|
|
containerSize: CGSize(width: width, height: expandedHeight)
|
|
)
|
|
transition.updateFrame(view: self.entityKeyboardView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: entityKeyboardSize))
|
|
|
|
return (expandedHeight, 0.0)
|
|
}
|
|
}
|