import Foundation import SwiftSignalKit import Postbox import TelegramCore import AccountContext import MultiplexedVideoNode import EntityKeyboard public final class ChatMediaInputGifPaneTrendingState { public let files: [MultiplexedVideoNodeFile] public let nextOffset: String? public init(files: [MultiplexedVideoNodeFile], nextOffset: String?) { self.files = files self.nextOffset = nextOffset } } public final class EntityKeyboardGifContent: Equatable { public let hasRecentGifs: Bool public let component: GifPagerContentComponent public init(hasRecentGifs: Bool, component: GifPagerContentComponent) { self.hasRecentGifs = hasRecentGifs self.component = component } public static func ==(lhs: EntityKeyboardGifContent, rhs: EntityKeyboardGifContent) -> Bool { if lhs.hasRecentGifs != rhs.hasRecentGifs { return false } if lhs.component != rhs.component { return false } return true } } public class PaneGifSearchForQueryResult { public let files: [MultiplexedVideoNodeFile] public let nextOffset: String? public let isComplete: Bool public let isStale: Bool public init(files: [MultiplexedVideoNodeFile], nextOffset: String?, isComplete: Bool, isStale: Bool) { self.files = files self.nextOffset = nextOffset self.isComplete = isComplete self.isStale = isStale } } public func paneGifSearchForQuery(context: AccountContext, query: String, offset: String?, incompleteResults: Bool = false, staleCachedResults: Bool = false, delayRequest: Bool = true, updateActivity: ((Bool) -> Void)?) -> Signal { let contextBot = context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) |> mapToSignal { searchBots -> Signal in let botName = searchBots.gifBotUsername ?? "gif" return context.engine.peers.resolvePeerByName(name: botName, referrer: nil) |> mapToSignal { result in guard case let .result(result) = result else { return .complete() } return .single(result) } } |> mapToSignal { peer -> Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> in if case let .user(user) = peer, let botInfo = user.botInfo, let _ = botInfo.inlinePlaceholder { let results = requestContextResults(engine: context.engine, botId: user.id, query: query, peerId: context.account.peerId, offset: offset ?? "", incompleteResults: incompleteResults, staleCachedResults: staleCachedResults, limit: 1) |> map { results -> (ChatPresentationInputQueryResult?, Bool, Bool) in return (.contextRequestResult(.user(user), results?.results), results != nil, results?.isStale ?? false) } let maybeDelayedContextResults: Signal<(ChatPresentationInputQueryResult?, Bool, Bool), NoError> if delayRequest { maybeDelayedContextResults = results |> delay(0.4, queue: Queue.concurrentDefaultQueue()) } else { maybeDelayedContextResults = results } return maybeDelayedContextResults } else { return .single((nil, true, false)) } } return contextBot |> mapToSignal { result -> Signal in if let r = result.0, case let .contextRequestResult(_, maybeCollection) = r, let collection = maybeCollection { let results = collection.results var references: [MultiplexedVideoNodeFile] = [] for result in results { switch result { case let .externalReference(externalReference): var imageResource: TelegramMediaResource? var thumbnailResource: TelegramMediaResource? var thumbnailIsVideo: Bool = false var uniqueId: Int64? if let content = externalReference.content { imageResource = content.resource if let resource = content.resource as? WebFileReferenceMediaResource { uniqueId = Int64(HashFunctions.murMurHash32(resource.url)) } } if let thumbnail = externalReference.thumbnail { thumbnailResource = thumbnail.resource if thumbnail.mimeType.hasPrefix("video/") { thumbnailIsVideo = true } } if externalReference.type == "gif", let resource = imageResource, let content = externalReference.content, let dimensions = content.dimensions { var previews: [TelegramMediaImageRepresentation] = [] var videoThumbnails: [TelegramMediaFile.VideoThumbnail] = [] if let thumbnailResource = thumbnailResource { if thumbnailIsVideo { videoThumbnails.append(TelegramMediaFile.VideoThumbnail( dimensions: dimensions, resource: thumbnailResource )) } else { previews.append(TelegramMediaImageRepresentation( dimensions: dimensions, resource: thumbnailResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false )) } } let file = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: uniqueId ?? 0), partialReference: nil, resource: resource, previewRepresentations: previews, videoThumbnails: videoThumbnails, immediateThumbnailData: nil, mimeType: "video/mp4", size: nil, attributes: [.Animated, .Video(duration: 0, size: dimensions, flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil)], alternativeRepresentations: []) references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result))) } case let .internalReference(internalReference): if let file = internalReference.file { references.append(MultiplexedVideoNodeFile(file: FileMediaReference.standalone(media: file), contextResult: (collection, result))) } } } return .single(PaneGifSearchForQueryResult(files: references, nextOffset: collection.nextOffset, isComplete: result.1, isStale: result.2)) } else if incompleteResults { return .single(nil) } else { return .complete() } } |> deliverOnMainQueue |> beforeStarted { updateActivity?(true) } |> afterCompleted { updateActivity?(false) } } public final class GifContext { private var componentValue: EntityKeyboardGifContent? { didSet { if let componentValue = self.componentValue { self.componentResult.set(.single(componentValue)) } } } private let componentPromise = Promise() private let componentResult = Promise() public var component: Signal { return self.componentResult.get() } private var componentDisposable: Disposable? private let context: AccountContext private let subject: GifPagerContentComponent.Subject private let gifInputInteraction: GifPagerContentComponent.InputInteraction private var loadingMoreToken: String? public init(context: AccountContext, subject: GifPagerContentComponent.Subject, gifInputInteraction: GifPagerContentComponent.InputInteraction, trendingGifs: Signal) { self.context = context self.subject = subject self.gifInputInteraction = gifInputInteraction let hideBackground = gifInputInteraction.hideBackground let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) |> map { savedGifs -> Bool in return !savedGifs.isEmpty } let searchCategories: Signal = context.engine.stickers.emojiSearchCategories(kind: .emoji) let presentationData = context.sharedContext.currentPresentationData.with { $0 } let gifItems: Signal switch subject { case .recent: gifItems = combineLatest( context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)), searchCategories ) |> map { savedGifs, searchCategories -> EntityKeyboardGifContent in var items: [GifPagerContentComponent.Item] = [] for gifItem in savedGifs { items.append(GifPagerContentComponent.Item( file: .savedGif(media: gifItem.contents.get(RecentMediaItem.self)!.media._parse()), contextResult: nil )) } return EntityKeyboardGifContent( hasRecentGifs: true, component: GifPagerContentComponent( context: context, inputInteraction: gifInputInteraction, subject: subject, items: items, isLoading: false, loadMoreToken: nil, displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil, searchCategories: searchCategories, searchInitiallyHidden: true, searchState: .empty(hasResults: false), hideBackground: hideBackground ) ) } case .trending: gifItems = combineLatest(hasRecentGifs, trendingGifs, searchCategories) |> map { hasRecentGifs, trendingGifs, searchCategories -> EntityKeyboardGifContent in var items: [GifPagerContentComponent.Item] = [] var isLoading = false if let trendingGifs = trendingGifs { for file in trendingGifs.files { items.append(GifPagerContentComponent.Item( file: file.file, contextResult: file.contextResult )) } } else { isLoading = true } return EntityKeyboardGifContent( hasRecentGifs: hasRecentGifs, component: GifPagerContentComponent( context: context, inputInteraction: gifInputInteraction, subject: subject, items: items, isLoading: isLoading, loadMoreToken: nil, displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil, searchCategories: searchCategories, searchInitiallyHidden: true, searchState: .empty(hasResults: false), hideBackground: hideBackground ) ) } case let .emojiSearch(query): gifItems = combineLatest( hasRecentGifs, paneGifSearchForQuery(context: context, query: query.joined(separator: ""), offset: nil, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil), searchCategories ) |> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in var items: [GifPagerContentComponent.Item] = [] var loadMoreToken: String? var isLoading = false if let result = result { for file in result.files { items.append(GifPagerContentComponent.Item( file: file.file, contextResult: file.contextResult )) } loadMoreToken = result.nextOffset } else { isLoading = true } return EntityKeyboardGifContent( hasRecentGifs: hasRecentGifs, component: GifPagerContentComponent( context: context, inputInteraction: gifInputInteraction, subject: subject, items: items, isLoading: isLoading, loadMoreToken: loadMoreToken, displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil, searchCategories: searchCategories, searchInitiallyHidden: true, searchState: .active, hideBackground: gifInputInteraction.hideBackground ) ) } } self.componentPromise.set(gifItems) self.componentDisposable = (self.componentPromise.get() |> deliverOnMainQueue).start(next: { [weak self] result in guard let strongSelf = self else { return } strongSelf.componentValue = result }) } deinit { self.componentDisposable?.dispose() } public func loadMore(token: String) { if self.loadingMoreToken == token { return } self.loadingMoreToken = token guard let componentValue = self.componentValue else { return } let context = self.context let subject = self.subject let gifInputInteraction = self.gifInputInteraction switch self.subject { case let .emojiSearch(query): let hasRecentGifs = context.engine.data.subscribe(TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: Namespaces.OrderedItemList.CloudRecentGifs)) |> map { savedGifs -> Bool in return !savedGifs.isEmpty } let searchCategories: Signal = context.engine.stickers.emojiSearchCategories(kind: .emoji) let presentationData = context.sharedContext.currentPresentationData.with { $0 } let gifItems: Signal gifItems = combineLatest(hasRecentGifs, paneGifSearchForQuery(context: context, query: query.joined(separator: ""), offset: token, incompleteResults: true, staleCachedResults: true, delayRequest: false, updateActivity: nil), searchCategories) |> map { hasRecentGifs, result, searchCategories -> EntityKeyboardGifContent in var items: [GifPagerContentComponent.Item] = [] var existingIds = Set() for item in componentValue.component.items { items.append(item) existingIds.insert(item.file.media.fileId) } var loadMoreToken: String? var isLoading = false if let result = result { for file in result.files { if existingIds.contains(file.file.media.fileId) { continue } existingIds.insert(file.file.media.fileId) items.append(GifPagerContentComponent.Item( file: file.file, contextResult: file.contextResult )) } if !result.isComplete { loadMoreToken = result.nextOffset } } else { isLoading = true } return EntityKeyboardGifContent( hasRecentGifs: hasRecentGifs, component: GifPagerContentComponent( context: context, inputInteraction: gifInputInteraction, subject: subject, items: items, isLoading: isLoading, loadMoreToken: loadMoreToken, displaySearchWithPlaceholder: gifInputInteraction.hasSearch ? presentationData.strings.Common_Search : nil, searchCategories: searchCategories, searchInitiallyHidden: true, searchState: .active, hideBackground: gifInputInteraction.hideBackground ) ) } self.componentPromise.set(gifItems) default: break } } }